2023-11-04 12:42:53 -06:00
__debug_build__ = False
2022-10-11 06:20:01 -06:00
2023-10-28 14:59:52 -06:00
import sys
2022-09-17 08:00:55 -06:00
import time
2022-09-20 09:28:39 -06:00
import RNS
from os import environ
2022-10-01 03:18:14 -06:00
from kivy . logger import Logger , LOG_LEVELS
2022-10-11 06:20:01 -06:00
2022-10-13 13:22:16 -06:00
if __debug_build__ :
2022-10-11 06:20:01 -06:00
Logger . setLevel ( LOG_LEVELS [ " debug " ] )
else :
Logger . setLevel ( LOG_LEVELS [ " error " ] )
2022-09-17 08:00:55 -06:00
2022-10-01 03:18:14 -06:00
if RNS . vendor . platformutils . get_platform ( ) == " android " :
from jnius import autoclass , cast
2023-10-26 05:21:49 -06:00
# Squelch excessive method signature logging
import jnius . reflect
class redirect_log ( ) :
def isEnabledFor ( self , arg ) :
return False
def debug ( self , arg ) :
pass
def mod ( method , name , signature ) :
pass
jnius . reflect . log_method = mod
jnius . reflect . log = redirect_log ( )
############################################
2022-10-01 17:14:29 -06:00
from android import python_act
2022-10-20 07:36:29 -06:00
android_api_version = autoclass ( ' android.os.Build$VERSION ' ) . SDK_INT
2023-12-02 18:08:04 -07:00
2022-10-01 17:14:29 -06:00
Intent = autoclass ( ' android.content.Intent ' )
BitmapFactory = autoclass ( ' android.graphics.BitmapFactory ' )
Icon = autoclass ( " android.graphics.drawable.Icon " )
PendingIntent = autoclass ( ' android.app.PendingIntent ' )
AndroidString = autoclass ( ' java.lang.String ' )
NotificationManager = autoclass ( ' android.app.NotificationManager ' )
Context = autoclass ( ' android.content.Context ' )
2024-09-21 16:10:40 -06:00
JString = autoclass ( ' java.lang.String ' )
2022-10-20 07:36:29 -06:00
if android_api_version > = 26 :
NotificationBuilder = autoclass ( ' android.app.Notification$Builder ' )
NotificationChannel = autoclass ( ' android.app.NotificationChannel ' )
2024-09-21 12:56:14 -06:00
RingtoneManager = autoclass ( ' android.media.RingtoneManager ' )
2022-10-20 07:36:29 -06:00
2022-10-11 06:20:01 -06:00
from usb4a import usb
from usbserial4a import serial4a
2022-10-01 03:18:14 -06:00
from sideband . core import SidebandCore
2022-09-28 05:12:14 -06:00
2022-10-01 03:18:14 -06:00
else :
from sbapp . sideband . core import SidebandCore
2022-09-28 05:12:14 -06:00
2022-10-01 03:18:14 -06:00
class SidebandService ( ) :
2023-10-14 17:10:10 -06:00
usb_device_filter = {
0x0403 : [ 0x6001 , 0x6010 , 0x6011 , 0x6014 , 0x6015 ] , # FTDI
0x10C4 : [ 0xea60 , 0xea70 , 0xea71 ] , # SiLabs
0x067B : [ 0x2303 , 0x23a3 , 0x23b3 , 0x23c3 , 0x23d3 , 0x23e3 , 0x23f3 ] , # Prolific
0x1a86 : [ 0x5523 , 0x7523 , 0x55D4 ] , # Qinheng
0x0483 : [ 0x5740 ] , # ST CDC
0x2E8A : [ 0x0005 , 0x000A ] , # Raspberry Pi Pico
2024-07-08 04:54:15 -06:00
0x239A : [ 0x8029 ] , # Adafruit (RAK4631)
2024-08-30 07:55:19 -06:00
0x303A : [ 0x1001 ] , # ESP-32S3
2023-10-14 17:10:10 -06:00
}
2023-12-02 18:08:04 -07:00
2022-10-01 17:14:29 -06:00
def android_notification ( self , title = " " , content = " " , ticker = " " , group = None , context_id = None ) :
2022-10-20 07:36:29 -06:00
if android_api_version < 26 :
return
2022-10-01 17:14:29 -06:00
else :
2022-10-20 07:36:29 -06:00
package_name = " io.unsigned.sideband "
if not self . notification_service :
self . notification_service = cast ( NotificationManager , self . app_context . getSystemService (
Context . NOTIFICATION_SERVICE
) )
channel_id = package_name
group_id = " "
if group != None :
channel_id + = " . " + str ( group )
group_id + = str ( group )
if context_id != None :
channel_id + = " . " + str ( context_id )
group_id + = " . " + str ( context_id )
if not title or title == " " :
channel_name = " Sideband "
else :
channel_name = title
self . notification_channel = NotificationChannel ( channel_id , channel_name , NotificationManager . IMPORTANCE_DEFAULT )
self . notification_channel . enableVibration ( True )
2024-09-21 16:10:40 -06:00
self . notification_channel . setSound ( RingtoneManager . getDefaultUri ( RingtoneManager . TYPE_NOTIFICATION ) , None )
2022-10-20 07:36:29 -06:00
self . notification_channel . setShowBadge ( True )
self . notification_service . createNotificationChannel ( self . notification_channel )
notification = NotificationBuilder ( self . app_context , channel_id )
notification . setContentTitle ( title )
notification . setContentText ( AndroidString ( content ) )
2024-09-21 16:10:40 -06:00
notification . setSound ( RingtoneManager . getDefaultUri ( RingtoneManager . TYPE_NOTIFICATION ) )
2022-10-20 07:36:29 -06:00
# if group != None:
# notification.setGroup(group_id)
if not self . notification_small_icon :
2024-09-21 16:10:40 -06:00
# path = self.sideband.notification_icon
path = self . sideband . notif_icon_black
2022-10-20 07:36:29 -06:00
bitmap = BitmapFactory . decodeFile ( path )
self . notification_small_icon = Icon . createWithBitmap ( bitmap )
notification . setSmallIcon ( self . notification_small_icon )
2024-09-21 16:10:40 -06:00
# notification.setLargeIcon(self.notification_small_icon)
2022-10-20 07:36:29 -06:00
# large_icon_path = self.sideband.icon
# bitmap_icon = BitmapFactory.decodeFile(large_icon_path)
# notification.setLargeIcon(bitmap_icon)
2024-09-21 16:36:35 -06:00
notification_intent = Intent ( self . app_context , python_act )
notification_intent . setFlags ( Intent . FLAG_ACTIVITY_SINGLE_TOP )
notification_intent . setAction ( Intent . ACTION_MAIN )
notification_intent . addCategory ( Intent . CATEGORY_LAUNCHER )
if context_id != None :
cstr = f " conversation. { context_id } "
notification_intent . putExtra ( JString ( " intent_action " ) , JString ( cstr ) )
self . notification_intent = PendingIntent . getActivity ( self . app_context , 0 , notification_intent , PendingIntent . FLAG_IMMUTABLE | PendingIntent . FLAG_UPDATE_CURRENT )
2022-10-20 07:36:29 -06:00
notification . setContentIntent ( self . notification_intent )
notification . setAutoCancel ( True )
built_notification = notification . build ( )
self . notification_service . notify ( 0 , built_notification )
2022-09-17 08:00:55 -06:00
2023-12-02 18:08:04 -07:00
def check_permission ( self , permission ) :
if RNS . vendor . platformutils . is_android ( ) :
try :
result = self . android_service . checkSelfPermission ( " android.permission. " + permission )
if result == 0 :
return True
except Exception as e :
RNS . log ( " Error while checking permission: " + str ( e ) , RNS . LOG_ERROR )
return False
else :
return False
def background_location_allowed ( self ) :
if not RNS . vendor . platformutils . is_android ( ) :
return False
perms = [ " ACCESS_FINE_LOCATION " , " ACCESS_COARSE_LOCATION " , " ACCESS_BACKGROUND_LOCATION " ]
for perm in perms :
if not self . check_permission ( perm ) :
return False
return True
2024-09-21 12:56:14 -06:00
def update_power_restrictions ( self ) :
package_name = " io.unsigned.sideband "
if RNS . vendor . platformutils . is_android ( ) :
if android_api_version > = 28 :
if self . power_manager != None :
if self . power_manager . isIgnoringBatteryOptimizations ( package_name ) :
self . power_restricted = False
else :
self . power_restricted = True
self . sideband . setstate ( " android.power_restricted " , self . power_restricted )
2023-12-02 18:08:04 -07:00
def android_location_callback ( self , * * kwargs ) :
self . _raw_gps = kwargs
self . _last_gps_update = time . time ( )
def should_update_location ( self ) :
if self . sideband . config [ " telemetry_enabled " ] and self . sideband . config [ " telemetry_s_location " ] and self . background_location_allowed ( ) :
return True
else :
return False
def update_location_provider ( self ) :
if RNS . vendor . platformutils . is_android ( ) :
if self . should_update_location ( ) :
if not self . _gps_started :
RNS . log ( " Starting service location provider " , RNS . LOG_DEBUG )
if self . gps == None :
from plyer import gps
self . gps = gps
self . gps . configure ( on_location = self . android_location_callback )
self . gps . start ( minTime = self . _gps_stale_time , minDistance = self . _gps_min_distance )
self . _gps_started = True
else :
if self . _gps_started :
RNS . log ( " Stopping service location provider " , RNS . LOG_DEBUG )
if self . gps != None :
self . gps . stop ( )
self . _gps_started = False
self . _raw_gps = None
def get_location ( self ) :
return self . _last_gps_update , self . _raw_gps
2022-09-17 08:00:55 -06:00
def __init__ ( self ) :
2022-09-20 09:28:39 -06:00
self . argument = environ . get ( ' PYTHON_SERVICE_ARGUMENT ' , ' ' )
2022-10-01 03:18:14 -06:00
self . app_dir = self . argument
2022-09-20 09:28:39 -06:00
self . multicast_lock = None
self . wake_lock = None
2022-10-01 03:18:14 -06:00
self . should_run = False
2023-12-02 18:08:04 -07:00
self . gps = None
self . _gps_started = False
2023-12-04 03:16:51 -07:00
self . _gps_stale_time = 300 - 1
self . _gps_min_distance = 3
2023-12-02 18:08:04 -07:00
self . _raw_gps = None
2022-09-20 09:28:39 -06:00
2022-10-01 03:18:14 -06:00
self . android_service = None
self . app_context = None
self . wifi_manager = None
2022-10-01 18:26:58 -06:00
self . power_manager = None
2024-09-21 12:56:14 -06:00
self . power_restricted = False
2022-10-11 06:20:01 -06:00
self . usb_devices = [ ]
2023-10-14 17:10:10 -06:00
self . usb_device_filter = SidebandService . usb_device_filter
2022-10-01 03:18:14 -06:00
2022-10-01 17:14:29 -06:00
self . notification_service = None
self . notification_channel = None
self . notification_intent = None
self . notification_small_icon = None
2022-10-11 06:20:01 -06:00
if RNS . vendor . platformutils . is_android ( ) :
2022-10-01 03:18:14 -06:00
self . android_service = autoclass ( ' org.kivy.android.PythonService ' ) . mService
self . app_context = self . android_service . getApplication ( ) . getApplicationContext ( )
2024-03-19 04:55:52 -06:00
try :
self . wifi_manager = self . app_context . getSystemService ( Context . WIFI_SERVICE )
except Exception as e :
self . wifi_manager = None
RNS . log ( " Could not acquire Android WiFi Manager! Keeping WiFi-based interfaces up will be unavailable. " , RNS . LOG_ERROR )
RNS . log ( " The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
try :
self . power_manager = self . app_context . getSystemService ( Context . POWER_SERVICE )
except Exception as e :
self . power_manager = None
RNS . log ( " Could not acquire Android Power Manager! Taking wakelocks and keeping the CPU running will be unavailable. " , RNS . LOG_ERROR )
RNS . log ( " The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2022-10-11 06:20:01 -06:00
self . discover_usb_devices ( )
2022-09-20 09:28:39 -06:00
2023-12-02 18:08:04 -07:00
self . sideband = SidebandCore ( self , is_service = True , android_app_dir = self . app_dir , verbose = __debug_build__ , owner_service = self , service_context = self . android_service )
2022-12-18 18:06:41 -07:00
if self . sideband . config [ " debug " ] :
Logger . setLevel ( LOG_LEVELS [ " debug " ] )
2022-10-01 03:18:14 -06:00
self . sideband . start ( )
2022-10-02 12:43:56 -06:00
self . update_connectivity_type ( )
2024-09-21 12:56:14 -06:00
self . update_power_restrictions ( )
2022-10-11 06:20:01 -06:00
if RNS . vendor . platformutils . is_android ( ) :
2022-12-22 10:21:38 -07:00
RNS . log ( " Discovered USB devices: " + str ( self . usb_devices ) , RNS . LOG_EXTREME )
2023-12-02 18:08:04 -07:00
self . update_location_provider ( )
2022-10-11 06:20:01 -06:00
def discover_usb_devices ( self ) :
self . usb_devices = [ ]
2022-12-22 10:21:38 -07:00
RNS . log ( " Discovering attached USB devices... " , RNS . LOG_EXTREME )
2022-10-11 06:20:01 -06:00
try :
devices = usb . get_usb_device_list ( )
for device in devices :
device_entry = {
" port " : device . getDeviceName ( ) ,
" vid " : device . getVendorId ( ) ,
" pid " : device . getProductId ( ) ,
" manufacturer " : device . getManufacturerName ( ) ,
" productname " : device . getProductName ( ) ,
}
2023-10-14 17:10:10 -06:00
if device_entry [ " vid " ] in self . usb_device_filter :
if device_entry [ " pid " ] in self . usb_device_filter [ device_entry [ " vid " ] ] :
self . usb_devices . append ( device_entry )
2022-10-11 06:20:01 -06:00
except Exception as e :
RNS . log ( " Could not list USB devices. The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2022-10-01 03:18:14 -06:00
def start ( self ) :
self . should_run = True
2022-09-20 09:28:39 -06:00
self . take_locks ( )
2022-09-17 08:00:55 -06:00
self . run ( )
2022-10-01 03:18:14 -06:00
def stop ( self ) :
self . should_run = False
2022-12-22 10:21:38 -07:00
def take_locks ( self , force_multicast = False ) :
2022-10-01 03:18:14 -06:00
if RNS . vendor . platformutils . get_platform ( ) == " android " :
2022-12-22 10:21:38 -07:00
if self . multicast_lock == None or force_multicast :
2024-03-19 04:55:52 -06:00
if self . wifi_manager != None :
RNS . log ( " Creating multicast lock " , RNS . LOG_DEBUG )
self . multicast_lock = self . wifi_manager . createMulticastLock ( " sideband_service " )
2022-09-20 09:28:39 -06:00
2024-03-19 04:55:52 -06:00
if self . multicast_lock != None :
if not self . multicast_lock . isHeld ( ) :
RNS . log ( " Taking multicast lock " , RNS . LOG_DEBUG )
self . multicast_lock . acquire ( )
else :
RNS . log ( " Multicast lock already held " , RNS . LOG_DEBUG )
2022-09-20 09:28:39 -06:00
2022-10-01 18:26:58 -06:00
if self . wake_lock == None :
2024-03-19 04:55:52 -06:00
if self . power_manager != None :
RNS . log ( " Creating wake lock " , RNS . LOG_DEBUG )
self . wake_lock = self . power_manager . newWakeLock ( self . power_manager . PARTIAL_WAKE_LOCK , " sideband_service " )
if self . wake_lock != None :
if not self . wake_lock . isHeld ( ) :
RNS . log ( " Taking wake lock " , RNS . LOG_DEBUG )
self . wake_lock . acquire ( )
else :
RNS . log ( " Wake lock already held " , RNS . LOG_DEBUG )
2022-10-01 18:26:58 -06:00
def release_locks ( self ) :
2022-10-01 03:18:14 -06:00
if RNS . vendor . platformutils . get_platform ( ) == " android " :
if not self . multicast_lock == None and self . multicast_lock . isHeld ( ) :
2022-10-02 02:58:00 -06:00
RNS . log ( " Releasing multicast lock " )
2022-10-01 03:18:14 -06:00
self . multicast_lock . release ( )
2022-09-20 09:28:39 -06:00
2022-10-01 18:26:58 -06:00
if not self . wake_lock == None and self . wake_lock . isHeld ( ) :
2022-10-02 02:58:00 -06:00
RNS . log ( " Releasing wake lock " )
2022-10-01 18:26:58 -06:00
self . wake_lock . release ( )
2022-10-02 12:43:56 -06:00
def update_connectivity_type ( self ) :
if self . sideband . reticulum . is_connected_to_shared_instance :
is_controlling = False
else :
is_controlling = True
self . sideband . setpersistent ( " service.is_controlling_connectivity " , is_controlling )
def get_connectivity_status ( self ) :
if self . sideband . reticulum . is_connected_to_shared_instance :
return " [size=22dp][b]Connectivity Status[/b][/size] \n \n Sideband is connected via a shared Reticulum instance running on this system. Use the rnstatus utility to obtain full connectivity info. "
else :
ws = " Disabled "
ts = " Disabled "
i2s = " Disabled "
2022-11-23 02:38:56 -07:00
# stat = "[size=22dp][b]Connectivity Status[/b][/size]\n\n"
stat = " "
2022-10-13 14:40:48 -06:00
2022-10-02 12:43:56 -06:00
if self . sideband . interface_local != None :
2022-10-19 03:58:54 -06:00
netdevs = self . sideband . interface_local . adopted_interfaces
if len ( netdevs ) > 0 :
ds = " Using "
for netdev in netdevs :
ds + = " [i] " + str ( netdev ) + " [/i], "
ds = ds [ : - 2 ]
else :
ds = " No usable network devices "
2022-10-02 12:43:56 -06:00
np = len ( self . sideband . interface_local . peers )
if np == 1 :
ws = " 1 reachable peer "
else :
ws = str ( np ) + " reachable peers "
2022-10-19 03:58:54 -06:00
stat + = " [b]Local[/b] \n {ds} \n {ws} \n \n " . format ( ds = ds , ws = ws )
2022-10-13 14:40:48 -06:00
if self . sideband . interface_rnode != None :
if self . sideband . interface_rnode . online :
rs = " On-air at " + str ( self . sideband . interface_rnode . bitrate_kbps ) + " Kbps "
else :
rs = " Interface Down "
2024-10-01 07:46:19 -06:00
bs = " "
bat_state = self . sideband . interface_rnode . get_battery_state_string ( )
bat_percent = self . sideband . interface_rnode . get_battery_percent ( )
if bat_state != " unknown " :
bs = f " \n Battery at { bat_percent } % "
stat + = f " [b]RNode[/b] \n { rs } { bs } \n \n "
2022-10-13 14:40:48 -06:00
2022-10-15 08:39:08 -06:00
if self . sideband . interface_modem != None :
if self . sideband . interface_modem . online :
rm = " Connected "
else :
rm = " Interface Down "
stat + = " [b]Radio Modem[/b] \n {rm} \n \n " . format ( rm = rm )
if self . sideband . interface_serial != None :
if self . sideband . interface_serial . online :
rs = " Running at " + RNS . prettysize ( self . sideband . interface_serial . bitrate / 8 , suffix = " b " ) + " ps "
else :
rs = " Interface Down "
stat + = " [b]Serial Port[/b] \n {rs} \n \n " . format ( rs = rs )
2022-10-02 12:43:56 -06:00
if self . sideband . interface_tcp != None :
if self . sideband . interface_tcp . online :
ts = " Connected to " + str ( self . sideband . interface_tcp . target_ip ) + " : " + str ( self . sideband . interface_tcp . target_port )
else :
ts = " Interface Down "
2022-10-13 14:40:48 -06:00
stat + = " [b]TCP[/b] \n {ts} \n \n " . format ( ts = ts )
2022-10-02 12:43:56 -06:00
if self . sideband . interface_i2p != None :
2022-11-03 12:52:27 -06:00
i2s = " Unknown "
if hasattr ( self . sideband . interface_i2p , " i2p_tunnel_state " ) and self . sideband . interface_i2p . i2p_tunnel_state != None :
if self . sideband . interface_i2p . i2p_tunnel_state == RNS . Interfaces . I2PInterface . I2PInterfacePeer . TUNNEL_STATE_INIT :
i2s = " Tunnel Connecting "
elif self . sideband . interface_i2p . i2p_tunnel_state == RNS . Interfaces . I2PInterface . I2PInterfacePeer . TUNNEL_STATE_ACTIVE :
i2s = " Tunnel Active "
elif self . sideband . interface_i2p . i2p_tunnel_state == RNS . Interfaces . I2PInterface . I2PInterfacePeer . TUNNEL_STATE_STALE :
i2s = " Tunnel Unresponsive "
2022-10-02 12:43:56 -06:00
else :
2022-11-03 12:52:27 -06:00
if self . sideband . interface_i2p . online :
i2s = " Connected "
else :
i2s = " Connecting to I2P "
2022-10-02 12:43:56 -06:00
2022-10-15 11:12:39 -06:00
stat + = " [b]I2P[/b] \n {i2s} \n \n " . format ( i2s = i2s )
total_rxb = 0
total_txb = 0
if self . sideband . interface_local != None :
total_rxb + = self . sideband . interface_local . rxb
total_txb + = self . sideband . interface_local . txb
if self . sideband . interface_rnode != None :
total_rxb + = self . sideband . interface_rnode . rxb
total_txb + = self . sideband . interface_rnode . txb
if self . sideband . interface_modem != None :
total_rxb + = self . sideband . interface_modem . rxb
total_txb + = self . sideband . interface_modem . txb
if self . sideband . interface_serial != None :
total_rxb + = self . sideband . interface_serial . rxb
total_txb + = self . sideband . interface_serial . txb
if self . sideband . interface_tcp != None :
total_rxb + = self . sideband . interface_tcp . rxb
total_txb + = self . sideband . interface_tcp . txb
if self . sideband . interface_i2p != None :
total_rxb + = self . sideband . interface_i2p . rxb
total_txb + = self . sideband . interface_i2p . txb
if RNS . Reticulum . transport_enabled ( ) :
stat + = " [b]Transport Instance[/b] \n Routing Traffic \n \n "
stat + = " [b]Traffic[/b] \n In: {inb} \n Out: {outb} \n \n " . format ( inb = RNS . prettysize ( total_rxb ) , outb = RNS . prettysize ( total_txb ) )
if stat . endswith ( " \n \n " ) :
stat = stat [ : - 2 ]
2022-10-13 14:40:48 -06:00
return stat
2022-10-02 12:43:56 -06:00
2022-09-17 08:00:55 -06:00
def run ( self ) :
2022-10-01 03:18:14 -06:00
while self . should_run :
2022-10-01 18:26:58 -06:00
sleep_time = 1
2022-10-01 17:31:47 -06:00
self . sideband . setstate ( " service.heartbeat " , time . time ( ) )
2022-10-02 12:43:56 -06:00
self . sideband . setstate ( " service.connectivity_status " , self . get_connectivity_status ( ) )
2022-10-01 18:26:58 -06:00
if self . sideband . getstate ( " wants.service_stop " ) :
2023-11-04 12:42:06 -06:00
self . sideband . service_stopped = True
2022-10-01 18:26:58 -06:00
self . should_run = False
sleep_time = 0
2022-10-02 04:15:37 -06:00
if self . sideband . getstate ( " wants.clear_notifications " ) :
self . sideband . setstate ( " wants.clear_notifications " , False )
if self . notification_service != None :
self . notification_service . cancelAll ( )
2022-10-02 04:45:06 -06:00
if self . sideband . getstate ( " wants.settings_reload " ) :
self . sideband . setstate ( " wants.settings_reload " , False )
self . sideband . reload_configuration ( )
2022-10-01 18:26:58 -06:00
time . sleep ( sleep_time )
2022-10-01 03:18:14 -06:00
2022-10-29 08:39:54 -06:00
self . sideband . cleanup ( )
2022-10-01 03:18:14 -06:00
self . release_locks ( )
2023-10-28 14:59:52 -06:00
def handle_exception ( exc_type , exc_value , exc_traceback ) :
if issubclass ( exc_type , KeyboardInterrupt ) :
sys . __excepthook__ ( exc_type , exc_value , exc_traceback )
return
if exc_type == SystemExit :
sys . __excepthook__ ( exc_type , exc_value , exc_traceback )
return
import traceback
exc_text = " " . join ( traceback . format_exception ( exc_type , exc_value , exc_traceback ) )
RNS . log ( f " An unhandled { str ( exc_type ) } exception occurred: { str ( exc_value ) } " , RNS . LOG_ERROR )
RNS . log ( exc_text , RNS . LOG_ERROR )
sys . excepthook = handle_exception
2022-10-15 15:16:44 -06:00
SidebandService ( ) . start ( )