Merge remote-tracking branch 'upstream/main'

This commit is contained in:
jacob.eva 2024-12-14 21:18:51 +00:00
commit 55f6574297
No known key found for this signature in database
GPG Key ID: 0B92E083BBCCAA1E
20 changed files with 1147 additions and 238 deletions

View File

@ -7,7 +7,7 @@ clean:
-(rm ./__pycache__ -r)
-(rm ./app_storage -r)
-(rm ./share/pkg/* -r)
-(rm ./share/mirrors/* -r)
-(rm ./share/mirrors/* -rf)
-(rm ./bin -r)
cleanlibs:
@ -66,13 +66,14 @@ fetchshare:
cp ../../dist_archive/lxmf-*-py3-none-any.whl ./share/pkg/
cp ../../dist_archive/nomadnet-*-py3-none-any.whl ./share/pkg/
cp ../../dist_archive/rnsh-*-py3-none-any.whl ./share/pkg/
# cp ../../dist_archive/sbapp-*-py3-none-any.whl ./share/pkg/
cp ../../dist_archive/RNode_Firmware_*_Source.zip ./share/pkg/
zip --junk-paths ./share/pkg/example_plugins.zip ../docs/example_plugins/*.py
cp -r ../../dist_archive/reticulum.network ./share/mirrors/
cp -r ../../dist_archive/unsigned.io ./share/mirrors/
cp ../../dist_archive/Reticulum\ Manual.pdf ./share/mirrors/Reticulum_Manual.pdf
cp ../../dist_archive/Reticulum\ Manual.epub ./share/mirrors/Reticulum_Manual.epub
cp -r ../../rnode-flasher ./share/mirrors/
-(rm ./share/mirrors/rnode-flasher/.git -rf)
release:
. venv/bin/activate; buildozer android release

Binary file not shown.

Binary file not shown.

View File

@ -10,7 +10,7 @@ source.exclude_patterns = app_storage/*,venv/*,Makefile,./Makefil*,requirements,
version.regex = __version__ = ['"](.*)['"]
version.filename = %(source.dir)s/main.py
android.numeric_version = 20241020
android.numeric_version = 20241213
requirements = kivy==2.3.0,libbz2,pillow==10.2.0,qrcode==7.3.1,usb4a,usbserial4a,able_recipe,libwebp,libogg,libopus,opusfile,numpy,cryptography,ffpyplayer,codec2,pycodec2,sh,pynacl,typing-extensions

View File

@ -24,6 +24,9 @@ import base64
import threading
import RNS.vendor.umsgpack as msgpack
WINDOW_DEFAULT_WIDTH = "494"
WINDOW_DEFAULT_HEIGHT = "800"
app_ui_scaling_path = None
def apply_ui_scale():
global app_ui_scaling_path
@ -171,6 +174,11 @@ if not args.daemon:
local = os.path.dirname(__file__)
sys.path.append(local)
if not RNS.vendor.platformutils.is_android():
from kivy.config import Config
Config.set("graphics", "width", WINDOW_DEFAULT_WIDTH)
Config.set("graphics", "height", WINDOW_DEFAULT_HEIGHT)
if args.daemon:
from .sideband.core import SidebandCore
class DaemonElement():
@ -558,6 +566,15 @@ class SidebandApp(MDApp):
fn_italic=fb_path+"NotoSans-Italic.ttf",
fn_bolditalic=fb_path+"NotoSans-BoldItalic.ttf")
LabelBase.register(name="mono",
fn_regular=fb_path+"RobotoMonoNerdFont-Regular.ttf")
LabelBase.register(name="term",
fn_regular=fb_path+"BigBlueTerm437NerdFont-Regular.ttf")
LabelBase.register(name="nf",
fn_regular=fb_path+"RobotoMonoNerdFont-Regular.ttf")
def update_input_language(self):
language = self.sideband.config["input_language"]
if language == None:
@ -1406,6 +1423,8 @@ class SidebandApp(MDApp):
self.close_sub_telemetry_action()
elif self.root.ids.screen_manager.current == "rnstatus_screen":
self.close_sub_utilities_action()
elif self.root.ids.screen_manager.current == "logviewer_screen":
self.close_sub_utilities_action()
else:
self.open_conversations(direction="right")
@ -2501,30 +2520,35 @@ class SidebandApp(MDApp):
return "Could not retrieve connectivity status"
def connectivity_status(self, sender):
hs = dp(22)
if RNS.vendor.platformutils.is_android():
hs = dp(22)
yes_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
dialog = MDDialog(
title="Connectivity Status",
text=str(self.get_connectivity_text()),
buttons=[ yes_button ],
# elevation=0,
)
def cs_updater(dt):
dialog.text = str(self.get_connectivity_text())
def dl_yes(s):
self.connectivity_updater.cancel()
dialog.dismiss()
if self.connectivity_updater != None:
self.connectivity_updater.cancel()
yes_button.bind(on_release=dl_yes)
dialog.open()
yes_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
dialog = MDDialog(
title="Connectivity Status",
text=str(self.get_connectivity_text()),
buttons=[ yes_button ],
# elevation=0,
)
def cs_updater(dt):
dialog.text = str(self.get_connectivity_text())
def dl_yes(s):
self.connectivity_updater.cancel()
dialog.dismiss()
if self.connectivity_updater != None:
self.connectivity_updater.cancel()
yes_button.bind(on_release=dl_yes)
dialog.open()
self.connectivity_updater = Clock.schedule_interval(cs_updater, 2.0)
if self.connectivity_updater != None:
self.connectivity_updater.cancel()
self.connectivity_updater = Clock.schedule_interval(cs_updater, 2.0)
else:
if not self.utilities_ready:
self.utilities_init()
self.utilities_screen.rnstatus_action()
def ingest_lxm_action(self, sender):
def cb(dt):
@ -3009,6 +3033,11 @@ class SidebandApp(MDApp):
self.sideband.save_configuration()
self.update_ui_theme()
def save_classic_message_colors(sender=None, event=None):
self.sideband.config["classic_message_colors"] = self.settings_screen.ids.settings_classic_message_colors.active
self.sideband.save_configuration()
self.update_ui_theme()
def save_display_style_in_contact_list(sender=None, event=None):
self.sideband.config["display_style_in_contact_list"] = self.settings_screen.ids.display_style_in_contact_list.active
self.sideband.save_configuration()
@ -3019,6 +3048,10 @@ class SidebandApp(MDApp):
self.sideband.save_configuration()
self.sideband.setstate("wants.viewupdate.conversations", True)
def save_trusted_markup_only(sender=None, event=None):
self.sideband.config["trusted_markup_only"] = self.settings_screen.ids.settings_trusted_markup_only.active
self.sideband.save_configuration()
def save_advanced_stats(sender=None, event=None):
self.sideband.config["advanced_stats"] = self.settings_screen.ids.settings_advanced_statistics.active
self.sideband.save_configuration()
@ -3170,6 +3203,9 @@ class SidebandApp(MDApp):
self.settings_screen.ids.settings_eink_mode.active = self.sideband.config["eink_mode"]
self.settings_screen.ids.settings_eink_mode.bind(active=save_eink_mode)
self.settings_screen.ids.settings_classic_message_colors.active = self.sideband.config["classic_message_colors"]
self.settings_screen.ids.settings_classic_message_colors.bind(active=save_classic_message_colors)
self.settings_screen.ids.display_style_in_contact_list.active = self.sideband.config["display_style_in_contact_list"]
self.settings_screen.ids.display_style_in_contact_list.bind(active=save_display_style_in_contact_list)
@ -3191,6 +3227,9 @@ class SidebandApp(MDApp):
self.settings_screen.ids.settings_lxmf_ignore_unknown.active = self.sideband.config["lxmf_ignore_unknown"]
self.settings_screen.ids.settings_lxmf_ignore_unknown.bind(active=save_lxmf_ignore_unknown)
self.settings_screen.ids.settings_trusted_markup_only.active = self.sideband.config["trusted_markup_only"]
self.settings_screen.ids.settings_trusted_markup_only.bind(active=save_trusted_markup_only)
self.settings_screen.ids.settings_ignore_invalid_stamps.active = self.sideband.config["lxmf_ignore_invalid_stamps"]
self.settings_screen.ids.settings_ignore_invalid_stamps.bind(active=save_lxmf_ignore_invalid_stamps)
@ -3639,6 +3678,7 @@ class SidebandApp(MDApp):
######################################
def repository_action(self, sender=None, direction="left"):
if self.repository_ready:
self.repository_update_info()
self.repository_open(direction=direction)
else:
self.loader_action(direction=direction)
@ -3675,9 +3715,9 @@ class SidebandApp(MDApp):
info += "If you want to share the openCom Companion application itself via the repository server, you must first download it into the local repository, using the \"Update Content\" button below.\n\n"
info += "To make the repository available on your local network, simply start it below, and it will become browsable on a local IP address for anyone connected to the same WiFi or wired network.\n\n"
if self.sideband.webshare_server != None:
if RNS.vendor.platformutils.is_android():
def getIP():
adrs = []
def getIP():
adrs = []
if RNS.vendor.platformutils.is_android():
try:
from jnius import autoclass
import ipaddress
@ -3700,24 +3740,30 @@ class SidebandApp(MDApp):
RNS.log("Error while getting repository IP address: "+str(e), RNS.LOG_ERROR)
return None
return adrs
ips = getIP()
if ips == None or len(ips) == 0:
info += "The repository server is running, but the local device IP address could not be determined.\n\nYou can access the repository by pointing a browser to: http://DEVICE_IP:4444/"
self.reposository_url = None
else:
ipstr = ""
for ip in ips:
ipstr += "http://"+str(ip)+":4444/\n"
self.reposository_url = ipstr
import socket
adrs.append(socket.gethostbyname(socket.gethostname()))
ms = "" if len(ips) == 1 else "es"
info += "The repository server is running at the following address"+ms+":\n [u][ref=link]"+ipstr+"[/ref][u]"
self.repository_screen.ids.repository_info.bind(on_ref_press=self.repository_link_action)
return adrs
self.repository_screen.ids.repository_enable_button.disabled = True
self.repository_screen.ids.repository_disable_button.disabled = False
ips = getIP()
if ips == None or len(ips) == 0:
info += "The repository server is running, but the local device IP address could not be determined.\n\nYou can access the repository by pointing a browser to: https://DEVICE_IP:4444/"
self.reposository_url = None
else:
ipstr = ""
for ip in ips:
ipstr += "https://"+str(ip)+":4444/\n"
self.reposository_url = ipstr
ms = "" if len(ips) == 1 else "es"
info += "The repository server is running at the following address"+ms+":\n [u][ref=link]"+ipstr+"[/ref][u]"
self.repository_screen.ids.repository_info.bind(on_ref_press=self.repository_link_action)
def cb(dt):
self.repository_screen.ids.repository_enable_button.disabled = True
self.repository_screen.ids.repository_disable_button.disabled = False
Clock.schedule_once(cb, 0.1)
else:
self.repository_screen.ids.repository_enable_button.disabled = False
@ -3739,39 +3785,85 @@ class SidebandApp(MDApp):
def update_job(sender=None):
try:
import requests
### RNode Firmwares ###########
if True:
downloads = []
try:
release_url = "https://api.github.com/repos/markqvist/rnode_firmware/releases"
with requests.get(release_url) as response:
releases = response.json()
release = releases[0]
assets = release["assets"]
for asset in assets:
if asset["name"].lower().startswith("rnode_firmware"):
fw_url = asset["browser_download_url"]
pkgname = asset["name"]
fw_version = release["tag_name"]
RNS.log(f"Found version {fw_version} artefact {pkgname} at {fw_url}", RNS.LOG_DEBUG)
downloads.append([fw_url, pkgname, fw_version])
# Get release info
apk_version = None
apk_url = None
pkgname = None
try:
release_url = "https://api.github.com/repos/markqvist/sideband/releases"
with requests.get(release_url) as response:
releases = response.json()
release = releases[0]
assets = release["assets"]
for asset in assets:
if asset["name"].lower().endswith(".apk"):
apk_url = asset["browser_download_url"]
pkgname = asset["name"]
apk_version = release["tag_name"]
RNS.log(f"Found version {apk_version} artefact {pkgname} at {apk_url}")
except Exception as e:
self.repository_screen.ids.repository_update.text = f"Downloading release info failed with the error:\n"+str(e)
return
except Exception as e:
self.repository_screen.ids.repository_update.text = f"Downloading RNode firmware release info failed with the error:\n"+str(e)
return
self.repository_screen.ids.repository_update.text = "Downloading: "+str(apk_url)
with requests.get(apk_url, stream=True) as response:
with open("./dl_tmp", "wb") as tmp_file:
cs = 32*1024
tds = 0
for chunk in response.iter_content(chunk_size=cs):
tmp_file.write(chunk)
tds += cs
self.repository_screen.ids.repository_update.text = "Downloaded "+RNS.prettysize(tds)+" of "+str(pkgname)
try:
for download in downloads:
fw_url = download[0]
pkgname = download[1]
self.repository_screen.ids.repository_update.text = "Downloading: "+str(pkgname)
with requests.get(fw_url, stream=True) as response:
with open("./dl_tmp", "wb") as tmp_file:
cs = 32*1024
tds = 0
for chunk in response.iter_content(chunk_size=cs):
tmp_file.write(chunk)
tds += cs
self.repository_screen.ids.repository_update.text = "Downloaded "+RNS.prettysize(tds)+" of "+str(pkgname)
os.rename("./dl_tmp", f"{self.sideband.webshare_dir}/pkg/{pkgname}")
self.repository_screen.ids.repository_update.text = f"Added {pkgname} to the repository!"
except Exception as e:
self.repository_screen.ids.repository_update.text = f"Downloading RNode firmware failed with the error:\n"+str(e)
return
### Sideband APK File #########
if True:
# Get release info
apk_version = None
apk_url = None
pkgname = None
try:
release_url = "https://api.github.com/repos/markqvist/sideband/releases"
with requests.get(release_url) as response:
releases = response.json()
release = releases[0]
assets = release["assets"]
for asset in assets:
if asset["name"].lower().endswith(".apk"):
apk_url = asset["browser_download_url"]
pkgname = asset["name"]
apk_version = release["tag_name"]
RNS.log(f"Found version {apk_version} artefact {pkgname} at {apk_url}", RNS.LOG_DEBUG)
except Exception as e:
self.repository_screen.ids.repository_update.text = f"Downloading Sideband APK release info failed with the error:\n"+str(e)
return
self.repository_screen.ids.repository_update.text = "Downloading: "+str(pkgname)
with requests.get(apk_url, stream=True) as response:
with open("./dl_tmp", "wb") as tmp_file:
cs = 32*1024
tds = 0
for chunk in response.iter_content(chunk_size=cs):
tmp_file.write(chunk)
tds += cs
self.repository_screen.ids.repository_update.text = "Downloaded "+RNS.prettysize(tds)+" of "+str(pkgname)
os.rename("./dl_tmp", f"{self.sideband.webshare_dir}/pkg/{pkgname}")
self.repository_screen.ids.repository_update.text = f"Added {pkgname} to the repository!"
self.repository_screen.ids.repository_update.text = f"Repository contents updated successfully!"
os.rename("./dl_tmp", f"./share/pkg/{pkgname}")
self.repository_screen.ids.repository_update.text = f"Added {pkgname} to the repository!"
except Exception as e:
self.repository_screen.ids.repository_update.text = f"Downloading contents failed with the error:\n"+str(e)
@ -3788,15 +3880,7 @@ class SidebandApp(MDApp):
self.root.ids.screen_manager.add_widget(self.repository_screen)
self.repository_screen.ids.repository_scrollview.effect_cls = ScrollEffect
self.repository_update_info()
if not RNS.vendor.platformutils.is_android():
self.widget_hide(self.repository_screen.ids.repository_enable_button)
self.widget_hide(self.repository_screen.ids.repository_disable_button)
self.widget_hide(self.repository_screen.ids.repository_download_button)
self.repository_screen.ids.repository_info.text = "\nThe [b]Repository Webserver[/b] feature is currently only available on mobile devices."
self.repository_ready = True
def close_repository_action(self, sender=None):

33
sbapp/share/flasher.html Normal file
View File

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="css/water.css">
<link rel="shortcut icon" type="image/x-icon" href="gfx/icon.png">
<meta charset="utf-8"/>
<title>Sideband Repository</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<span class="logo">RNode Flasher</span>
<p><center><span class="menu"><a href="index.html">Start</a> | <a href="pkgs.html">Software</a> | <a href="flasher.html">RNode Flasher</a> | <a href="guides.html">Guides</a></span></center></p><hr>
<br/>Sideband includes a copy of the web-based RNode Flasher developed by <a href="https://github.com/liamcottle/rnode-flasher">Liam Cottle</a>. You can use this flasher to install and provision the RNode firmware on any compatible boards.<br/>
<br/>
<b>Please note!</b> Your browser must support Web-USB for this to work.<br/>
<br/>
To use the flasher, you will need firmware packages for the boards you want use. You can obtain these in different ways:
<ul>
<li>Sideband can automatically download the latest release of these, and include them directly in this repository.
<ul>
<li>To do so, go to the <b>Repository</b> menu item, and select <b>Update Contents</b>.</li>
<li>After that, they will be available on the <a href="pkgs.html">Software</a> page of this repository.</li>
</ul>
</li>
<li>You can download them manually from <a href="https://github.com/markqvist/RNode_Firmware/releases/latest">the latest release page on GitHub</a>.</li>
<li>You can compile them yourself from the RNode Firmware source code package <a href="pkgs.html">included in this repository</a>.</li>
</ul>
<br/>
<center><a href="/mirrors/rnode-flasher/index.html"><button type="button" id="task-replicate">Launch Web Flasher</button></a></center>
<br/>
<hr>
<p><center></p>
</body></html>

View File

@ -9,7 +9,7 @@
</head>
<body>
<span class="logo">Guides</span>
<p><center><span class="menu"><a href="index.html">Start</a> | <a href="pkgs.html">Software</a> | <a href="guides.html">Guides</a></span></center></p><hr>
<p><center><span class="menu"><a href="index.html">Start</a> | <a href="pkgs.html">Software</a> | <a href="flasher.html">RNode Flasher</a> | <a href="guides.html">Guides</a></span></center></p><hr><br/>
Welcome to the <b>Guide Section</b>!<br/><br/>From here, you can browse or download various included manuals, documentation, references and guides.
<ul>
@ -19,6 +19,7 @@ Welcome to the <b>Guide Section</b>!<br/><br/>From here, you can browse or downl
<li><a href="./mirrors/Reticulum_Manual.epub">Download the Reticulum Manual in EPUB format</a></li>
<li><a href="./mirrors/reticulum.network/index.html">Browse a local copy of the Reticulum website</a></li>
</ul>
<br/>
<hr>
<p><center></p>
</body></html>

View File

@ -9,8 +9,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<<<<<<< HEAD
<span class="logo">openCom Companion Repo</span>
<p><center><span class="menu"><a href="index.html">Start</a> | <a href="pkgs.html">Software</a> | <a href="guides.html">Guides</a></span></center></p><hr><h2>Hello!</h2>
<p><center><span class="menu"><a href="index.html">Start</a> | <a href="pkgs.html">Software</a> | <a href="flasher.html">RNode Flasher</a> |<a href="guides.html">Guides</a></span></center></p><hr><h2>Hello!</h2>
<table style="margin-bottom: 1.5em;">
<tbody>
<tr>

View File

@ -9,10 +9,10 @@
</head>
<body>
<span class="logo">Software</span>
<p><center><span class="menu"><a href="index.html">Start</a> | <a href="pkgs.html">Software</a> | <a href="guides.html">Guides</a></span></center></p><hr>
<p><center><span class="menu"><a href="index.html">Start</a> | <a href="pkgs.html">Software</a> | <a href="flasher.html">RNode Flasher</a> | <a href="guides.html">Guides</a></span></center></p><hr><br/>
Welcome to the <b>Software Library</b>!<br/><br/>From here, you can download installable Python Wheel packages of Reticulum and various other auxillary programs and utilities.
<ul id="filelist">
</ul>
</ul><br/>
<hr>
<p><center></p>
</body></html>

101
sbapp/sideband/certgen.py Normal file
View File

@ -0,0 +1,101 @@
# MIT License
#
# Copyright (c) 2024 Mark Qvist / unsigned.io.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
KEY_PASSPHRASE = None
LOADED_KEY = None
import os
import RNS
import datetime
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography import __version__ as cryptography_version_str
try:
cryptography_major_version = int(cryptography_version_str.split(".")[0])
except:
RNS.log(f"Could not determine PyCA/cryptography version: {e}", RNS.LOG_ERROR)
RNS.log(f"Assuming recent version with automatic backend selection", RNS.LOG_ERROR)
def get_key(key_path, force_reload=False):
KEY_PATH = key_path
key = None
if LOADED_KEY != None and not force_reload:
return LOADED_KEY
elif os.path.isfile(KEY_PATH):
with open(KEY_PATH, "rb") as f:
key = load_pem_private_key(f.read(), KEY_PASSPHRASE)
else:
if cryptography_major_version > 3:
key = ec.generate_private_key(curve=ec.SECP256R1())
else:
from cryptography.hazmat.backends import default_backend
key = ec.generate_private_key(curve=ec.SECP256R1(), backend=default_backend())
if KEY_PASSPHRASE == None:
key_encryption = serialization.NoEncryption()
else:
key_encryption = serialization.BestAvailableEncryption(KEY_PASSPHRASE)
with open(KEY_PATH, "wb") as f:
f.write(key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=key_encryption))
return key
def gen_cert(cert_path, key):
CERT_PATH = cert_path
cert_attrs = [x509.NameAttribute(NameOID.COUNTRY_NAME, "NA"),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "None"),
x509.NameAttribute(NameOID.LOCALITY_NAME, "Earth"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Sideband"),
x509.NameAttribute(NameOID.COMMON_NAME, "Sideband Repository")]
issuer = x509.Name(cert_attrs)
subject = issuer
cb = x509.CertificateBuilder()
cb = cb.subject_name(subject)
cb = cb.issuer_name(issuer)
cb = cb.public_key(key.public_key())
cb = cb.serial_number(x509.random_serial_number())
cb = cb.not_valid_before(datetime.datetime.now(datetime.timezone.utc)+datetime.timedelta(days=-14))
cb = cb.not_valid_after(datetime.datetime.now(datetime.timezone.utc)+datetime.timedelta(days=3652))
cb = cb.add_extension(x509.SubjectAlternativeName([x509.DNSName("localhost")]), critical=False)
if cryptography_major_version > 3:
cert = cb.sign(key, hashes.SHA256())
else:
from cryptography.hazmat.backends import default_backend
cert = cb.sign(key, hashes.SHA256(), backend=default_backend())
with open(CERT_PATH, "wb") as f:
f.write(cert.public_bytes(serialization.Encoding.PEM))
def ensure_certificate(key_path, cert_path):
gen_cert(cert_path, get_key(key_path))
return cert_path

View File

@ -16,6 +16,7 @@ import multiprocessing.connection
from copy import deepcopy
from threading import Lock
from collections import deque
from .res import sideband_fb_data
from .sense import Telemeter, Commands
from .plugins import SidebandCommandPlugin, SidebandServicePlugin, SidebandTelemetryPlugin
@ -44,7 +45,7 @@ class PropagationNodeDetector():
aspect_filter = "lxmf.propagation"
def received_announce(self, destination_hash, announced_identity, app_data):
def received_announce(self, destination_hash, announced_identity, app_data, announce_packet_hash):
try:
if app_data != None and len(app_data) > 0:
if pn_announce_data_is_valid(app_data):
@ -63,8 +64,12 @@ class PropagationNodeDetector():
# age = 0
pass
link_stats = {"rssi": self.owner_app.sideband.reticulum.get_packet_rssi(announce_packet_hash),
"snr": self.owner_app.sideband.reticulum.get_packet_snr(announce_packet_hash),
"q": self.owner_app.sideband.reticulum.get_packet_q(announce_packet_hash)}
RNS.log("Detected active propagation node "+RNS.prettyhexrep(destination_hash)+" emission "+str(age)+" seconds ago, "+str(hops)+" hops away")
self.owner.log_announce(destination_hash, app_data, dest_type=PropagationNodeDetector.aspect_filter)
self.owner.log_announce(destination_hash, app_data, dest_type=PropagationNodeDetector.aspect_filter, link_stats=link_stats)
if self.owner.config["lxmf_propagation_node"] == None:
if self.owner.active_propagation_node == None:
@ -110,11 +115,17 @@ class SidebandCore():
DEFAULT_APPEARANCE = ["account", [0,0,0,1], [1,1,1,1]]
LOG_DEQUE_MAXLEN = 128
aspect_filter = "lxmf.delivery"
def received_announce(self, destination_hash, announced_identity, app_data):
def received_announce(self, destination_hash, announced_identity, app_data, announce_packet_hash):
# Add the announce to the directory announce
# stream logger
link_stats = {"rssi": self.reticulum.get_packet_rssi(announce_packet_hash),
"snr": self.reticulum.get_packet_snr(announce_packet_hash),
"q": self.reticulum.get_packet_q(announce_packet_hash)}
# This reformats the new v0.5.0 announce data back to the expected format
# for Sidebands database and other handling functions.
dn = LXMF.display_name_from_app_data(app_data)
@ -123,7 +134,7 @@ class SidebandCore():
if dn != None:
app_data = dn.encode("utf-8")
self.log_announce(destination_hash, app_data, dest_type=SidebandCore.aspect_filter, stamp_cost=sc)
self.log_announce(destination_hash, app_data, dest_type=SidebandCore.aspect_filter, stamp_cost=sc, link_stats=link_stats)
def __init__(self, owner_app, config_path = None, is_service=False, is_client=False, android_app_dir=None, verbose=False, owner_service=None, service_context=None, is_daemon=False, load_config_only=False):
self.is_service = is_service
@ -142,6 +153,7 @@ class SidebandCore():
self.is_standalone = False
self.log_verbose = verbose
self.log_deque = deque(maxlen=self.LOG_DEQUE_MAXLEN)
self.owner_app = owner_app
self.reticulum = None
self.webshare_server = None
@ -156,6 +168,7 @@ class SidebandCore():
self.telemetry_send_blocked_until = 0
self.pending_telemetry_request = False
self.telemetry_request_max_history = 7*24*60*60
self.live_tracked_objects = {}
self.default_lxm_limit = 128*1000
self.state_db = {}
self.state_lock = Lock()
@ -167,6 +180,7 @@ class SidebandCore():
self.owner_service = owner_service
self.allow_service_dispatch = True
self.version_str = ""
self.config_template = rns_config
if config_path == None:
self.app_dir = plyer.storagepath.get_home_dir()+"/.config/occ"
@ -228,7 +242,14 @@ class SidebandCore():
self.log_dir = self.app_dir+"/app_storage/"
self.tmp_dir = self.app_dir+"/app_storage/tmp"
self.exports_dir = self.app_dir+"/exports"
self.webshare_dir = "./share/"
if RNS.vendor.platformutils.is_android():
self.webshare_dir = "./share/"
else:
sideband_dir = os.path.dirname(os.path.abspath(__file__))
self.webshare_dir = os.path.abspath(os.path.join(sideband_dir, "..", "share"))
self.webshare_ssl_key_path = self.app_dir+"/app_storage/ssl_key.pem"
self.webshare_ssl_cert_path = self.app_dir+"/app_storage/ssl_cert.pem"
self.first_run = True
self.saving_configuration = False
@ -267,6 +288,29 @@ class SidebandCore():
if load_config_only:
return
if RNS.vendor.platformutils.is_android():
if self.config["config_template"] != None:
try:
if not os.path.isfile(self.rns_configdir+"/config_template_invalid"):
if self.is_service:
with open(self.rns_configdir+"/config_template_invalid", "w") as invalidation_file:
invalidation_file.write("\n")
ct = self.config["config_template"]
RNS.log(f"Loading modified RNS config template", RNS.LOG_WARNING)
self.config_template = ct
else:
RNS.log("Custom configuration template invalid, using default configuration template", RNS.LOG_WARNING)
self.config_template = rns_config
if self.is_service:
self.setstate("hardware_operation.error", "At the previous start, Sideband could not initialise Reticulum. Custom configuration template loading has been temporarily disabled. Please check and fix any errors in your configuration template.")
except Exception as e:
RNS.log(f"An error occurred while setting RNS configuration template: {e}", RNS.LOG_ERROR)
RNS.log(f"Using default configuration template", RNS.LOG_ERROR)
self.config_template = rns_config
# Initialise Reticulum configuration
if RNS.vendor.platformutils.get_platform() == "android":
try:
@ -277,11 +321,10 @@ class SidebandCore():
RNS.log("Configuring Reticulum instance...")
if self.config["connect_transport"]:
RNS.log("Enabling Reticulum Transport")
generated_config = rns_config.replace("TRANSPORT_IS_ENABLED", "Yes")
generated_config = self.config_template.replace("TRANSPORT_IS_ENABLED", "Yes")
else:
RNS.log("Not enabling Reticulum Transport")
generated_config = rns_config.replace("TRANSPORT_IS_ENABLED", "No")
generated_config = self.config_template.replace("TRANSPORT_IS_ENABLED", "No")
config_file = open(self.rns_configdir+"/config", "wb")
config_file.write(generated_config.encode("utf-8"))
@ -387,7 +430,7 @@ class SidebandCore():
self.config["debug"] = False
self.config["display_name"] = "Anonymous Peer"
self.config["notifications_on"] = True
self.config["dark_ui"] = False
self.config["dark_ui"] = True
self.config["start_announce"] = True
self.config["propagation_by_default"] = False
self.config["home_node_as_broadcast_repeater"] = False
@ -403,8 +446,9 @@ class SidebandCore():
self.config["last_lxmf_propagation_node"] = None
self.config["nn_home_node"] = None
self.config["print_command"] = "lp"
self.config["eink_mode"] = False
self.config["eink_mode"] = True
self.config["lxm_limit_1mb"] = True
self.config["trusted_markup_only"] = False
# Connectivity
self.config["connect_transport"] = False
@ -569,7 +613,7 @@ class SidebandCore():
if not "dark_ui" in self.config:
self.config["dark_ui"] = True
if not "advanced_stats" in self.config:
self.config["advanced_stats"] = False
self.config["advanced_stats"] = True
if not "lxmf_periodic_sync" in self.config:
self.config["lxmf_periodic_sync"] = False
if not "lxmf_ignore_unknown" in self.config:
@ -589,13 +633,17 @@ class SidebandCore():
if not "print_command" in self.config:
self.config["print_command"] = "lp"
if not "eink_mode" in self.config:
self.config["eink_mode"] = False
self.config["eink_mode"] = True
if not "classic_message_colors" in self.config:
self.config["classic_message_colors"] = False
if not "display_style_in_contact_list" in self.config:
self.config["display_style_in_contact_list"] = False
self.config["display_style_in_contact_list"] = True
if not "lxm_limit_1mb" in self.config:
self.config["lxm_limit_1mb"] = True
if not "hq_ptt" in self.config:
self.config["hq_ptt"] = False
if not "trusted_markup_only" in self.config:
self.config["trusted_markup_only"] = False
if not "input_language" in self.config:
self.config["input_language"] = None
@ -604,6 +652,8 @@ class SidebandCore():
if not "block_predictive_text" in self.config:
self.config["block_predictive_text"] = False
if not "config_template" in self.config:
self.config["config_template"] = None
if not "connect_transport" in self.config:
self.config["connect_transport"] = False
if not "connect_rnode" in self.config:
@ -739,11 +789,11 @@ class SidebandCore():
if not "telemetry_bg" in self.config:
self.config["telemetry_bg"] = SidebandCore.DEFAULT_APPEARANCE[2]
if not "telemetry_send_appearance" in self.config:
self.config["telemetry_send_appearance"] = False
self.config["telemetry_send_appearance"] = True
if not "telemetry_display_trusted_only" in self.config:
self.config["telemetry_display_trusted_only"] = False
if not "display_style_from_all" in self.config:
self.config["display_style_from_all"] = False
self.config["display_style_from_all"] = True
if not "telemetry_receive_trusted_only" in self.config:
self.config["telemetry_receive_trusted_only"] = False
@ -854,9 +904,8 @@ class SidebandCore():
time.sleep(0.15)
try:
self.saving_configuration = True
config_file = open(self.config_path, "wb")
config_file.write(msgpack.packb(self.config))
config_file.close()
with open(self.config_path, "wb") as config_file:
config_file.write(msgpack.packb(self.config))
self.saving_configuration = False
except Exception as e:
self.saving_configuration = False
@ -987,14 +1036,14 @@ class SidebandCore():
else:
plyer.notification.notify(title, content, app_icon=self.icon_32)
def log_announce(self, dest, app_data, dest_type, stamp_cost=None):
def log_announce(self, dest, app_data, dest_type, stamp_cost=None, link_stats=None):
try:
if app_data == None:
app_data = b""
if type(app_data) != bytes:
app_data = msgpack.packb([app_data, stamp_cost])
RNS.log("Received "+str(dest_type)+" announce for "+RNS.prettyhexrep(dest)+" with data: "+str(app_data), RNS.LOG_DEBUG)
self._db_save_announce(dest, app_data, dest_type)
self._db_save_announce(dest, app_data, dest_type, link_stats)
self.setstate("app.flags.new_announces", True)
except Exception as e:
@ -1316,7 +1365,7 @@ class SidebandCore():
else:
return False
def request_latest_telemetry(self, from_addr=None):
def request_latest_telemetry(self, from_addr=None, is_livetrack=False):
if self.allow_service_dispatch and self.is_client:
try:
return self._service_request_latest_telemetry(from_addr)
@ -1350,7 +1399,11 @@ class SidebandCore():
if self.config["telemetry_use_propagation_only"] == True:
desired_method = LXMF.LXMessage.PROPAGATED
else:
desired_method = LXMF.LXMessage.DIRECT
if not self.message_router.delivery_link_available(from_addr) and RNS.Identity.current_ratchet_id(from_addr) != None:
RNS.log(f"Have ratchet for {RNS.prettyhexrep(from_addr)}, requesting opportunistic delivery of telemetry request", RNS.LOG_DEBUG)
desired_method = LXMF.LXMessage.OPPORTUNISTIC
else:
desired_method = LXMF.LXMessage.DIRECT
request_timebase = self.getpersistent(f"telemetry.{RNS.hexrep(from_addr, delimit=False)}.timebase") or now - self.telemetry_request_max_history
lxm_fields = { LXMF.FIELD_COMMANDS: [
@ -1363,7 +1416,7 @@ class SidebandCore():
lxm.register_failed_callback(self.telemetry_request_finished)
if self.message_router.get_outbound_propagation_node() != None:
if self.config["telemetry_try_propagation_on_fail"]:
if self.config["telemetry_try_propagation_on_fail"] and not is_livetrack:
lxm.try_propagation_on_fail = True
RNS.log(f"Sending telemetry request with timebase {request_timebase}", RNS.LOG_DEBUG)
@ -1375,6 +1428,62 @@ class SidebandCore():
else:
return "not_sent"
def _is_tracking(self, object_addr):
return object_addr in self.live_tracked_objects
def is_tracking(self, object_addr, allow_cache=False):
if not RNS.vendor.platformutils.is_android():
return self._is_tracking(object_addr)
else:
if self.is_service:
return self._is_tracking(object_addr)
else:
try:
return self.service_rpc_request({"is_tracking": object_addr})
except Exception as e:
ed = "Error while getting tracking state over RPC: "+str(e)
RNS.log(ed, RNS.LOG_DEBUG)
return ed
def _start_tracking(self, object_addr, interval, duration):
RNS.log("Starting tracking of "+RNS.prettyhexrep(object_addr), RNS.LOG_DEBUG)
self.live_tracked_objects[object_addr] = [interval, 0, time.time()+duration]
def start_tracking(self, object_addr, interval, duration, allow_cache=False):
if not RNS.vendor.platformutils.is_android():
return self._start_tracking(object_addr, interval, duration)
else:
if self.is_service:
return self._start_tracking(object_addr, interval, duration)
else:
try:
args = {"object_addr": object_addr, "interval": interval, "duration": duration}
return self.service_rpc_request({"start_tracking": args})
except Exception as e:
ed = "Error while starting tracking over RPC: "+str(e)
RNS.log(ed, RNS.LOG_DEBUG)
return ed
def _stop_tracking(self, object_addr):
RNS.log("Stopping tracking of "+RNS.prettyhexrep(object_addr), RNS.LOG_DEBUG)
if object_addr in self.live_tracked_objects:
self.live_tracked_objects.pop(object_addr)
def stop_tracking(self, object_addr, allow_cache=False):
if not RNS.vendor.platformutils.is_android():
return self._stop_tracking(object_addr)
else:
if self.is_service:
return self._stop_tracking(object_addr)
else:
try:
args = {"object_addr": object_addr}
return self.service_rpc_request({"stop_tracking": args})
except Exception as e:
ed = "Error while stopping tracking over RPC: "+str(e)
RNS.log(ed, RNS.LOG_DEBUG)
return ed
def _service_send_latest_telemetry(self, to_addr=None, stream=None, is_authorized_telemetry_request=False):
if not RNS.vendor.platformutils.is_android():
return False
@ -1430,7 +1539,11 @@ class SidebandCore():
if self.config["telemetry_use_propagation_only"] == True:
desired_method = LXMF.LXMessage.PROPAGATED
else:
desired_method = LXMF.LXMessage.DIRECT
if not self.message_router.delivery_link_available(to_addr) and RNS.Identity.current_ratchet_id(to_addr) != None:
RNS.log(f"Have ratchet for {RNS.prettyhexrep(to_addr)}, requesting opportunistic delivery of telemetry", RNS.LOG_DEBUG)
desired_method = LXMF.LXMessage.OPPORTUNISTIC
else:
desired_method = LXMF.LXMessage.DIRECT
lxm_fields = self.get_message_fields(to_addr, is_authorized_telemetry_request=is_authorized_telemetry_request, signal_already_sent=True)
if lxm_fields == False and stream == None:
@ -1841,6 +1954,14 @@ class SidebandCore():
elif "get_lxm_stamp_cost" in call:
args = call["get_lxm_stamp_cost"]
connection.send(self.get_lxm_stamp_cost(args["lxm_hash"]))
elif "is_tracking" in call:
connection.send(self.is_tracking(call["is_tracking"]))
elif "start_tracking" in call:
args = call["start_tracking"]
connection.send(self.start_tracking(object_addr=args["object_addr"], interval=args["interval"], duration=args["duration"]))
elif "stop_tracking" in call:
args = call["stop_tracking"]
connection.send(self.stop_tracking(object_addr=args["object_addr"]))
else:
connection.send(None)
@ -1934,10 +2055,10 @@ class SidebandCore():
# TODO: Remove this again at some point in the future
db = self.__db_connect()
dbc = db.cursor()
dbc.execute("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'lxm' AND sql LIKE '%extra%'")
dbc.execute("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'announce' AND sql LIKE '%extra%'")
result = dbc.fetchall()
if len(result) == 0:
dbc.execute("ALTER TABLE lxm ADD COLUMN extra BLOB")
dbc.execute("ALTER TABLE announce ADD COLUMN extra BLOB")
db.commit()
def _db_initstate(self):
@ -2494,8 +2615,16 @@ class SidebandCore():
for entry in result:
try:
if not entry[2] in added_dests:
app_data = entry[3]
app_data = entry[3]
dest_type = entry[4]
if entry[5] != None:
try:
extras = msgpack.unpackb(entry[5])
except Exception as e:
RNS.log(f"Error while unpacking extras from announce: {e}", RNS.LOG_ERROR)
extras = None
else:
extras = None
if dest_type == "lxmf.delivery":
announced_name = LXMF.display_name_from_app_data(app_data)
announced_cost = self.message_router.get_outbound_stamp_cost(entry[2])
@ -2503,11 +2632,12 @@ class SidebandCore():
announced_name = None
announced_cost = None
announce = {
"dest": entry[2],
"name": announced_name,
"cost": announced_cost,
"time": entry[1],
"type": dest_type
"dest" : entry[2],
"name" : announced_name,
"cost" : announced_cost,
"time" : entry[1],
"type" : dest_type,
"extras": extras,
}
added_dests.append(entry[2])
announces.append(announce)
@ -2921,7 +3051,7 @@ class SidebandCore():
self.__event_conversation_changed(context_dest)
def _db_save_announce(self, destination_hash, app_data, dest_type="lxmf.delivery"):
def _db_save_announce(self, destination_hash, app_data, dest_type="lxmf.delivery", link_stats = None):
with self.db_lock:
db = self.__db_connect()
dbc = db.cursor()
@ -2935,14 +3065,16 @@ class SidebandCore():
now = time.time()
hash_material = str(time).encode("utf-8")+destination_hash+app_data+dest_type.encode("utf-8")
announce_hash = RNS.Identity.full_hash(hash_material)
extras = msgpack.packb({"link_stats": link_stats})
query = "INSERT INTO announce (id, received, source, data, dest_type) values (?, ?, ?, ?, ?)"
query = "INSERT INTO announce (id, received, source, data, dest_type, extra) values (?, ?, ?, ?, ?, ?)"
data = (
announce_hash,
now,
destination_hash,
app_data,
dest_type,
extras,
)
dbc.execute(query, data)
@ -3466,6 +3598,28 @@ class SidebandCore():
except Exception as e:
RNS.log("An error occurred while requesting scheduled telemetry from collector: "+str(e), RNS.LOG_ERROR)
stale_entries = []
if len(self.live_tracked_objects) > 0:
now = time.time()
for object_hash in self.live_tracked_objects:
tracking_entry = self.live_tracked_objects[object_hash]
tracking_int = tracking_entry[0]
tracking_last = tracking_entry[1]
tracking_end = tracking_entry[2]
if now < tracking_end:
if now > tracking_last+tracking_int:
RNS.log("Next live tracking request time reached for "+str(RNS.prettyhexrep(object_hash)))
self.request_latest_telemetry(from_addr=object_hash, is_livetrack=True)
tracking_entry[1] = time.time()
else:
stale_entries.append(object_hash)
for object_hash in stale_entries:
RNS.log("Terminating live tracking for "+RNS.prettyhexrep(object_hash)+", tracking duration reached", RNS.LOG_DEBUG)
self.live_tracked_objects.pop(object_hash)
def __start_jobs_deferred(self):
if self.is_service:
self.service_thread = threading.Thread(target=self._service_jobs, daemon=True)
@ -3724,6 +3878,14 @@ class SidebandCore():
if self.is_client:
self.service_rpc_set_debug(debug)
def _log_handler(self, message):
self.log_deque.append(message)
print(message)
# TODO: Get service log on Android
def get_log(self):
return "\n".join(self.log_deque)
def __start_jobs_immediate(self):
if self.log_verbose:
selected_level = 7 # debugging purposes
@ -3731,7 +3893,20 @@ class SidebandCore():
selected_level = 2
self.setstate("init.loadingstate", "Substantiating Reticulum")
self.reticulum = RNS.Reticulum(configdir=self.rns_configdir, loglevel=selected_level)
try:
self.reticulum = RNS.Reticulum(configdir=self.rns_configdir, loglevel=selected_level, logdest=self._log_handler)
if RNS.vendor.platformutils.is_android():
if self.is_service:
if os.path.isfile(self.rns_configdir+"/config_template_invalid"):
os.unlink(self.rns_configdir+"/config_template_invalid")
else:
pass
except Exception as e:
RNS.log(f"Error while instantiating Reticulum: {e}", RNS.LOG_ERROR)
RNS.log(f"Local configuration template changes will be ignored on next start", RNS.LOG_ERROR)
exit(255)
if self.is_service:
self.__start_rpc_listener()
@ -4615,6 +4790,7 @@ class SidebandCore():
from http import server
import socketserver
import json
import ssl
webshare_dir = self.webshare_dir
port = 4444
@ -4635,7 +4811,7 @@ class SidebandCore():
self.send_response(200)
self.send_header("Content-type", "text/json")
self.end_headers()
json_result = json.dumps(os.listdir(serve_root+"/pkg"))
json_result = json.dumps(sorted(os.listdir(serve_root+"/pkg")))
self.wfile.write(json_result.encode("utf-8"))
except Exception as e:
self.send_response(500)
@ -4650,6 +4826,8 @@ class SidebandCore():
self.send_response(200)
if path.lower().endswith(".apk"):
self.send_header("Content-type", "application/vnd.android.package-archive")
elif path.lower().endswith(".js"):
self.send_header("Content-type", "text/javascript")
self.end_headers()
self.wfile.write(data)
except Exception as e:
@ -4659,7 +4837,36 @@ class SidebandCore():
es = "Error"
self.wfile.write(es.encode("utf-8"))
with socketserver.TCPServer(("", port), RequestHandler) as webserver:
#######################################################
# Override BaseHTTPRequestHandler method to squelch
# excessive exception logging when client signals
# invalid certificate to the server. This will always
# happen from some clients when using a self-signed
# certificate, so we don't care.
if not hasattr(server.BaseHTTPRequestHandler, "handle_orig"):
server.BaseHTTPRequestHandler.handle_orig = server.BaseHTTPRequestHandler.handle
def handle(self):
try:
self.handle_orig()
except ssl.SSLError:
pass
except Exception as e:
RNS.log("HTTP server exception: "+str(e), RNS.LOG_ERROR)
server.BaseHTTPRequestHandler.handle = handle
#######################################################
socketserver.TCPServer.allow_reuse_address = True
class ThreadedHTTPServer(socketserver.ThreadingMixIn, server.HTTPServer):
daemon_threads = True
with ThreadedHTTPServer(("", port), RequestHandler) as webserver:
from sideband.certgen import ensure_certificate
ensure_certificate(self.webshare_ssl_key_path, self.webshare_ssl_cert_path)
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ssl_context.load_cert_chain(certfile=self.webshare_ssl_cert_path, keyfile=self.webshare_ssl_key_path)
webserver.socket = ssl_context.wrap_socket(webserver.socket, do_handshake_on_connect=False, server_side=True)
self.webshare_server = webserver
webserver.serve_forever()
self.webshare_server = None
@ -4864,15 +5071,41 @@ class SidebandCore():
if not self.reticulum.is_connected_to_shared_instance:
RNS.Transport.detach_interfaces()
rns_config = """
rns_config = """# This template is used to generate a
# running configuration for Sideband's
# internal RNS instance. Incorrect changes
# or addition here may cause Sideband to
# fail starting up or working properly.
#
# If Sideband detects that Reticulum
# aborts at startup, due to an error in
# configuration, any template changes
# will be reset to this default.
[reticulum]
enable_transport = TRANSPORT_IS_ENABLED
share_instance = Yes
shared_instance_port = 37428
instance_control_port = 37429
panic_on_interface_error = No
# Don't change this line, use the UI
# setting for selecting whether RNS
# transport is enabled or disabled
enable_transport = TRANSPORT_IS_ENABLED
# Changing this setting will cause
# Sideband to not work.
share_instance = Yes
# Changing these options should only
# be done if you know what you're doing.
shared_instance_port = 37428
instance_control_port = 37429
panic_on_interface_error = No
# Logging is controlled by settings
# in the UI, so this section is mostly
# not relevant in Sideband.
[logging]
loglevel = 3
loglevel = 3
# No additional interfaces are currently
# defined, but you can use this section
# to do so.
[interfaces]
"""

View File

@ -778,9 +778,9 @@ class Location(Sensor):
return [
struct.pack("!i", int(round(d["latitude"], 6)*1e6)),
struct.pack("!i", int(round(d["longitude"], 6)*1e6)),
struct.pack("!I", int(round(d["altitude"], 2)*1e2)),
struct.pack("!i", int(round(d["altitude"], 2)*1e2)),
struct.pack("!I", int(round(d["speed"], 2)*1e2)),
struct.pack("!I", int(round(d["bearing"], 2)*1e2)),
struct.pack("!i", int(round(d["bearing"], 2)*1e2)),
struct.pack("!H", int(round(d["accuracy"], 2)*1e2)),
d["last_update"],
]
@ -796,9 +796,9 @@ class Location(Sensor):
return {
"latitude": struct.unpack("!i", packed[0])[0]/1e6,
"longitude": struct.unpack("!i", packed[1])[0]/1e6,
"altitude": struct.unpack("!I", packed[2])[0]/1e2,
"altitude": struct.unpack("!i", packed[2])[0]/1e2,
"speed": struct.unpack("!I", packed[3])[0]/1e2,
"bearing": struct.unpack("!I", packed[4])[0]/1e2,
"bearing": struct.unpack("!i", packed[4])[0]/1e2,
"accuracy": struct.unpack("!H", packed[5])[0]/1e2,
"last_update": packed[6],
}

View File

@ -18,9 +18,9 @@ from kivy.lang.builder import Builder
from kivy.utils import escape_markup
if RNS.vendor.platformutils.get_platform() == "android":
from ui.helpers import multilingual_markup
from ui.helpers import multilingual_markup, sig_icon_for_q
else:
from .helpers import multilingual_markup
from .helpers import multilingual_markup, sig_icon_for_q
if RNS.vendor.platformutils.get_platform() == "android":
from ui.helpers import ts_format
@ -92,6 +92,25 @@ class Announces():
a_name = announce["name"]
a_cost = announce["cost"]
dest_type = announce["type"]
a_rssi = None
a_snr = None
a_q = None
link_extras_str = ""
link_extras_full = ""
if "extras" in announce and announce["extras"] != None:
extras = announce["extras"]
if "link_stats" in extras:
link_stats = extras["link_stats"]
if "rssi" in link_stats and "snr" in link_stats and "q" in link_stats:
a_rssi = link_stats["rssi"]
a_snr = link_stats["snr"]
a_q = link_stats["q"]
if a_rssi != None and a_snr != None and a_q != None:
link_extras_str = f" ([b]RSSI[/b] {a_rssi} [b]SNR[/b] {a_snr})"
link_extras_full = f"\n[b]Link Quality[/b] {a_q}%[/b]\n[b]RSSI[/b] {a_rssi}\n[b]SNR[/b] {a_snr}"
sig_icon = multilingual_markup(sig_icon_for_q(a_q).encode("utf-8")).decode("utf-8")
if not context_dest in self.added_item_dests:
if self.app.sideband.is_trusted(context_dest):
@ -99,16 +118,16 @@ class Announces():
else:
trust_icon = "account-question"
def gen_info(ts, dest, name, cost, dtype):
def gen_info(ts, dest, name, cost, dtype, link_extras):
name = multilingual_markup(escape_markup(str(name)).encode("utf-8")).decode("utf-8")
cost = str(cost)
def x(sender):
yes_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
if dtype == "lxmf.delivery":
ad_text = "[size=22dp]LXMF Peer[/size]\n\n[b]Received[/b] "+ts+"\n[b]Address[/b] "+RNS.prettyhexrep(dest)+"\n[b]Name[/b] "+name+"\n[b]Stamp Cost[/b] "+cost
ad_text = "[size=22dp]LXMF Peer[/size]\n\n[b]Received[/b] "+ts+"\n[b]Address[/b] "+RNS.prettyhexrep(dest)+"\n[b]Name[/b] "+name+"\n[b]Stamp Cost[/b] "+cost+link_extras
if dtype == "lxmf.propagation":
ad_text = "[size=22dp]LXMF Propagation Node[/size]\n\n[b]Received[/b] "+ts+"\n[b]Address[/b] "+RNS.prettyhexrep(dest)
ad_text = "[size=22dp]LXMF Propagation Node[/size]\n\n[b]Received[/b] "+ts+"\n[b]Address[/b] "+RNS.prettyhexrep(dest)+link_extras
dialog = MDDialog(
text=ad_text,
@ -123,7 +142,8 @@ class Announces():
dialog.open()
return x
time_string = time.strftime(ts_format, time.localtime(ts))
time_string = sig_icon + " " + time.strftime(ts_format, time.localtime(ts)) + link_extras_str
time_string_plain = time.strftime(ts_format, time.localtime(ts))
if dest_type == "lxmf.delivery":
disp_name = multilingual_markup(escape_markup(str(self.app.sideband.peer_display_name(context_dest))).encode("utf-8")).decode("utf-8")
@ -137,7 +157,7 @@ class Announces():
disp_name = "Unknown Announce"
iconl = IconLeftWidget(icon="progress-question")
item = TwoLineAvatarIconListItem(text=time_string, secondary_text=disp_name, on_release=gen_info(time_string, context_dest, a_name, a_cost, dest_type))
item = TwoLineAvatarIconListItem(text=time_string, secondary_text=disp_name, on_release=gen_info(time_string_plain, context_dest, a_name, a_cost, dest_type, link_extras_full))
item.add_widget(iconl)
item.sb_uid = context_dest
item.ts = ts

View File

@ -133,6 +133,7 @@ class Conversations():
unread = conv["unread"]
last_activity = conv["last_activity"]
trusted = conv["trust"] == 1
appearance_from_all = self.app.sideband.config["display_style_from_all"]
appearance = self.app.sideband.peer_appearance(context_dest, conv=conv)
is_object = self.app.sideband.is_object(context_dest, conv_data=conv)
da = self.app.sideband.DEFAULT_APPEARANCE
@ -141,7 +142,7 @@ class Conversations():
conv_icon = self.trust_icon(conv)
fg = None; bg = None; ti_color = None
if trusted and self.app.sideband.config["display_style_in_contact_list"] and appearance != None and appearance != da:
if (trusted or appearance_from_all) and self.app.sideband.config["display_style_in_contact_list"] and appearance != None and appearance != da:
fg = appearance[1] or da[1]; bg = appearance[2] or da[2]
ti_color = "Custom"
else:

View File

@ -4,6 +4,7 @@ from kivy.uix.screenmanager import ScreenManager, Screen
from kivymd.theming import ThemableBehavior
from kivymd.uix.list import OneLineIconListItem, MDList, IconLeftWidget, IconRightWidget
from kivy.properties import StringProperty
import re
ts_format = "%Y-%m-%d %H:%M:%S"
file_ts_format = "%Y_%m_%d_%H_%M_%S"
@ -25,6 +26,19 @@ intensity_msgs_light = "500"
intensity_play_dark = "600"
intensity_play_light = "300"
intensity_msgs_dark_alt = "800"
intensity_msgs_light_alt = "400"
intensity_delivered_alt_dark = "800"
color_received_alt = "BlueGray"
color_received_alt_light = "BlueGray"
color_delivered_alt = "Indigo"
color_propagated_alt = "DeepPurple"
color_paper_alt = "DeepPurple"
color_playing_alt = "Amber"
color_failed_alt = "Red"
color_unknown_alt = "Gray"
class ContentNavigationDrawer(Screen):
pass
@ -45,13 +59,12 @@ def strip_emojis(str_input):
return output
def multilingual_markup(data):
# TODO: Remove
# import time
# ts = time.time()
do = ""
rfont = "default"
ds = data.decode("utf-8")
di = 0
persistent_regions = [(m.start(), m.end()) for m in re.finditer("(?s)\[font=(?:nf|term)\].*?\[/font\]", ds)]
for cp in ds:
match = False
switch = False
@ -63,6 +76,10 @@ def multilingual_markup(data):
switch = True
rfont = "emoji"
in_persistent = False
if any(x[0] < di and x[1] > di for x in persistent_regions):
in_persistent = True
if not match:
for range_start in codepoint_map:
range_end = codepoint_map[range_start][0]
@ -71,8 +88,9 @@ def multilingual_markup(data):
if range_end >= ord(cp) >= range_start:
match = True
if rfont != mapped_font:
rfont = mapped_font
switch = True
if not in_persistent:
rfont = mapped_font
switch = True
break
if (not match) and rfont != "default":
@ -86,15 +104,30 @@ def multilingual_markup(data):
do += "[font="+str(rfont)+"]"
do += cp
di += 1
if rfont != "default":
do += "[/font]"
# TODO: Remove
# print(do+"\n\n"+str(time.time()-ts))
return do.encode("utf-8")
def sig_icon_for_q(q):
if q == None:
return "󰴽"
elif q > 90:
return "󰣺"
elif q > 70:
return "󰣸"
elif q > 50:
return "󰣶"
elif q > 30:
return "󰣴"
elif q > 10:
return "󰣾"
persistent_fonts = ["nf", "term"]
nf_mapped = "nf"
codepoint_map = {
0x0590: [0x05ff, "hebrew"],
0x2e3a: [0x2e3b, "chinese"],
@ -128,6 +161,29 @@ codepoint_map = {
0xac00: [0xd7af, "korean"],
0xd7b0: [0xd7ff, "korean"],
0x0900: [0x097f, "combined"], # Devanagari
0xe5fa: [0xe6b7, nf_mapped], # Seti-UI + Custom
0xe700: [0xe8ef, nf_mapped], # Devicons
0xed00: [0xf2ff, nf_mapped], # Font Awesome
0xe200: [0xe2a9, nf_mapped], # Font Awesome Extension
0xf0001: [0xf1af0, nf_mapped], # Material Design Icons
0xe300: [0xe3e3, nf_mapped], # Weather
0xf400: [0xf533, nf_mapped], # Octicons
0x2665: [0x2665, nf_mapped], # Octicons
0x26a1: [0x26a1, nf_mapped], # Octicons
0xe0a0: [0xe0a2, nf_mapped], # Powerline Symbols
0xe0b0: [0xe0b3, nf_mapped], # Powerline Symbols
0xe0a3: [0xe0a3, nf_mapped], # Powerline Extra Symbols
0xe0b4: [0xe0c8, nf_mapped], # Powerline Extra Symbols
0xe0ca: [0xe0ca, nf_mapped], # Powerline Extra Symbols
0xe0cc: [0xe0d7, nf_mapped], # Powerline Extra Symbols
0x23fb: [0x23fe, nf_mapped], # IEC Power Symbols
0x2b58: [0x2b58, nf_mapped], # IEC Power Symbols
0xf300: [0xf381, nf_mapped], # Font logos
0xe000: [0xe00a, nf_mapped], # Pomicons
0xea60: [0xec1e, nf_mapped], # Codicons
0x276c: [0x2771, nf_mapped], # Heavy Angle Brackets
0x2500: [0x259f, nf_mapped], # Box Drawing
0xee00: [0xee0b, nf_mapped], # Progress
}
emoji_lookup = [

View File

@ -80,13 +80,13 @@ MDNavigationLayout:
on_release: root.ids.screen_manager.app.map_action(self)
OneLineIconListItem:
text: "Overview"
on_release: root.ids.screen_manager.app.overview_action(self)
# OneLineIconListItem:
# text: "Overview"
# on_release: root.ids.screen_manager.app.overview_action(self)
IconLeftWidget:
icon: "view-dashboard-outline"
on_release: root.ids.screen_manager.app.overview_action(self)
# IconLeftWidget:
# icon: "view-dashboard-outline"
# on_release: root.ids.screen_manager.app.overview_action(self)
OneLineIconListItem:
@ -1287,7 +1287,7 @@ layout_settings_screen = """
MDLabel:
id: scaling_info
markup: True
text: "You can scale the entire Sideband UI by specifying a scaling factor in the field below. After setting it, restart sideband for the scaling to take effect.\\n\\nSet to 0.0 to disable scaling adjustments."
text: "You can scale the entire Sideband UI by specifying a scaling factor in the field below. After setting it, restart sideband for the scaling to take effect.\\n\\nSet to 0.0 to disable scaling adjustments.\\n\\n[b]Please note![/b] On some devices, the default scaling factor will be higher than 1.0, and setting a smaller value will result in miniscule UI elements."
size_hint_y: None
text_size: self.width, None
height: self.texture_size[1]
@ -1494,6 +1494,21 @@ MDScreen:
pos_hint: {"center_y": 0.3}
active: False
MDBoxLayout:
orientation: "horizontal"
size_hint_y: None
padding: [0,0,dp(24),dp(0)]
height: dp(48)
MDLabel:
text: "Classic message colors"
font_style: "H6"
MDSwitch:
id: settings_classic_message_colors
pos_hint: {"center_y": 0.3}
active: False
MDBoxLayout:
orientation: "horizontal"
size_hint_y: None
@ -1568,7 +1583,7 @@ MDScreen:
height: dp(48)
MDLabel:
text: "Announce Automatically"
text: "Announce automatically"
font_style: "H6"
MDSwitch:
@ -1583,7 +1598,7 @@ MDScreen:
height: dp(48)
MDLabel:
text: "Try propagation on direct delivery failure"
text: "Try propagation automatically"
font_style: "H6"
MDSwitch:
@ -1624,6 +1639,22 @@ MDScreen:
disabled: False
active: False
MDBoxLayout:
orientation: "horizontal"
size_hint_y: None
padding: [0,0,dp(24),dp(0)]
height: dp(48)
MDLabel:
text: "Only render markup from trusted"
font_style: "H6"
MDSwitch:
id: settings_trusted_markup_only
pos_hint: {"center_y": 0.3}
disabled: False
active: False
MDBoxLayout:
orientation: "horizontal"
size_hint_y: None
@ -1752,21 +1783,21 @@ MDScreen:
disabled: False
active: False
MDBoxLayout:
orientation: "horizontal"
size_hint_y: None
padding: [0,0,dp(24),dp(0)]
height: dp(48)
# MDBoxLayout:
# orientation: "horizontal"
# size_hint_y: None
# padding: [0,0,dp(24),dp(0)]
# height: dp(48)
MDLabel:
text: "Use Home Node as Broadcast Repeater"
font_style: "H6"
# MDLabel:
# text: "Use Home Node as Broadcast Repeater"
# font_style: "H6"
MDSwitch:
id: settings_home_node_as_broadcast_repeater
pos_hint: {"center_y": 0.3}
active: False
disabled: True
# MDSwitch:
# id: settings_home_node_as_broadcast_repeater
# pos_hint: {"center_y": 0.3}
# active: False
# disabled: True
MDBoxLayout:
orientation: "horizontal"

View File

@ -35,11 +35,13 @@ if RNS.vendor.platformutils.get_platform() == "android":
from sideband.sense import Telemeter, Commands
from ui.helpers import ts_format, file_ts_format, mdc
from ui.helpers import color_playing, color_received, color_delivered, color_propagated, color_paper, color_failed, color_unknown, intensity_msgs_dark, intensity_msgs_light, intensity_play_dark, intensity_play_light
from ui.helpers import color_received_alt, color_received_alt_light, color_delivered_alt, color_propagated_alt, color_paper_alt, color_failed_alt, color_unknown_alt, color_playing_alt, intensity_msgs_dark_alt, intensity_msgs_light_alt, intensity_delivered_alt_dark
else:
import sbapp.plyer as plyer
from sbapp.sideband.sense import Telemeter, Commands
from .helpers import ts_format, file_ts_format, mdc
from .helpers import color_playing, color_received, color_delivered, color_propagated, color_paper, color_failed, color_unknown, intensity_msgs_dark, intensity_msgs_light, intensity_play_dark, intensity_play_light
from .helpers import color_received_alt, color_received_alt_light, color_delivered_alt, color_propagated_alt, color_paper_alt, color_failed_alt, color_unknown_alt, color_playing_alt, intensity_msgs_dark_alt, intensity_msgs_light_alt, intensity_delivered_alt_dark
if RNS.vendor.platformutils.is_darwin():
from PIL import Image as PilImage
@ -203,6 +205,23 @@ class Messages():
self.ids.message_text.input_type = "text"
self.ids.message_text.keyboard_suggestions = True
if not self.app.sideband.config["classic_message_colors"]:
c_delivered = color_delivered_alt
c_received = color_received_alt
c_propagated = color_propagated_alt
c_playing = color_playing_alt
c_paper = color_paper_alt
c_unknown = color_unknown_alt
c_failed = color_failed_alt
else:
c_delivered = color_delivered
c_received = color_received
c_propagated = color_propagated
c_playing = color_playing
c_paper = color_paper
c_unknown = color_unknown
c_failed = color_failed
for new_message in self.app.sideband.list_messages(self.context_dest, after=self.latest_message_timestamp,limit=limit):
self.new_messages.append(new_message)
@ -241,12 +260,24 @@ class Messages():
if (len(self.added_item_hashes) < self.db_message_count) and not self.load_more_button in self.list.children:
self.list.add_widget(self.load_more_button, len(self.list.children))
if self.app.sideband.config["dark_ui"]:
intensity_msgs = intensity_msgs_dark
intensity_play = intensity_play_dark
if self.app.sideband.config["classic_message_colors"]:
if self.app.sideband.config["dark_ui"]:
intensity_msgs = intensity_msgs_dark
intensity_play = intensity_play_dark
intensity_delivered = intensity_msgs
else:
intensity_msgs = intensity_msgs_light
intensity_play = intensity_play_light
intensity_delivered = intensity_msgs
else:
intensity_msgs = intensity_msgs_light
intensity_play = intensity_play_light
if self.app.sideband.config["dark_ui"]:
intensity_msgs = intensity_msgs_dark_alt
intensity_play = intensity_play_dark
intensity_delivered = intensity_delivered_alt_dark
else:
intensity_msgs = intensity_msgs_light_alt
intensity_play = intensity_play_light
intensity_delivered = intensity_msgs
for w in self.widgets:
m = w.m
@ -271,7 +302,7 @@ class Messages():
delivery_syms = multilingual_markup(delivery_syms.encode("utf-8")).decode("utf-8")
if msg["state"] == LXMF.LXMessage.OUTBOUND or msg["state"] == LXMF.LXMessage.SENDING or msg["state"] == LXMF.LXMessage.SENT:
w.md_bg_color = msg_color = mdc(color_unknown, intensity_msgs)
w.md_bg_color = msg_color = mdc(c_unknown, intensity_msgs)
txstr = time.strftime(ts_format, time.localtime(msg["sent"]))
titlestr = ""
prgstr = ""
@ -305,7 +336,7 @@ class Messages():
if msg["state"] == LXMF.LXMessage.DELIVERED:
w.md_bg_color = msg_color = mdc(color_delivered, intensity_msgs)
w.md_bg_color = msg_color = mdc(c_delivered, intensity_delivered)
txstr = time.strftime(ts_format, time.localtime(msg["sent"]))
titlestr = ""
if msg["title"]:
@ -317,7 +348,7 @@ class Messages():
m["state"] = msg["state"]
if msg["method"] == LXMF.LXMessage.PAPER:
w.md_bg_color = msg_color = mdc(color_paper, intensity_msgs)
w.md_bg_color = msg_color = mdc(c_paper, intensity_msgs)
txstr = time.strftime(ts_format, time.localtime(msg["sent"]))
titlestr = ""
if msg["title"]:
@ -326,7 +357,7 @@ class Messages():
m["state"] = msg["state"]
if msg["method"] == LXMF.LXMessage.PROPAGATED and msg["state"] == LXMF.LXMessage.SENT:
w.md_bg_color = msg_color = mdc(color_propagated, intensity_msgs)
w.md_bg_color = msg_color = mdc(c_propagated, intensity_msgs)
txstr = time.strftime(ts_format, time.localtime(msg["sent"]))
titlestr = ""
if msg["title"]:
@ -338,7 +369,7 @@ class Messages():
m["state"] = msg["state"]
if msg["state"] == LXMF.LXMessage.FAILED:
w.md_bg_color = msg_color = mdc(color_failed, intensity_msgs)
w.md_bg_color = msg_color = mdc(c_failed, intensity_msgs)
txstr = time.strftime(ts_format, time.localtime(msg["sent"]))
titlestr = ""
if msg["title"]:
@ -361,14 +392,49 @@ class Messages():
wid.height, wid.size_hint_y, wid.opacity, wid.disabled = 0, None, 0, True
def update_widget(self):
if self.app.sideband.config["dark_ui"]:
intensity_msgs = intensity_msgs_dark
intensity_play = intensity_play_dark
mt_color = [1.0, 1.0, 1.0, 0.8]
if self.app.sideband.config["classic_message_colors"]:
if self.app.sideband.config["dark_ui"]:
intensity_msgs = intensity_msgs_dark
intensity_play = intensity_play_dark
intensity_delivered = intensity_msgs
mt_color = [1.0, 1.0, 1.0, 0.8]
else:
intensity_msgs = intensity_msgs_light
intensity_play = intensity_play_light
intensity_delivered = intensity_msgs
mt_color = [1.0, 1.0, 1.0, 0.95]
else:
intensity_msgs = intensity_msgs_light
intensity_play = intensity_play_light
mt_color = [1.0, 1.0, 1.0, 0.95]
if self.app.sideband.config["dark_ui"]:
intensity_msgs = intensity_msgs_dark_alt
intensity_play = intensity_play_dark
intensity_delivered = intensity_delivered_alt_dark
mt_color = [1.0, 1.0, 1.0, 0.8]
else:
intensity_msgs = intensity_msgs_light_alt
intensity_play = intensity_play_light
intensity_delivered = intensity_msgs
mt_color = [1.0, 1.0, 1.0, 0.95]
if not self.app.sideband.config["classic_message_colors"]:
if self.app.sideband.config["dark_ui"]:
c_received = color_received_alt
else:
c_received = color_received_alt_light
c_delivered = color_delivered_alt
c_propagated = color_propagated_alt
c_playing = color_playing_alt
c_paper = color_paper_alt
c_unknown = color_unknown_alt
c_failed = color_failed_alt
else:
c_delivered = color_delivered
c_received = color_received
c_propagated = color_propagated
c_playing = color_playing
c_paper = color_paper
c_unknown = color_unknown
c_failed = color_failed
self.ids.message_text.font_name = self.app.input_font
@ -378,7 +444,7 @@ class Messages():
for m in self.new_messages:
if not m["hash"] in self.added_item_hashes:
try:
if not self.is_trusted:
if self.app.sideband.config["trusted_markup_only"] and not self.is_trusted:
message_input = str( escape_markup(m["content"].decode("utf-8")) ).encode("utf-8")
else:
message_input = m["content"]
@ -524,31 +590,31 @@ class Messages():
if m["source"] == self.app.sideband.lxmf_destination.hash:
if m["state"] == LXMF.LXMessage.DELIVERED:
msg_color = mdc(color_delivered, intensity_msgs)
msg_color = mdc(c_delivered, intensity_delivered)
heading_str = titlestr+"[b]Sent[/b] "+txstr+delivery_syms+"\n[b]State[/b] Delivered"
elif m["method"] == LXMF.LXMessage.PROPAGATED and m["state"] == LXMF.LXMessage.SENT:
msg_color = mdc(color_propagated, intensity_msgs)
msg_color = mdc(c_propagated, intensity_msgs)
heading_str = titlestr+"[b]Sent[/b] "+txstr+delivery_syms+"\n[b]State[/b] On Propagation Net"
elif m["method"] == LXMF.LXMessage.PAPER:
msg_color = mdc(color_paper, intensity_msgs)
msg_color = mdc(c_paper, intensity_msgs)
heading_str = titlestr+"[b]Created[/b] "+txstr+"\n[b]State[/b] Paper Message"
elif m["state"] == LXMF.LXMessage.FAILED:
msg_color = mdc(color_failed, intensity_msgs)
msg_color = mdc(c_failed, intensity_msgs)
heading_str = titlestr+"[b]Sent[/b] "+txstr+"\n[b]State[/b] Failed"
elif m["state"] == LXMF.LXMessage.OUTBOUND or m["state"] == LXMF.LXMessage.SENDING:
msg_color = mdc(color_unknown, intensity_msgs)
msg_color = mdc(c_unknown, intensity_msgs)
heading_str = titlestr+"[b]Sent[/b] "+txstr+"\n[b]State[/b] Sending "
else:
msg_color = mdc(color_unknown, intensity_msgs)
msg_color = mdc(c_unknown, intensity_msgs)
heading_str = titlestr+"[b]Sent[/b] "+txstr+"\n[b]State[/b] Unknown"
else:
msg_color = mdc(color_received, intensity_msgs)
msg_color = mdc(c_received, intensity_msgs)
heading_str = titlestr
if phy_stats_str != "" and self.app.sideband.config["advanced_stats"]:
heading_str += phy_stats_str+"\n"
@ -598,9 +664,9 @@ class Messages():
self.app.play_audio_field(sender.audio_field)
stored_color = sender.md_bg_color
if sender.lsource == self.app.sideband.lxmf_destination.hash:
sender.md_bg_color = mdc(color_delivered, intensity_play)
sender.md_bg_color = mdc(c_delivered, intensity_play)
else:
sender.md_bg_color = mdc(color_received, intensity_play)
sender.md_bg_color = mdc(c_received, intensity_play)
def cb(dt):
sender.md_bg_color = stored_color
@ -653,6 +719,19 @@ class Messages():
item.ids.content_text.owner = item
item.ids.content_text.bind(texture_size=check_textures)
def cbf(w):
def x(dt):
if w.texture_size[0] == 0 and w.texture_size[1] == 0:
w.markup = False
escaped_content = escape_markup(w.text)
def deferred(dt):
w.text = "[i]This message could not be rendered correctly, likely due to an error in its markup. Falling back to plain-text rendering.[/i]\n\n"+escaped_content
w.markup = True
Clock.schedule_once(deferred, 0.1)
return x
Clock.schedule_once(cbf(item.ids.content_text), 0.25)
if not RNS.vendor.platformutils.is_android():
item.radius = dp(5)

View File

@ -148,6 +148,15 @@ class ObjectDetails():
else:
self.from_objects = False
if self.viewing_self:
self.screen.ids.track_button.disabled = True
else:
self.screen.ids.track_button.disabled = False
if self.app.sideband.is_tracking(source_dest):
self.screen.ids.track_button.text = "Stop Live Tracking"
else:
self.screen.ids.track_button.text = "Start Live Tracking"
self.coords = None
self.telemetry_list.data = []
pds = multilingual_markup(escape_markup(str(self.app.sideband.peer_display_name(source_dest))).encode("utf-8")).decode("utf-8")
@ -218,6 +227,15 @@ class ObjectDetails():
self.clear_widget()
self.update()
def live_tracking(self, sender):
if not self.viewing_self:
if not self.app.sideband.is_tracking(self.object_hash):
self.app.sideband.start_tracking(self.object_hash, interval=59, duration=7*24*60*60)
self.screen.ids.track_button.text = "Stop Live Tracking"
else:
self.app.sideband.stop_tracking(self.object_hash)
self.screen.ids.track_button.text = "Start Live Tracking"
def send_update(self):
if not self.viewing_self:
result = self.app.sideband.send_latest_telemetry(to_addr=self.object_hash)
@ -643,10 +661,9 @@ class RVDetails(MDRecycleView):
alt_str = RNS.prettydistance(alt)
formatted_values = f"Coordinates [b]{fcoords}[/b], altitude [b]{alt_str}[/b]"
if speed != None:
if speed > 0.02:
if speed > 0.1:
speed_formatted_values = f"Speed [b]{speed} Km/h[/b], heading [b]{heading}°[/b]"
else:
# speed_formatted_values = f"Speed [b]0 Km/h[/b]"
speed_formatted_values = f"Object is [b]stationary[/b]"
else:
speed_formatted_values = None
@ -972,22 +989,22 @@ MDScreen:
on_release: root.delegate.request_update()
disabled: False
# MDBoxLayout:
# orientation: "horizontal"
# spacing: dp(16)
# size_hint_y: None
# height: self.minimum_height
# padding: [dp(24), dp(16), dp(24), dp(24)]
MDBoxLayout:
orientation: "horizontal"
spacing: dp(16)
size_hint_y: None
height: self.minimum_height
padding: [dp(24), dp(0), dp(24), dp(24)]
# MDRectangleFlatIconButton:
# id: delete_button
# icon: "trash-can-outline"
# text: "Delete All Telemetry"
# padding: [dp(0), dp(14), dp(0), dp(14)]
# icon_size: dp(24)
# font_size: dp(16)
# size_hint: [1.0, None]
# on_release: root.delegate.copy_telemetry(self)
# disabled: False
MDRectangleFlatIconButton:
id: track_button
icon: "crosshairs-gps"
text: "Start Live Tracking"
padding: [dp(0), dp(14), dp(0), dp(14)]
icon_size: dp(24)
font_size: dp(16)
size_hint: [1.0, None]
on_release: root.delegate.live_tracking(self)
disabled: False
"""

View File

@ -9,7 +9,10 @@ from kivy.utils import escape_markup
from kivymd.uix.recycleview import MDRecycleView
from kivymd.uix.list import OneLineIconListItem
from kivymd.uix.pickers import MDColorPicker
from kivymd.uix.button import MDRectangleFlatButton
from kivymd.uix.dialog import MDDialog
from kivymd.icon_definitions import md_icons
from kivymd.toast import toast
from kivy.properties import StringProperty, BooleanProperty
from kivy.effects.scroll import ScrollEffect
from kivy.clock import Clock
@ -29,6 +32,7 @@ class Utilities():
self.screen = None
self.rnstatus_screen = None
self.rnstatus_instance = None
self.logviewer_screen = None
if not self.app.root.ids.screen_manager.has_screen("utilities_screen"):
self.screen = Builder.load_string(layout_utilities_screen)
@ -37,13 +41,40 @@ class Utilities():
self.app.root.ids.screen_manager.add_widget(self.screen)
self.screen.ids.telemetry_scrollview.effect_cls = ScrollEffect
info = "\nYou can use various RNS utilities from Sideband. "
info += ""
info = "This section contains various utilities and diagnostics tools, "
info += "that can be helpful while using Sideband and Reticulum."
if self.app.theme_cls.theme_style == "Dark":
info = "[color=#"+self.app.dark_theme_text_color+"]"+info+"[/color]"
self.screen.ids.telemetry_info.text = info
self.screen.ids.utilities_info.text = info
### RNode Flasher
######################################
def flasher_action(self, sender=None):
yes_button = MDRectangleFlatButton(text="Launch",font_size=dp(18), theme_text_color="Custom", line_color=self.app.color_accept, text_color=self.app.color_accept)
no_button = MDRectangleFlatButton(text="Back",font_size=dp(18))
dialog = MDDialog(
title="RNode Flasher",
text="You can use the included web-based RNode flasher, by starting Sideband's built-in repository server, and accessing the RNode Flasher page.",
buttons=[ no_button, yes_button ],
# elevation=0,
)
def dl_yes(s):
dialog.dismiss()
self.app.sideband.start_webshare()
def cb(dt):
self.app.repository_action()
Clock.schedule_once(cb, 0.6)
def dl_no(s):
dialog.dismiss()
yes_button.bind(on_release=dl_yes)
no_button.bind(on_release=dl_no)
dialog.open()
### rnstatus screen
@ -72,24 +103,91 @@ class Utilities():
import io
from contextlib import redirect_stdout
output_marker = "===begin rnstatus output==="
output = "None"
with io.StringIO() as buffer, redirect_stdout(buffer):
print(output_marker, end="")
self.rnstatus_instance.main(rns_instance=RNS.Reticulum.get_instance())
output = buffer.getvalue()
remainder = output[:output.find(output_marker)]
output = output[output.find(output_marker)+len(output_marker):]
print(remainder, end="")
with RNS.logging_lock:
self.rnstatus_instance.main(rns_instance=RNS.Reticulum.get_instance())
output = buffer.getvalue()
def cb(dt):
self.rnstatus_screen.ids.rnstatus_output.text = f"[font=RobotoMono-Regular]{output}[/font]"
self.rnstatus_screen.ids.rnstatus_output.text = f"[font=RobotoMono-Regular][size={int(dp(12))}]{output}[/size][/font]"
Clock.schedule_once(cb, 0.2)
if self.app.root.ids.screen_manager.current == "rnstatus_screen":
Clock.schedule_once(self.update_rnstatus, 1)
### Advanced Configuration screen
######################################
def advanced_action(self, sender=None):
if not self.app.root.ids.screen_manager.has_screen("advanced_screen"):
self.advanced_screen = Builder.load_string(layout_advanced_screen)
self.advanced_screen.app = self.app
self.advanced_screen.delegate = self
self.app.root.ids.screen_manager.add_widget(self.advanced_screen)
self.app.root.ids.screen_manager.transition.direction = "left"
self.app.root.ids.screen_manager.current = "advanced_screen"
self.app.sideband.setstate("app.displaying", self.app.root.ids.screen_manager.current)
self.update_advanced()
def update_advanced(self, sender=None):
if RNS.vendor.platformutils.is_android():
ct = self.app.sideband.config["config_template"]
self.advanced_screen.ids.config_template.text = f"[font=RobotoMono-Regular][size={int(dp(12))}]{ct}[/size][/font]"
else:
self.advanced_screen.ids.config_template.text = f"[font=RobotoMono-Regular][size={int(dp(12))}]On this platform, Reticulum configuration is managed by the system. You can change the configuration by editing the file located at:\n\n{self.app.sideband.reticulum.configpath}[/size][/font]"
def copy_config(self, sender=None):
if RNS.vendor.platformutils.is_android():
Clipboard.copy(self.app.sideband.config_template)
def paste_config(self, sender=None):
if RNS.vendor.platformutils.is_android():
self.app.sideband.config_template = Clipboard.paste()
self.app.sideband.config["config_template"] = self.app.sideband.config_template
self.app.sideband.save_configuration()
self.update_advanced()
### Log viewer screen
######################################
def logviewer_action(self, sender=None):
if not self.app.root.ids.screen_manager.has_screen("logviewer_screen"):
self.logviewer_screen = Builder.load_string(layout_logviewer_screen)
self.logviewer_screen.app = self.app
self.logviewer_screen.delegate = self
self.app.root.ids.screen_manager.add_widget(self.logviewer_screen)
self.app.root.ids.screen_manager.transition.direction = "left"
self.app.root.ids.screen_manager.current = "logviewer_screen"
self.app.sideband.setstate("app.displaying", self.app.root.ids.screen_manager.current)
self.update_logviewer()
def update_logviewer(self, sender=None):
threading.Thread(target=self.update_logviewer_job, daemon=True).start()
def update_logviewer_job(self, sender=None):
try:
output = self.app.sideband.get_log()
except Exception as e:
output = f"An error occurred while retrieving log entries:\n{e}"
self.logviewer_screen.log_contents = output
def cb(dt):
self.logviewer_screen.ids.logviewer_output.text = f"[font=RobotoMono-Regular][size={int(dp(12))}]{output}[/size][/font]"
Clock.schedule_once(cb, 0.2)
if self.app.root.ids.screen_manager.current == "logviewer_screen":
Clock.schedule_once(self.update_logviewer, 1)
def logviewer_copy(self, sender=None):
Clipboard.copy(self.logviewer_screen.log_contents)
if True or RNS.vendor.platformutils.is_android():
toast("Log copied to clipboard")
layout_utilities_screen = """
MDScreen:
@ -116,14 +214,14 @@ MDScreen:
orientation: "vertical"
size_hint_y: None
height: self.minimum_height
padding: [dp(28), dp(48), dp(28), dp(16)]
padding: [dp(28), dp(32), dp(28), dp(16)]
# MDLabel:
# text: "Utilities & Tools"
# font_style: "H6"
MDLabel:
text: "Utilities & Tools"
font_style: "H6"
MDLabel:
id: telemetry_info
id: utilities_info
markup: True
text: ""
size_hint_y: None
@ -156,8 +254,30 @@ MDScreen:
icon_size: dp(24)
font_size: dp(16)
size_hint: [1.0, None]
on_release: root.delegate.rnstatus_action(self)
disabled: True
on_release: root.delegate.logviewer_action(self)
disabled: False
MDRectangleFlatIconButton:
id: flasher_button
icon: "radio-handheld"
text: "RNode Flasher"
padding: [dp(0), dp(14), dp(0), dp(14)]
icon_size: dp(24)
font_size: dp(16)
size_hint: [1.0, None]
on_release: root.delegate.flasher_action(self)
disabled: False
MDRectangleFlatIconButton:
id: advanced_button
icon: "network-pos"
text: "Advanced RNS Configuration"
padding: [dp(0), dp(14), dp(0), dp(14)]
icon_size: dp(24)
font_size: dp(16)
size_hint: [1.0, None]
on_release: root.delegate.advanced_action(self)
disabled: False
"""
@ -177,12 +297,12 @@ MDScreen:
[['menu', lambda x: root.app.nav_drawer.set_state("open")]]
right_action_items:
[
['refresh', lambda x: root.delegate.update_rnstatus()],
# ['refresh', lambda x: root.delegate.update_rnstatus()],
['close', lambda x: root.app.close_sub_utilities_action(self)],
]
MDScrollView:
id: sensors_scrollview
id: rnstatus_scrollview
size_hint_x: 1
size_hint_y: None
size: [root.width, root.height-root.ids.top_bar.height]
@ -202,4 +322,120 @@ MDScreen:
size_hint_y: None
text_size: self.width, None
height: self.texture_size[1]
"""
"""
layout_logviewer_screen = """
MDScreen:
name: "logviewer_screen"
BoxLayout:
orientation: "vertical"
MDTopAppBar:
id: top_bar
title: "Log Viewer"
anchor_title: "left"
elevation: 0
left_action_items:
[['menu', lambda x: root.app.nav_drawer.set_state("open")]]
right_action_items:
[
['content-copy', lambda x: root.delegate.logviewer_copy()],
['close', lambda x: root.app.close_sub_utilities_action(self)],
]
MDScrollView:
id: logviewer_scrollview
size_hint_x: 1
size_hint_y: None
size: [root.width, root.height-root.ids.top_bar.height]
do_scroll_x: False
do_scroll_y: True
MDGridLayout:
cols: 1
padding: [dp(28), dp(14), dp(28), dp(28)]
size_hint_y: None
height: self.minimum_height
MDLabel:
id: logviewer_output
markup: True
text: ""
size_hint_y: None
text_size: self.width, None
height: self.texture_size[1]
"""
layout_advanced_screen = """
MDScreen:
name: "advanced_screen"
BoxLayout:
orientation: "vertical"
MDTopAppBar:
id: top_bar
title: "RNS Configuration"
anchor_title: "left"
elevation: 0
left_action_items:
[['menu', lambda x: root.app.nav_drawer.set_state("open")]]
right_action_items:
[
# ['refresh', lambda x: root.delegate.update_rnstatus()],
['close', lambda x: root.app.close_sub_utilities_action(self)],
]
MDScrollView:
id: advanced_scrollview
size_hint_x: 1
size_hint_y: None
size: [root.width, root.height-root.ids.top_bar.height]
do_scroll_x: False
do_scroll_y: True
MDGridLayout:
cols: 1
padding: [dp(28), dp(14), dp(28), dp(28)]
size_hint_y: None
height: self.minimum_height
MDBoxLayout:
orientation: "horizontal"
spacing: dp(24)
size_hint_y: None
height: self.minimum_height
padding: [dp(0), dp(14), dp(0), dp(24)]
MDRectangleFlatIconButton:
id: telemetry_button
icon: "content-copy"
text: "Copy Configuration"
padding: [dp(0), dp(14), dp(0), dp(14)]
icon_size: dp(24)
font_size: dp(16)
size_hint: [1.0, None]
on_release: root.delegate.copy_config(self)
disabled: False
MDRectangleFlatIconButton:
id: coordinates_button
icon: "download"
text: "Paste Configuration"
padding: [dp(0), dp(14), dp(0), dp(14)]
icon_size: dp(24)
font_size: dp(16)
size_hint: [1.0, None]
on_release: root.delegate.paste_config(self)
disabled: False
MDLabel:
id: config_template
markup: True
text: ""
size_hint_y: None
text_size: self.width, None
height: self.texture_size[1]
"""

View File

@ -47,6 +47,20 @@ def glob_paths(pattern):
return out_files
def glob_share():
out_files = []
src_path = os.path.join(os.path.dirname(__file__), "sbapp/share")
print(src_path)
for root, dirs, files in os.walk(src_path):
for file in files:
filepath = os.path.join(str(Path(*Path(root).parts[1:])), file)
if not "mirrors/unsigned.io" in str(filepath):
out_files.append(filepath.split(f"sbapp{os.sep}")[1])
return out_files
packages = setuptools.find_packages(
exclude=[
"sbapp.plyer.platforms.android",
@ -63,6 +77,7 @@ package_data = {
"kivymd/images/*",
"kivymd/*",
"mapview/icons/*",
*glob_share(),
*glob_paths(".kv")
]
}
@ -99,7 +114,7 @@ setuptools.setup(
]
},
install_requires=[
"rns>=0.8.7",
"rns>=0.8.8",
"lxmf>=0.5.8",
"kivy>=2.3.0",
"pillow>=10.2.0",