Compare commits

...

13 commits

Author SHA1 Message Date
Mark Qvist
cba08b6999 Cleanup 2026-04-27 16:20:08 +02:00
Mark Qvist
9f0d776e3f Added basic Raspberry Pi telemetry plugin example 2026-04-27 16:19:55 +02:00
Mark Qvist
5705e24dde Added basic server telemetry plugin example 2026-04-27 16:19:45 +02:00
Mark Qvist
39f00554a8 Improved announce logging handling 2026-04-23 01:19:55 +02:00
Mark Qvist
fcc0341fe9 Updated versions 2026-04-22 13:42:08 +02:00
Mark Qvist
04908eb45b Versions 2026-04-21 19:17:14 +02:00
Mark Qvist
73b9bd032f Updated versions 2026-04-21 10:51:25 +02:00
Mark Qvist
6f8f4351f6 Fixed message loading performance 2026-04-21 10:24:07 +02:00
Mark Qvist
cd323c9ca8 Background data persist on app pause on Android 2026-04-19 01:31:04 +02:00
Mark Qvist
e798bac4c7 Updated versions 2026-04-18 16:37:33 +02:00
Mark Qvist
2529a70339 Updated makefile 2026-04-13 19:15:33 +02:00
Mark Qvist
75373f3aa0 Fixed button navigation on keys screen 2026-04-13 18:28:15 +02:00
Mark Qvist
73b0f6d4b2 Updated readme 2026-04-13 16:55:25 +02:00
9 changed files with 339 additions and 71 deletions

View file

@ -79,6 +79,7 @@ prepare_win_pkg: clean build_spkg
rm build/winpkg/sbapp/buildozer.spec
cp winbuild.bat build/
mv build/winpkg build/sideband_sources
chmod -R a+rw build/sideband_sources
cd build; zip -r winbuild.zip sideband_sources winbuild.bat
mv build/winbuild.zip dist/winbuild.zip

View file

