diff --git a/config.template b/config.template index e247481..3e34d86 100644 --- a/config.template +++ b/config.template @@ -12,7 +12,7 @@ port = /dev/ttyACM0 # hostname = localhost # mac = 00:11:22:33:44:55 -# Additional interface for dual radio support +# Additional interface for multi radio support [interface2] enabled = False type = serial @@ -22,6 +22,8 @@ port = /dev/ttyUSB0 # hostname = meshtastic.local # mac = 00:11:22:33:44:55 +# example, the third interface would be [interface3] up to 9 + [general] # if False will respond on all channels but the default channel respond_by_dm_only = True @@ -91,7 +93,7 @@ emailSentryAlerts = False # radius in meters to detect someone close to the bot SentryRadius = 100 # channel to send a message to when the watchdog is triggered -SentryChannel = 9 +SentryChannel = 2 # holdoff time multiplied by seconds(20) of the watchdog SentryHoldoff = 9 # list of ignored nodes numbers ex: 2813308004,4258675309 diff --git a/logs/README.md b/logs/README.md index e18eccc..1413dc4 100644 --- a/logs/README.md +++ b/logs/README.md @@ -23,4 +23,7 @@ To change the stdout (what you see on the console) logging level (default is DEB ``` # Set level for stdout handler stdout_handler.setLevel(logging.INFO) -``` \ No newline at end of file +``` + +There is a web-server module you can run `python modules/web.py` from the project root directory and it will serve up the web content. +by default. http://localhost:8420 \ No newline at end of file diff --git a/mesh_bot.py b/mesh_bot.py index 0d6ccad..ee3085e 100755 --- a/mesh_bot.py +++ b/mesh_bot.py @@ -134,6 +134,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number): global multiPing + myNodeNum = globals().get(f'myNodeNum{deviceID}', 777) if "?" in message and isDM: return message.split("?")[0].title() + " command returns SNR and RSSI, or hopcount from your message. Try adding e.g. @place or #tag" @@ -153,10 +154,7 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann msg = random.choice(["✋ACK-ACK!\n", "✋Ack to you!\n"]) type = "✋ACK" elif "cqcq" in message.lower() or "cq" in message.lower() or "cqcqcq" in message.lower(): - if deviceID == 1: - myname = get_name_from_number(myNodeNum1, 'short', 1) - elif deviceID == 2: - myname = get_name_from_number(myNodeNum2, 'short', 2) + myname = get_name_from_number(myNodeNum, 'short', deviceID) msg = f"QSP QSL OM DE {myname} K\n" else: msg = "🔊 Can you hear me now?" @@ -221,6 +219,7 @@ def handle_alertBell(message_from_id, deviceID, message): return random.choice(msg) def handle_emergency(message_from_id, deviceID, message): + myNodeNum = globals().get(f'myNodeNum{deviceID}', 777) # if user in bbs_ban_list return if str(message_from_id) in bbs_ban_list: # silent discard @@ -228,13 +227,11 @@ def handle_emergency(message_from_id, deviceID, message): return '' # trgger alert to emergency_responder_alert_channel if message_from_id != 0: - if deviceID == 1: rxNode = myNodeNum1 - elif deviceID == 2: rxNode = myNodeNum2 nodeLocation = get_node_location(message_from_id, deviceID) # if default location is returned set to Unknown if nodeLocation[0] == latitudeValue and nodeLocation[1] == longitudeValue: nodeLocation = ["?", "?"] - nodeInfo = f"{get_name_from_number(message_from_id, 'short', deviceID)} detected by {get_name_from_number(rxNode, 'short', deviceID)} lastGPS {nodeLocation[0]}, {nodeLocation[1]}" + nodeInfo = f"{get_name_from_number(message_from_id, 'short', deviceID)} detected by {get_name_from_number(myNodeNum, 'short', deviceID)} lastGPS {nodeLocation[0]}, {nodeLocation[1]}" msg = f"🔔🚨Intercepted Possible Emergency Assistance needed for: {nodeInfo}" # alert the emergency_responder_alert_channel time.sleep(responseDelay) @@ -1048,7 +1045,7 @@ def onReceive(packet, interface): message_string = message_bytes.decode('utf-8') # check if the packet is from us - if message_from_id == myNodeNum1 or message_from_id == myNodeNum2: + if message_from_id in [myNodeNum1, myNodeNum2, myNodeNum3, myNodeNum4, myNodeNum5, myNodeNum6, myNodeNum7, myNodeNum8, myNodeNum9]: logger.warning(f"System: Packet from self {message_from_id} loop or traffic replay deteted") # get the signal strength and snr if available @@ -1110,7 +1107,7 @@ def onReceive(packet, interface): return # If the packet is a DM (Direct Message) respond to it, otherwise validate its a message for us on the channel - if packet['to'] == myNodeNum1 or packet['to'] == myNodeNum2: + if packet['to'] in [myNodeNum1, myNodeNum2, myNodeNum3, myNodeNum4, myNodeNum5, myNodeNum6, myNodeNum7, myNodeNum8, myNodeNum9]: # message is DM to us isDM = True # check if the message contains a trap word, DMs are always responded to @@ -1229,18 +1226,22 @@ def onReceive(packet, interface): async def start_rx(): print (CustomFormatter.bold_white + f"\nMeshtastic Autoresponder Bot CTL+C to exit\n" + CustomFormatter.reset) - if llm_enabled: - logger.debug(f"System: Ollama LLM Enabled, loading model {llmModel} please wait") - llm_query(" ", myNodeNum1) - logger.debug(f"System: LLM model {llmModel} loaded") + # Start the receive subscriber using pubsub via meshtastic library pub.subscribe(onReceive, 'meshtastic.receive') pub.subscribe(onDisconnect, 'meshtastic.connection.lost') - logger.info(f"System: Autoresponder Started for Device1 {get_name_from_number(myNodeNum1, 'long', 1)}," - f"{get_name_from_number(myNodeNum1, 'short', 1)}. NodeID: {myNodeNum1}, {decimal_to_hex(myNodeNum1)}") - if interface2_enabled: - logger.info(f"System: Autoresponder Started for Device2 {get_name_from_number(myNodeNum2, 'long', 2)}," - f"{get_name_from_number(myNodeNum2, 'short', 2)}. NodeID: {myNodeNum2}, {decimal_to_hex(myNodeNum2)}") + + for i in range(1, 10): + if globals().get(f'interface{i}_enabled', False): + myNodeNum = globals().get(f'myNodeNum{i}', 0) + logger.info(f"System: Autoresponder Started for Device{i} {get_name_from_number(myNodeNum, 'long', i)}," + f"{get_name_from_number(myNodeNum, 'short', i)}. NodeID: {myNodeNum}, {decimal_to_hex(myNodeNum)}") + + if llm_enabled: + logger.debug(f"System: Ollama LLM Enabled, loading model {llmModel} please wait") + llm_query(" ") + logger.debug(f"System: LLM model {llmModel} loaded") + if log_messages_to_file: logger.debug("System: Logging Messages to disk") if syslog_to_file: diff --git a/modules/locationdata.py b/modules/locationdata.py index 421c821..78cd6d6 100644 --- a/modules/locationdata.py +++ b/modules/locationdata.py @@ -357,11 +357,10 @@ def getWeatherAlertsNOAA(lat=0, lon=0, useDefaultLatLon=False): alerts = "" alertxml = xml.dom.minidom.parseString(alert_data.text) - for i in alertxml.getElementsByTagName("entry"): - alerts += ( - i.getElementsByTagName("title")[0].childNodes[0].nodeValue + "\n" - ) + title = i.getElementsByTagName("title")[0].childNodes[0].nodeValue + area_desc = i.getElementsByTagName("cap:areaDesc")[0].childNodes[0].nodeValue + alerts += f"{title}. {area_desc.replace(' ', '')}\n" if alerts == "" or alerts == None: return NO_ALERTS diff --git a/modules/settings.py b/modules/settings.py index 034ae8c..0b1a0b0 100644 --- a/modules/settings.py +++ b/modules/settings.py @@ -96,6 +96,7 @@ interface1_type = config['interface'].get('type', 'serial') port1 = config['interface'].get('port', '') hostname1 = config['interface'].get('hostname', '') mac1 = config['interface'].get('mac', '') +interface1_enabled = True # gotta have at least one interface # interface2 settings if 'interface2' in config: @@ -107,6 +108,77 @@ if 'interface2' in config: else: interface2_enabled = False +# interface3 settings +if 'interface3' in config: + interface3_type = config['interface3'].get('type', 'serial') + port3 = config['interface3'].get('port', '') + hostname3 = config['interface3'].get('hostname', '') + mac3 = config['interface3'].get('mac', '') + interface3_enabled = config['interface3'].getboolean('enabled', False) +else: + interface3_enabled = False + +# interface4 settings +if 'interface4' in config: + interface4_type = config['interface4'].get('type', 'serial') + port4 = config['interface4'].get('port', '') + hostname4 = config['interface4'].get('hostname', '') + mac4 = config['interface4'].get('mac', '') + interface4_enabled = config['interface4'].getboolean('enabled', False) +else: + interface4_enabled = False + +# interface5 settings +if 'interface5' in config: + interface5_type = config['interface5'].get('type', 'serial') + port5 = config['interface5'].get('port', '') + hostname5 = config['interface5'].get('hostname', '') + mac5 = config['interface5'].get('mac', '') + interface5_enabled = config['interface5'].getboolean('enabled', False) +else: + interface5_enabled = False + +# interface6 settings +if 'interface6' in config: + interface6_type = config['interface6'].get('type', 'serial') + port6 = config['interface6'].get('port', '') + hostname6 = config['interface6'].get('hostname', '') + mac6 = config['interface6'].get('mac', '') + interface6_enabled = config['interface6'].getboolean('enabled', False) +else: + interface6_enabled = False + +# interface7 settings +if 'interface7' in config: + interface7_type = config['interface7'].get('type', 'serial') + port7 = config['interface7'].get('port', '') + hostname7 = config['interface7'].get('hostname', '') + mac7 = config['interface7'].get('mac', '') + interface7_enabled = config['interface7'].getboolean('enabled', False) +else: + interface7_enabled = False + +# interface8 settings +if 'interface8' in config: + interface8_type = config['interface8'].get('type', 'serial') + port8 = config['interface8'].get('port', '') + hostname8 = config['interface8'].get('hostname', '') + mac8 = config['interface8'].get('mac', '') + interface8_enabled = config['interface8'].getboolean('enabled', False) +else: + interface8_enabled = False + +# interface9 settings +if 'interface9' in config: + interface9_type = config['interface9'].get('type', 'serial') + port9 = config['interface9'].get('port', '') + hostname9 = config['interface9'].get('hostname', '') + mac9 = config['interface9'].get('mac', '') + interface9_enabled = config['interface9'].getboolean('enabled', False) +else: + interface9_enabled = False + + # variables from the config.ini file try: # general diff --git a/modules/system.py b/modules/system.py index 2e9f657..2b4b95b 100644 --- a/modules/system.py +++ b/modules/system.py @@ -209,62 +209,45 @@ if len(help_message) > 20: help_message = ", ".join(help_message) # BLE dual interface prevention -if interface1_type == 'ble' and interface2_type == 'ble': - logger.critical(f"System: BLE Interface1 and Interface2 cannot both be BLE. Exiting") +ble_count = sum(1 for i in range(1, 10) if globals().get(f'interface{i}_type') == 'ble') +if ble_count > 1: + logger.critical(f"System: Multiple BLE interfaces detected. Only one BLE interface is allowed. Exiting") exit() -#initialize_interfaces(): -# Interface1 Configuration -try: - logger.debug(f"System: Initializing Interface1") - if interface1_type == 'serial': - interface1 = meshtastic.serial_interface.SerialInterface(port1) - elif interface1_type == 'tcp': - interface1 = meshtastic.tcp_interface.TCPInterface(hostname1) - elif interface1_type == 'ble': - interface1 = meshtastic.ble_interface.BLEInterface(mac1) +# Initialize interfaces +logger.debug(f"System: Initializing Interfaces") +interface1 = interface2 = interface3 = interface4 = interface5 = interface6 = interface7 = interface8 = interface9 = None +retry_int1 = retry_int2 = retry_int3 = retry_int4 = retry_int5 = retry_int6 = retry_int7 = retry_int8 = retry_int9 = False +for i in range(1, 10): + interface_type = globals().get(f'interface{i}_type') + if not interface_type or interface_type == 'none' or globals().get(f'interface{i}_enabled') == False: + # no valid interface found + continue + try: + if globals().get(f'interface{i}_enabled'): + if interface_type == 'serial': + globals()[f'interface{i}'] = meshtastic.serial_interface.SerialInterface(globals().get(f'port{i}')) + elif interface_type == 'tcp': + globals()[f'interface{i}'] = meshtastic.tcp_interface.TCPInterface(globals().get(f'hostname{i}')) + elif interface_type == 'ble': + globals()[f'interface{i}'] = meshtastic.ble_interface.BLEInterface(globals().get(f'mac{i}')) + else: + logger.critical(f"System: Interface Type: {interface_type} not supported. Validate your config against config.template Exiting") + exit() + except Exception as e: + logger.critical(f"System: abort. Initializing Interface{i} {e}") + exit() + +# Get the node number of the devices, check if the devices are connected meshtastic devices +for i in range(1, 10): + if globals().get(f'interface{i}') and globals().get(f'interface{i}_enabled'): + try: + globals()[f'myNodeNum{i}'] = globals()[f'interface{i}'].getMyNodeInfo()['num'] + logger.debug(f"System: Initalized Radio Device{i} Node Number: {globals()[f'myNodeNum{i}']}") + except Exception as e: + logger.critical(f"System: critical error initializing interface{i} {e}") else: - logger.critical(f"System: Interface Type: {interface1_type} not supported. Validate your config against config.template Exiting") - exit() -except Exception as e: - logger.critical(f"System: script abort. Initializing Interface1 {e}") - exit() - -# Interface2 Configuration -if interface2_enabled: - logger.debug(f"System: Initializing Interface2") - try: - if interface2_type == 'serial': - interface2 = meshtastic.serial_interface.SerialInterface(port2) - elif interface2_type == 'tcp': - interface2 = meshtastic.tcp_interface.TCPInterface(hostname2) - elif interface2_type == 'ble': - interface2 = meshtastic.ble_interface.BLEInterface(mac2) - else: - logger.critical(f"System: Interface Type: {interface2_type} not supported. Validate your config against config.template Exiting") - exit() - except Exception as e: - logger.critical(f"System: script abort. Initializing Interface2 {e}") - exit() - -#Get the node number of the device, check if the device is connected -try: - myinfo = interface1.getMyNodeInfo() - myNodeNum1 = myinfo['num'] -except Exception as e: - logger.critical(f"System: script abort. {e}") - exit() - -if interface2_enabled: - try: - myinfo2 = interface2.getMyNodeInfo() - myNodeNum2 = myinfo2['num'] - except Exception as e: - logger.critical(f"System: script abort. {e}") - exit() -else: - myNodeNum2 = 777 - + globals()[f'myNodeNum{i}'] = 777 #### FUN-ctions #### @@ -272,7 +255,7 @@ def decimal_to_hex(decimal_number): return f"!{decimal_number:08x}" def get_name_from_number(number, type='long', nodeInt=1): - interface = interface1 if nodeInt == 1 else interface2 + interface = globals()[f'interface{nodeInt}'] name = "" for node in interface.nodes.values(): @@ -289,7 +272,7 @@ def get_name_from_number(number, type='long', nodeInt=1): def get_num_from_short_name(short_name, nodeInt=1): - interface = interface1 if nodeInt == 1 else interface2 + interface = globals()[f'interface{nodeInt}'] # Get the node number from the short name, converting all to lowercase for comparison (good practice?) logger.debug(f"System: Getting Node Number from Short Name: {short_name} on Device: {nodeInt}") for node in interface.nodes.values(): @@ -299,6 +282,7 @@ def get_num_from_short_name(short_name, nodeInt=1): elif str(short_name.lower()) == node['user']['shortName'].lower(): return node['num'] else: + # TODO check the other interface if interface2_enabled: interface = interface2 if nodeInt == 1 else interface1 # check the other interface for node in interface.nodes.values(): @@ -309,7 +293,7 @@ def get_num_from_short_name(short_name, nodeInt=1): return 0 def get_node_list(nodeInt=1): - interface = interface1 if nodeInt == 1 else interface2 + interface = globals()[f'interface{nodeInt}'] # Get a list of nodes on the device node_list = "" node_list1 = [] @@ -319,7 +303,7 @@ def get_node_list(nodeInt=1): if interface.nodes: for node in interface.nodes.values(): # ignore own - if node['num'] != myNodeNum2 and node['num'] != myNodeNum1: + if all(node['num'] != globals().get(f'myNodeNum{i}') for i in range(1, 10)): node_name = get_name_from_number(node['num'], 'short', nodeInt) snr = node.get('snr', 0) @@ -363,7 +347,7 @@ def get_node_list(nodeInt=1): return node_list def get_node_location(number, nodeInt=1, channel=0): - interface = interface1 if nodeInt == 1 else interface2 + interface = globals()[f'interface{nodeInt}'] # Get the location of a node by its number from nodeDB on device latitude = latitudeValue longitude = longitudeValue @@ -396,7 +380,7 @@ def get_node_location(number, nodeInt=1, channel=0): def get_closest_nodes(nodeInt=1,returnCount=3): - interface = interface1 if nodeInt == 1 else interface2 + interface = globals()[f'interface{nodeInt}'] node_list = [] if interface.nodes: @@ -417,7 +401,7 @@ def get_closest_nodes(nodeInt=1,returnCount=3): distance = round(geopy.distance.geodesic((latitudeValue, longitudeValue), (latitude, longitude)).m, 2) if (distance < sentry_radius): - if nodeID != myNodeNum1 and myNodeNum2 and str(nodeID) not in sentryIgnoreList: + if (nodeID not in [globals().get(f'myNodeNum{i}') for i in range(1, 10)]) and str(nodeID) not in sentryIgnoreList: node_list.append({'id': nodeID, 'latitude': latitude, 'longitude': longitude, 'distance': distance}) except Exception as e: @@ -510,7 +494,7 @@ def messageChunker(message): def send_message(message, ch, nodeid=0, nodeInt=1, bypassChuncking=False): # Send a message to a channel or DM - interface = interface1 if nodeInt == 1 else interface2 + interface = globals()[f'interface{nodeInt}'] # Check if the message is empty if message == "" or message == None or len(message) == 0: return False @@ -730,30 +714,18 @@ def handleAlertBroadcast(deviceID=1): return True def onDisconnect(interface): - global retry_int1, retry_int2 + global retry_int1, retry_int2, retry_int3, retry_int4, retry_int5, retry_int6, retry_int7, retry_int8, retry_int9 rxType = type(interface).__name__ - if rxType == 'SerialInterface': - rxInterface = interface.__dict__.get('devPath', 'unknown') - logger.critical("System: Lost Connection to Device {rxInterface}") - if port1 in rxInterface: - retry_int1 = True - elif interface2_enabled and port2 in rxInterface: - retry_int2 = True - - if rxType == 'TCPInterface': - rxHost = interface.__dict__.get('hostname', 'unknown') - logger.critical("System: Lost Connection to Device {rxHost}") - if hostname1 in rxHost and interface1_type == 'tcp': - retry_int1 = True - elif interface2_enabled and hostname2 in rxHost and interface2_type == 'tcp': - retry_int2 = True - - if rxType == 'BLEInterface': - logger.critical("System: Lost Connection to Device BLE") - if interface1_type == 'ble': - retry_int1 = True - elif interface2_enabled and interface2_type == 'ble': - retry_int2 = True + if rxType in ['SerialInterface', 'TCPInterface', 'BLEInterface']: + identifier = interface.__dict__.get('devPath', interface.__dict__.get('hostname', 'BLE')) + logger.critical(f"System: Lost Connection to Device {identifier}") + for i in range(1, 10): + if globals().get(f'interface{i}_enabled'): + if (rxType == 'SerialInterface' and globals().get(f'port{i}') in identifier) or \ + (rxType == 'TCPInterface' and globals().get(f'hostname{i}') in identifier) or \ + (rxType == 'BLEInterface' and globals().get(f'interface{i}_type') == 'ble'): + globals()[f'retry_int{i}'] = True + break def exit_handler(): # Close the interface and save the BBS messages @@ -778,14 +750,16 @@ def exit_handler(): # Telemetry Functions telemetryData = {} def initialize_telemetryData(): - telemetryData[0] = {'interface1': 0, 'interface2': 0, 'lastAlert1': '', 'lastAlert2': ''} - telemetryData[1] = {'numPacketsTx': 0, 'numPacketsRx': 0, 'numOnlineNodes': 0, 'numPacketsTxErr': 0, 'numPacketsRxErr': 0, 'numTotalNodes': 0} - telemetryData[2] = {'numPacketsTx': 0, 'numPacketsRx': 0, 'numOnlineNodes': 0, 'numPacketsTxErr': 0, 'numPacketsRxErr': 0, 'numTotalNodes': 0} + telemetryData[0] = {f'interface{i}': 0 for i in range(1, 10)} + telemetryData[0].update({f'lastAlert{i}': '' for i in range(1, 10)}) + for i in range(1, 10): + telemetryData[i] = {'numPacketsTx': 0, 'numPacketsRx': 0, 'numOnlineNodes': 0, 'numPacketsTxErr': 0, 'numPacketsRxErr': 0, 'numTotalNodes': 0} + # indented to be called from the main loop initialize_telemetryData() def getNodeFirmware(nodeID=0, nodeInt=1): - interface = interface1 if nodeInt == 1 else interface2 + interface = globals()[f'interface{nodeInt}'] # get the firmware version of the node # this is a workaround because .localNode.getMetadata spits out a lot of debug info which cant be suppressed # Create a StringIO object to capture the @@ -799,18 +773,15 @@ def getNodeFirmware(nodeID=0, nodeInt=1): return -1 def displayNodeTelemetry(nodeID=0, rxNode=0, userRequested=False): - interface = interface1 if rxNode == 1 else interface2 + interface = globals()[f'interface{rxNode}'] + myNodeNum = globals().get(f'myNodeNum{rxNode}') global telemetryData # throttle the telemetry requests to prevent spamming the device - if rxNode == 1: - if time.time() - telemetryData[0]['interface1'] < 600 and not userRequested: + if 1 <= rxNode <= 9: + if time.time() - telemetryData[0][f'interface{rxNode}'] < 600 and not userRequested: return -1 - telemetryData[0]['interface1'] = time.time() - elif rxNode == 2: - if time.time() - telemetryData[0]['interface2'] < 600 and not userRequested: - return -1 - telemetryData[0]['interface2'] = time.time() + telemetryData[0][f'interface{rxNode}'] = time.time() # some telemetry data is not available in python-meshtastic? # bring in values from the last telemetry dump for the node @@ -822,13 +793,13 @@ def displayNodeTelemetry(nodeID=0, rxNode=0, userRequested=False): totalOnlineNodes = telemetryData[rxNode]['numOnlineNodes'] # get the telemetry data for a node - chutil = round(interface.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("channelUtilization", 0), 1) - airUtilTx = round(interface.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("airUtilTx", 0), 1) - uptimeSeconds = interface.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("uptimeSeconds", 0) - batteryLevel = interface.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("batteryLevel", 0) - voltage = interface.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("voltage", 0) - #numPacketsRx = interface.nodes.get(decimal_to_hex(myNodeNum1), {}).get("localStats", {}).get("numPacketsRx", 0) - #numPacketsTx = interface.nodes.get(decimal_to_hex(myNodeNum1), {}).get("localStats", {}).get("numPacketsTx", 0) + chutil = round(interface.nodes.get(decimal_to_hex(myNodeNum), {}).get("deviceMetrics", {}).get("channelUtilization", 0), 1) + airUtilTx = round(interface.nodes.get(decimal_to_hex(myNodeNum), {}).get("deviceMetrics", {}).get("airUtilTx", 0), 1) + uptimeSeconds = interface.nodes.get(decimal_to_hex(myNodeNum), {}).get("deviceMetrics", {}).get("uptimeSeconds", 0) + batteryLevel = interface.nodes.get(decimal_to_hex(myNodeNum), {}).get("deviceMetrics", {}).get("batteryLevel", 0) + voltage = interface.nodes.get(decimal_to_hex(myNodeNum), {}).get("deviceMetrics", {}).get("voltage", 0) + #numPacketsRx = interface.nodes.get(decimal_to_hex(myNodeNum), {}).get("localStats", {}).get("numPacketsRx", 0) + #numPacketsTx = interface.nodes.get(decimal_to_hex(myNodeNum), {}).get("localStats", {}).get("numPacketsTx", 0) numTotalNodes = len(interface.nodes) dataResponse = f"Telemetry:{rxNode}" @@ -1033,157 +1004,124 @@ async def handleFileWatcher(): await asyncio.sleep(1) pass -async def retry_interface(nodeID=1): - global interface1, interface2, retry_int1, retry_int2, max_retry_count1, max_retry_count2 - interface = interface1 if nodeID == 1 else interface2 - retry_int = retry_int1 if nodeID == 1 else retry_int2 - # retry connecting to the interface - # add a check to see if the interface is already open or trying to open +async def retry_interface(nodeID): + global max_retry_count + interface = globals()[f'interface{nodeID}'] + retry_int = globals()[f'retry_int{nodeID}'] + max_retry_count = globals()[f'max_retry_count{nodeID}'] + if interface is not None: retry_int = True - max_retry_count1 -= 1 + max_retry_count -= 1 try: interface.close() except Exception as e: logger.error(f"System: closing interface{nodeID}: {e}") - + logger.debug(f"System: Retrying interface{nodeID} in 15 seconds") - if max_retry_count1 == 0: - logger.critical(f"System: Max retry count reached for interface1") + if max_retry_count == 0: + logger.critical(f"System: Max retry count reached for interface{nodeID}") exit_handler() - if max_retry_count2 == 0: - logger.critical(f"System: Max retry count reached for interface2") - exit_handler() - # wait 15 seconds before retrying + await asyncio.sleep(15) - # retry the interface try: if retry_int: interface = None - if nodeID == 1: - interface1 = None - if nodeID == 2: - interface2 = None + globals()[f'interface{nodeID}'] = None logger.debug(f"System: Retrying Interface{nodeID}") - interface_type = interface1_type if nodeID == 1 else interface2_type + interface_type = globals()[f'interface{nodeID}_type'] if interface_type == 'serial': - interface1 = meshtastic.serial_interface.SerialInterface(port1) + globals()[f'interface{nodeID}'] = meshtastic.serial_interface.SerialInterface(globals().get(f'port{nodeID}')) elif interface_type == 'tcp': - interface1 = meshtastic.tcp_interface.TCPInterface(hostname1) + globals()[f'interface{nodeID}'] = meshtastic.tcp_interface.TCPInterface(globals().get(f'hostname{nodeID}')) elif interface_type == 'ble': - interface1 = meshtastic.ble_interface.BLEInterface(mac1) - logger.debug(f"System: Interface1 Opened!") - retry_int1 = False + globals()[f'interface{nodeID}'] = meshtastic.ble_interface.BLEInterface(globals().get(f'mac{nodeID}')) + logger.debug(f"System: Interface{nodeID} Opened!") + globals()[f'retry_int{nodeID}'] = False except Exception as e: logger.error(f"System: Error Opening interface{nodeID} on: {e}") - -handleSentinel_spotted = "" +handleSentinel_spotted = [] handleSentinel_loop = 0 -async def handleSentinel(deviceID=1): +async def handleSentinel(deviceID): global handleSentinel_spotted, handleSentinel_loop - # Locate Closest Nodes and report them to a secure channel - # async function for possibly demanding back location data - enemySpotted = "" + detectedNearby = "" resolution = "unknown" closest_nodes = get_closest_nodes(deviceID) + closest_node = closest_nodes[0]['id'] if closest_nodes != ERROR_FETCHING_DATA and closest_nodes else None + closest_distance = closest_nodes[0]['distance'] if closest_nodes != ERROR_FETCHING_DATA and closest_nodes else None + + # check if the handleSentinel_spotted list contains the closest node already + if closest_node in [i['id'] for i in handleSentinel_spotted]: + # check if the distance is closer than the last time, if not just return + for i in range(len(handleSentinel_spotted)): + if handleSentinel_spotted[i]['id'] == closest_node and closest_distance is not None and closest_distance < handleSentinel_spotted[i]['distance']: + handleSentinel_spotted[i]['distance'] = closest_distance + break + else: + return + if closest_nodes != ERROR_FETCHING_DATA and closest_nodes: if closest_nodes[0]['id'] is not None: - enemySpotted = get_name_from_number(closest_nodes[0]['id'], 'long', 1) - enemySpotted += ", " + get_name_from_number(closest_nodes[0]['id'], 'short', 1) - enemySpotted += ", " + str(closest_nodes[0]['id']) - enemySpotted += ", " + decimal_to_hex(closest_nodes[0]['id']) - enemySpotted += f" at {closest_nodes[0]['distance']}m" - - if handleSentinel_loop >= sentry_holdoff and handleSentinel_spotted != enemySpotted: - # check the positionMetadata for nodeID and get metadata + detectedNearby = get_name_from_number(closest_node, 'long', deviceID) + detectedNearby += ", " + get_name_from_number(closest_nodes[0]['id'], 'short', deviceID) + detectedNearby += ", " + str(closest_nodes[0]['id']) + detectedNearby += ", " + decimal_to_hex(closest_nodes[0]['id']) + detectedNearby += f" at {closest_distance}m" + + if handleSentinel_loop >= sentry_holdoff and detectedNearby not in ["", None]: if closest_nodes and positionMetadata and closest_nodes[0]['id'] in positionMetadata: metadata = positionMetadata[closest_nodes[0]['id']] if metadata.get('precisionBits') is not None: resolution = metadata.get('precisionBits') - - logger.warning(f"System: {enemySpotted} is close to your location on Interface1 Accuracy is {resolution}bits") - send_message(f"Sentry{deviceID}: {enemySpotted}", secure_channel, 0, deviceID) + + logger.warning(f"System: {detectedNearby} is close to your location on Interface{deviceID} Accuracy is {resolution}bits") + for i in range(1, 10): + if globals().get(f'interface{i}_enabled'): + send_message(f"Sentry{deviceID}: {detectedNearby}", secure_channel, 0, i) + time.sleep(responseDelay + 1) if enableSMTP and email_sentry_alerts: for email in sysopEmails: - send_email(email, f"Sentry{deviceID}: {enemySpotted}") + send_email(email, f"Sentry{deviceID}: {detectedNearby}") handleSentinel_loop = 0 - handleSentinel_spotted = enemySpotted + handleSentinel_spotted.append({'id': closest_node, 'distance': closest_distance}) else: handleSentinel_loop += 1 async def watchdog(): - global retry_int1, retry_int2, telemetryData - int1Data, int2Data = "", "" + global telemetryData, retry_int1, retry_int2, retry_int3, retry_int4, retry_int5, retry_int6, retry_int7, retry_int8, retry_int9 while True: await asyncio.sleep(20) - #print(f"MeshBot System: watchdog running\r", end="") - if interface1 is not None and not retry_int1: - # getting firmware is a heartbeat to check if the interface is still connected - try: - firmware = getNodeFirmware(0, 1) - except Exception as e: - logger.error(f"System: communicating with interface1, trying to reconnect: {e}") - retry_int1 = True - - if not retry_int1: - # Locate Closest Nodes and report them to a secure channel - if sentry_enabled: - await handleSentinel(1) - - # multiPing handler - handleMultiPing(0,1) - - # Alert Broadcast - if wxAlertBroadcastEnabled or emergencyAlertBrodcastEnabled: - # weather alerts - handleAlertBroadcast(1) - - # Telemetry data - int1Data = displayNodeTelemetry(0, 1) - if int1Data != -1 and telemetryData[0]['lastAlert1'] != int1Data: - logger.debug(int1Data + f" Firmware:{firmware}") - telemetryData[0]['lastAlert1'] = int1Data - - if retry_int1: - try: - await retry_interface(1) - except Exception as e: - logger.error(f"System: retrying interface1: {e}") - - if interface2_enabled: - if interface2 is not None and not retry_int2: - # getting firmware is a heartbeat to check if the interface is still connected + # check all interfaces + for i in range(1, 10): + interface = globals().get(f'interface{i}') + retry_int = globals().get(f'retry_int{i}') + if interface is not None and not retry_int and globals().get(f'interface{i}_enabled'): try: - firmware2 = getNodeFirmware(0, 1) + firmware = getNodeFirmware(0, i) except Exception as e: - logger.error(f"System: communicating with interface1, trying to reconnect: {e}") - retry_int2 = True + logger.error(f"System: communicating with interface{i}, trying to reconnect: {e}") + globals()[f'retry_int{i}'] = True - if not retry_int2: - # Locate Closest Nodes and report them to a secure channel + if not globals()[f'retry_int{i}']: if sentry_enabled: - await handleSentinel(2) + await handleSentinel(i) - # multiPing handler - handleMultiPing(0,1) + handleMultiPing(0, i) - # Alert Broadcast if wxAlertBroadcastEnabled or emergencyAlertBrodcastEnabled: - # weather alerts - handleAlertBroadcast(2) + handleAlertBroadcast(i) - # Telemetry data - int2Data = displayNodeTelemetry(0, 2) - if int2Data != -1 and telemetryData[0]['lastAlert2'] != int2Data: - logger.debug(int2Data + f" Firmware:{firmware2}") - telemetryData[0]['lastAlert2'] = int2Data - - if retry_int2: + intData = displayNodeTelemetry(0, i) + if intData != -1 and telemetryData[0][f'lastAlert{i}'] != intData: + logger.debug(intData + f" Firmware:{firmware}") + telemetryData[0][f'lastAlert{i}'] = intData + + if globals()[f'retry_int{i}'] and globals()[f'interface{i}_enabled']: try: - await retry_interface(2) + await retry_interface(i) except Exception as e: - logger.error(f"System: retrying interface2: {e}") + logger.error(f"System: retrying interface{i}: {e}") diff --git a/modules/web.py b/modules/web.py new file mode 100644 index 0000000..a340622 --- /dev/null +++ b/modules/web.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# This is a simple web server that serves up the content of the webRoot directory +# The reporting data is all that is currently being served up +# TODO - add interaction to mesh? +# to use this today run it seperately and open a browser to http://localhost:8420 + +import os +import http.server + +# Set the port for the server +PORT = 8420 + +# set webRoot index.html location +webRoot = "etc/www" + +# Set to True to enable logging sdtout +webServerLogs = False + +# Generate with: openssl req -new -x509 -keyout server.pem -out server.pem -days 365 -nodes +SSL = False + +if SSL: + import ssl + +# disable logging +class QuietHandler(http.server.SimpleHTTPRequestHandler): + def log_message(self, format, *args): + if webServerLogs: + super().log_message(format, *args) + +# Change the current working directory to webRoot +os.chdir(webRoot) + +# boot up simple HTTP server +httpd = http.server.HTTPServer(('127.0.0.1', PORT), QuietHandler) + +if SSL: + ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + try: + ctx.load_cert_chain(certfile='./server.pem') + except FileNotFoundError: + print("SSL certificate file not found. Please generate it using the command provided in the comments.") + exit(1) + httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True) + +print(f"Serving reports at http://localhost:{PORT} Press ^C to quit.\n\n") +if not webServerLogs: + print("Server Logs are disabled") +# Serve forever, that is until the user interrupts the process +httpd.serve_forever() +exit(0)