diff --git a/README.md b/README.md index 2021c2a..eb17092 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ The following plugins can be used in the `pipelines` section of `config.yaml`: | ----------------- | -------------------------------------------------------------------- | | `debugger` | Log the packet to the system console | | `message_filter` | Filters out packets from the bridge that match a specific criteria | -| `distance_filter` | Filters out packets that originate too far from a specified `device` | +| `location_filter` | Filters out packets that originate too far from a specified `device` | | `webhook` | Send HTTP requests with custom payloads using packet information | | `mqtt_plugin` | Send packets to a MQTT server | | `encrypt_filter` | Encrypt a packet for a desired MQTT recipient | @@ -131,28 +131,37 @@ Useful for troubleshooting. - **app** Name of meshtastic application to allow or disallow - **from** The packet `fromId` values to allow or disallow - **to** The packet `toId` values to allow or disallow +- **message** The packet `message` values to allow or disallow. Supports Regex. For example: ``` message_filter: - app: + from: allow: - !bd5ba0ec - !f85bc0bc disallow: - !c15ba2ec + message: + disallow: + - Good night ``` -### distance_filter - Allow or block packets based on distance from origin to radio +### location_filter - Filter packets by location from current node (default) or specific location - **log_level** `debug` or `info`. Default `info` -- **max_distance_km** Number of kilometers +- **max_distance_km** Filter packets more than a certain distance +- **comparison** `within` or `outside`. Default `within` +- **compare_latitude** Set the latitude to compare against +- **compare_longitude** Set the longitude to compare against +- **latitude** Set the latitude +- **longitude** Set the longitude -For example: +For example ``` -distance_filter: +location_filter: max_distance_km: 1000 ``` @@ -225,7 +234,7 @@ decrypt_filter: key: '/home/user/keys/key.pem' ``` -### send_plugin - Send a packet to a radio +### radio_message_plugin - Send a packet to a radio - **log_level** `debug` or `info`. Default `info` - **active** Plugin is active. Values: `true` or `false`. Default `true`. @@ -242,7 +251,7 @@ For example: Broadcasts all packets to the "remote" radio network that are destined to the node `12354345`. ``` -send_plugin: +radio_message_plugin: device: remote node_mapping: 12354345: ^all diff --git a/main.py b/main.py index 873071d..7ca7c02 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,6 @@ import json import logging +from re import I import meshtastic import meshtastic.serial_interface import meshtastic.tcp_interface @@ -20,7 +21,15 @@ logger = logging.getLogger(name="meshtastic.bridge") logger.setLevel(logging.DEBUG) +class CustomTCPInterface(meshtastic.tcp_interface.TCPInterface): + def __init__(self, hostname, device_name): + self.device_name = device_name + self.hostname = hostname + super(CustomTCPInterface, self).__init__(hostname) + + def onReceive(packet, interface): # called when a packet arrives + nodeInfo = interface.getMyNodeInfo() for pipeline, pipeline_plugins in bridge_config["pipelines"].items(): logger.debug(f"Pipeline {pipeline} initiated") @@ -29,6 +38,9 @@ def onReceive(packet, interface): # called when a packet arrives pipeline_packet = p.do_action(packet) for plugin in pipeline_plugins: + if not pipeline_packet: + continue + for plugin_key, plugin_config in plugin.items(): logger.debug(f"Processing plugin: {pipeline}/{plugin_key}") @@ -57,8 +69,17 @@ def onConnection( ) +def onLost(interface): + logger.debug(f"Connecting to {interface.hostname} ...") + devices[interface.device_name] = CustomTCPInterface( + hostname=interface.hostname, device_name=interface.device_name + ) + logger.debug(f"Connected to {interface.hostname}") + + pub.subscribe(onReceive, "meshtastic.receive") pub.subscribe(onConnection, "meshtastic.connection.established") +pub.subscribe(onLost, "meshtastic.connection.lost") with open("config.yaml") as f: bridge_config = yaml.load(f, Loader=SafeLoader) @@ -75,99 +96,109 @@ for device in bridge_config["devices"]: devPath=device["serial"] ) elif "tcp" in device: - devices[device["name"]] = meshtastic.tcp_interface.TCPInterface( - hostname=device["tcp"] + logger.debug(f"Connecting to {device['tcp']} ...") + devices[device["name"]] = CustomTCPInterface( + hostname=device["tcp"], device_name=device["name"] ) + logger.debug(f"Connected to {device['tcp']}") else: devices[device["name"]] = meshtastic.serial_interface.SerialInterface() -for config in bridge_config["mqtt_servers"]: - required_options = [ - "name", - "server", - "port", - ] +if "mqtt_servers" in bridge_config: + for config in bridge_config["mqtt_servers"]: + required_options = [ + "name", + "server", + "port", + ] - for option in required_options: - if option not in config: - logger.warning("Missing config: {option}") + for option in required_options: + if option not in config: + logger.warning("Missing config: {option}") - client_id = config["client_id"] if "client_id" in config else None - username = config["username"] if "username" in config else None - password = config["password"] if "password" in config else None + client_id = config["client_id"] if "client_id" in config else None + username = config["username"] if "username" in config else None + password = config["password"] if "password" in config else None - if client_id: - mqttc = mqtt.Client(client_id) - else: - mqttc = mqtt.Client() + if client_id: + mqttc = mqtt.Client(client_id) + else: + mqttc = mqtt.Client() - if username and password: - mqttc.username_pw_set(username, password) + if username and password: + mqttc.username_pw_set(username, password) - mqtt_servers[config["name"]] = mqttc + mqtt_servers[config["name"]] = mqttc - def on_connect(mqttc, obj, flags, rc): - logger.debug(f"Connected to MQTT {config['name']}") + def on_connect(mqttc, obj, flags, rc): + logger.debug(f"Connected to MQTT {config['name']}") - def on_message(mqttc, obj, msg): - packet = msg.payload.decode() + def on_message(mqttc, obj, msg): + orig_packet = msg.payload.decode() - logger.debug(f"MQTT {config['name']}: on_message") + logger.debug(f"MQTT {config['name']}: on_message") - if "pipelines" not in config: - logger.warning(f"MQTT {config['name']}: no pipeline") - return + if "pipelines" not in config: + logger.warning(f"MQTT {config['name']}: no pipeline") + return - for pipeline, pipeline_plugins in config["pipelines"].items(): + for pipeline, pipeline_plugins in config["pipelines"].items(): - logger.debug(f"MQTT {config['name']} pipeline {pipeline} started") - if not packet: - continue + packet = orig_packet - for plugin in pipeline_plugins: - for plugin_key, plugin_config in plugin.items(): - if plugin_key not in plugins: - logger.error(f"No such plugin: {plugin_key}. Skipping") + logger.debug(f"MQTT {config['name']} pipeline {pipeline} started") + if not packet: + continue + + for plugin in pipeline_plugins: + if not packet: continue - p = plugins[plugin_key] - p.configure(devices, mqtt_servers, plugin_config) + for plugin_key, plugin_config in plugin.items(): + if plugin_key not in plugins: + logger.error(f"No such plugin: {plugin_key}. Skipping") + continue - try: - packet = p.do_action(packet) - except Exception as e: - logger.error(f"Hit an error: {e}", exc_info=True) - logger.debug(f"MQTT {config['name']} pipeline {pipeline} finished") + p = plugins[plugin_key] + p.configure(devices, mqtt_servers, plugin_config) - def on_publish(mqttc, obj, mid): - logger.debug(f"MQTT {config['name']}: on_publish: {mid}") + try: + packet = p.do_action(packet) + except Exception as e: + logger.error(f"Hit an error: {e}", exc_info=True) + logger.debug(f"MQTT {config['name']} pipeline {pipeline} finished") - def on_subscribe(mqttc, obj, mid, granted_qos): - logger.debug(f"MQTT {config['name']}: on_subscribe: {mid}") + def on_publish(mqttc, obj, mid): + logger.debug(f"MQTT {config['name']}: on_publish: {mid}") - mqttc.on_message = on_message - mqttc.on_connect = on_connect - mqttc.on_publish = on_publish - mqttc.on_subscribe = on_subscribe + def on_subscribe(mqttc, obj, mid, granted_qos): + logger.debug(f"MQTT {config['name']}: on_subscribe: {mid}") - import ssl + mqttc.on_message = on_message + mqttc.on_connect = on_connect + mqttc.on_publish = on_publish + mqttc.on_subscribe = on_subscribe - if "insecure" in config and config["insecure"]: - mqttc.tls_set(cert_reqs=ssl.CERT_NONE) - mqttc.tls_insecure_set(True) + import ssl - mqttc.connect(config["server"], config["port"], 60) + if "insecure" in config and config["insecure"]: + mqttc.tls_set(cert_reqs=ssl.CERT_NONE) + mqttc.tls_insecure_set(True) - if "topic" in config: - mqttc.subscribe(config["topic"], 0) + mqttc.connect(config["server"], config["port"], 60) - mqttc.loop_start() + if "topic" in config: + mqttc.subscribe(config["topic"], 0) + + mqttc.loop_start() while True: time.sleep(1000) -for device, instance in devices.items(): - instance.close() +if devices: + for device, instance in devices.items(): + instance.close() -for server, instance in mqtt_servers.items(): - instance.disconnect() +if mqtt_servers: + for server, instance in mqtt_servers.items(): + instance.disconnect() diff --git a/plugins.py b/plugins.py index 43a4c85..eb5899f 100644 --- a/plugins.py +++ b/plugins.py @@ -1,10 +1,11 @@ from haversine import haversine from meshtastic import mesh_pb2 -from meshtastic.__init__ import BROADCAST_ADDR +from random import randrange import base64 import json import logging import os +import re plugins = {} @@ -74,6 +75,31 @@ class MessageFilter(Plugin): self.logger.error("Missing packet") return packet + text = packet["decoded"]["text"] if "text" in packet["decoded"] else None + + if text and "message" in self.config: + if "allow" in self.config["message"]: + matches = False + for allow_regex in self.config["message"]["allow"]: + if not matches and re.search(allow_regex, text): + matches = True + + if not matches: + self.logger.debug( + f"Dropped because it doesn't match message allow filter" + ) + return None + + if text and "disallow" in self.config["message"]: + matches = False + for disallow_regex in self.config["message"]["disallow"]: + if not matches and re.search(disallow_regex, text): + matches = True + + if matches: + self.logger.debug(f"Dropped because it matches message disallow filter") + return None + filters = { "app": packet["decoded"]["portnum"], "from": packet["fromId"], @@ -83,12 +109,15 @@ class MessageFilter(Plugin): for filter_key, value in filters.items(): if filter_key in self.config: filter_val = self.config[filter_key] + if ( "allow" in filter_val and filter_val["allow"] and value not in filter_val["allow"] ): - self.logger.debug(f"Dropped because it doesn't match allow filter") + self.logger.debug( + f"Dropped because it doesn't match {filter_key} allow filter" + ) return None if ( @@ -96,7 +125,9 @@ class MessageFilter(Plugin): and filter_val["disallow"] and value in filter_val["disallow"] ): - self.logger.debug(f"Dropped because it matches disallow filter") + self.logger.debug( + f"Dropped because it matches {filter_key} disallow filter" + ) return None self.logger.debug(f"Accepted") @@ -106,21 +137,24 @@ class MessageFilter(Plugin): plugins["message_filter"] = MessageFilter() -class DistanceFilter(Plugin): +class LocationFilter(Plugin): logger = logging.getLogger(name="meshtastic.bridge.filter.distance") def do_action(self, packet): - if "device" not in self.config: - return packet - - if "position" not in packet["decoded"]: - return packet - message_source_position = None current_local_position = None + if "device" in self.config and self.config["device"] in self.devices: + nodeInfo = self.devices[self.config["device"]].getMyNodeInfo() + current_local_position = ( + nodeInfo["position"]["latitude"], + nodeInfo["position"]["longitude"], + ) + if ( - "latitude" in packet["decoded"]["position"] + "decoded" in packet + and "position" in packet["decoded"] + and "latitude" in packet["decoded"]["position"] and "longitude" in packet["decoded"]["position"] ): message_source_position = ( @@ -128,31 +162,43 @@ class DistanceFilter(Plugin): packet["decoded"]["position"]["longitude"], ) - nodeInfo = self.devices[self.config["device"]].getMyNodeInfo() + if "compare_latitude" in self.config and "compare_longitude" in self.config: current_local_position = ( - nodeInfo["position"]["latitude"], - nodeInfo["position"]["longitude"], + self.config["compare_latitude"], + self.config["compare_longitude"], ) if message_source_position and current_local_position: - distance_km = haversine(message_source_position, current_local_position) + comparison = ( + self.config["comparison"] if "comparison" in self.config else "within" + ) + # message originates from too far a distance - if ( - "max_distance_km" in self.config - and self.config["max_distance_km"] > 0 - and distance_km > self.config["max_distance_km"] - ): - logger.debug( - f"Packet from too far: {distance_km} > {SUPPORTED_BRIDGE_DISTANCE_KM}" - ) - return None + if "max_distance_km" in self.config and self.config["max_distance_km"] > 0: + acceptable_distance = self.config["max_distance_km"] + + if comparison == "within" and distance_km > acceptable_distance: + self.logger.debug( + f"Packet from too far: {distance_km} > {acceptable_distance}" + ) + return None + elif comparison == "outside" and distance_km < acceptable_distance: + self.logger.debug( + f"Packet too close: {distance_km} < {acceptable_distance}" + ) + return None + + if "latitude" in self.config: + packet["decoded"]["position"]["latitude"] = self.config["latitude"] + if "longitude" in self.config: + packet["decoded"]["position"]["longitude"] = self.config["longitude"] return packet -plugins["distance_filter"] = DistanceFilter() +plugins["location_filter"] = LocationFilter() class WebhookPlugin(Plugin): @@ -311,7 +357,7 @@ class DecryptFilter(Plugin): plugins["decrypt_filter"] = DecryptFilter() -class SendPlugin(Plugin): +class RadioMessagePlugin(Plugin): logger = logging.getLogger(name="meshtastic.bridge.plugin.send") def do_action(self, packet): @@ -386,4 +432,4 @@ class SendPlugin(Plugin): return packet -plugins["send_plugin"] = SendPlugin() +plugins["radio_message_plugin"] = RadioMessagePlugin() diff --git a/sample_config.yaml b/sample_config.yaml index 649dd6c..fa94b3f 100644 --- a/sample_config.yaml +++ b/sample_config.yaml @@ -1,5 +1,5 @@ devices: - - name: local + - name: local pipelines: - - debugger: - log_level: info + - debugger: + log_level: info