diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml new file mode 100644 index 0000000..1f4f6bb --- /dev/null +++ b/.github/workflows/main.yaml @@ -0,0 +1,24 @@ +on: + push: + tags: + - "*" + +jobs: + build: + name: Build, push + runs-on: ubuntu-latest + steps: + - name: Checkout master + uses: actions/checkout@main + + - name: Build container + run: docker build --tag gwhittington/meshtastic-bridge:latest + + - name: Log in to Container Registry with short-lived credentials + run: docker login --username=gwhittington --password "${{secrets.DOCKER_HUB}}" + + - name: Push image to Container Registry + run: docker push gwhittington/meshtastic-bridge:latest + + - name: Logout from Container Registry + run: docker logout diff --git a/main.py b/main.py index c177f59..5a08d42 100644 --- a/main.py +++ b/main.py @@ -7,105 +7,61 @@ import time from meshtastic import portnums_pb2, mesh_pb2 from meshtastic.__init__ import LOCAL_ADDR, BROADCAST_NUM, BROADCAST_ADDR import os - +from plugins import plugins from pubsub import pub +import yaml +from yaml.loader import SafeLoader -local_interface = None -remote_interface = None -bridge_logger = logging.getLogger(name="meshtastic.bridge") +logger = logging.getLogger(name="meshtastic.bridge") +logger.setLevel(logging.DEBUG) -BRIDGE_LOG = os.environ['BRIDGE_LOG'] if 'BRIDGE_LOG' in os.environ else 'INFO' -NODE_LOG = os.environ['NODE_LOG'] if 'NODE_LOG' in os.environ else 'INFO' +with open("config.yaml") as f: + bridge_config = yaml.load(f, Loader=SafeLoader) -if BRIDGE_LOG == 'DEBUG': - bridge_logger.setLevel(logging.DEBUG) -elif BRIDGE_LOG == 'INFO': - bridge_logger.setLevel(logging.INFO) +devices = {} -if NODE_LOG == 'DEBUG': - logging.basicConfig(level=logging.DEBUG) -elif NODE_LOG == 'INFO': - logging.basicConfig(level=logging.INFO) +for device in bridge_config["devices"]: + if "serial" in device: + devices[device["name"]] = meshtastic.serial_interface.SerialInterface( + devPath=device["serial"] + ) + elif "tcp" in device: + devices[device["name"]] = meshtastic.tcp_interface.TCPInterface( + hostname=device["tcp"] + ) + else: + devices[device["name"]] = meshtastic.serial_interface.SerialInterface() -local_node_addr = os.environ['LOCAL_NODE_ADDR'] if 'LOCAL_NODE_ADDR' in os.environ else None -remote_node_addr = os.environ['REMOTE_NODE_ADDR'] -if local_node_addr and '/' in local_node_addr: - bridge_logger.debug(f"Connecting to local node via serial port: {local_node_addr} ...") - local_interface = meshtastic.serial_interface.SerialInterface(devPath=local_node_addr) -elif local_node_addr: - bridge_logger.debug(f"Connecting to local node via TCP: {local_node_addr} ...") - local_interface = meshtastic.tcp_interface.TCPInterface(hostname=local_node_addr) -else: - bridge_logger.debug(f"Connecting to local node via serial port ...") - local_interface = meshtastic.serial_interface.SerialInterface() +def onReceive(packet, interface): # called when a packet arrives + for pipeline in bridge_config["pipelines"]: -bridge_logger.info(f"Connected to local node") + pipeline_packet = packet -bridge_logger.debug(f"Connecting to remote node via TCP: {remote_node_addr} ...") -remote_interface = meshtastic.tcp_interface.TCPInterface(hostname=remote_node_addr) -bridge_logger.info(f"Connected to remote node") -ourNode = local_interface.getNode('^local') + for key, config in pipeline.items(): -SUPPORTED_MESSAGES = [ - 'POSITION_APP', - 'TEXT_MESSAGE_APP' -] + if not pipeline_packet: + continue -SUPPORTED_BRIDGE_DISTANCE_KM = int(os.environ['BRIDGE_DISTANCE_KM']) if 'BRIDGE_DISTANCE_KM' in os.environ else 0 -CHANNEL_INDEX = 0 -NODE_PROXY = {BROADCAST_ADDR: BROADCAST_ADDR} + if key not in plugins: + logger.error(f"No such plugin: {key}. Skipping") + continue -def onReceive(packet, interface): # called when a packet arrives + p = plugins[key] + p.configure(devices, config) - bridge_logger.debug(f"Packet received: {packet}") + pipeline_packet = p.do_action(pipeline_packet) - if packet['decoded']['portnum'] not in SUPPORTED_MESSAGES: - bridge_logger.debug(f"Dropping {packet['decoded']['portnum']}") - return - message_source_position = None - current_local_position = None +def onConnection( + interface, topic=pub.AUTO_TOPIC +): # called when we (re)connect to the radio + nodeInfo = interface.getMyNodeInfo() - if 'position' in packet['decoded']: + logger.info( + f"Connected to node: userId={nodeInfo['user']['id']} hwModel={nodeInfo['user']['hwModel']}" + ) - if 'latitude' in packet['decoded']['position'] and 'longitude' in packet['decoded']['position']: - message_source_position = (packet['decoded']['position']['latitude'], packet['decoded']['position']['longitude']) - - nodeInfo = local_interface.getMyNodeInfo() - current_local_position = (nodeInfo['position']['latitude'], nodeInfo['position']['longitude']) - - if message_source_position and current_local_position: - - distance_km = haversine(message_source_position, current_local_position) - - # message originates from too far a distance - if SUPPORTED_BRIDGE_DISTANCE_KM > 0 and distance_km > SUPPORTED_BRIDGE_DISTANCE_KM: - bridge_logger.debug(f"Packet from too far: {distance_km} > {SUPPORTED_BRIDGE_DISTANCE_KM}") - return - - if 'to' in packet: - # Broadcast messages or specific - if packet['to'] in NODE_PROXY: - destinationId = NODE_PROXY[packet['to']] - else: - destinationId = packet['to'] - - channelIndex = CHANNEL_INDEX - - meshPacket = mesh_pb2.MeshPacket() - meshPacket.channel = channelIndex - meshPacket.decoded.payload = packet['decoded']['payload'] - meshPacket.decoded.portnum = packet['decoded']['portnum'] - meshPacket.decoded.want_response = False - meshPacket.id = remote_interface._generatePacketId() - - if destinationId == BROADCAST_ADDR or destinationId in remote_interface.nodes: - bridge_logger.debug(f"Sending packet {meshPacket.id} to TCP server") - remote_interface._sendPacket(meshPacket=meshPacket, destinationId=destinationId) - -def onConnection(interface, topic=pub.AUTO_TOPIC): # called when we (re)connect to the radio - print("Connected.") pub.subscribe(onReceive, "meshtastic.receive") pub.subscribe(onConnection, "meshtastic.connection.established") @@ -113,5 +69,5 @@ pub.subscribe(onConnection, "meshtastic.connection.established") while True: time.sleep(1000) -local_interface.close() -remote_interface.close() +for device, instance in devices.items(): + instance.close() diff --git a/requirements.txt b/requirements.txt index 55f1da6..7a6cf8a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,4 @@ haversine meshtastic +requests +pyyaml