@ -30,7 +30,7 @@ Sideband provides many useful and interesting functions, such as:
- Remote **telemetry querying**, with strong, secure and cryptographically robust authentication and control.
- **Plugin system** that allows you to easily **create your own commands**, services and telemetry sources.
Sideband is fully compatible with other LXMF clients, such as [MeshChat](https://github.com/liamcottle/reticulum-meshchat), and [Nomad Network](https://github.com/markqvist/nomadnet). The Nomad Network client also allows you to easily host Propagation Nodes for your LXMF network, and more.
Sideband is fully compatible with other LXMF clients, such as [MeshChatX](https://git.quad4.io/RNS-Things/MeshChatX), and [Nomad Network](https://github.com/markqvist/nomadnet). The Nomad Network client also allows you to easily host Propagation Nodes for your LXMF network, and more.
# Installation

View file

@ -0,0 +1,152 @@
# This is a basic Raspberry Pi telemetry plugin
# example that you can build upon to
# implement your own telemetry plugins.
import os
import RNS
import time
import psutil
import shutil
from threading import Thread
import urllib.request, json
class RasPiTelemetryPlugin(SidebandTelemetryPlugin):
plugin_name = "raspi_telemetry"
def start(self):
# Do any initialisation work here
RNS.log("Raspberry Pi Telemetry plugin starting...")
self.initialise_values()
self.power_stats = False
self.storage_stats = True
self.target_disk = {"blkid": "mmcblk0p2", "label": "SD Card"}
self.should_run = True
self.update_thread = Thread(target=self.update_job, daemon=True)
self.update_thread.start()
# And finally call start on superclass
super().start()
def stop(self):
# Do any teardown work here
self.should_run = False
# And finally call stop on superclass
super().stop()
def initialise_values(self):
self.battery_percent = None
self.power_production = None
self.power_consumption = None
self.battery_charging = None
self.battery_temperature = None
self.uptime = None
def update_job(self):
while self.should_run:
try:
# Update uptime
self.uptime = RNS.prettytime(time.time()-psutil.boot_time())
# Update power values if enabled
if self.power_stats:
with urllib.request.urlopen("http://some_host/status.json") as url:
data = json.loads(url.read().decode())
self.power_production = data["solar_yield"]
self.power_consumption = data["inverter_load"]+data["dc_consumption"]
self.battery_charging = data["battery_current"] >= 0.0
self.battery_charge = data["battery_charge"]
self.battery_temperature = data["battery_temperature"]
except Exception as e:
RNS.log("Error while updating plugin telemetry: "+str(e), RNS.LOG_ERROR)
time.sleep(15)
def update_telemetry(self, telemeter):
if telemeter != None:
if self.power_stats:
# Create power consumption sensor
telemeter.synthesize("power_consumption")
telemeter.sensors["power_consumption"].update_consumer(self.power_consumption, type_label="Power consumption")
# Create power production sensor
telemeter.synthesize("power_production")
telemeter.sensors["power_production"].update_producer(self.power_production, type_label="Solar production", custom_icon="solar-power-variant")
# Create battery sensor
telemeter.synthesize("battery")
telemeter.sensors["battery"].data = {"charge_percent": round(self.battery_charge, 1), "charging": self.battery_charging}
# Create NVM sensor if enabled
if self.storage_stats:
mount_point = None
for partition in psutil.disk_partitions(all=False):
if self.target_disk["blkid"] in partition.device:
mount_point = partition.mountpoint
break
if mount_point:
st = shutil.disk_usage(mount_point)
telemeter.synthesize("nvm")
telemeter.sensors["nvm"].update_entry(capacity=st.total, used=st.used, type_label=self.target_disk["label"])
# Create RAM sensors
ms = psutil.virtual_memory()
telemeter.synthesize("ram")
telemeter.sensors["ram"].update_entry(capacity=ms.total, used=ms.used, type_label="RAM")
# Create CPU sensor
a = psutil.getloadavg()
cps = 0; cpms = 5
for m in range(cpms):
cps += psutil.cpu_percent()/100.0
time.sleep(0.05)
cp = cps/cpms
telemeter.synthesize("processor")
telemeter.sensors["processor"].update_entry(current_load=cp, clock=round(psutil.cpu_freq().current*1e6, 0), load_avgs=[a[0], a[1], a[2]], type_label="CPU")
# Create custom sensor for uptime
telemeter.synthesize("custom")
telemeter.sensors["custom"].update_entry(self.uptime, type_label="Uptime is", custom_icon="timer-refresh-outline")
# Read temperature using built-in sensor
try:
cpu_temp_path = '/sys/class/thermal/thermal_zone0/temp'
with open(cpu_temp_path) as f: temp = int(f.read().strip()) / 1000.0
telemeter.synthesize("temperature")
telemeter.sensors["temperature"].data = {"c": round(temp, 2)}
except Exception as e: RNS.log(f"Getting temperature reading failed: {e}", RNS.LOG_ERROR)
# Network Interfaces
try:
for iface in os.listdir('/sys/class/net'):
carrier = None
speed = None
try:
with open(f'/sys/class/net/{iface}/carrier') as f: carrier = int(f.read().strip()) == 1
except FileNotFoundError: pass
if iface.startswith('eth') or iface.startswith('wlan'):
try:
with open(f'/sys/class/net/{iface}/speed') as f: speed = int(f.read().strip())
except (FileNotFoundError, ValueError): pass
telemeter.synthesize("custom")
speed_str = f" at {RNS.prettyspeed(speed*1e6)}" if speed else ""
sensors_str = f"carrier{speed_str}" if carrier else "has no carrier"
telemeter.sensors["custom"].update_entry(value=f"{sensors_str}", type_label=f"{iface}", custom_icon="lan" if carrier else "lan-disconnect")
except Exception as e: RNS.log(f"Getting network stats failed: {e}", RNS.LOG_DEBUG)
# Finally, tell Sideband what class in this
# file is the actual plugin class.
plugin_class = RasPiTelemetryPlugin

View file

@ -0,0 +1,119 @@
# This is a basic server telemetry plugin
# example that you can build upon to
# implement your own telemetry plugins.
import RNS
import time
import psutil
import shutil
from threading import Thread
import urllib.request, json
class ServerTelemetryPlugin(SidebandTelemetryPlugin):
plugin_name = "server_telemetry"
def start(self):
# Do any initialisation work here
RNS.log("Server Telemetry plugin starting...")
self.initialise_values()
self.power_stats = False
self.storage_stats = True
self.target_disk = {"blkid": "mmcblk0p2", "label": "SD Card"}
self.should_run = True
self.update_thread = Thread(target=self.update_job, daemon=True)
self.update_thread.start()
# And finally call start on superclass
super().start()
def stop(self):
# Do any teardown work here
self.should_run = False
# And finally call stop on superclass
super().stop()
def initialise_values(self):
self.battery_percent = None
self.power_production = None
self.power_consumption = None
self.battery_charging = None
self.battery_temperature = None
self.uptime = None
def update_job(self):
while self.should_run:
try:
# Update uptime
self.uptime = RNS.prettytime(time.time()-psutil.boot_time())
# Update power values if enabled
if self.power_stats:
with urllib.request.urlopen("http://some_host/status.json") as url:
data = json.loads(url.read().decode())
self.power_production = data["solar_yield"]
self.power_consumption = data["inverter_load"]+data["dc_consumption"]
self.battery_charging = data["battery_current"] >= 0.0
self.battery_charge = data["battery_charge"]
self.battery_temperature = data["battery_temperature"]
except Exception as e:
RNS.log("Error while updating plugin telemetry: "+str(e), RNS.LOG_ERROR)
time.sleep(15)
def update_telemetry(self, telemeter):
if telemeter != None:
if self.power_stats:
# Create power consumption sensor
telemeter.synthesize("power_consumption")
telemeter.sensors["power_consumption"].update_consumer(self.power_consumption, type_label="Power consumption")
# Create power production sensor
telemeter.synthesize("power_production")
telemeter.sensors["power_production"].update_producer(self.power_production, type_label="Solar production", custom_icon="solar-power-variant")
# Create battery sensor
telemeter.synthesize("battery")
telemeter.sensors["battery"].data = {"charge_percent": round(self.battery_charge, 1), "charging": self.battery_charging}
# Create NVM sensor if enabled
if self.storage_stats:
mount_point = None
for partition in psutil.disk_partitions(all=False):
if self.target_disk["blkid"] in partition.device:
mount_point = partition.mountpoint
break
if mount_point:
st = shutil.disk_usage(mount_point)
telemeter.synthesize("nvm")
telemeter.sensors["nvm"].update_entry(capacity=st.total, used=st.used, type_label=self.target_disk["label"])
# Create RAM sensors
ms = psutil.virtual_memory()
telemeter.synthesize("ram")
telemeter.sensors["ram"].update_entry(capacity=ms.total, used=ms.used, type_label="RAM")
# Create CPU sensor
a = psutil.getloadavg()
cps = 0; cpms = 5
for m in range(cpms):
cps += psutil.cpu_percent()/100.0
time.sleep(0.05)
cp = cps/cpms
telemeter.synthesize("processor")
telemeter.sensors["processor"].update_entry(current_load=cp, clock=round(psutil.cpu_freq().current*1e6, 0), load_avgs=[a[0], a[1], a[2]], type_label="CPU")
# Create custom sensor for uptime
telemeter.synthesize("custom")
telemeter.sensors["custom"].update_entry(self.uptime, type_label="Uptime is", custom_icon="timer-refresh-outline")
# Finally, tell Sideband what class in this
# file is the actual plugin class.
plugin_class = ServerTelemetryPlugin

View file

@ -1,6 +1,6 @@
__debug_build__ = False
__disable_shaders__ = False
__version__ = "1.9.0"
__version__ = "1.9.2"
__variant__ = ""
import sys
@ -449,6 +449,7 @@ else:
from ui.objectdetails import ObjectDetails
from ui.announces import Announces
from ui.messages import Messages, ts_format, messages_screen_kv
# from ui.messages_recycle import Messages, ts_format, messages_screen_kv
from ui.helpers import ContentNavigationDrawer, DrawerList, IconListItem
from ui.helpers import multilingual_markup, mdc, dark_theme_text_color
from kivymd.toast import toast
@ -483,6 +484,7 @@ else:
from .ui.hardware import Hardware
from .ui.objectdetails import ObjectDetails
from .ui.messages import Messages, ts_format, messages_screen_kv
# from .ui.messages_recycle import Messages, ts_format, messages_screen_kv
from .ui.helpers import ContentNavigationDrawer, DrawerList, IconListItem
from .ui.helpers import multilingual_markup, mdc, dark_theme_text_color
@ -1062,7 +1064,7 @@ class SidebandApp(MDApp):
self.sideband.setstate("app.running", True)
self.sideband.setstate("app.foreground", False)
self.app_state = SidebandApp.PAUSED
self.sideband.should_persist_data()
self.sideband.should_persist_data(background=False)
RNS.log("App paused", RNS.LOG_DEBUG)
return True
@ -2870,7 +2872,7 @@ class SidebandApp(MDApp):
else:
self.messages_view.ids.messages_scrollview.dest_known = False
if self.messages_view.ids.nokeys_text.text == "":
keys_str = "The crytographic keys for the destination address are unknown at this time. You can wait for an announce to arrive, or query the network for the necessary keys."
keys_str = "The cryptographic keys for the destination address are unknown at this time. You can wait for an announce to arrive, or query the network for the necessary keys."
self.messages_view.ids.nokeys_text.text = keys_str
self.widget_hide(self.messages_view.ids.message_input_part, True)
self.widget_hide(self.messages_view.ids.message_ptt, True)

View file

@ -90,10 +90,10 @@ class PropagationNodeDetector():
pass
else:
RNS.log(f"Received malformed propagation node announce from {RNS.prettyhexrep(destination_hash)} with data: {app_data}", RNS.LOG_EXTREME)
RNS.log(f"Received malformed propagation node announce from {RNS.prettyhexrep(destination_hash)}", RNS.LOG_EXTREME)
else:
RNS.log(f"Received malformed propagation node announce from {RNS.prettyhexrep(destination_hash)} with data: {app_data}", RNS.LOG_EXTREME)
RNS.log(f"Received malformed propagation node announce from {RNS.prettyhexrep(destination_hash)}", RNS.LOG_EXTREME)
except Exception as e:
RNS.log("Error while processing received propagation node announce: "+str(e))
@ -371,8 +371,9 @@ class SidebandCore():
self.active_propagation_node = None
self.propagation_detector = PropagationNodeDetector(self)
RNS.Transport.register_announce_handler(self)
RNS.Transport.register_announce_handler(self.propagation_detector)
if self.is_service or self.is_standalone:
RNS.Transport.register_announce_handler(self)
RNS.Transport.register_announce_handler(self.propagation_detector)
self.active_command_plugins = {}
self.active_service_plugins = {}
@ -580,10 +581,8 @@ class SidebandCore():
total += entry.stat(follow_symlinks=False).st_size
return total
def should_persist_data(self):
if self.reticulum != None:
self.reticulum._should_persist_data()
def should_persist_data(self, background=False):
if self.reticulum != None: self.reticulum._should_persist_data(background=background)
self.save_configuration()
def __load_telemetry_collector_excluded(self):
@ -808,8 +807,7 @@ class SidebandCore():
def __save_config(self, no_thread=False):
RNS.log("Saving Sideband configuration...", RNS.LOG_DEBUG)
def save_function():
while self.saving_configuration:
time.sleep(0.15)
while self.saving_configuration: time.sleep(0.15)
try:
self.saving_configuration = True
with open(self.config_path, "wb") as config_file: config_file.write(msgpack.packb(self.config))
@ -828,8 +826,8 @@ class SidebandCore():
if no_thread: save_function()
else: threading.Thread(target=save_function, daemon=True).start()
if self.is_client:
self.setstate("wants.settings_reload", True)
if self.is_client: self.setstate("wants.settings_reload", True)
RNS.log("Sideband configuration saved", RNS.LOG_DEBUG)
def __load_plugins(self):
plugins_path = self.config["command_plugins_path"]
@ -959,10 +957,8 @@ class SidebandCore():
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])
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), RNS.LOG_DEBUG)
self._db_save_announce(dest, app_data, dest_type, link_stats)
self.setstate("app.flags.new_announces", True)
@ -3001,19 +2997,32 @@ class SidebandCore():
with self.db_lock:
db = self.__db_connect()
dbc = db.cursor()
base_condition = "(dest=:context_dest or source=:context_dest)"
if after != None and before == None:
query = "select * from lxm where (dest=:context_dest or source=:context_dest) and rx_ts>:after_ts"
dbc.execute(query, {"context_dest": context_dest, "after_ts": after})
query = f"select * from lxm where {base_condition} and rx_ts>:after_ts ORDER BY rx_ts DESC"
params = {"context_dest": context_dest, "after_ts": after}
if limit is not None:
query += f" LIMIT :limit_val"; params["limit_val"] = int(limit)
dbc.execute(query, params)
elif after == None and before != None:
query = "select * from lxm where (dest=:context_dest or source=:context_dest) and rx_ts<:before_ts"
dbc.execute(query, {"context_dest": context_dest, "before_ts": before})
query = f"select * from lxm where {base_condition} and rx_ts<:before_ts ORDER BY rx_ts DESC"
params = {"context_dest": context_dest, "before_ts": before}
if limit is not None:
query += f" LIMIT :limit_val"; params["limit_val"] = int(limit)
dbc.execute(query, params)
elif after != None and before != None:
query = "select * from lxm where (dest=:context_dest or source=:context_dest) and rx_ts<:before_ts and rx_ts>:after_ts"
dbc.execute(query, {"context_dest": context_dest, "before_ts": before, "after_ts": after})
query = f"select * from lxm where {base_condition} and rx_ts<:before_ts and rx_ts>:after_ts ORDER BY rx_ts DESC"
params = {"context_dest": context_dest, "before_ts": before, "after_ts": after}
if limit is not None:
query += f" LIMIT :limit_val"; params["limit_val"] = int(limit)
dbc.execute(query, params)
else:
query = "select * from lxm where dest=:context_dest or source=:context_dest"
dbc.execute(query, {"context_dest": context_dest})
query = f"select * from lxm where {base_condition} ORDER BY rx_ts DESC"
params = {"context_dest": context_dest}
if limit is not None:
query += f" LIMIT :limit_val"; params["limit_val"] = int(limit)
dbc.execute(query, params)
result = dbc.fetchall()
@ -3036,10 +3045,8 @@ class SidebandCore():
lxm.paper_packed = paper_packed_lxm
extras = None
try:
extras = msgpack.unpackb(entry[11])
except:
pass
try: extras = msgpack.unpackb(entry[11])
except: pass
message = {
"hash": lxm.hash,
@ -3056,8 +3063,9 @@ class SidebandCore():
}
messages.append(message)
if len(messages) > limit:
messages = messages[-limit:]
messages.reverse()
if len(messages) > limit: messages = messages[-limit:]
return messages
def _db_save_lxm(self, lxm, context_dest, originator = False, own_command = False, is_retry = False):
@ -3271,42 +3279,31 @@ class SidebandCore():
def update_telemetry(self):
try:
try:
latest_telemetry = deepcopy(self.latest_telemetry)
except:
latest_telemetry = None
try: latest_telemetry = deepcopy(self.latest_telemetry)
except: latest_telemetry = None
telemetry = self.get_telemetry()
packed_telemetry = self.get_packed_telemetry()
telemetry_changed = False
if telemetry != None and packed_telemetry != None:
if latest_telemetry == None or len(telemetry) != len(latest_telemetry):
telemetry_changed = True
if latest_telemetry == None or len(telemetry) != len(latest_telemetry): telemetry_changed = True
if latest_telemetry != None:
if not telemetry_changed:
for sn in telemetry:
if telemetry_changed:
break
if telemetry_changed: break
if sn != "time":
if sn in latest_telemetry:
if telemetry[sn] != latest_telemetry[sn]:
telemetry_changed = True
else:
telemetry_changed = True
if telemetry[sn] != latest_telemetry[sn]: telemetry_changed = True
else: telemetry_changed = True
if not telemetry_changed:
for sn in latest_telemetry:
if telemetry_changed:
break
if telemetry_changed: break
if sn != "time":
if not sn in telemetry:
telemetry_changed = True
if not sn in telemetry: telemetry_changed = True
if telemetry_changed:
self.telemetry_changes += 1
@ -3315,10 +3312,8 @@ class SidebandCore():
self.setstate("app.flags.last_telemetry", time.time())
if self.is_client:
try:
self.service_set_latest_telemetry(self.latest_telemetry, self.latest_packed_telemetry)
except Exception as e:
RNS.log("Error while sending latest telemetry to service: "+str(e), RNS.LOG_ERROR)
try: self.service_set_latest_telemetry(self.latest_telemetry, self.latest_packed_telemetry)
except Exception as e: RNS.log("Error while sending latest telemetry to service: "+str(e), RNS.LOG_ERROR)
except Exception as e:
import traceback
@ -3408,14 +3403,10 @@ class SidebandCore():
def is_known(self, dest_hash):
try:
source_identity = RNS.Identity.recall(dest_hash)
if source_identity: return True
else: return False
if source_identity:
return True
else:
return False
except Exception as e:
return False
except Exception as e: return False
def request_key(self, dest_hash):
try:

