diff --git a/Dockerfile b/Dockerfile index cd6152d..b34222b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,24 +1,21 @@ FROM python:3.13-slim ENV PYTHONUNBUFFERED=1 -RUN apt-get update && apt-get install -y gettext tzdata locales && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y gettext tzdata locales nano && rm -rf /var/lib/apt/lists/* # Set the locale default to en_US.UTF-8 RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \ dpkg-reconfigure --frontend=noninteractive locales && \ update-locale LANG=en_US.UTF-8 -ENV LANG=en_US.UTF-8 +ENV LANG="en_US.UTF-8" ENV TZ="America/Los_Angeles" WORKDIR /app COPY . /app -COPY requirements.txt . +COPY config.template /app/config.ini RUN pip install -r requirements.txt -COPY . . -COPY config.ini /app/config.ini -COPY entrypoint.sh /app/entrypoint.sh +RUN chmod +x /app/script/docker/entrypoint.sh -RUN chmod +x /app/entrypoint.sh -ENTRYPOINT ["/app/entrypoint.sh"] +ENTRYPOINT ["/bin/bash", "/app/script/docker/entrypoint.sh"] diff --git a/README.md b/README.md index 4c9bdbb..83e0281 100644 --- a/README.md +++ b/README.md @@ -73,23 +73,12 @@ git clone https://github.com/spudgunman/meshing-around ``` The code is under active development, so make sure to pull the latest changes regularly! -#### Optional Automation of setup +#### Automation of setup - **Automated Installation**: `install.sh` will automate optional venv and requirements installation. - **Launch Script**: `launch.sh` will activate and launch the app in the venv #### Docker Installation -If you prefer to use Docker, follow these steps: - -1. Ensure your serial port is properly shared. -2. Build the Docker image: - ```sh - cd meshing-around - docker build -t meshing-around . - ``` -3. Run the Docker container: - ```sh - docker run --rm -it --device=/dev/ttyUSB0 meshing-around - ``` +If you prefer to use [Docker](script/docker/README.md) #### Custom Install Install the required dependencies using pip: diff --git a/config.template b/config.template index bd5cac3..d503094 100644 --- a/config.template +++ b/config.template @@ -11,7 +11,7 @@ port = /dev/ttyACM0 # hostname = meshtastic.local # mac = 00:11:22:33:44:55 -# Additional interface for dual radio support +# Additional interface for multi radio support [interface2] enabled = False type = serial @@ -21,6 +21,8 @@ port = /dev/ttyUSB0 # hostname = localhost # 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 @@ -136,6 +138,8 @@ riverListDefault = wxAlertBroadcastEnabled = False # EAS Alert Broadcast Channels wxAlertBroadcastCh = 2 +# Add extra location to the weather alert +enableExtraLocationWx = False # Goverment IPAWS/CAP Alert Broadcast eAlertBroadcastEnabled = False diff --git a/entrypoint.sh b/entrypoint.sh deleted file mode 100644 index 0fd9377..0000000 --- a/entrypoint.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -# instruction set the meshing-around docker container - -# Substitute environment variables in the config file -envsubst < /app/config.ini > /app/config.tmp && mv /app/config.tmp /app/config.ini - -exec python /app/mesh_bot.py \ No newline at end of file diff --git a/logs/README.md b/logs/README.md index 1520215..f54d005 100644 --- a/logs/README.md +++ b/logs/README.md @@ -25,4 +25,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 d2ff88a..32373ac 100755 --- a/mesh_bot.py +++ b/mesh_bot.py @@ -139,6 +139,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" @@ -158,10 +159,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?" @@ -226,6 +224,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 @@ -233,13 +232,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) @@ -498,8 +495,9 @@ def handleLemonade(message, nodeID, deviceID): if highScore != 0: if highScore['userID'] != 0: nodeName = get_name_from_number(highScore['userID']) - if nodeName.isnumeric() and interface2_enabled: - nodeName = get_name_from_number(highScore['userID'], 'long', 2) + if nodeName.isnumeric() and multiple_interface: + logger.debug(f"System: TODO is multiple interface fix mention this please nodeName: {nodeName}") + #nodeName = get_name_from_number(highScore['userID'], 'long', 2) msg += f" HighScore🥇{nodeName} 💰{round(highScore['cash'], 2)}k " msg += start_lemonade(nodeID=nodeID, message=message, celsius=False) @@ -538,8 +536,9 @@ def handleBlackJack(message, nodeID, deviceID): if highScore != 0: if highScore['nodeID'] != 0: nodeName = get_name_from_number(highScore['nodeID']) - if nodeName.isnumeric() and interface2_enabled: - nodeName = get_name_from_number(highScore['nodeID'], 'long', 2) + if nodeName.isnumeric() and multiple_interface: + logger.debug(f"System: TODO is multiple interface fix mention this please nodeName: {nodeName}") + #nodeName = get_name_from_number(highScore['nodeID'], 'long', 2) msg += f" HighScore🥇{nodeName} with {highScore['highScore']} chips. " time.sleep(responseDelay + 1) # short answers with long replies can cause message collision added wait return msg @@ -573,8 +572,9 @@ def handleVideoPoker(message, nodeID, deviceID): if highScore != 0: if highScore['nodeID'] != 0: nodeName = get_name_from_number(highScore['nodeID']) - if nodeName.isnumeric() and interface2_enabled: - nodeName = get_name_from_number(highScore['nodeID'], 'long', 2) + if nodeName.isnumeric() and multiple_interface: + logger.debug(f"System: TODO is multiple interface fix mention this please nodeName: {nodeName}") + #nodeName = get_name_from_number(highScore['nodeID'], 'long', 2) msg += f" HighScore🥇{nodeName} with {highScore['highScore']} coins. " if last_cmd != "" and nodeID != 0: @@ -775,7 +775,7 @@ def sysinfo(message, message_from_id, deviceID): return "sysinfo command returns system information." else: if enable_runShellCmd and file_monitor_enabled: - shellData = call_external_script(None, "sysEnv.sh").rstrip() + shellData = call_external_script(None, "script/sysEnv.sh").rstrip() return get_sysinfo(message_from_id, deviceID) + "\n" + shellData else: return get_sysinfo(message_from_id, deviceID) @@ -1005,23 +1005,38 @@ def onReceive(packet, interface): # set the value for the incomming interface if rxType == 'SerialInterface': rxInterface = interface.__dict__.get('devPath', 'unknown') - if port1 in rxInterface: - rxNode = 1 - elif interface2_enabled and port2 in rxInterface: - rxNode = 2 + if port1 in rxInterface: rxNode = 1 + elif multiple_interface and port2 in rxInterface: rxNode = 2 + elif multiple_interface and port3 in rxInterface: rxNode = 3 + elif multiple_interface and port4 in rxInterface: rxNode = 4 + elif multiple_interface and port5 in rxInterface: rxNode = 5 + elif multiple_interface and port6 in rxInterface: rxNode = 6 + elif multiple_interface and port7 in rxInterface: rxNode = 7 + elif multiple_interface and port8 in rxInterface: rxNode = 8 + elif multiple_interface and port9 in rxInterface: rxNode = 9 if rxType == 'TCPInterface': rxHost = interface.__dict__.get('hostname', 'unknown') - if hostname1 in rxHost and interface1_type == 'tcp': - rxNode = 1 - elif interface2_enabled and hostname2 in rxHost and interface2_type == 'tcp': - rxNode = 2 + if hostname1 in rxHost and interface1_type == 'tcp': rxNode = 1 + elif multiple_interface and hostname2 in rxHost and interface2_type == 'tcp': rxNode = 2 + elif multiple_interface and hostname3 in rxHost and interface3_type == 'tcp': rxNode = 3 + elif multiple_interface and hostname4 in rxHost and interface4_type == 'tcp': rxNode = 4 + elif multiple_interface and hostname5 in rxHost and interface5_type == 'tcp': rxNode = 5 + elif multiple_interface and hostname6 in rxHost and interface6_type == 'tcp': rxNode = 6 + elif multiple_interface and hostname7 in rxHost and interface7_type == 'tcp': rxNode = 7 + elif multiple_interface and hostname8 in rxHost and interface8_type == 'tcp': rxNode = 8 + elif multiple_interface and hostname9 in rxHost and interface9_type == 'tcp': rxNode = 9 if rxType == 'BLEInterface': - if interface1_type == 'ble': - rxNode = 1 - elif interface2_enabled and interface2_type == 'ble': - rxNode = 2 + if interface1_type == 'ble': rxNode = 1 + elif multiple_interface and interface2_type == 'ble': rxNode = 2 + elif multiple_interface and interface3_type == 'ble': rxNode = 3 + elif multiple_interface and interface4_type == 'ble': rxNode = 4 + elif multiple_interface and interface5_type == 'ble': rxNode = 5 + elif multiple_interface and interface6_type == 'ble': rxNode = 6 + elif multiple_interface and interface7_type == 'ble': rxNode = 7 + elif multiple_interface and interface8_type == 'ble': rxNode = 8 + elif multiple_interface and interface9_type == 'ble': rxNode = 9 # check if the packet has a channel flag use it if packet.get('channel'): @@ -1053,7 +1068,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 @@ -1115,7 +1130,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 @@ -1213,18 +1228,17 @@ def onReceive(packet, interface): msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-')) # repeat the message on the other device - if repeater_enabled and interface2_enabled: + if repeater_enabled and multiple_interface: # wait a responseDelay to avoid message collision from lora-ack. time.sleep(responseDelay) rMsg = (f"{message_string} From:{get_name_from_number(message_from_id, 'short', rxNode)}") # if channel found in the repeater list repeat the message if str(channel_number) in repeater_channels: - if rxNode == 1: - logger.debug(f"Repeating message on Device2 Channel:{channel_number}") - send_message(rMsg, channel_number, 0, 2) - elif rxNode == 2: - logger.debug(f"Repeating message on Device1 Channel:{channel_number}") - send_message(rMsg, channel_number, 0, 1) + for i in range(1, 10): + if globals().get(f'interface{i}_enabled', False) and i != rxNode: + logger.debug(f"Repeating message on Device{i} Channel:{channel_number}") + send_message(rMsg, channel_number, 0, i) + time.sleep(responseDelay) else: # Evaluate non TEXT_MESSAGE_APP packets consumeMetadata(packet, rxNode) @@ -1234,18 +1248,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: @@ -1278,7 +1296,7 @@ async def start_rx(): logger.debug(f"System: Store and Forward Enabled using limit: {storeFlimit}") if useDMForResponse: logger.debug(f"System: Respond by DM only") - if repeater_enabled and interface2_enabled: + if repeater_enabled and multiple_interface: logger.debug(f"System: Repeater Enabled for Channels: {repeater_channels}") if radio_detection_enabled: logger.debug(f"System: Radio Detection Enabled using rigctld at {rigControlServerAddress} brodcasting to channels: {sigWatchBroadcastCh} for {get_freq_common_name(get_hamlib('f'))}") diff --git a/modules/filemon.py b/modules/filemon.py index d309b96..41cd3c2 100644 --- a/modules/filemon.py +++ b/modules/filemon.py @@ -63,7 +63,7 @@ async def watch_file(): return content await asyncio.sleep(1) # Check every -def call_external_script(message, script="runShell.sh"): +def call_external_script(message, script="script/runShell.sh"): try: # Debugging: Print the current working directory and resolved script path current_working_directory = os.getcwd() diff --git a/modules/locationdata.py b/modules/locationdata.py index 1594caf..652608e 100644 --- a/modules/locationdata.py +++ b/modules/locationdata.py @@ -357,16 +357,13 @@ def getWeatherAlertsNOAA(lat=0, lon=0, useDefaultLatLon=False): alerts = "" alertxml = xml.dom.minidom.parseString(alert_data.text) - #old for i in alertxml.getElementsByTagName("entry"): - alerts += ( - i.getElementsByTagName("title")[0].childNodes[0].nodeValue + "\n" - ) - #new - # for i in alertxml.getElementsByTagName("entry"): - # title = i.getElementsByTagName("title")[0].childNodes[0].nodeValue - # area_desc = i.getElementsByTagName("cap:areaDesc")[0].childNodes[0].nodeValue - # alerts += f"{title}\nArea: {area_desc}\n\n" + title = i.getElementsByTagName("title")[0].childNodes[0].nodeValue + area_desc = i.getElementsByTagName("cap:areaDesc")[0].childNodes[0].nodeValue + if enableExtraLocationWx: + alerts += f"{title}. {area_desc.replace(' ', '')}\n" + else: + alerts += f"{title}\n" if alerts == "" or alerts == None: return NO_ALERTS @@ -525,7 +522,7 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False): if geocode_type == "SAME": sameVal = geocode_value except Exception as e: - logger.warning(f"System: iPAWS Error extracting alert data: {link}") + logger.debug(f"System: iPAWS Error extracting alert data: {link}") #print(f"DEBUG: {info.toprettyxml()}") continue @@ -604,7 +601,7 @@ def get_flood_noaa(lat=0, lon=0, uid=0): # except TypeError as e: # print(f"Type error in data: {e}") except Exception as e: - logger.warning("Location:Error extracting flood gauge data from NOAA for " + str(uid)) + logger.debug("Location:Error extracting flood gauge data from NOAA for " + str(uid)) return ERROR_FETCHING_DATA # format the flood data diff --git a/modules/settings.py b/modules/settings.py index 73af4a7..351ecda 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,81 @@ 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 + +multiple_interface = False +if interface2_enabled or interface3_enabled or interface4_enabled or interface5_enabled or interface6_enabled or interface7_enabled or interface8_enabled or interface9_enabled: + multiple_interface = True + + # variables from the config.ini file try: # general @@ -169,6 +245,7 @@ try: mySAME = config['location'].get('mySAME', '').split(',') # default empty forecastDuration = config['location'].getint('NOAAforecastDuration', 4) # NOAA forcast days numWxAlerts = config['location'].getint('NOAAalertCount', 2) # default 2 alerts + enableExtraLocationWx = config['location'].getboolean('enableExtraLocationWx', False) # default False ipawsPIN = config['location'].get('ipawsPIN', '000000') # default 000000 ignoreFEMAtest = config['location'].getboolean('ignoreFEMAtest', True) # default True wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh', '2').split(',') # default Channel 2 diff --git a/modules/system.py b/modules/system.py index 7caea4b..f9aed18 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,17 +282,18 @@ def get_num_from_short_name(short_name, nodeInt=1): elif str(short_name.lower()) == node['user']['shortName'].lower(): return node['num'] else: - if interface2_enabled: - interface = interface2 if nodeInt == 1 else interface1 # check the other interface - for node in interface.nodes.values(): - if short_name == node['user']['shortName']: - return node['num'] - elif str(short_name.lower()) == node['user']['shortName'].lower(): - return node['num'] + for int in range(1, 10): + if globals().get(f'interface{int}_enabled') and int != nodeInt: + other_interface = globals().get(f'interface{int}') + for node in other_interface.nodes.values(): + if short_name == node['user']['shortName']: + return node['num'] + elif str(short_name.lower()) == node['user']['shortName'].lower(): + return node['num'] 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) @@ -337,13 +321,14 @@ def get_node_list(nodeInt=1): #print (f"Node List: {node_list1[:5]}\n") node_list1.sort(key=lambda x: x[1] if x[1] is not None else 0, reverse=True) #print (f"Node List: {node_list1[:5]}\n") - if interface2_enabled: + if multiple_interface: + logger.debug(f"System: FIX ME line 327 Multiple Interface Node List") node_list2.sort(key=lambda x: x[1] if x[1] is not None else 0, reverse=True) except Exception as e: logger.error(f"System: Error sorting node list: {e}") logger.debug(f"Node List1: {node_list1[:5]}\n") - if interface2_enabled: - logger.debug(f"Node List2: {node_list2[:5]}\n") + if multiple_interface: + logger.debug(f"FIX ME MULTI INTERFACE Node List2: {node_list2[:5]}\n") node_list = ERROR_FETCHING_DATA try: @@ -363,7 +348,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 +381,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 +402,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 +495,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,40 +715,30 @@ 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 logger.debug(f"System: Closing Autoresponder") - try: + try: + logger.debug(f"System: Closing Interface1") interface1.close() - logger.debug(f"System: Interface1 Closed") - if interface2_enabled: - interface2.close() - logger.debug(f"System: Interface2 Closed") + if multiple_interface: + for i in range(2, 10): + if globals().get(f'interface{i}_enabled'): + logger.debug(f"System: Closing Interface{i}") + globals()[f'interface{i}'].close() except Exception as e: logger.error(f"System: closing: {e}") if bbs_enabled: @@ -778,14 +753,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 +776,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 +796,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}" @@ -960,9 +934,6 @@ def get_sysinfo(nodeID=0, deviceID=1): # replace Telemetry with Int in string stats = stats.replace("Telemetry", "Int") sysinfo += f"📊{stats}" - if interface2_enabled: - sysinfo += f"📊{stats}" - return sysinfo async def BroadcastScheduler(): @@ -987,15 +958,23 @@ async def handleSignalWatcher(): for ch in sigWatchBroadcastCh: if antiSpam and ch != publicChannel: send_message(msg, int(ch), 0, 1) - if interface2_enabled: - send_message(msg, int(ch), 0, 2) + time.sleep(responseDelay) + if multiple_interface: + for i in range(2, 10): + if globals().get(f'interface{i}_enabled'): + send_message(msg, int(ch), 0, i) + time.sleep(responseDelay) else: logger.warning(f"System: antiSpam prevented Alert from Hamlib {msg}") else: if antiSpam and sigWatchBroadcastCh != publicChannel: send_message(msg, int(sigWatchBroadcastCh), 0, 1) - if interface2_enabled: - send_message(msg, int(sigWatchBroadcastCh), 0, 2) + time.sleep(responseDelay) + if multiple_interface: + for i in range(2, 10): + if globals().get(f'interface{i}_enabled'): + send_message(msg, int(sigWatchBroadcastCh), 0, i) + time.sleep(responseDelay) else: logger.warning(f"System: antiSpam prevented Alert from Hamlib {msg}") @@ -1018,172 +997,147 @@ async def handleFileWatcher(): for ch in file_monitor_broadcastCh: if antiSpam and ch != publicChannel: send_message(msg, int(ch), 0, 1) - if interface2_enabled: - send_message(msg, int(ch), 0, 2) + time.sleep(responseDelay) + if multiple_interface: + for i in range(2, 10): + if globals().get(f'interface{i}_enabled'): + send_message(msg, int(ch), 0, i) + time.sleep(responseDelay) else: logger.warning(f"System: antiSpam prevented Alert from FileWatcher") else: if antiSpam and file_monitor_broadcastCh != publicChannel: send_message(msg, int(file_monitor_broadcastCh), 0, 1) - if interface2_enabled: - send_message(msg, int(file_monitor_broadcastCh), 0, 2) + time.sleep(responseDelay) + if multiple_interface: + for i in range(2, 10): + if globals().get(f'interface{i}_enabled'): + send_message(msg, int(file_monitor_broadcastCh), 0, i) + time.sleep(responseDelay) else: logger.warning(f"System: antiSpam prevented Alert from FileWatcher") 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) diff --git a/pong_bot.py b/pong_bot.py index b0c4fe8..41ee06c 100755 --- a/pong_bot.py +++ b/pong_bot.py @@ -30,11 +30,11 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n "cq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number), "cqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number), "cqcqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number), - "lheard": lambda: handle_lheard(interface1, interface2_enabled, myNodeNum1, myNodeNum2), + "lheard": lambda: handle_lheard(message, message_from_id, deviceID, isDM), "motd": lambda: handle_motd(message, MOTD), "ping": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number), "pong": lambda: "🏓PING!!🛜", - "sitrep": lambda: handle_lheard(interface1, interface2_enabled, myNodeNum1, myNodeNum2), + "sitrep": lambda: lambda: handle_lheard(message, message_from_id, deviceID, isDM), "sysinfo": lambda: sysinfo(message, message_from_id, deviceID), "test": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number), "testing": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number), @@ -175,6 +175,7 @@ def handle_lheard(message, nodeid, deviceID, isDM): return bot_response def onReceive(packet, interface): + global seenNodes # Priocess the incoming packet, handles the responses to the packet with auto_response() # Sends the packet to the correct handler for processing @@ -184,6 +185,8 @@ def onReceive(packet, interface): # Valies assinged to the packet rxNode, message_from_id, snr, rssi, hop, hop_away, channel_number = 0, 0, 0, 0, 0, 0, 0 pkiStatus = (False, 'ABC') + replyIDset = False + emojiSeen = False isDM = False if DEBUGpacket: @@ -196,23 +199,42 @@ def onReceive(packet, interface): # set the value for the incomming interface if rxType == 'SerialInterface': rxInterface = interface.__dict__.get('devPath', 'unknown') - if port1 in rxInterface: - rxNode = 1 - elif interface2_enabled and port2 in rxInterface: - rxNode = 2 + if port1 in rxInterface: rxNode = 1 + elif multiple_interface and port2 in rxInterface: rxNode = 2 + elif multiple_interface and port3 in rxInterface: rxNode = 3 + elif multiple_interface and port4 in rxInterface: rxNode = 4 + elif multiple_interface and port5 in rxInterface: rxNode = 5 + elif multiple_interface and port6 in rxInterface: rxNode = 6 + elif multiple_interface and port7 in rxInterface: rxNode = 7 + elif multiple_interface and port8 in rxInterface: rxNode = 8 + elif multiple_interface and port9 in rxInterface: rxNode = 9 if rxType == 'TCPInterface': rxHost = interface.__dict__.get('hostname', 'unknown') - if hostname1 in rxHost and interface1_type == 'tcp': - rxNode = 1 - elif interface2_enabled and hostname2 in rxHost and interface2_type == 'tcp': - rxNode = 2 + if hostname1 in rxHost and interface1_type == 'tcp': rxNode = 1 + elif multiple_interface and hostname2 in rxHost and interface2_type == 'tcp': rxNode = 2 + elif multiple_interface and hostname3 in rxHost and interface3_type == 'tcp': rxNode = 3 + elif multiple_interface and hostname4 in rxHost and interface4_type == 'tcp': rxNode = 4 + elif multiple_interface and hostname5 in rxHost and interface5_type == 'tcp': rxNode = 5 + elif multiple_interface and hostname6 in rxHost and interface6_type == 'tcp': rxNode = 6 + elif multiple_interface and hostname7 in rxHost and interface7_type == 'tcp': rxNode = 7 + elif multiple_interface and hostname8 in rxHost and interface8_type == 'tcp': rxNode = 8 + elif multiple_interface and hostname9 in rxHost and interface9_type == 'tcp': rxNode = 9 if rxType == 'BLEInterface': - if interface1_type == 'ble': - rxNode = 1 - elif interface2_enabled and interface2_type == 'ble': - rxNode = 2 + if interface1_type == 'ble': rxNode = 1 + elif multiple_interface and interface2_type == 'ble': rxNode = 2 + elif multiple_interface and interface3_type == 'ble': rxNode = 3 + elif multiple_interface and interface4_type == 'ble': rxNode = 4 + elif multiple_interface and interface5_type == 'ble': rxNode = 5 + elif multiple_interface and interface6_type == 'ble': rxNode = 6 + elif multiple_interface and interface7_type == 'ble': rxNode = 7 + elif multiple_interface and interface8_type == 'ble': rxNode = 8 + elif multiple_interface and interface9_type == 'ble': rxNode = 9 + + # check if the packet has a channel flag use it + if packet.get('channel'): + channel_number = packet.get('channel', 0) # set the message_from_id message_from_id = packet['from'] @@ -339,18 +361,17 @@ def onReceive(packet, interface): msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-')) # repeat the message on the other device - if repeater_enabled and interface2_enabled: + if repeater_enabled and multiple_interface: # wait a responseDelay to avoid message collision from lora-ack. time.sleep(responseDelay) rMsg = (f"{message_string} From:{get_name_from_number(message_from_id, 'short', rxNode)}") # if channel found in the repeater list repeat the message if str(channel_number) in repeater_channels: - if rxNode == 1: - logger.debug(f"Repeating message on Device2 Channel:{channel_number}") - send_message(rMsg, channel_number, 0, 2) - elif rxNode == 2: - logger.debug(f"Repeating message on Device1 Channel:{channel_number}") - send_message(rMsg, channel_number, 0, 1) + for i in range(1, 10): + if globals().get(f'interface{i}_enabled', False) and i != rxNode: + logger.debug(f"Repeating message on Device{i} Channel:{channel_number}") + send_message(rMsg, channel_number, 0, i) + time.sleep(responseDelay) else: # Evaluate non TEXT_MESSAGE_APP packets consumeMetadata(packet, rxNode) @@ -363,11 +384,12 @@ async def start_rx(): # 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 log_messages_to_file: logger.debug("System: Logging Messages to disk") if syslog_to_file: @@ -382,7 +404,7 @@ async def start_rx(): logger.debug(f"System: Store and Forward Enabled using limit: {storeFlimit}") if useDMForResponse: logger.debug(f"System: Respond by DM only") - if repeater_enabled and interface2_enabled: + if repeater_enabled and multiple_interface: logger.debug(f"System: Repeater Enabled for Channels: {repeater_channels}") if file_monitor_enabled: logger.debug(f"System: File Monitor Enabled for {file_monitor_file_path}, broadcasting to channels: {file_monitor_broadcastCh}") diff --git a/script/docker/README.md b/script/docker/README.md new file mode 100644 index 0000000..712af80 --- /dev/null +++ b/script/docker/README.md @@ -0,0 +1,20 @@ +# How do I use this thing? +This is not a full turnkey setup for Docker yet but gets you most of the way there! + +## Setup New Image +`docker build -t meshing-around .` + +## Ollama Image with compose +still a WIP +`docker compose up -d` + +## Edit the config.ini in the docker +To edit the config.ini in the docker you can +`docker run -it --entrypoint /bin/bash meshing-around -c "nano /app/config.ini"` + +## other info +1. Ensure your serial port is properly shared. +2. Run the Docker container: + ```sh + docker run --rm -it --device=/dev/ttyUSB0 meshing-around + ``` \ No newline at end of file diff --git a/script/docker/compose.yaml b/script/docker/compose.yaml new file mode 100644 index 0000000..05f584a --- /dev/null +++ b/script/docker/compose.yaml @@ -0,0 +1,52 @@ +services: + meshing-around: + build: + context: ../.. + depends_on: + ollama: + condition: service_healthy + devices: + - /dev/ttyAMA10 # Replace this with your actual device! + configs: + - source: me_config + target: /app/config.ini + extra_hosts: + - "host.docker.internal:host-gateway" # Used to access a local linux meshtasticd device via tcp + ollama: + image: ollama/ollama:0.5.1 + volumes: + - ./ollama:/root/.ollama + - ./ollama-entrypoint.sh:./entrypoint.sh + container_name: ollama + pull_policy: always + tty: true + restart: always + entrypoint: + - /usr/bin/bash + - /script/docker/entrypoint.sh + expose: + - 11434 + healthcheck: + test: "apt update && apt install curl -y && curl -f http://localhost:11434/api/tags | grep -q llama3.2:3b" + interval: 30s + timeout: 10s + retries: 20 + node-exporter: + image: quay.io/prometheus/node-exporter:latest + volumes: + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /:/rootfs:ro + command: + - --path.procfs=/host/proc + - --path.rootfs=/rootfs + - --path.sysfs=/host/sys + - --collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/) + restart: unless-stopped + expose: + - 9100 + network_mode: host + pid: host +configs: + me_config: + file: ./config.ini \ No newline at end of file diff --git a/script/docker/docker-install.bat b/script/docker/docker-install.bat new file mode 100644 index 0000000..6f382f8 --- /dev/null +++ b/script/docker/docker-install.bat @@ -0,0 +1,6 @@ +REM batch file to install docker on windows +REM docker compose up -d +cd ../../ +docker build -t meshing-around . +REM docker-compose up -d +docker run -it --entrypoint /bin/bash meshing-around -c "nano /app/config.ini" \ No newline at end of file diff --git a/script/docker/docker-terminal.bat b/script/docker/docker-terminal.bat new file mode 100644 index 0000000..ecde9c5 --- /dev/null +++ b/script/docker/docker-terminal.bat @@ -0,0 +1,2 @@ +REM launch meshing-around container with a terminal +docker run -it --entrypoint /bin/bash meshing-around -c "nano /app/config.ini" \ No newline at end of file diff --git a/script/docker/entrypoint.sh b/script/docker/entrypoint.sh new file mode 100644 index 0000000..214285d --- /dev/null +++ b/script/docker/entrypoint.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# instruction set the meshing-around docker container entrypoint +# Substitute environment variables in the config file (what is the purpose of this?) +# envsubst < /app/config.ini > /app/config.tmp && mv /app/config.tmp /app/config.ini +# Run the bot +exec python /app/mesh_bot.py \ No newline at end of file diff --git a/script/docker/ollama-entrypoint.sh b/script/docker/ollama-entrypoint.sh new file mode 100644 index 0000000..ced1718 --- /dev/null +++ b/script/docker/ollama-entrypoint.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# Start Ollama in the background. +/bin/ollama serve & +# Record Process ID. +pid=$! + +# Pause for Ollama to start. +sleep 5 + +echo "🔴 Retrieve llama3.2:3b model..." +ollama pull llama3.2:3b +echo "🟢 Done!" + +# Wait for Ollama process to finish. +wait $pid diff --git a/runShell.sh b/script/runShell.sh similarity index 58% rename from runShell.sh rename to script/runShell.sh index 3e67e7e..98be45e 100644 --- a/runShell.sh +++ b/script/runShell.sh @@ -4,4 +4,4 @@ cd "$(dirname "$0")" program_path=$(pwd) -printf "Running meshing-around demo script for shell scripting\n" \ No newline at end of file +printf "Running meshing-around demo script for shell scripting from $program_path\n" \ No newline at end of file diff --git a/sysEnv.sh b/script/sysEnv.sh similarity index 85% rename from sysEnv.sh rename to script/sysEnv.sh index 69e18f4..e220b01 100644 --- a/sysEnv.sh +++ b/script/sysEnv.sh @@ -23,8 +23,5 @@ else tempf=$(echo "scale=2; $temp * 9 / 5 + 32" | bc) fi -# print telemetry data -printf "Disk:%s" "$free_space" -printf " RAM:%.2f%%" "$ram_usage" -printf " CPU:%.1f%%" "$cpu_usage" -printf " CPU-T:%.1f°C (%.1f°F)" "$temp" "$tempf" \ No newline at end of file +# print telemetry data rounded to 2 decimal places +printf "Disk:%s RAM:%.2f%% CPU:%.2f%% CPU-T:%.2f°C (%.2f°F)\n" "$free_space" "$ram_usage" "$cpu_usage" "$temp" "$tempf" \ No newline at end of file