import time import RNS from typing import Union 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 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 from sideband.sense import Telemeter import threading from datetime import datetime if RNS.vendor.platformutils.get_platform() == "android": from ui.helpers import ts_format from android.permissions import request_permissions, check_permission else: from .helpers import ts_format class Utilities(): def __init__(self, app): self.app = app 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) self.screen.app = self.app self.screen.delegate = self self.app.root.ids.screen_manager.add_widget(self.screen) self.screen.ids.telemetry_scrollview.effect_cls = ScrollEffect 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.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 ###################################### def rnstatus_action(self, sender=None): if not self.app.root.ids.screen_manager.has_screen("rnstatus_screen"): self.rnstatus_screen = Builder.load_string(layout_rnstatus_screen) self.rnstatus_screen.app = self.app self.rnstatus_screen.delegate = self self.app.root.ids.screen_manager.add_widget(self.rnstatus_screen) self.app.root.ids.screen_manager.transition.direction = "left" self.app.root.ids.screen_manager.current = "rnstatus_screen" self.app.sideband.setstate("app.displaying", self.app.root.ids.screen_manager.current) self.update_rnstatus() def update_rnstatus(self, sender=None): threading.Thread(target=self.update_rnstatus_job, daemon=True).start() def update_rnstatus_job(self, sender=None): if self.rnstatus_instance == None: import RNS.Utilities.rnstatus as rnstatus self.rnstatus_instance = rnstatus import io from contextlib import redirect_stdout output = "None" with io.StringIO() as buffer, redirect_stdout(buffer): 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][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: name: "utilities_screen" BoxLayout: orientation: "vertical" MDTopAppBar: title: "Utilities" anchor_title: "left" elevation: 0 left_action_items: [['menu', lambda x: root.app.nav_drawer.set_state("open")]] right_action_items: [ ['close', lambda x: root.app.close_any_action(self)], ] ScrollView: id: telemetry_scrollview MDBoxLayout: orientation: "vertical" size_hint_y: None height: self.minimum_height padding: [dp(28), dp(32), dp(28), dp(16)] # MDLabel: # text: "Utilities & Tools" # font_style: "H6" MDLabel: id: utilities_info markup: True text: "" size_hint_y: None text_size: self.width, None height: self.texture_size[1] MDBoxLayout: orientation: "vertical" spacing: "24dp" size_hint_y: None height: self.minimum_height padding: [dp(0), dp(35), dp(0), dp(35)] MDRectangleFlatIconButton: id: rnstatus_button icon: "wifi-check" text: "Reticulum Status" 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.rnstatus_action(self) disabled: False MDRectangleFlatIconButton: id: logview_button icon: "list-box-outline" text: "Log Viewer" 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.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 """ layout_rnstatus_screen = """ MDScreen: name: "rnstatus_screen" BoxLayout: orientation: "vertical" MDTopAppBar: id: top_bar title: "Reticulum Status" 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: rnstatus_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: rnstatus_output markup: True text: "" 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] """