View file

@ -37,6 +37,10 @@ class Keys():
self.keys_screen.ids.keys_info.text = info1
self.keys_screen.ids.backup_info.text = info2
def test_a(self, sender):
RNS.log("TEST ACTION");
RNS.log(self)
RNS.log(self.app)
def _profile_backup_job(self):
import tarfile
@ -160,10 +164,10 @@ MDScreen:
anchor_title: "left"
elevation: 0
left_action_items:
[['menu', lambda x: root.app.nav_drawer.set_state("open")]]
[['menu', lambda x: root.app.app.nav_drawer.set_state("open")]]
right_action_items:
[
['close', lambda x: root.app.close_keys_action(self)],
['close', lambda x: root.app.app.close_keys_action(self)],
]
ScrollView:

View file

@ -271,8 +271,7 @@ class Messages():
else: self.hide_widget(self.ids.message_ptt, True)
c_ts = time.time()
if len(self.new_messages) > 0:
self.update_widget()
if len(self.new_messages) > 0: self.update_widget()
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))

View file

@ -114,8 +114,8 @@ setuptools.setup(
]
},
install_requires=[
"rns>=1.1.5",
"lxmf>=0.9.4",
"rns>=1.2.0",
"lxmf>=0.9.6",
"lxst>=0.4.6",
"kivy>=2.3.0",
"numpy>=2.0.0",