Sideband/sbapp/ui/objectdetails.py
2024-12-08 21:17:41 +01:00

1010 lines
46 KiB
Python

import time
import RNS
from kivy.metrics import dp,sp
from kivy.lang.builder import Builder
from kivy.core.clipboard import Clipboard
from kivy.utils import escape_markup
from kivymd.uix.recycleview import MDRecycleView
from kivymd.uix.list import OneLineIconListItem
from kivy.properties import StringProperty, BooleanProperty
from kivy.effects.scroll import ScrollEffect
from kivy.clock import Clock
from sideband.sense import Telemeter
import threading
import webbrowser
from kivymd.uix.button import MDRectangleFlatButton
from kivymd.uix.dialog import MDDialog
from kivymd.toast import toast
from datetime import datetime
from kivy.utils import escape_markup
if RNS.vendor.platformutils.get_platform() == "android":
from ui.helpers import multilingual_markup
else:
from .helpers import multilingual_markup
if RNS.vendor.platformutils.get_platform() == "android":
from ui.helpers import ts_format
else:
from .helpers import ts_format
class ObjectDetails():
def __init__(self, app, object_hash = None):
self.app = app
self.widget = None
self.object_hash = object_hash
self.lastest_timestamp = 0
self.coords = None
self.raw_telemetry = None
self.from_telemetry = False
self.from_conv = False
self.from_objects = False
self.viewing_self = False
self.delete_dialog = None
if not self.app.root.ids.screen_manager.has_screen("object_details_screen"):
self.screen = Builder.load_string(layout_object_details)
self.screen.app = self.app
self.screen.delegate = self
self.ids = self.screen.ids
self.app.root.ids.screen_manager.add_widget(self.screen)
self.screen.ids.object_details_container.effect_cls = ScrollEffect
self.telemetry_list = RVDetails()
self.telemetry_list.delegate = self
self.telemetry_list.app = self.app
self.screen.ids.object_details_container.add_widget(self.telemetry_list)
ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
self.info_dialog = MDDialog(
title="Info",
text="",
buttons=[ ok_button ],
)
def dl_ok(s):
self.info_dialog.dismiss()
ok_button.bind(on_release=dl_ok)
Clock.schedule_interval(self.reload_job, 2)
def reload_job(self, dt=None):
if self.app.root.ids.screen_manager.current == "object_details_screen":
latest_telemetry = self.app.sideband.peer_telemetry(self.object_hash, limit=1)
if latest_telemetry != None and len(latest_telemetry) > 0:
telemetry_timestamp = latest_telemetry[0][0]
if telemetry_timestamp > self.lastest_timestamp:
self.reload_telemetry(notoast=True)
def close_action(self, sender=None):
if self.from_telemetry:
self.app.telemetry_action(direction="right")
else:
if self.from_conv:
self.app.open_conversation(self.object_hash, direction="right")
elif self.from_objects:
self.app.objects_action(direction="right")
else:
self.app.close_sub_map_action()
def confirm_delete_telemetry(self, sender=None):
self.app.sideband.clear_telemetry(self.object_hash)
def delete_telemetry_action(self, sender=None):
if self.delete_dialog == None:
yes_button = MDRectangleFlatButton(text="Yes",font_size=dp(18), theme_text_color="Custom", line_color=self.app.color_reject, text_color=self.app.color_reject)
no_button = MDRectangleFlatButton(text="No",font_size=dp(18))
self.delete_dialog = MDDialog(
title="Clear telemetry?",
text="This will permanently delete all collected telemetry for this object.",
buttons=[ yes_button, no_button ],
)
def dl_yes(s):
self.delete_dialog.dismiss()
self.confirm_delete_telemetry()
def cb(dt):
self.reload_telemetry(notoast=True)
Clock.schedule_once(cb, 0.2)
def dl_no(s):
self.delete_dialog.dismiss()
yes_button.bind(on_release=dl_yes)
no_button.bind(on_release=dl_no)
self.delete_dialog.open()
def reload_telemetry(self, sender=None, notoast=False):
if self.object_hash != None:
self.set_source(self.object_hash, from_conv=self.from_conv, from_objects=self.from_objects, from_telemetry=self.from_telemetry)
if not notoast:
toast("Reloaded telemetry for object")
def set_source(self, source_dest, from_conv=False, from_objects=False, from_telemetry=False, prefetched=None):
try:
self.object_hash = source_dest
own_address = self.app.sideband.lxmf_destination.hash
telemetry_allowed = self.app.sideband.should_send_telemetry(source_dest)
if source_dest == own_address:
self.viewing_self = True
else:
self.viewing_self = False
if from_telemetry:
self.from_telemetry = True
else:
self.from_telemetry = False
if from_conv:
self.from_conv = True
else:
self.from_conv = False
if from_objects:
self.from_objects = True
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")
appearance = self.app.sideband.peer_appearance(source_dest)
self.screen.ids.name_label.text = pds
if source_dest == own_address:
self.screen.ids.name_label.text = pds+" (this device)"
elif source_dest == self.app.sideband.config["telemetry_collector"]:
self.screen.ids.name_label.text = pds+" (collector)"
self.screen.ids.coordinates_button.disabled = True
self.screen.ids.object_appearance.icon = appearance[0]
self.screen.ids.object_appearance.icon_color = appearance[1]
self.screen.ids.object_appearance.md_bg_color = appearance[2]
def djob(dt):
if self.viewing_self:
self.screen.ids.request_button.disabled = True
self.screen.ids.send_button.disabled = True
else:
self.screen.ids.request_button.disabled = False
if telemetry_allowed:
self.screen.ids.send_button.disabled = False
else:
self.screen.ids.send_button.disabled = True
if prefetched != None:
latest_telemetry = prefetched
else:
latest_telemetry = self.app.sideband.peer_telemetry(source_dest, limit=1)
if latest_telemetry != None and len(latest_telemetry) > 0:
telemetry_timestamp = latest_telemetry[0][0]
self.lastest_timestamp = telemetry_timestamp
telemeter = Telemeter.from_packed(latest_telemetry[0][1])
self.raw_telemetry = telemeter.read_all()
relative_to = None
if source_dest != own_address:
relative_to = self.app.sideband.telemeter
rendered_telemetry = telemeter.render(relative_to=relative_to)
if "location" in telemeter.sensors:
def job(dt):
self.screen.ids.coordinates_button.disabled = False
Clock.schedule_once(job, 0.01)
self.telemetry_list.update_source(rendered_telemetry)
def job(dt):
self.screen.ids.telemetry_button.disabled = False
Clock.schedule_once(job, 0.01)
else:
def job(dt):
self.screen.ids.telemetry_button.disabled = True
Clock.schedule_once(job, 0.01)
self.telemetry_list.update_source(None)
self.telemetry_list.effect_cls = ScrollEffect
Clock.schedule_once(djob, 0.1)
except Exception as e:
import traceback
exception_info = "".join(traceback.TracebackException.from_exception(e).format())
RNS.log(f"An {str(type(e))} occurred while updating service telemetry: {str(e)}", RNS.LOG_ERROR)
RNS.log(exception_info, RNS.LOG_ERROR)
def reload(self):
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)
if result == "destination_unknown":
title_str = "Unknown Destination"
info_str = "No keys known for the destination. Connected reticules have been queried for the keys."
elif result == "in_progress":
title_str = "Transfer In Progress"
info_str = "There is already an outbound telemetry transfer in progress for this peer."
elif result == "already_sent":
title_str = "Already Delivered"
info_str = "The current telemetry data was already sent and delivered to the peer or propagation network."
elif result == "sent":
title_str = "Update Sent"
info_str = "A telemetry update was sent to the peer."
elif result == "not_sent":
title_str = "Not Sent"
info_str = "A telemetry update could not be sent."
else:
title_str = "Unknown Status"
info_str = "The status of the telemetry update is unknown."
self.info_dialog.title = title_str
self.info_dialog.text = info_str
self.info_dialog.open()
def request_update(self):
if not self.viewing_self:
result = self.app.sideband.request_latest_telemetry(from_addr=self.object_hash)
if result == "destination_unknown":
title_str = "Unknown Destination"
info_str = "No keys known for the destination. Connected reticules have been queried for the keys."
elif result == "in_progress":
title_str = "Transfer In Progress"
info_str = "There is already a telemetry request transfer in progress for this peer."
elif result == "sent":
title_str = "Request Sent"
info_str = "A telemetry request was sent to the peer. The peer should send any available telemetry shortly."
elif result == "not_sent":
title_str = "Not Sent"
info_str = "A telemetry request could not be sent."
else:
title_str = "Unknown Status"
info_str = "The status of the telemetry request is unknown."
self.info_dialog.title = title_str
self.info_dialog.text = info_str
self.info_dialog.open()
def clear_widget(self):
pass
def update(self):
us = time.time()
self.update_widget()
RNS.log("Updated object details in "+RNS.prettytime(time.time()-us), RNS.LOG_DEBUG)
def update_widget(self):
if self.widget == None:
self.widget = MDLabel(text=RNS.prettyhexrep(self.object_hash))
def get_widget(self):
return self.widget
def copy_coordinates(self, sender=None):
Clipboard.copy(str(self.coords or "No data"))
def copy_telemetry(self, sender=None):
Clipboard.copy(str(self.raw_telemetry or "No data"))
class ODView(OneLineIconListItem):
icon = StringProperty()
def __init__(self):
super().__init__()
class RVDetails(MDRecycleView):
def __init__(self):
super().__init__()
self.data = []
def update_source(self, rendered_telemetry=None):
try:
if not rendered_telemetry:
rendered_telemetry = []
sort = {
"Information": 5,
"Physical Link": 10,
"Location": 20,
"Ambient Light": 30,
"Ambient Temperature": 40,
"Relative Humidity": 50,
"Ambient Pressure": 60,
"Magnetic Field": 61,
"Gravity": 62,
"Angular Velocity": 63,
"Acceleration": 64,
"Proximity": 65,
"Battery": 70,
"Processor": 72,
"Random Access Memory": 74,
"Non-Volatile Memory": 76,
"Power Consumption": 80,
"Power Production": 81,
"Tank": 90,
"Fuel": 91,
"Custom": 100,
"Timestamp": 190,
"Received": 200,
}
def pass_job(sender=None):
pass
self.entries = []
rendered_telemetry.sort(key=lambda s: sort[s["name"]] if s["name"] in sort else 1000)
for s in rendered_telemetry:
try:
extra_entries = []
release_function = pass_job
formatted_values = None
name = s["name"]
if name == "Timestamp":
ts = s["values"]["UTC"]
if ts != None:
ts_str = datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S")
formatted_values = f"Recorded [b]{RNS.prettytime(time.time()-ts, compact=True)} ago[/b] ({ts_str})"
def copy_info(e=None):
Clipboard.copy(ts_str)
toast("Copied to clipboard")
release_function = copy_info
elif name == "Information":
info = s["values"]["contents"]
if info != None:
istr = str(info)
def copy_info(e=None):
Clipboard.copy(istr)
toast("Copied to clipboard")
release_function = copy_info
external_text = multilingual_markup(escape_markup(istr).encode("utf-8")).decode("utf-8")
formatted_values = f"[b]Information[/b]: {external_text}"
elif name == "Received":
formatted_values = ""
by = s["values"]["by"];
via = s["values"]["via"];
if by == self.app.sideband.lxmf_destination.hash:
if via == self.delegate.object_hash:
formatted_values = "Collected directly by [b]this device[/b], directly [b]from emitter[/b]"
else:
via_str = self.app.sideband.peer_display_name(via)
if via_str == None:
via_str = "an [b]unknown peer[/b]"
formatted_values = f"Collected directly by [b]this device[/b], via {via_str}"
else:
if via != None and via == by:
vstr = self.app.sideband.peer_display_name(via)
formatted_values = f"Received from, and collected by [b]{vstr}[/b]"
else:
if via != None:
vstr = self.app.sideband.peer_display_name(via)
via_str = f"Received from [b]{vstr}[/b]"
else:
via_str = "Received from an [b]unknown peer[/b]"
if by != None:
dstr = self.app.sideband.peer_display_name(by)
by_str = f", collected by [b]{dstr}[/b]"
else:
by_str = f", collected by an [b]unknown peer[/b]"
formatted_values = f"{via_str}{by_str}"
if formatted_values == "":
formatted_values = None
if not by == self.app.sideband.lxmf_destination.hash and not self.app.sideband.is_trusted(by):
extra_entries.append({"icon": "alert", "text": "Collected by a [b]non-trusted[/b] peer"})
elif name == "Battery":
p = s["values"]["percent"]
cs = s["values"]["_meta"]
t = None
if "temperature" in s["values"]:
t = s["values"]["temperature"]
if cs != None:
if t != None:
cs_str = f" ({cs}, {t}° C)"
else:
cs_str = f" ({cs})"
if p != None: formatted_values = f"{name} [b]{p}%[/b]"+cs_str
elif name == "Ambient Pressure":
p = s["values"]["mbar"]
if p != None: formatted_values = f"{name} [b]{p} mbar[/b]"
dt = "mbar"
if "deltas" in s and dt in s["deltas"] and s["deltas"][dt] != None:
d = s["deltas"][dt]
formatted_values += f" (Δ = {d} mbar)"
elif name == "Ambient Temperature":
c = s["values"]["c"]
if c != None: formatted_values = f"{name} [b]{c}° C[/b]"
dt = "c"
if "deltas" in s and dt in s["deltas"] and s["deltas"][dt] != None:
d = s["deltas"][dt]
formatted_values += f" (Δ = {d}° C)"
elif name == "Relative Humidity":
r = s["values"]["percent"]
if r != None: formatted_values = f"{name} [b]{r}%[/b]"
dt = "percent"
if "deltas" in s and dt in s["deltas"] and s["deltas"][dt] != None:
d = s["deltas"][dt]
formatted_values += f" (Δ = {d}%)"
elif name == "Physical Link":
rssi = s["values"]["rssi"]; rssi_str = None
snr = s["values"]["snr"]; snr_str = None
q = s["values"]["q"]; q_str = None
if q != None: q_str = f"Link Quality [b]{q}%[/b]"
if rssi != None:
rssi_str = f"RSSI [b]{rssi} dBm[/b]"
if q != None: rssi_str = ", "+rssi_str
if snr != None:
snr_str = f"SNR [b]{snr} dB[/b]"
if q != None or rssi != None: snr_str = ", "+snr_str
if q_str or rssi_str or snr_str:
formatted_values = q_str+rssi_str+snr_str
elif name == "Power Consumption":
cs = s["values"]
if cs != None:
for c in cs:
label = c["label"]
cicon = c["custom_icon"]
watts = c["w"]
prefix = ""
if watts < 1/1e6:
watts *= 1e9
prefix = "n"
elif watts < 1/1e3:
watts *= 1e6
prefix = "µ"
elif watts < 1:
watts *= 1e3
prefix = "m"
elif watts >= 1e15:
watts /= 1e15
prefix = "E"
elif watts >= 1e12:
watts /= 1e12
prefix = "T"
elif watts >= 1e9:
watts /= 1e9
prefix = "G"
elif watts >= 1e6:
watts /= 1e6
prefix = "M"
elif watts >= 1e3:
watts /= 1e3
prefix = "K"
if cicon:
set_icon = cicon
else:
set_icon = s["icon"]
watts = round(watts, 2)
p_text = f"{label} [b]{watts} {prefix}W[/b]"
extra_entries.append({"icon": set_icon, "text": p_text})
elif name == "Power Production":
cs = s["values"]
if cs != None:
for c in cs:
label = c["label"]
cicon = c["custom_icon"]
watts = c["w"]
prefix = ""
if watts < 1/1e6:
watts *= 1e9
prefix = "n"
elif watts < 1/1e3:
watts *= 1e6
prefix = "µ"
elif watts < 1:
watts *= 1e3
prefix = "m"
elif watts >= 1e15:
watts /= 1e15
prefix = "E"
elif watts >= 1e12:
watts /= 1e12
prefix = "T"
elif watts >= 1e9:
watts /= 1e9
prefix = "G"
elif watts >= 1e6:
watts /= 1e6
prefix = "M"
elif watts >= 1e3:
watts /= 1e3
prefix = "K"
if cicon:
set_icon = cicon
else:
set_icon = s["icon"]
watts = round(watts, 2)
p_text = f"{label} [b]{watts} {prefix}W[/b]"
extra_entries.append({"icon": set_icon, "text": p_text})
elif name == "Custom":
cs = s["values"]
if cs != None:
for c in cs:
label = c["label"]
cicon = c["custom_icon"]
value = str(c["value"])
set_icon = cicon if cicon else s["icon"]
e_text = f"{label} [b]{value}[/b]"
extra_entries.append({"icon": set_icon, "text": e_text})
elif name == "Tank":
cs = s["values"]
if cs != None:
for c in cs:
label = c["label"]
cicon = c["custom_icon"]
unit = c["unit"]
cap = round(c["capacity"], 1)
lvl = round(c["level"], 1)
free = round(c["free"], 1)
pct = round(c["percent"], 1)
set_icon = cicon if cicon else s["icon"]
e_text = f"{label} level is [b]{lvl} {unit}[/b] ([b]{pct}%[/b])"
extra_entries.append({"icon": set_icon, "text": e_text})
elif name == "Fuel":
cs = s["values"]
if cs != None:
for c in cs:
label = c["label"]
cicon = c["custom_icon"]
unit = c["unit"]
cap = round(c["capacity"], 1)
lvl = round(c["level"], 1)
free = round(c["free"], 1)
pct = round(c["percent"], 1)
set_icon = cicon if cicon else s["icon"]
e_text = f"{label} level is [b]{lvl} {unit}[/b] ([b]{pct}%[/b])"
extra_entries.append({"icon": set_icon, "text": e_text})
elif name == "Processor":
cs = s["values"]
if cs != None:
for c in cs:
label = c["label"]
load = c["current_load"]
avgs = c["load_avgs"]
clock = c["clock"]
pct = round(load*100.0, 1)
avgs_str = f", averages are [b]{round(avgs[0],2)}[/b], [b]{round(avgs[1],2)}[/b], [b]{round(avgs[2],2)}[/b]" if avgs != None and len(avgs) == 3 else ""
clock_str = " at [b]"+RNS.prettyfrequency(clock)+"[/b]" if clock != None else ""
e_text = f"Using [b]{pct}%[/b] of {label}{clock_str}{avgs_str}"
e_text = f"{label} use is [b]{pct}%[/b]{clock_str}{avgs_str}"
extra_entries.append({"icon": s["icon"], "text": e_text})
elif name == "Non-Volatile Memory":
cs = s["values"]
if cs != None:
for c in cs:
label = c["label"]
cap = RNS.prettysize(c["capacity"])
use = RNS.prettysize(c["used"])
free = RNS.prettysize(c["free"])
pct = round(c["percent"], 1)
e_text = f"{label} use is [b]{use}[/b] ([b]{pct}%[/b]) of [b]{cap}[/b], with [b]{free}[/b] free"
extra_entries.append({"icon": s["icon"], "text": e_text})
elif name == "Random Access Memory":
cs = s["values"]
if cs != None:
for c in cs:
label = c["label"]
cap = RNS.prettysize(c["capacity"])
use = RNS.prettysize(c["used"])
free = RNS.prettysize(c["free"])
pct = round(c["percent"], 1)
e_text = f"{label} use is [b]{use}[/b] ([b]{pct}%[/b]) of [b]{cap}[/b], with [b]{free}[/b] free"
extra_entries.append({"icon": s["icon"], "text": e_text})
elif name == "Location":
lat = s["values"]["latitude"]
lon = s["values"]["longitude"]
alt = s["values"]["altitude"]
speed = s["values"]["speed"]
heading = s["values"]["heading"]
accuracy = s["values"]["accuracy"]
updated = s["values"]["updated"]
updated_str = f", logged [b]{RNS.prettytime(time.time()-updated, compact=True)} ago[/b]"
coords = f"{lat}, {lon}"
fcoords = f"{round(lat,4)}, {round(lon,4)}"
self.delegate.coords = coords
if alt == 0:
alt_str = "0"
else:
alt_str = RNS.prettydistance(alt)
formatted_values = f"Coordinates [b]{fcoords}[/b], altitude [b]{alt_str}[/b]"
if speed != None:
if speed > 0.1:
speed_formatted_values = f"Speed [b]{speed} Km/h[/b], heading [b]{heading}°[/b]"
else:
speed_formatted_values = f"Object is [b]stationary[/b]"
else:
speed_formatted_values = None
extra_formatted_values = f"Uncertainty [b]{accuracy} meters[/b]"+updated_str
data = {"icon": s["icon"], "text": f"{formatted_values}"}
extra_entries.append({"icon": "map-marker-question", "text": extra_formatted_values})
if speed_formatted_values != None:
extra_entries.append({"icon": "speedometer", "text": speed_formatted_values})
if "distance" in s:
if "orthodromic" in s["distance"]:
od = s["distance"]["orthodromic"]
if od != None:
od_text = f"Geodesic distance [b]{RNS.prettydistance(od)}[/b]"
extra_entries.append({"icon": "earth", "text": od_text})
if "euclidian" in s["distance"]:
ed = s["distance"]["euclidian"]
if ed != None:
ed_text = f"Euclidian distance [b]{RNS.prettydistance(ed)}[/b]"
extra_entries.append({"icon": "axis-arrow", "text": ed_text})
if "vertical" in s["distance"]:
vd = s["distance"]["vertical"]
if vd != None:
if vd < 0:
relstr = "lower"
vd = abs(vd)
else:
relstr = "greater"
vd_text = f"Altitude is [b]{RNS.prettydistance(vd)}[/b] {relstr} than this device"
extra_entries.append({"icon": "altimeter", "text": vd_text})
if "angle_to_horizon" in s["values"]:
oath = s["values"]["angle_to_horizon"]
if oath != None:
if self.delegate.viewing_self:
oath_text = f"Local horizon is at [b]{round(oath,3)}°[/b]"
else:
oath_text = f"Object's horizon is at [b]{round(oath,3)}°[/b]"
extra_entries.append({"icon": "arrow-split-horizontal", "text": oath_text})
if self.delegate.viewing_self and "radio_horizon" in s["values"]:
orh = s["values"]["radio_horizon"]
if orh != None:
range_text = RNS.prettydistance(orh)
rh_formatted_text = f"Radio horizon of [b]{range_text}[/b]"
extra_entries.append({"icon": "radio-tower", "text": rh_formatted_text})
if "azalt" in s and "local_angle_to_horizon" in s["azalt"]:
lath = s["azalt"]["local_angle_to_horizon"]
if lath != None:
lath_text = f"Local horizon is at [b]{round(lath,3)}°[/b]"
extra_entries.append({"icon": "align-vertical-distribute", "text": lath_text})
if "azalt" in s:
azalt_formatted_text = ""
if "azimuth" in s["azalt"]:
az = s["azalt"]["azimuth"]
az_text = f"Azimuth [b]{round(az,3)}°[/b]"
azalt_formatted_text += az_text
if "altitude" in s["azalt"]:
al = s["azalt"]["altitude"]
al_text = f"altitude [b]{round(al,3)}°[/b]"
if len(azalt_formatted_text) != 0: azalt_formatted_text += ", "
azalt_formatted_text += al_text
extra_entries.append({"icon": "compass-rose", "text": azalt_formatted_text})
if "above_horizon" in s["azalt"]:
astr = "above" if s["azalt"]["above_horizon"] == True else "below"
dstr = str(round(s["azalt"]["altitude_delta"], 3))
ah_text = f"Object is [b]{astr}[/b] the horizon (Δ = {dstr}°)"
extra_entries.append({"icon": "angle-acute", "text": ah_text})
if not self.delegate.viewing_self and "radio_horizon" in s["values"]:
orh = s["values"]["radio_horizon"]
if orh != None:
range_text = RNS.prettydistance(orh)
rh_formatted_text = f"Object's radio horizon is [b]{range_text}[/b]"
extra_entries.append({"icon": "radio-tower", "text": rh_formatted_text})
if "radio_horizon" in s:
rh_icon = "circle-outline"
crange_text = RNS.prettydistance(s["radio_horizon"]["combined_range"])
if s["radio_horizon"]["within_range"]:
rh_formatted_text = f"[b]Within[/b] shared radio horizon of [b]{crange_text}[/b]"
rh_icon = "set-none"
else:
rh_formatted_text = f"[b]Outside[/b] shared radio horizon of [b]{crange_text}[/b]"
extra_entries.append({"icon": rh_icon, "text": rh_formatted_text})
def select(e=None):
geo_uri = f"geo:{lat},{lon}"
def lj():
webbrowser.open(geo_uri)
threading.Thread(target=lj, daemon=True).start()
release_function = select
else:
formatted_values = f"{name}"
for vn in s["values"]:
v = s["values"][vn]
formatted_values += f" [b]{v} {vn}[/b]"
dt = vn
if "deltas" in s and dt in s["deltas"] and s["deltas"][dt] != None:
d = s["deltas"][dt]
formatted_values += f" (Δ = {d} {vn})"
formatted_values += ", "
formatted_values = formatted_values[:-2]
data = None
if formatted_values != None:
if release_function:
data = {"icon": s["icon"], "text": f"{formatted_values}", "on_release": release_function}
else:
data = {"icon": s["icon"], "text": f"{formatted_values}", "on_release": pass_job}
if data != None:
self.entries.append(data)
for extra in extra_entries:
self.entries.append(extra)
except Exception as e:
RNS.log("An error ocurred while displaying telemetry for object", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
RNS.trace_exception(e)
try:
ratchet_id = RNS.Identity.current_ratchet_id(self.delegate.object_hash)
if ratchet_id:
self.entries.append({"icon": "lock-check-outline", "text": f"Using ratchet [b]{RNS.prettyhexrep(ratchet_id)}[/b]", "on_release": pass_job})
except Exception as e:
RNS.trace_exception(e)
try:
nh = RNS.Transport.hops_to(self.delegate.object_hash)
nhi = self.delegate.app.sideband.reticulum.get_next_hop_if_name(self.delegate.object_hash)
if nhi and nhi != "None":
self.entries.append({"icon": "routes", "text": f"Current path on [b]{nhi}[/b]", "on_release": pass_job})
try:
ler = self.delegate.app.sideband.get_destination_establishment_rate(self.delegate.object_hash)
if ler:
lers = RNS.prettyspeed(ler, "b")
self.entries.append({"icon": "lock-check-outline", "text": f"Direct link established, LER is [b]{lers}[/b]", "on_release": pass_job})
except Exception as e:
RNS.trace_exception(e)
if nh != RNS.Transport.PATHFINDER_M:
hs = "hop" if nh == 1 else "hops"
self.entries.append({"icon": "atom-variant", "text": f"Network distance is [b]{nh} {hs}[/b]", "on_release": pass_job})
except Exception as e:
RNS.trace_exception(e)
try:
ticket_expires = self.delegate.app.sideband.message_router.get_outbound_ticket_expiry(self.delegate.object_hash)
stamp_cost = self.delegate.app.sideband.message_router.get_outbound_stamp_cost(self.delegate.object_hash)
t_str = ""
if ticket_expires:
t_str = ", but have ticket"
if stamp_cost:
self.entries.append({"icon": "postage-stamp", "text": f"Required stamp cost [b]{stamp_cost}[/b]"+t_str, "on_release": pass_job})
else:
self.entries.append({"icon": "postage-stamp", "text": f"No required stamp cost", "on_release": pass_job})
if ticket_expires:
valid_for = ticket_expires - time.time()
self.entries.append({"icon": "ticket-confirmation", "text": f"Delivery ticket valid for [b]{RNS.prettytime(valid_for)}[/b]", "on_release": pass_job})
except Exception as e:
RNS.trace_exception(e)
if len(self.entries) == 0:
self.entries.append({"icon": "timeline-question-outline", "text": f"No telemetry available for this device"})
self.data = self.entries
except Exception as e:
import traceback
exception_info = "".join(traceback.TracebackException.from_exception(e).format())
RNS.log(f"An {str(type(e))} occurred while updating service telemetry: {str(e)}", RNS.LOG_ERROR)
RNS.log(exception_info, RNS.LOG_ERROR)
layout_object_details = """
#:import MDLabel kivymd.uix.label.MDLabel
#:import OneLineIconListItem kivymd.uix.list.OneLineIconListItem
#:import Button kivy.uix.button.Button
<ODView>
IconLeftWidget:
icon: root.icon
<RVDetails>:
viewclass: "ODView"
effect_cls: "ScrollEffect"
RecycleBoxLayout:
default_size: None, dp(50)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: "vertical"
MDScreen:
name: "object_details_screen"
BoxLayout:
orientation: "vertical"
MDTopAppBar:
id: details_bar
title: "Details"
anchor_title: "left"
elevation: 0
left_action_items:
[['menu', lambda x: root.app.nav_drawer.set_state("open")]]
right_action_items:
[
['map-search', lambda x: root.app.peer_show_location_action(root.delegate)],
['refresh', lambda x: root.delegate.reload_telemetry()],
['trash-can-outline', lambda x: root.delegate.delete_telemetry_action()],
['close', lambda x: root.delegate.close_action()],
]
MDBoxLayout:
id: object_header
orientation: "horizontal"
spacing: dp(24)
size_hint_y: None
height: self.minimum_height
padding: dp(24)
MDIconButton:
id: object_appearance
icon: "account-question"
icon_color: [0,0,0,1]
md_bg_color: [1,1,1,1]
theme_icon_color: "Custom"
icon_size: dp(32)
on_release: root.app.converse_from_telemetry()
MDLabel:
id: name_label
markup: True
text: "Object Name"
font_style: "H6"
MDBoxLayout:
orientation: "horizontal"
spacing: dp(24)
size_hint_y: None
height: self.minimum_height
padding: [dp(24), dp(0), dp(24), dp(24)]
MDRectangleFlatIconButton:
id: telemetry_button
icon: "content-copy"
text: "Copy 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: coordinates_button
icon: "map-marker-outline"
text: "Copy Coordinates"
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_coordinates(self)
disabled: False
MDSeparator:
orientation: "horizontal"
height: dp(1)
MDBoxLayout:
orientation: "vertical"
id: object_details_container
MDSeparator:
orientation: "horizontal"
height: dp(1)
MDBoxLayout:
orientation: "horizontal"
spacing: dp(24)
size_hint_y: None
height: self.minimum_height
padding: [dp(24), dp(24), dp(24), dp(24)]
MDRectangleFlatIconButton:
id: send_button
icon: "upload-lock"
text: "Send Update"
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.send_update()
disabled: False
MDRectangleFlatIconButton:
id: request_button
icon: "arrow-down-bold-hexagon-outline"
text: "Request Update"
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.request_update()
disabled: False
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: 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
"""