From df15fb54b02acf8c0acebcf2d1e5deb339d34fca Mon Sep 17 00:00:00 2001 From: Ryan Turner Date: Sat, 4 Jan 2025 19:39:23 -0600 Subject: [PATCH 01/53] Initial checkin --- Dockerfile | 8 ++--- script/docker/compose.yaml | 52 ++++++++++++++++++++++++++++++ script/docker/ollama-entrypoint.sh | 16 +++++++++ 3 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 script/docker/compose.yaml create mode 100644 script/docker/ollama-entrypoint.sh diff --git a/Dockerfile b/Dockerfile index cd6152d..13daf5f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,18 +7,14 @@ RUN apt-get update && apt-get install -y gettext tzdata locales && rm -rf /var/l 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 . RUN pip install -r requirements.txt -COPY . . - -COPY config.ini /app/config.ini -COPY entrypoint.sh /app/entrypoint.sh RUN chmod +x /app/entrypoint.sh + ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/script/docker/compose.yaml b/script/docker/compose.yaml new file mode 100644 index 0000000..2aa35cf --- /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 + # - /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/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 From 4f9c36fdad4e4c242c831ea3cae879d39d00accf Mon Sep 17 00:00:00 2001 From: Ryan Turner Date: Sat, 4 Jan 2025 19:41:41 -0600 Subject: [PATCH 02/53] fixup! Initial checkin --- script/docker/README.md | 3 +++ script/docker/compose.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 script/docker/README.md diff --git a/script/docker/README.md b/script/docker/README.md new file mode 100644 index 0000000..ec76b4e --- /dev/null +++ b/script/docker/README.md @@ -0,0 +1,3 @@ +# How do I use this thing? + +`docker compose up` diff --git a/script/docker/compose.yaml b/script/docker/compose.yaml index 2aa35cf..66f5444 100644 --- a/script/docker/compose.yaml +++ b/script/docker/compose.yaml @@ -1,7 +1,7 @@ services: meshing-around: build: - context: .. + context: ../.. # depends_on: # ollama: # condition: service_healthy From d3f07ae5249ec55e477fcca2c16d20cfbeaa694b Mon Sep 17 00:00:00 2001 From: Ryan Turner Date: Sat, 4 Jan 2025 19:58:12 -0600 Subject: [PATCH 03/53] fixup! fixup! Initial checkin --- script/docker/README.md | 2 +- script/docker/compose.yaml | 70 +++++++++++++++++++------------------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/script/docker/README.md b/script/docker/README.md index ec76b4e..dc803c0 100644 --- a/script/docker/README.md +++ b/script/docker/README.md @@ -1,3 +1,3 @@ # How do I use this thing? -`docker compose up` +`docker compose up -d` diff --git a/script/docker/compose.yaml b/script/docker/compose.yaml index 66f5444..e432fda 100644 --- a/script/docker/compose.yaml +++ b/script/docker/compose.yaml @@ -12,41 +12,41 @@ services: 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 - # - /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 + 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 + - /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 From 70e11117f1004fb907d2c41901c8c143208407b9 Mon Sep 17 00:00:00 2001 From: Ryan Turner Date: Sat, 4 Jan 2025 20:18:35 -0600 Subject: [PATCH 04/53] fixup! fixup! fixup! Initial checkin --- script/docker/compose.yaml | 2 +- script/docker/ollama-entrypoint.sh | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/script/docker/compose.yaml b/script/docker/compose.yaml index e432fda..3410cc5 100644 --- a/script/docker/compose.yaml +++ b/script/docker/compose.yaml @@ -27,7 +27,7 @@ services: expose: - 11434 healthcheck: - test: "apt update && apt install curl -y && curl -f http://localhost:11434/api/tags | grep -q llama3.2:3b" + test: "apt update && apt install curl -y && curl -f http://localhost:11434/api/tags | grep -q llama3.3:3b" interval: 30s timeout: 10s retries: 20 diff --git a/script/docker/ollama-entrypoint.sh b/script/docker/ollama-entrypoint.sh index ced1718..18e1550 100644 --- a/script/docker/ollama-entrypoint.sh +++ b/script/docker/ollama-entrypoint.sh @@ -8,8 +8,8 @@ pid=$! # Pause for Ollama to start. sleep 5 -echo "🔴 Retrieve llama3.2:3b model..." -ollama pull llama3.2:3b +echo "🔴 Retrieve llama3.3:3b model..." +ollama pull llama3.3:3b echo "🟢 Done!" # Wait for Ollama process to finish. From 89a088460049ed2c6a855a9e652807b061640f44 Mon Sep 17 00:00:00 2001 From: Ryan Turner Date: Sat, 4 Jan 2025 20:22:40 -0600 Subject: [PATCH 05/53] fixup! fixup! fixup! fixup! Initial checkin --- script/docker/compose.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/script/docker/compose.yaml b/script/docker/compose.yaml index 3410cc5..a1c059d 100644 --- a/script/docker/compose.yaml +++ b/script/docker/compose.yaml @@ -2,9 +2,9 @@ services: meshing-around: build: context: ../.. - # depends_on: - # ollama: - # condition: service_healthy + depends_on: + ollama: + condition: service_healthy devices: - /dev/ttyAMA10 # Replace this with your actual device! configs: From acf39d08709822140947f270d62bbe90a432751d Mon Sep 17 00:00:00 2001 From: Ryan Turner Date: Sat, 4 Jan 2025 20:40:27 -0600 Subject: [PATCH 06/53] fixup! fixup! fixup! fixup! fixup! Initial checkin --- script/docker/compose.yaml | 2 +- script/docker/ollama-entrypoint.sh | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/script/docker/compose.yaml b/script/docker/compose.yaml index a1c059d..c4e4a20 100644 --- a/script/docker/compose.yaml +++ b/script/docker/compose.yaml @@ -27,7 +27,7 @@ services: expose: - 11434 healthcheck: - test: "apt update && apt install curl -y && curl -f http://localhost:11434/api/tags | grep -q llama3.3:3b" + 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 diff --git a/script/docker/ollama-entrypoint.sh b/script/docker/ollama-entrypoint.sh index 18e1550..ced1718 100644 --- a/script/docker/ollama-entrypoint.sh +++ b/script/docker/ollama-entrypoint.sh @@ -8,8 +8,8 @@ pid=$! # Pause for Ollama to start. sleep 5 -echo "🔴 Retrieve llama3.3:3b model..." -ollama pull llama3.3:3b +echo "🔴 Retrieve llama3.2:3b model..." +ollama pull llama3.2:3b echo "🟢 Done!" # Wait for Ollama process to finish. From 6f492ef38236babe84a79704448368aab2a29c6c Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 5 Jan 2025 13:15:54 -0800 Subject: [PATCH 07/53] interface Expansion --- config.template | 4 +- mesh_bot.py | 22 ++++++---- modules/settings.py | 72 +++++++++++++++++++++++++++++++ modules/system.py | 102 ++++++++++++++++++-------------------------- modules/web.py | 1 + 5 files changed, 132 insertions(+), 69 deletions(-) create mode 100644 modules/web.py diff --git a/config.template b/config.template index 430a093..fe22350 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 diff --git a/mesh_bot.py b/mesh_bot.py index 52d9c52..a99d869 100755 --- a/mesh_bot.py +++ b/mesh_bot.py @@ -1229,18 +1229,24 @@ def onReceive(packet, interface): async def start_rx(): print (CustomFormatter.bold_white + f"\nMeshtastic Autoresponder Bot CTL+C to exit\n" + CustomFormatter.reset) + + # 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)}") + 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)}") + if log_messages_to_file: logger.debug("System: Logging Messages to disk") if syslog_to_file: 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 84dc77a..aa9d6b8 100644 --- a/modules/system.py +++ b/modules/system.py @@ -208,62 +208,44 @@ 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 +interface1 = interface2 = interface3 = interface4 = interface5 = interface6 = interface7 = interface8 = interface9 = None +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}'): + if 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 #### @@ -271,7 +253,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(): @@ -288,7 +270,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(): @@ -308,7 +290,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 = [] @@ -362,7 +344,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 @@ -395,7 +377,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: @@ -509,7 +491,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 @@ -784,7 +766,7 @@ def initialize_telemetryData(): 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 diff --git a/modules/web.py b/modules/web.py new file mode 100644 index 0000000..59d76c4 --- /dev/null +++ b/modules/web.py @@ -0,0 +1 @@ +# investigate python3 -m http.server to serve up reports and config data? From b78cf4d0222142599fd0bdd7e7ee90521a05cf6b Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 5 Jan 2025 13:18:21 -0800 Subject: [PATCH 08/53] Update system.py --- modules/system.py | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/system.py b/modules/system.py index aa9d6b8..c419507 100644 --- a/modules/system.py +++ b/modules/system.py @@ -214,6 +214,7 @@ if ble_count > 1: exit() # Initialize interfaces +logger.debug(f"System: Initializing Interfaces") interface1 = interface2 = interface3 = interface4 = interface5 = interface6 = interface7 = interface8 = interface9 = None for i in range(1, 10): interface_type = globals().get(f'interface{i}_type') From c7b7b182b95897b03bc13fd0695bfb5285a808a6 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 5 Jan 2025 13:36:24 -0800 Subject: [PATCH 09/53] Update system.py --- modules/system.py | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/modules/system.py b/modules/system.py index c419507..907beba 100644 --- a/modules/system.py +++ b/modules/system.py @@ -281,6 +281,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(): @@ -301,7 +302,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}', 777) for i in range(1, 10)): node_name = get_name_from_number(node['num'], 'short', nodeInt) snr = node.get('snr', 0) @@ -399,7 +400,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}', 777) 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: @@ -781,18 +782,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}', 777) 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 @@ -804,13 +802,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}" @@ -1015,8 +1013,8 @@ async def handleFileWatcher(): 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 + interface = globals()[f'interface{nodeID}'] + retry_int = globals()[f'interface{nodeID}'] # retry connecting to the interface # add a check to see if the interface is already open or trying to open if interface is not None: From ab54dc06d789559dffd5a3713c8ab5e8be52868a Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 5 Jan 2025 14:02:30 -0800 Subject: [PATCH 10/53] enhance --- mesh_bot.py | 29 ++++++++++++----------------- modules/system.py | 19 +++++++++---------- 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/mesh_bot.py b/mesh_bot.py index a99d869..a7de28f 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 @@ -1234,17 +1231,15 @@ async def start_rx(): 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)}") + 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 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)}") - if llm_enabled: logger.debug(f"System: Ollama LLM Enabled, loading model {llmModel} please wait") - llm_query(" ", myNodeNum1) + llm_query(" ") logger.debug(f"System: LLM model {llmModel} loaded") if log_messages_to_file: diff --git a/modules/system.py b/modules/system.py index 907beba..c64ad16 100644 --- a/modules/system.py +++ b/modules/system.py @@ -238,13 +238,12 @@ for i in range(1, 10): # 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}'): - if 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}") + 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: globals()[f'myNodeNum{i}'] = 777 @@ -302,7 +301,7 @@ def get_node_list(nodeInt=1): if interface.nodes: for node in interface.nodes.values(): # ignore own - if all(node['num'] != globals().get(f'myNodeNum{i}', 777) for i in range(1, 10)): + 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) @@ -400,7 +399,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 not in [globals().get(f'myNodeNum{i}', 777) for i in range(1, 10)]) 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: @@ -783,7 +782,7 @@ def getNodeFirmware(nodeID=0, nodeInt=1): def displayNodeTelemetry(nodeID=0, rxNode=0, userRequested=False): interface = globals()[f'interface{rxNode}'] - myNodeNum = globals().get(f'myNodeNum{rxNode}', 777) + myNodeNum = globals().get(f'myNodeNum{rxNode}') global telemetryData # throttle the telemetry requests to prevent spamming the device From f917df709c18f070ec3d8b4eafa349a210f6a2d6 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 5 Jan 2025 14:58:48 -0800 Subject: [PATCH 11/53] refactorWatchDog --- modules/system.py | 171 +++++++++++++++------------------------------- 1 file changed, 55 insertions(+), 116 deletions(-) diff --git a/modules/system.py b/modules/system.py index c64ad16..f99fd8b 100644 --- a/modules/system.py +++ b/modules/system.py @@ -216,6 +216,7 @@ if ble_count > 1: # 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: @@ -712,30 +713,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 @@ -1010,77 +999,66 @@ 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 +async def retry_interface(nodeID): + global max_retry_count interface = globals()[f'interface{nodeID}'] - retry_int = globals()[f'interface{nodeID}'] - # retry connecting to the interface - # add a check to see if the interface is already open or trying to open + 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_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 = "" resolution = "unknown" closest_nodes = get_closest_nodes(deviceID) 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 = get_name_from_number(closest_nodes[0]['id'], 'long', deviceID) + enemySpotted += ", " + get_name_from_number(closest_nodes[0]['id'], 'short', deviceID) 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 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") + + logger.warning(f"System: {enemySpotted} is close to your location on Interface{deviceID} Accuracy is {resolution}bits") send_message(f"Sentry{deviceID}: {enemySpotted}", secure_channel, 0, deviceID) if enableSMTP and email_sentry_alerts: for email in sysopEmails: @@ -1091,76 +1069,37 @@ async def handleSentinel(deviceID=1): 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 + 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: 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(1) + 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}") From 868009b650db8f55df7e1655e518f5910510a8e1 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 5 Jan 2025 15:07:54 -0800 Subject: [PATCH 12/53] Update system.py --- modules/system.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/system.py b/modules/system.py index f99fd8b..6b1fa97 100644 --- a/modules/system.py +++ b/modules/system.py @@ -1060,6 +1060,9 @@ async def handleSentinel(deviceID): logger.warning(f"System: {enemySpotted} is close to your location on Interface{deviceID} Accuracy is {resolution}bits") send_message(f"Sentry{deviceID}: {enemySpotted}", secure_channel, 0, deviceID) + for i in range(1, 10): + if globals().get(f'interface{i}_enabled'): + send_message(f"Sentry{deviceID}: {enemySpotted}", secure_channel, 0, i) if enableSMTP and email_sentry_alerts: for email in sysopEmails: send_email(email, f"Sentry{deviceID}: {enemySpotted}") From b16d9322e3251282d85105f213af19dbbc366b95 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 5 Jan 2025 15:21:20 -0800 Subject: [PATCH 13/53] Update system.py --- modules/system.py | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/system.py b/modules/system.py index 6b1fa97..4377f1d 100644 --- a/modules/system.py +++ b/modules/system.py @@ -1059,7 +1059,6 @@ async def handleSentinel(deviceID): resolution = metadata.get('precisionBits') logger.warning(f"System: {enemySpotted} is close to your location on Interface{deviceID} Accuracy is {resolution}bits") - send_message(f"Sentry{deviceID}: {enemySpotted}", secure_channel, 0, deviceID) for i in range(1, 10): if globals().get(f'interface{i}_enabled'): send_message(f"Sentry{deviceID}: {enemySpotted}", secure_channel, 0, i) From 0ac683b5c0c15f409d036ff0a0f4d1b6b4d0c8fc Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 5 Jan 2025 15:33:03 -0800 Subject: [PATCH 14/53] Update locationdata.py --- modules/locationdata.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/modules/locationdata.py b/modules/locationdata.py index 421c821..1594caf 100644 --- a/modules/locationdata.py +++ b/modules/locationdata.py @@ -357,11 +357,16 @@ 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" if alerts == "" or alerts == None: return NO_ALERTS From d163bffba67094f74f71246fa5f6bb3d35de49a5 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 5 Jan 2025 15:35:22 -0800 Subject: [PATCH 15/53] Update locationdata.py --- modules/locationdata.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/modules/locationdata.py b/modules/locationdata.py index 1594caf..01bacf1 100644 --- a/modules/locationdata.py +++ b/modules/locationdata.py @@ -357,16 +357,10 @@ 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 + alerts += f"{title}\nArea: {area_desc}\n\n" if alerts == "" or alerts == None: return NO_ALERTS From 9aaebaad62c0f49fa913c61435b7d229a8d50a6f Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 5 Jan 2025 15:36:37 -0800 Subject: [PATCH 16/53] Update locationdata.py --- modules/locationdata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/locationdata.py b/modules/locationdata.py index 01bacf1..177ff37 100644 --- a/modules/locationdata.py +++ b/modules/locationdata.py @@ -360,7 +360,7 @@ def getWeatherAlertsNOAA(lat=0, lon=0, useDefaultLatLon=False): 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" + alerts += f"{title}\nArea: {area_desc}\n" if alerts == "" or alerts == None: return NO_ALERTS From 502a4f266693148c8241a4b70600c3dfd242ca78 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 5 Jan 2025 15:37:41 -0800 Subject: [PATCH 17/53] Update locationdata.py --- modules/locationdata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/locationdata.py b/modules/locationdata.py index 177ff37..a5f2f1e 100644 --- a/modules/locationdata.py +++ b/modules/locationdata.py @@ -360,7 +360,7 @@ def getWeatherAlertsNOAA(lat=0, lon=0, useDefaultLatLon=False): 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" + alerts += f"{title}. {area_desc}\n" if alerts == "" or alerts == None: return NO_ALERTS From 725cbd8045a0d3b41f3561b2cfb7053e3009d678 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 5 Jan 2025 16:01:12 -0800 Subject: [PATCH 18/53] Update locationdata.py --- modules/locationdata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/locationdata.py b/modules/locationdata.py index a5f2f1e..78cd6d6 100644 --- a/modules/locationdata.py +++ b/modules/locationdata.py @@ -360,7 +360,7 @@ def getWeatherAlertsNOAA(lat=0, lon=0, useDefaultLatLon=False): 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}. {area_desc}\n" + alerts += f"{title}. {area_desc.replace(' ', '')}\n" if alerts == "" or alerts == None: return NO_ALERTS From 45c67024e7c687ade6967122e033f242d9449ca7 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 5 Jan 2025 16:19:59 -0800 Subject: [PATCH 19/53] enhanceSpotter --- modules/web.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/modules/web.py b/modules/web.py index 59d76c4..a84f2a9 100644 --- a/modules/web.py +++ b/modules/web.py @@ -1 +1,25 @@ -# investigate python3 -m http.server to serve up reports and config data? +#!/usr/bin/env python3 + +SSL = False +PORT = 8008 # python3 -m http.server 8008 + +import http.server +if SSL: + import ssl + +# boot up simple HTTP server +httpd = http.server.HTTPServer(('127.0.0.1', PORT), http.server.SimpleHTTPRequestHandler) + +if SSL: + # Generate with: openssl req -new -x509 -keyout server.pem -out server.pem -days 365 -nodes + 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) + #ctx.load_cert_chain(certfile='server.crt', keyfile='server.key') + httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True) + + +httpd.serve_forever() \ No newline at end of file From f12fa0fe9bfae2438193d98232c4a4888048f2d4 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 5 Jan 2025 16:20:17 -0800 Subject: [PATCH 20/53] enhance --- modules/system.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/modules/system.py b/modules/system.py index 4377f1d..544e798 100644 --- a/modules/system.py +++ b/modules/system.py @@ -1037,36 +1037,36 @@ async def retry_interface(nodeID): 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): global handleSentinel_spotted, handleSentinel_loop - enemySpotted = "" + detectedNearby = "" resolution = "unknown" closest_nodes = get_closest_nodes(deviceID) 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', deviceID) - enemySpotted += ", " + get_name_from_number(closest_nodes[0]['id'], 'short', deviceID) - enemySpotted += ", " + str(closest_nodes[0]['id']) - enemySpotted += ", " + decimal_to_hex(closest_nodes[0]['id']) - enemySpotted += f" at {closest_nodes[0]['distance']}m" + detectedNearby = get_name_from_number(closest_nodes[0]['id'], '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_nodes[0]['distance']}m" - if handleSentinel_loop >= sentry_holdoff and handleSentinel_spotted != enemySpotted: + if handleSentinel_loop >= sentry_holdoff and closest_nodes[0]['id'] not in handleSentinel_spotted: 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 Interface{deviceID} Accuracy is {resolution}bits") + 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}: {enemySpotted}", secure_channel, 0, i) + send_message(f"Sentry{deviceID}: {detectedNearby}", secure_channel, 0, i) 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(closest_nodes[0]['id']) else: handleSentinel_loop += 1 From 1ff5895bad0572ef8f83c078e6b2da9544893227 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 5 Jan 2025 16:39:00 -0800 Subject: [PATCH 21/53] reporting server @g7kse check this out --- modules/web.py | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/modules/web.py b/modules/web.py index a84f2a9..5cfa42d 100644 --- a/modules/web.py +++ b/modules/web.py @@ -1,25 +1,46 @@ #!/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 -SSL = False -PORT = 8008 # python3 -m http.server 8008 - +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), http.server.SimpleHTTPRequestHandler) +httpd = http.server.HTTPServer(('127.0.0.1', PORT), QuietHandler) if SSL: - # Generate with: openssl req -new -x509 -keyout server.pem -out server.pem -days 365 -nodes 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) - #ctx.load_cert_chain(certfile='server.crt', keyfile='server.key') httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True) - httpd.serve_forever() \ No newline at end of file From af734ccb1f67401e2d55d8452274e960c638c8c1 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 5 Jan 2025 17:01:07 -0800 Subject: [PATCH 22/53] enhanceSentry --- modules/system.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/modules/system.py b/modules/system.py index 544e798..2f2c9c3 100644 --- a/modules/system.py +++ b/modules/system.py @@ -1044,15 +1044,28 @@ async def handleSentinel(deviceID): 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 < 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: - detectedNearby = get_name_from_number(closest_nodes[0]['id'], 'long', deviceID) + 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_nodes[0]['distance']}m" + detectedNearby += f" at {closest_distance}m" - if handleSentinel_loop >= sentry_holdoff and closest_nodes[0]['id'] not in handleSentinel_spotted: + if handleSentinel_loop >= sentry_holdoff: 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: @@ -1066,7 +1079,7 @@ async def handleSentinel(deviceID): for email in sysopEmails: send_email(email, f"Sentry{deviceID}: {detectedNearby}") handleSentinel_loop = 0 - handleSentinel_spotted.append(closest_nodes[0]['id']) + handleSentinel_spotted.append({'id': closest_node, 'distance': closest_distance}) else: handleSentinel_loop += 1 From ae844f8ecd4a4751c4e9f6b5daabf5b58a032135 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 5 Jan 2025 17:05:04 -0800 Subject: [PATCH 23/53] Update system.py --- modules/system.py | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/system.py b/modules/system.py index 2f2c9c3..7856218 100644 --- a/modules/system.py +++ b/modules/system.py @@ -1075,6 +1075,7 @@ async def handleSentinel(deviceID): 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}: {detectedNearby}") From 7069ba1f4333a1dbc9de9de8ceeef8a7a66974c0 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 5 Jan 2025 17:29:00 -0800 Subject: [PATCH 24/53] Update system.py --- modules/system.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/system.py b/modules/system.py index 7856218..f2a6157 100644 --- a/modules/system.py +++ b/modules/system.py @@ -1089,10 +1089,11 @@ async def watchdog(): while True: await asyncio.sleep(20) + # 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: + if interface is not None and not retry_int and globals().get(f'interface{i}_enabled'): try: firmware = getNodeFirmware(0, i) except Exception as e: From 9bae30bcb1c95eef6e2d745de06dc5885f9a3698 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 5 Jan 2025 17:42:29 -0800 Subject: [PATCH 25/53] Update config.template --- config.template | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.template b/config.template index fe22350..1a890c8 100644 --- a/config.template +++ b/config.template @@ -93,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 From 9014a7e8f9d465d3130214baf17f132f8cf17e15 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 5 Jan 2025 18:13:38 -0800 Subject: [PATCH 26/53] Update system.py --- modules/system.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/system.py b/modules/system.py index f2a6157..b9be57c 100644 --- a/modules/system.py +++ b/modules/system.py @@ -1051,7 +1051,7 @@ async def handleSentinel(deviceID): 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 < handleSentinel_spotted[i]['distance']: + 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: From f0a93b0191c245f109cdcf28f10806976b0695c3 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 5 Jan 2025 18:24:11 -0800 Subject: [PATCH 27/53] Update system.py --- modules/system.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/system.py b/modules/system.py index b9be57c..db4150d 100644 --- a/modules/system.py +++ b/modules/system.py @@ -1065,7 +1065,7 @@ async def handleSentinel(deviceID): detectedNearby += ", " + decimal_to_hex(closest_nodes[0]['id']) detectedNearby += f" at {closest_distance}m" - if handleSentinel_loop >= sentry_holdoff: + 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: From 5829cdcef9eba190dcbc2e13f8d05e1facb79cc4 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 5 Jan 2025 20:18:02 -0800 Subject: [PATCH 28/53] reportingEnhance --- logs/README.md | 5 ++++- modules/web.py | 10 ++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) 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/modules/web.py b/modules/web.py index 5cfa42d..2ae2359 100644 --- a/modules/web.py +++ b/modules/web.py @@ -11,7 +11,7 @@ import http.server PORT = 8420 # set webRoot index.html location -webRoot = "../etc/www" +webRoot = "etc/www" # Set to True to enable logging sdtout webServerLogs = False @@ -43,4 +43,10 @@ if SSL: exit(1) httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True) -httpd.serve_forever() \ No newline at end of file +print("Serving reports at http://localhost:", PORT) +print("Press ^C to quit.") +if not webServerLogs: + print("Server Logs are disabled") +# Serve forever, that is until the user interrupts the process +httpd.serve_forever() +exit(0) From b44fa22c11c1a2b44177c299ea553bc1bacf85bb Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 5 Jan 2025 20:20:34 -0800 Subject: [PATCH 29/53] Update web.py --- modules/web.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/web.py b/modules/web.py index 2ae2359..a340622 100644 --- a/modules/web.py +++ b/modules/web.py @@ -43,8 +43,7 @@ if SSL: exit(1) httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True) -print("Serving reports at http://localhost:", PORT) -print("Press ^C to quit.") +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 From 54e716d2ccd32d01c789c5b0ceb0366885c8e07a Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 5 Jan 2025 20:53:30 -0800 Subject: [PATCH 30/53] enhanceMultiNodeTelemetry --- modules/system.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/system.py b/modules/system.py index db4150d..a550586 100644 --- a/modules/system.py +++ b/modules/system.py @@ -749,9 +749,11 @@ 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() From 4a3cd2560c9db346705d755f69e24fc7a081a5c8 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 5 Jan 2025 21:27:25 -0800 Subject: [PATCH 31/53] labCleanupDone --- mesh_bot.py | 2 +- modules/filemon.py | 2 +- runShell.sh => script/runShell.sh | 0 sysEnv.sh => script/sysEnv.sh | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename runShell.sh => script/runShell.sh (100%) rename sysEnv.sh => script/sysEnv.sh (100%) diff --git a/mesh_bot.py b/mesh_bot.py index b0e29fe..e079566 100755 --- a/mesh_bot.py +++ b/mesh_bot.py @@ -772,7 +772,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) 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/runShell.sh b/script/runShell.sh similarity index 100% rename from runShell.sh rename to script/runShell.sh diff --git a/sysEnv.sh b/script/sysEnv.sh similarity index 100% rename from sysEnv.sh rename to script/sysEnv.sh From 10109672a76da0e7ec329fdd10e40f811dce9d3d Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 5 Jan 2025 21:34:05 -0800 Subject: [PATCH 32/53] Update sysEnv.sh --- script/sysEnv.sh | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/script/sysEnv.sh b/script/sysEnv.sh index 69e18f4..e220b01 100644 --- a/script/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 From b16b4e3c125b25fb365a54f025a75e5552c79af4 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 5 Jan 2025 21:35:19 -0800 Subject: [PATCH 33/53] Update runShell.sh --- script/runShell.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/runShell.sh b/script/runShell.sh index 3e67e7e..98be45e 100644 --- a/script/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 From d689495ee7f04eaa9a6dc162ea685dab5a86b182 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 5 Jan 2025 21:40:20 -0800 Subject: [PATCH 34/53] Cleanup scripts note here https://github.com/SpudGunMan/meshing-around/pull/103 and @turnrye can you review this branch and commit --- Dockerfile | 4 ++-- script/docker/compose.yaml | 4 ++-- entrypoint.sh => script/docker/entrypoint.sh | 0 3 files changed, 4 insertions(+), 4 deletions(-) rename entrypoint.sh => script/docker/entrypoint.sh (100%) diff --git a/Dockerfile b/Dockerfile index 13daf5f..c4f2a75 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,6 +15,6 @@ COPY . /app RUN pip install -r requirements.txt -RUN chmod +x /app/entrypoint.sh +RUN chmod +x /app/script/docker/entrypoint.sh -ENTRYPOINT ["/app/entrypoint.sh"] +ENTRYPOINT ["/app/script/docker/entrypoint.sh"] diff --git a/script/docker/compose.yaml b/script/docker/compose.yaml index c4e4a20..05f584a 100644 --- a/script/docker/compose.yaml +++ b/script/docker/compose.yaml @@ -16,14 +16,14 @@ services: image: ollama/ollama:0.5.1 volumes: - ./ollama:/root/.ollama - - ./ollama-entrypoint.sh:/entrypoint.sh + - ./ollama-entrypoint.sh:./entrypoint.sh container_name: ollama pull_policy: always tty: true restart: always entrypoint: - /usr/bin/bash - - /entrypoint.sh + - /script/docker/entrypoint.sh expose: - 11434 healthcheck: diff --git a/entrypoint.sh b/script/docker/entrypoint.sh similarity index 100% rename from entrypoint.sh rename to script/docker/entrypoint.sh From d2fd1337435f621d41dba9c23206de13769e9fea Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 5 Jan 2025 21:50:36 -0800 Subject: [PATCH 35/53] extraLocation @turnrye another thing to check out --- config.template | 2 ++ modules/locationdata.py | 5 ++++- modules/settings.py | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/config.template b/config.template index 86dc89a..d503094 100644 --- a/config.template +++ b/config.template @@ -138,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/modules/locationdata.py b/modules/locationdata.py index 78cd6d6..7ab277d 100644 --- a/modules/locationdata.py +++ b/modules/locationdata.py @@ -360,7 +360,10 @@ def getWeatherAlertsNOAA(lat=0, lon=0, useDefaultLatLon=False): 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}. {area_desc.replace(' ', '')}\n" + if enableExtraLocationWx: + alerts += f"{title}. {area_desc.replace(' ', '')}\n" + else: + alerts += f"{title}\n" if alerts == "" or alerts == None: return NO_ALERTS diff --git a/modules/settings.py b/modules/settings.py index d095713..3a4c51f 100644 --- a/modules/settings.py +++ b/modules/settings.py @@ -241,6 +241,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 From 4b0654971cc216519659f31cf779ec3082851779 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Wed, 8 Jan 2025 21:51:52 -0800 Subject: [PATCH 36/53] downgrade this log --- modules/locationdata.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/locationdata.py b/modules/locationdata.py index 7ab277d..652608e 100644 --- a/modules/locationdata.py +++ b/modules/locationdata.py @@ -522,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 @@ -601,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 From 0553a43a01e094a0f06483a6de18d14625f52efd Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 12 Jan 2025 10:10:48 -0800 Subject: [PATCH 37/53] Update Dockerfile --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index c4f2a75..491b030 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,7 @@ ENV TZ="America/Los_Angeles" WORKDIR /app COPY . /app +COPY /app/config.template /app/config.ini RUN pip install -r requirements.txt From 921b66f9e19b57ee4a632ff8e64d19239a70abe9 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 12 Jan 2025 10:12:06 -0800 Subject: [PATCH 38/53] Update entrypoint.sh --- script/docker/entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/docker/entrypoint.sh b/script/docker/entrypoint.sh index 0fd9377..a427d3d 100644 --- a/script/docker/entrypoint.sh +++ b/script/docker/entrypoint.sh @@ -2,6 +2,6 @@ # 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 +#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 From a1ffc8b1f63724e826c8ea2e855891bb5a53d25e Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 12 Jan 2025 10:21:14 -0800 Subject: [PATCH 39/53] Update Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 491b030..d55aea6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ ENV TZ="America/Los_Angeles" WORKDIR /app COPY . /app -COPY /app/config.template /app/config.ini +COPY config.template /app/config.ini RUN pip install -r requirements.txt From dcef6da5bc049fe19be0cc90f17783a0ca5e6465 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 12 Jan 2025 10:36:34 -0800 Subject: [PATCH 40/53] Update Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index d55aea6..5df28ae 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,4 +18,4 @@ RUN pip install -r requirements.txt RUN chmod +x /app/script/docker/entrypoint.sh -ENTRYPOINT ["/app/script/docker/entrypoint.sh"] +ENTRYPOINT ["/script/docker/entrypoint.sh"] From bf32eca47d2d6a7ec11438ed9d12bb3e7c6a2443 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 12 Jan 2025 10:45:33 -0800 Subject: [PATCH 41/53] Update Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 5df28ae..d55aea6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,4 +18,4 @@ RUN pip install -r requirements.txt RUN chmod +x /app/script/docker/entrypoint.sh -ENTRYPOINT ["/script/docker/entrypoint.sh"] +ENTRYPOINT ["/app/script/docker/entrypoint.sh"] From 5d0dae236caed750951df4c378a3350d8e64cd27 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 12 Jan 2025 11:03:41 -0800 Subject: [PATCH 42/53] Update Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index d55aea6..d3571bb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,4 +18,4 @@ RUN pip install -r requirements.txt RUN chmod +x /app/script/docker/entrypoint.sh -ENTRYPOINT ["/app/script/docker/entrypoint.sh"] +ENTRYPOINT ["/bin/bash", "/app/script/docker/entrypoint.sh"] From 9f676a4c8d028650148dcac72adea9a40531db6c Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 12 Jan 2025 11:07:42 -0800 Subject: [PATCH 43/53] Update entrypoint.sh --- script/docker/entrypoint.sh | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/script/docker/entrypoint.sh b/script/docker/entrypoint.sh index a427d3d..214285d 100644 --- a/script/docker/entrypoint.sh +++ b/script/docker/entrypoint.sh @@ -1,7 +1,6 @@ #!/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 - +# 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 From d482f2ccc93453d8e8a0546fc64c5ef638f20fc2 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 12 Jan 2025 11:19:27 -0800 Subject: [PATCH 44/53] docker enhancements --- Dockerfile | 2 +- script/docker/docker-install.bat | 6 ++++++ script/docker/docker-terminal.bat | 2 ++ 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 script/docker/docker-install.bat create mode 100644 script/docker/docker-terminal.bat diff --git a/Dockerfile b/Dockerfile index d3571bb..b34222b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ 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 && \ 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 From de8266b95574f11d4f00c5849c8a635472754373 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 12 Jan 2025 11:19:36 -0800 Subject: [PATCH 45/53] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4c9bdbb..dc26ccb 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ 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 From f3d07eed977157c419163051494439d3bbac738d Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 12 Jan 2025 11:27:29 -0800 Subject: [PATCH 46/53] Update README.md --- script/docker/README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/script/docker/README.md b/script/docker/README.md index dc803c0..7520a83 100644 --- a/script/docker/README.md +++ b/script/docker/README.md @@ -1,3 +1,13 @@ # 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"` \ No newline at end of file From 1dffa0987d97e909bf03a4ef193e1996cb4b723f Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 12 Jan 2025 11:47:57 -0800 Subject: [PATCH 47/53] Update settings.py --- modules/settings.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/settings.py b/modules/settings.py index 3a4c51f..351ecda 100644 --- a/modules/settings.py +++ b/modules/settings.py @@ -177,6 +177,10 @@ if 'interface9' in config: 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 From 5ae496702daa8f9b97caa91c89689c294a2fd88b Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 12 Jan 2025 11:59:48 -0800 Subject: [PATCH 48/53] multiInterfaceRefactors --- mesh_bot.py | 69 +++++++++++++++++++++++++++++------------------ modules/system.py | 61 ++++++++++++++++++++++++----------------- 2 files changed, 80 insertions(+), 50 deletions(-) diff --git a/mesh_bot.py b/mesh_bot.py index e079566..32373ac 100755 --- a/mesh_bot.py +++ b/mesh_bot.py @@ -495,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) @@ -535,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 @@ -570,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: @@ -1002,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'): @@ -1210,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) @@ -1279,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/system.py b/modules/system.py index b838b5f..29d9571 100644 --- a/modules/system.py +++ b/modules/system.py @@ -283,13 +283,15 @@ def get_num_from_short_name(short_name, nodeInt=1): 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(): - if short_name == node['user']['shortName']: - return node['num'] - elif str(short_name.lower()) == node['user']['shortName'].lower(): - return node['num'] + if multiple_interface: + 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): @@ -321,13 +323,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: @@ -733,9 +736,10 @@ def exit_handler(): try: interface1.close() logger.debug(f"System: Interface1 Closed") - if interface2_enabled: - interface2.close() - logger.debug(f"System: Interface2 Closed") + for i in range(1, 10): + if globals().get(f'interface{i}_enabled'): + globals()[f'interface{i}'].close() + logger.debug(f"System: Interface{i} Closed") except Exception as e: logger.error(f"System: closing: {e}") if bbs_enabled: @@ -931,9 +935,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(): @@ -958,15 +959,21 @@ 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) + if multiple_interface: + for i in range(1, 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) + if multiple_interface: + for i in range(1, 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}") @@ -989,15 +996,21 @@ 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) + if multiple_interface: + for i in range(1, 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) + if multiple_interface: + for i in range(1, 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") From a85cc8c59384d110025fd6c7236e430685b98d34 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 12 Jan 2025 12:09:51 -0800 Subject: [PATCH 49/53] Update system.py --- modules/system.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/modules/system.py b/modules/system.py index 29d9571..37ab46f 100644 --- a/modules/system.py +++ b/modules/system.py @@ -733,13 +733,14 @@ def onDisconnect(interface): 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") - for i in range(1, 10): - if globals().get(f'interface{i}_enabled'): - globals()[f'interface{i}'].close() - logger.debug(f"System: Interface{i} 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: From 5a30cc75112c488a0ce5e820196af82b69e78ae9 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 12 Jan 2025 12:16:08 -0800 Subject: [PATCH 50/53] Update system.py --- modules/system.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/modules/system.py b/modules/system.py index 37ab46f..f9aed18 100644 --- a/modules/system.py +++ b/modules/system.py @@ -282,16 +282,14 @@ 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 multiple_interface: - 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'] + 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): @@ -960,8 +958,9 @@ async def handleSignalWatcher(): for ch in sigWatchBroadcastCh: if antiSpam and ch != publicChannel: send_message(msg, int(ch), 0, 1) + time.sleep(responseDelay) if multiple_interface: - for i in range(1, 10): + for i in range(2, 10): if globals().get(f'interface{i}_enabled'): send_message(msg, int(ch), 0, i) time.sleep(responseDelay) @@ -970,8 +969,9 @@ async def handleSignalWatcher(): else: if antiSpam and sigWatchBroadcastCh != publicChannel: send_message(msg, int(sigWatchBroadcastCh), 0, 1) + time.sleep(responseDelay) if multiple_interface: - for i in range(1, 10): + for i in range(2, 10): if globals().get(f'interface{i}_enabled'): send_message(msg, int(sigWatchBroadcastCh), 0, i) time.sleep(responseDelay) @@ -997,8 +997,9 @@ async def handleFileWatcher(): for ch in file_monitor_broadcastCh: if antiSpam and ch != publicChannel: send_message(msg, int(ch), 0, 1) + time.sleep(responseDelay) if multiple_interface: - for i in range(1, 10): + for i in range(2, 10): if globals().get(f'interface{i}_enabled'): send_message(msg, int(ch), 0, i) time.sleep(responseDelay) @@ -1007,8 +1008,9 @@ async def handleFileWatcher(): else: if antiSpam and file_monitor_broadcastCh != publicChannel: send_message(msg, int(file_monitor_broadcastCh), 0, 1) + time.sleep(responseDelay) if multiple_interface: - for i in range(1, 10): + 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) From 49b8206e76de67a7c4e5f89b4f2b04f61267b6e4 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 12 Jan 2025 12:36:08 -0800 Subject: [PATCH 51/53] Update pong_bot.py --- pong_bot.py | 46 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/pong_bot.py b/pong_bot.py index b0c4fe8..e66f6dc 100755 --- a/pong_bot.py +++ b/pong_bot.py @@ -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'] From 7ae0d5e927f47bb9494e4cf92f69d8a1e9b8715e Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 12 Jan 2025 12:39:37 -0800 Subject: [PATCH 52/53] Update pong_bot.py --- pong_bot.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/pong_bot.py b/pong_bot.py index e66f6dc..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), @@ -361,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) @@ -385,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: @@ -404,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}") From 902b4f22ee03bdc9e47629b6838aadab4df80333 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 12 Jan 2025 12:43:44 -0800 Subject: [PATCH 53/53] readme --- README.md | 13 +------------ script/docker/README.md | 9 ++++++++- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index dc26ccb..83e0281 100644 --- a/README.md +++ b/README.md @@ -78,18 +78,7 @@ The code is under active development, so make sure to pull the latest changes re - **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/script/docker/README.md b/script/docker/README.md index 7520a83..712af80 100644 --- a/script/docker/README.md +++ b/script/docker/README.md @@ -10,4 +10,11 @@ still a WIP ## 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"` \ No newline at end of file +`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