diff --git a/sbapp/main.py b/sbapp/main.py index 8e4c6c0..9c17059 100644 --- a/sbapp/main.py +++ b/sbapp/main.py @@ -2981,6 +2981,12 @@ class SidebandApp(MDApp): self.telemetry_screen.ids.telemetry_s_proximity.active = self.sideband.config["telemetry_s_proximity"] self.telemetry_screen.ids.telemetry_s_proximity.bind(active=self.telemetry_save) + + self.telemetry_screen.ids.telemetry_s_information.active = self.sideband.config["telemetry_s_information"] + self.telemetry_screen.ids.telemetry_s_information.bind(active=self.telemetry_save) + self.telemetry_screen.ids.telemetry_s_information_text.text = str(self.sideband.config["telemetry_s_information_text"]) + self.telemetry_screen.ids.telemetry_s_information_text.bind(focus=self.telemetry_save) + self.telemetry_ready = True @@ -3048,6 +3054,8 @@ class SidebandApp(MDApp): self.sideband.config["telemetry_s_angular_velocity"] = self.telemetry_screen.ids.telemetry_s_gyroscope.active self.sideband.config["telemetry_s_acceleration"] = self.telemetry_screen.ids.telemetry_s_accelerometer.active self.sideband.config["telemetry_s_proximity"] = self.telemetry_screen.ids.telemetry_s_proximity.active + self.sideband.config["telemetry_s_information"] = self.telemetry_screen.ids.telemetry_s_information.active + self.sideband.config["telemetry_s_information_text"] = self.telemetry_screen.ids.telemetry_s_information_text.text run_telemetry_update = False try: @@ -3264,18 +3272,27 @@ class SidebandApp(MDApp): def map_display_telemetry(self, sender=None): self.object_details_action(sender) + def map_display_own_telemetry(self, sender=None): + self.object_details_action(source_dest=self.sideband.lxmf_destination.hash,from_telemetry=True) + def close_sub_map_action(self, sender=None): self.map_action(direction="right") - def object_details_action(self, sender=None, from_conv=False): + def object_details_action(self, sender=None, from_conv=False, from_telemetry=False, source_dest=None): self.root.ids.screen_manager.transition.direction = "left" self.root.ids.nav_drawer.set_state("closed") - if sender != None and hasattr(sender, "source_dest") and sender.source_dest != None: + if source_dest != None: + telemetry_source = source_dest + else: + if sender != None and hasattr(sender, "source_dest") and sender.source_dest != None: + telemetry_source = sender.source_dest + + if telemetry_source != None: if self.object_details_screen == None: self.object_details_screen = ObjectDetails(self) - Clock.schedule_once(lambda dt: self.object_details_screen.set_source(sender.source_dest, from_conv=from_conv), 0.0) + Clock.schedule_once(lambda dt: self.object_details_screen.set_source(telemetry_source, from_conv=from_conv, from_telemetry=from_telemetry), 0.0) def vj(dt): self.root.ids.screen_manager.current = "object_details_screen" diff --git a/sbapp/sideband/core.py b/sbapp/sideband/core.py index 69d261d..f20106c 100644 --- a/sbapp/sideband/core.py +++ b/sbapp/sideband/core.py @@ -503,6 +503,10 @@ class SidebandCore(): self.config["telemetry_s_fixed_latlon"] = [0.0, 0.0] if not "telemetry_s_fixed_altitude" in self.config: self.config["telemetry_s_fixed_altitude"] = 0.0 + if not "telemetry_s_information" in self.config: + self.config["telemetry_s_information"] = False + if not "telemetry_s_information_text" in self.config: + self.config["telemetry_s_information_text"] = "" if not "map_history_limit" in self.config: self.config["map_history_limit"] = 7*24*60*60 @@ -1216,7 +1220,7 @@ class SidebandCore(): return results - def _db_save_telemetry(self, context_dest, telemetry, physical_link = None): + def _db_save_telemetry(self, context_dest, telemetry, physical_link = None, source_dest = None): try: remote_telemeter = Telemeter.from_packed(telemetry) telemetry_timestamp = remote_telemeter.read_all()["time"]["utc"] @@ -1238,13 +1242,36 @@ class SidebandCore(): if "q" in physical_link: remote_telemeter.sensors["physical_link"].q = physical_link["q"] remote_telemeter.sensors["physical_link"].update_data() telemetry = remote_telemeter.packed() - + + if source_dest != None: + remote_telemeter.synthesize("received") + remote_telemeter.sensors["received"].by = self.lxmf_destination.hash + remote_telemeter.sensors["received"].via = source_dest + + rl = remote_telemeter.read("location") + if rl and "latitude" in rl and "longtitude" in rl and "altitude" in rl: + if self.latest_telemetry != None and "location" in self.latest_telemetry: + ol = self.latest_telemetry["location"] + if "latitude" in ol and "longtitude" in ol and "altitude" in ol: + olat = ol["latitude"]; olon = ol["longtitude"]; oalt = ol["altitude"] + rlat = rl["latitude"]; rlon = rl["longtitude"]; ralt = rl["altitude"] + if olat != None and olon != None and oalt != None: + if rlat != None and rlon != None and ralt != None: + remote_telemeter.sensors["received"].set_distance( + (olat, olon, oalt), (rlat, rlon, ralt) + ) + + remote_telemeter.sensors["received"].update_data() + telemetry = remote_telemeter.packed() + query = "INSERT INTO telemetry (dest_context, ts, data) values (?, ?, ?)" data = (context_dest, telemetry_timestamp, telemetry) dbc.execute(query, data) db.commit() self.setstate("app.flags.last_telemetry", time.time()) + return telemetry + except Exception as e: RNS.log("An error occurred while saving telemetry to database: "+str(e), RNS.LOG_ERROR) self.db = None @@ -1632,6 +1659,19 @@ class SidebandCore(): def _db_save_lxm(self, lxm, context_dest, originator = False): state = lxm.state + packed_telemetry = None + if not originator and lxm.fields != None: + if LXMF.FIELD_ICON_APPEARANCE in lxm.fields: + self._db_update_appearance(context_dest, lxm.timestamp, lxm.fields[LXMF.FIELD_ICON_APPEARANCE]) + + if LXMF.FIELD_TELEMETRY in lxm.fields: + physical_link = {} + if lxm.rssi or lxm.snr or lxm.q: + physical_link["rssi"] = lxm.rssi + physical_link["snr"] = lxm.snr + physical_link["q"] = lxm.q + packed_telemetry = self._db_save_telemetry(context_dest, lxm.fields[LXMF.FIELD_TELEMETRY], physical_link=physical_link, source_dest=context_dest) + db = self.__db_connect() dbc = db.cursor() @@ -1648,6 +1688,10 @@ class SidebandCore(): extras["rssi"] = lxm.rssi extras["snr"] = lxm.snr extras["q"] = lxm.q + + if packed_telemetry != None: + extras["packed_telemetry"] = packed_telemetry + extras = msgpack.packb(extras) query = "INSERT INTO lxm (lxm_hash, dest, source, title, tx_ts, rx_ts, state, method, t_encrypted, t_encryption, data, extra) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" @@ -1669,18 +1713,6 @@ class SidebandCore(): dbc.execute(query, data) db.commit() - if not originator and lxm.fields != None: - if LXMF.FIELD_ICON_APPEARANCE in lxm.fields: - self._db_update_appearance(context_dest, lxm.timestamp, lxm.fields[LXMF.FIELD_ICON_APPEARANCE]) - - if LXMF.FIELD_TELEMETRY in lxm.fields: - physical_link = {} - if lxm.rssi or lxm.snr or lxm.q: - physical_link["rssi"] = lxm.rssi - physical_link["snr"] = lxm.snr - physical_link["q"] = lxm.q - self._db_save_telemetry(context_dest, lxm.fields[LXMF.FIELD_TELEMETRY], physical_link=physical_link) - self.__event_conversation_changed(context_dest) def _db_save_announce(self, destination_hash, app_data, dest_type="lxmf.delivery"): @@ -1782,6 +1814,9 @@ class SidebandCore(): self.telemeter.sensors["location"].longtitude = self.config["telemetry_s_fixed_latlon"][1] self.telemeter.sensors["location"].altitude = self.config["telemetry_s_fixed_altitude"] + if self.config["telemetry_s_information"]: + self.telemeter.synthesize("information") + self.telemeter.sensors["information"].contents = self.config["telemetry_s_information_text"] def get_telemetry(self): if self.config["telemetry_enabled"] == True: diff --git a/sbapp/ui/layouts.py b/sbapp/ui/layouts.py index c4b022c..415692d 100644 --- a/sbapp/ui/layouts.py +++ b/sbapp/ui/layouts.py @@ -927,10 +927,10 @@ MDScreen: spacing: dp(24) size_hint_y: None padding: [dp(0),dp(24),dp(0),dp(0)] - height: dp(160) + height: dp(232) MDRectangleFlatIconButton: - id: telemetry_icons_button + id: telemetry_send_update_button icon: "upload-lock" text: "Send Telemetry Update Now" padding: [dp(0), dp(14), dp(0), dp(14)] @@ -941,7 +941,7 @@ MDScreen: disabled: False MDRectangleFlatIconButton: - id: telemetry_icons_button + id: telemetry_request_button icon: "arrow-down-bold-hexagon-outline" text: "Request Telemetry From Collector" padding: [dp(0), dp(14), dp(0), dp(14)] @@ -952,7 +952,7 @@ MDScreen: disabled: False MDRectangleFlatIconButton: - id: telemetry_icons_button + id: telemetry_copy_button icon: "content-copy" text: "Copy Telemetry Data To Clipboard" padding: [dp(0), dp(14), dp(0), dp(14)] @@ -962,6 +962,17 @@ MDScreen: on_release: root.app.telemetry_copy(self) disabled: False + MDRectangleFlatIconButton: + id: telemetry_own_button + icon: "database-eye-outline" + text: "Display Own 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.app.map_display_own_telemetry(self) + disabled: False + MDBoxLayout: id: telemetry_enabled_fields orientation: "vertical" @@ -1324,6 +1335,37 @@ 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: "Information" + font_style: "H6" + + MDSwitch: + id: telemetry_s_information + pos_hint: {"center_y": 0.3} + active: False + + MDBoxLayout: + id: telemetry_information_fields + orientation: "horizontal" + size_hint_y: None + spacing: dp(16) + height: dp(64) + padding: [0, dp(0), 0, dp(0)] + + MDTextField: + id: telemetry_s_information_text + size_hint: [1.0, None] + hint_text: "Custom information text" + max_text_length: 256 + text: "" + font_size: dp(24) + MDBoxLayout: orientation: "horizontal" size_hint_y: None @@ -1660,7 +1702,7 @@ MDScreen: height: dp(48) MDLabel: - text: "Advanced Statistics" + text: "Advanced Metrics" font_style: "H6" MDSwitch: diff --git a/sbapp/ui/messages.py b/sbapp/ui/messages.py index e06416f..0cad39d 100644 --- a/sbapp/ui/messages.py +++ b/sbapp/ui/messages.py @@ -42,6 +42,7 @@ class Messages(): self.app = app self.context_dest = context_dest self.source_dest = context_dest + self.is_trusted = self.app.sideband.is_trusted(self.context_dest) self.screen = self.app.root.ids.screen_manager.get_screen("messages_screen") self.ids = self.screen.ids @@ -178,6 +179,33 @@ class Messages(): rxstr = time.strftime(ts_format, time.localtime(m["received"])) titlestr = "" extra_telemetry = {} + telemeter = None + if "extras" in m and m["extras"] != None and "packed_telemetry" in m["extras"]: + try: + telemeter = Telemeter.from_packed(m["extras"]["packed_telemetry"]) + except Exception as e: + pass + + if telemeter == None and "lxm" in m and m["lxm"] and m["lxm"].fields != None and LXMF.FIELD_TELEMETRY in m["lxm"].fields: + try: + packed_telemetry = m["lxm"].fields[LXMF.FIELD_TELEMETRY] + telemeter = Telemeter.from_packed(packed_telemetry) + except Exception as e: + pass + + rcvd_d_str = "" + + trcvd = telemeter.read("received") if telemeter else None + if trcvd and "distance" in trcvd: + d = trcvd["distance"] + if "euclidian" in d: + edst = d["euclidian"] + if edst != None: + rcvd_d_str = " [b]Distance[/b] "+RNS.prettydistance(edst) + elif "geodesic" in d: + gdst = d["geodesic"] + if gdst != None: + rcvd_d_str = " [b]Distance[/b] "+RNS.prettydistance(gdst) phy_stats_str = "" if "extras" in m and m["extras"] != None: @@ -238,7 +266,12 @@ class Messages(): if phy_stats_str != "" and self.app.sideband.config["advanced_stats"]: heading_str += phy_stats_str+"\n" - heading_str += "[b]Received[/b] "+rxstr+"\n[b]Sent[/b] "+txstr + heading_str += "[b]Received[/b] "+rxstr + + if rcvd_d_str != "" and self.app.sideband.config["advanced_stats"]: + heading_str += rcvd_d_str + + heading_str += "\n[b]Sent[/b] "+txstr item = ListLXMessageCard( text=m["content"].decode("utf-8"), @@ -253,6 +286,7 @@ class Messages(): item.ids.content_text.text_color = mt_color item.ids.msg_submenu.theme_text_color = "Custom" item.ids.msg_submenu.text_color = mt_color + item.ids.content_text.markup = self.is_trusted def gen_del(mhash, item): def x(): @@ -299,10 +333,10 @@ class Messages(): return x - def gen_copy_telemetry(packed_telemetry, extra_telemetry, item): + def gen_copy_telemetry(telemeter, extra_telemetry, item): def x(): try: - telemeter = Telemeter.from_packed(packed_telemetry) + telemeter if extra_telemetry and len(extra_telemetry) != 0: physical_link = extra_telemetry telemeter.synthesize("physical_link") @@ -507,8 +541,7 @@ class Messages(): } ] else: - if "lxm" in m and m["lxm"] and m["lxm"].fields != None and LXMF.FIELD_TELEMETRY in m["lxm"].fields: - packed_telemetry = m["lxm"].fields[LXMF.FIELD_TELEMETRY] + if telemeter != None: dm_items = [ { "viewclass": "OneLineListItem", @@ -520,7 +553,7 @@ class Messages(): "viewclass": "OneLineListItem", "text": "Copy telemetry", "height": dp(40), - "on_release": gen_copy_telemetry(packed_telemetry, extra_telemetry, item) + "on_release": gen_copy_telemetry(telemeter, extra_telemetry, item) }, { "text": "Delete", @@ -694,11 +727,9 @@ Builder.load_string(""" MDLabel: id: content_text text: root.text - # adaptive_size: True + markup: False size_hint_y: None text_size: self.width, None - # theme_text_color: 'Custom' - # text_color: rgba(255,255,255,216) height: self.texture_size[1] diff --git a/sbapp/ui/objectdetails.py b/sbapp/ui/objectdetails.py index 6a33465..d3c3e5e 100644 --- a/sbapp/ui/objectdetails.py +++ b/sbapp/ui/objectdetails.py @@ -4,6 +4,7 @@ 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 @@ -28,6 +29,7 @@ class ObjectDetails(): self.object_hash = object_hash self.coords = None self.raw_telemetry = None + self.from_telemetry = False self.from_conv = False self.viewing_self = False @@ -45,23 +47,31 @@ class ObjectDetails(): self.screen.ids.object_details_container.add_widget(self.telemetry_list) def close_action(self, sender=None): - if self.from_conv: - self.app.open_conversation(self.object_hash, direction="right") + if self.from_telemetry: + self.app.telemetry_action(direction="right") else: - self.app.close_sub_map_action() + if self.from_conv: + self.app.open_conversation(self.object_hash, direction="right") + else: + self.app.close_sub_map_action() - def set_source(self, source_dest, from_conv=False): + def set_source(self, source_dest, from_conv=False, from_telemetry=False): self.object_hash = source_dest - if from_conv: - self.from_conv = True + if from_telemetry: + self.from_telemetry = True else: - self.from_conv = False + self.from_telemetry = False + if from_conv: + self.from_conv = True + else: + self.from_conv = False self.coords = None self.telemetry_list.data = [] + pds = self.app.sideband.peer_display_name(source_dest) appearance = self.app.sideband.peer_appearance(source_dest) - self.screen.ids.name_label.text = self.app.sideband.peer_display_name(source_dest) + self.screen.ids.name_label.text = pds self.screen.ids.coordinates_button.disabled = True self.screen.ids.object_appearance.icon = appearance[0] self.screen.ids.object_appearance.icon_color = appearance[1] @@ -79,6 +89,7 @@ class ObjectDetails(): self.viewing_self = False else: self.viewing_self = True + self.screen.ids.name_label.text = pds+" (this device)" rendered_telemetry = telemeter.render(relative_to=relative_to) if "location" in telemeter.sensors: @@ -145,7 +156,9 @@ class RVDetails(MDRecycleView): "Relative Humidity": 50, "Ambient Pressure": 60, "Battery": 70, - "Timestamp": 80, + "Timestamp": 80, + "Received": 90, + "Information": 100, } self.entries = [] rendered_telemetry.sort(key=lambda s: sort[s["name"]] if s["name"] in sort else 1000) @@ -161,6 +174,37 @@ class RVDetails(MDRecycleView): 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})" + elif name == "Information": + info = s["values"]["contents"] + if info != None: + istr = str(info) + external_text = escape_markup(istr) + formatted_values = f"[b]Information[/b]: {external_text}" + elif name == "Received": + formatted_values = "" + by = s["values"]["by"]; by_str = "" + if by != None: + if by == self.app.sideband.lxmf_destination.hash: + by_str = "Directly by [b]this device[/b]" + else: + dstr = self.app.sideband.peer_display_name(by) + by_str = f"By [b]{dstr}[/b]" + formatted_values+=by_str + + via = s["values"]["via"]; via_str = "" + if via != None: + if via == self.delegate.object_hash: + via_str = "directly [b]from emitter[/b]" + else: + dstr = self.app.sideband.peer_display_name(by) + via_str = f"via [b]{dstr}[/b]" + if len(formatted_values) != 0: formatted_values += ", " + formatted_values += via_str + + if formatted_values != "": + formatted_values = f"Collected {formatted_values}" + else: + formatted_values = None elif name == "Battery": p = s["values"]["percent"] cs = s["values"]["_meta"] @@ -213,7 +257,11 @@ class RVDetails(MDRecycleView): coords = f"{lat}, {lon}" fcoords = f"{round(lat,4)}, {round(lon,4)}" self.delegate.coords = coords - formatted_values = f"Coordinates [b]{fcoords}[/b], altitude [b]{alt} meters[/b]" + if alt == 0: + alt_str = "0" + else: + alt_str = RNS.prettydistance(alt) + formatted_values = f"Coordinates [b]{fcoords}[/b], altitude [b]{alt_str}[/b]" speed_formatted_values = f"Speed [b]{speed} Km/h[/b], heading [b]{heading}°[/b]" extra_formatted_values = f"Uncertainty [b]{accuracy} meters[/b]"+updated_str @@ -277,14 +325,23 @@ class RVDetails(MDRecycleView): 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": "radio-tower", "text": rh_formatted_text}) + extra_entries.append({"icon": rh_icon, "text": rh_formatted_text}) extra_entries.append({"icon": "speedometer", "text": speed_formatted_values})