26 Commits

Author SHA1 Message Date
geoffwhittington d4a17de70a Merge pull request #5 from jp-bennett/main
Fix Docker image and work on Owntracks plugin
2024-09-24 10:54:50 -04:00
Jonathan Bennett b23b82173c Allow Owntracks plugin to handle negative and hex node numbers 2023-09-26 20:28:58 -05:00
Jonathan Bennett 61116198fc Bump Dockerfile Python version to fix crash 2023-09-26 17:20:09 -05:00
geoffwhittington 5ce046de13 Merge pull request #2 from jp-bennett/main
Adds owntracks-plugin
2023-06-16 09:43:25 -04:00
Jonathan Bennett 0eea64f2e4 Add more Owntracks_plugin documentation 2023-06-11 18:55:03 -05:00
Jonathan Bennett 6d186ea339 Add documentation to the use-cases 2023-06-11 18:48:23 -05:00
Jonathan Bennett 7d20077166 Remove wait for mqtt, fixes deadlock 2023-06-11 18:42:31 -05:00
Jonathan Bennett 5a4ca73e8c Adds plugin to push location messages to an Owntracks mqtt instance 2023-06-11 18:31:56 -05:00
Jonathan Bennett 42a4e4a76a Add support for MQTT-only operation 2023-06-11 14:48:29 -05:00
Geoff Whittington 170d52ef14 Add support for NoStr 2023-02-11 22:46:06 -05:00
Geoff Whittington 0fccf6ea9e Add support for NoStr 2023-02-11 22:43:51 -05:00
Geoff Whittington 461d540c30 Updates 2022-12-04 19:43:09 -05:00
Geoff Whittington 6585f52f91 Updats 2022-12-04 19:42:42 -05:00
Geoff Whittington 1f4027829e Linux Docker deployments support host network 2022-12-04 19:12:54 -05:00
Geoff Whittington 159b3b097d Added use case config, fixed bugs 2022-12-04 14:25:38 -05:00
Geoff Whittington dd83f29806 Merge branch 'main' of github.com:geoffwhittington/meshtastic-bridge into main 2022-11-21 14:17:50 -05:00
Geoff Whittington 26d540fead fix type bug 2022-11-21 14:17:36 -05:00
geoffwhittington b72102c402 Update Dockerfile 2022-11-20 18:53:35 -05:00
geoffwhittington 0930aa917e Update Dockerfile 2022-11-20 18:51:16 -05:00
geoffwhittington 7ad584830a Update Dockerfile 2022-11-20 18:47:10 -05:00
geoffwhittington fe5e119059 Update Dockerfile 2022-11-20 18:41:47 -05:00
geoffwhittington 23c1250ccb Update Dockerfile 2022-11-20 18:26:30 -05:00
geoffwhittington c5134fd5b4 Update README.md 2022-11-20 18:17:04 -05:00
geoffwhittington c3ff3b3a4f Update main.yaml 2022-11-20 18:10:11 -05:00
geoffwhittington 0f1e5a908d Add multiple platform support 2022-11-20 18:08:14 -05:00
Geoff Whittington 74114f1acc Support for 1.3 (2.0) 2022-10-31 17:07:30 -04:00
11 changed files with 615 additions and 210 deletions
+8 -13
View File
@@ -4,21 +4,16 @@ on:
- "*" - "*"
jobs: jobs:
build: docker-buildx:
name: Build, push
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout main - name: Checkout main
uses: actions/checkout@main uses: actions/checkout@main
- name: Build container - name: Build and publish image
run: docker build --tag gwhittington/meshtastic-bridge:latest . uses: zmingxie/docker_buildx@master
with:
- name: Log in to Container Registry with short-lived credentials publish: true
run: docker login --username=gwhittington --password "${{secrets.DOCKER_HUB}}" imageName: gwhittington/meshtastic-bridge
dockerHubUser: gwhittington
- name: Push image to Container Registry dockerHubPassword: ${{secrets.DOCKER_HUB}}
run: docker push gwhittington/meshtastic-bridge:latest
- name: Logout from Container Registry
run: docker logout
+5 -1
View File
@@ -1,5 +1,5 @@
# set base image (host OS) # set base image (host OS)
FROM python:3.8 FROM python:3.9
# set the working directory in the container # set the working directory in the container
WORKDIR /code WORKDIR /code
@@ -7,7 +7,11 @@ WORKDIR /code
# copy the dependencies file to the working directory # copy the dependencies file to the working directory
COPY requirements.txt . COPY requirements.txt .
RUN apt-get update && apt-get install -y cargo
# install dependencies # install dependencies
RUN pip install -U pip
RUN pip install setuptools_rust wheel
RUN pip install -r requirements.txt RUN pip install -r requirements.txt
# copy the content of the local src directory to the working directory # copy the content of the local src directory to the working directory
+81 -20
View File
@@ -66,7 +66,7 @@ mqtt_servers:
mqtt-to-radio: mqtt-to-radio:
- decrypt_filter: - decrypt_filter:
key: '/home/user/keys/key.pem' key: '/home/user/keys/key.pem'
- send_plugin: - radio_message_plugin:
device: remote device: remote
pipelines: pipelines:
radio-to-mqtt: radio-to-mqtt:
@@ -101,16 +101,18 @@ NOTE: If `tcp` or `serial` are not given the bridge will attempt to detect a rad
The following plugins can be used in the `pipelines` section of `config.yaml`: The following plugins can be used in the `pipelines` section of `config.yaml`:
| Plugin | Description | | Plugin | Description |
| ----------------- | -------------------------------------------------------------------- | | ---------------------- | -------------------------------------------------------------------- |
| `debugger` | Log the packet to the system console | | `debugger` | Log the packet to the system console |
| `message_filter` | Filters out packets from the bridge that match a specific criteria | | `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 | | `webhook` | Send HTTP requests with custom payloads using packet information |
| `mqtt_plugin` | Send packets to a MQTT server | | `mqtt_plugin` | Send packets to a MQTT server |
| `encrypt_filter` | Encrypt a packet for a desired MQTT recipient | | `encrypt_filter` | Encrypt a packet for a desired MQTT recipient |
| `decrypt_filter` | Decrypt a packet originating from MQTT | | `decrypt_filter` | Decrypt a packet originating from MQTT |
| `send_plugin` | Send a packet to a specified `device` | | `radio_message_plugin` | Send a packet to a specified `device` |
| `nostr_plugin` | Send a NoStr event to a relay |
| `owntracks_plugin` | Send location data to MQTT server for Owntracks |
### debugger - Output the contents of a packet ### debugger - Output the contents of a packet
@@ -131,28 +133,37 @@ Useful for troubleshooting.
- **app** Name of meshtastic application to allow or disallow - **app** Name of meshtastic application to allow or disallow
- **from** The packet `fromId` values to allow or disallow - **from** The packet `fromId` values to allow or disallow
- **to** The packet `toId` 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: For example:
``` ```
message_filter: message_filter:
app: from:
allow: allow:
- !bd5ba0ec - !bd5ba0ec
- !f85bc0bc - !f85bc0bc
disallow: disallow:
- !c15ba2ec - !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` - **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** latitude to compare against
- **compare_longitude** longitude to compare against
- **latitude** Set the latitude
- **longitude** Set the longitude
For example: For example
``` ```
distance_filter: location_filter:
max_distance_km: 1000 max_distance_km: 1000
``` ```
@@ -190,7 +201,7 @@ webhook:
- **log_level** `debug` or `info`. Default `info` - **log_level** `debug` or `info`. Default `info`
- **active** Plugin is active. Values: `true` or `false`. Default = `true`. - **active** Plugin is active. Values: `true` or `false`. Default = `true`.
- **name** Reference of an existing MQTT server configured in the top-level `mqtt_servers` configuration - **name** Reference of an existing MQTT server configured in the top-level `mqtt_servers` configuration
- **message** Override the packet message with a custom value - **message** Override the packet message with a custom value.
- **topic** The message topic - **topic** The message topic
For example: For example:
@@ -201,6 +212,10 @@ mqtt_plugin:
topic: meshtastic/topic topic: meshtastic/topic
``` ```
Placeholders can be used with the **message** value:
- `{MSG}` - Packet text
### encrypt_filter - Encrypt a packet before sending it to a MQTT server ### encrypt_filter - Encrypt a packet before sending it to a MQTT server
- **log_level** `debug` or `info`. Default `info` - **log_level** `debug` or `info`. Default `info`
@@ -225,7 +240,50 @@ decrypt_filter:
key: '/home/user/keys/key.pem' key: '/home/user/keys/key.pem'
``` ```
### send_plugin - Send a packet to a radio ### nostr_plugin - Send a NoStr event
- **log_level** `debug` or `info`. Default `info`
- **private_key** The private key for a NoStr user. Secrets can be passed using ENV variables
- **public_key** The public key for the NoStr user associated with the private key.
- **message** A specific message (Optional)
- **relays** List of NoStr relays. Default `wss://nostr-pub.wellorder.net`, and `wss://relay.damus.io`
For example:
```
nostr_plugin:
private_key: "{NOSTR_PRIVATE_KEY}"
public_key: "npub1d0ja5d.......xw7jys4eqnk0"
relays:
- "wss://nostr-pub.wellorder.net"
```
Placeholders can be used with the **message** value:
- `{MSG}` - Packet text
### owntracks_plugin - Send location data to MQTT server for Owntracks
- **log_level** `debug` or `info`. Default `info`
- **server_name** The mqtt server to send owntracks messages to
- **tid_table** Table of the numeric from IDs of each node, mapped to an Owntracks name and TID
For example:
```
owntracks_plugin:
server_name: external
tid_table:
"1234": ["Van", "GV"]
"-5678": ["Home", "HR"]
```
Placeholders can be used with the **message** value:
- `{MSG}` - Packet text
### radio_message_plugin - Send a packet to a radio
- **log_level** `debug` or `info`. Default `info` - **log_level** `debug` or `info`. Default `info`
- **active** Plugin is active. Values: `true` or `false`. Default `true`. - **active** Plugin is active. Values: `true` or `false`. Default `true`.
@@ -242,7 +300,7 @@ For example:
Broadcasts all packets to the "remote" radio network that are destined to the node `12354345`. Broadcasts all packets to the "remote" radio network that are destined to the node `12354345`.
``` ```
send_plugin: radio_message_plugin:
device: remote device: remote
node_mapping: node_mapping:
12354345: ^all 12354345: ^all
@@ -268,10 +326,13 @@ python main.py
Create a `config.yaml` with the desired settings and run the following Docker command: Create a `config.yaml` with the desired settings and run the following Docker command:
#### Linux
``` ```
docker run -v $(pwd)/config.yaml:/code/config.yaml gwhittington/meshtastic-bridge:latest docker run --rm --network host -v $(pwd)/config.yaml:/code/config.yaml gwhittington/meshtastic-bridge:latest
``` ```
## Resources ## Resources
- Example guidance for creating [PEM](https://www.suse.com/support/kb/doc/?id=000018152) key files. - Example guidance for creating [PEM](https://www.suse.com/support/kb/doc/?id=000018152) key files.
- Test webhooks using [Webhooks.site](https://webhook.site/)
+133 -91
View File
@@ -1,5 +1,6 @@
import json import json
import logging import logging
from re import I
import meshtastic import meshtastic
import meshtastic.serial_interface import meshtastic.serial_interface
import meshtastic.tcp_interface import meshtastic.tcp_interface
@@ -20,32 +21,43 @@ logger = logging.getLogger(name="meshtastic.bridge")
logger.setLevel(logging.DEBUG) 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 def onReceive(packet, interface): # called when a packet arrives
nodeInfo = interface.getMyNodeInfo()
for pipeline, pipeline_plugins in bridge_config["pipelines"].items(): if "pipelines" in bridge_config:
logger.debug(f"Pipeline {pipeline} initiated") for pipeline, pipeline_plugins in bridge_config["pipelines"].items():
logger.debug(f"Pipeline {pipeline} initiated")
p = plugins["packet_filter"] p = plugins["packet_filter"]
pipeline_packet = p.do_action(packet) pipeline_packet = p.do_action(packet)
for plugin in pipeline_plugins: for plugin in pipeline_plugins:
for plugin_key, plugin_config in plugin.items():
logger.debug(f"Processing plugin: {pipeline}/{plugin_key}")
if not pipeline_packet: if not pipeline_packet:
logger.debug("Skipping since the packet is null")
continue continue
if plugin_key not in plugins: for plugin_key, plugin_config in plugin.items():
logger.error(f"No such plugin: {plugin_key}. Skipping") logger.debug(f"Processing plugin: {pipeline}/{plugin_key}")
continue if not pipeline_packet:
logger.debug("Skipping since the packet is null")
continue
p = plugins[plugin_key] if plugin_key not in plugins:
p.configure(devices, mqtt_servers, plugin_config) logger.error(f"No such plugin: {plugin_key}. Skipping")
continue
pipeline_packet = p.do_action(pipeline_packet) p = plugins[plugin_key]
p.configure(devices, mqtt_servers, plugin_config)
logger.debug(f"Pipeline {pipeline} completed") pipeline_packet = p.do_action(pipeline_packet)
logger.debug(f"Pipeline {pipeline} completed")
def onConnection( def onConnection(
@@ -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(onReceive, "meshtastic.receive")
pub.subscribe(onConnection, "meshtastic.connection.established") pub.subscribe(onConnection, "meshtastic.connection.established")
pub.subscribe(onLost, "meshtastic.connection.lost")
with open("config.yaml") as f: with open("config.yaml") as f:
bridge_config = yaml.load(f, Loader=SafeLoader) bridge_config = yaml.load(f, Loader=SafeLoader)
@@ -66,108 +87,129 @@ with open("config.yaml") as f:
devices = {} devices = {}
mqtt_servers = {} mqtt_servers = {}
for device in bridge_config["devices"]: if "devices" in bridge_config:
if "active" in device and not device["active"]: for device in bridge_config["devices"]:
continue if "active" in device and not device["active"]:
continue
if "serial" in device: if "serial" in device:
devices[device["name"]] = meshtastic.serial_interface.SerialInterface( devices[device["name"]] = meshtastic.serial_interface.SerialInterface(
devPath=device["serial"] devPath=device["serial"]
) )
elif "tcp" in device: elif "tcp" in device:
devices[device["name"]] = meshtastic.tcp_interface.TCPInterface( logger.debug(f"Connecting to {device['tcp']} ...")
hostname=device["tcp"] devices[device["name"]] = CustomTCPInterface(
) hostname=device["tcp"], device_name=device["name"]
else: )
devices[device["name"]] = meshtastic.serial_interface.SerialInterface() logger.debug(f"Connected to {device['tcp']}")
else:
devices[device["name"]] = meshtastic.serial_interface.SerialInterface()
for config in bridge_config["mqtt_servers"]: if "mqtt_servers" in bridge_config:
required_options = [ for config in bridge_config["mqtt_servers"]:
"name", required_options = [
"server", "name",
"port", "server",
] "port",
]
for option in required_options: for option in required_options:
if option not in config: if option not in config:
logger.warning("Missing config: {option}") logger.warning("Missing config: {option}")
client_id = config["client_id"] if "client_id" 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 username = config["username"] if "username" in config else None
password = config["password"] if "password" in config else None password = config["password"] if "password" in config else None
if client_id: logger.info(f"Connected to MQTT {config['name']}")
mqttc = mqtt.Client(client_id)
else:
mqttc = mqtt.Client()
if username and password: if client_id:
mqttc.username_pw_set(username, password) mqttc = mqtt.Client(client_id)
else:
mqttc = mqtt.Client()
mqtt_servers[config["name"]] = mqttc if username and password:
mqttc.username_pw_set(username, password)
def on_connect(mqttc, obj, flags, rc): def on_connect(mqttc, obj, flags, rc):
logger.debug(f"Connected to MQTT {config['name']}") logger.debug(f"Connected to MQTT {config['name']}")
def on_message(mqttc, obj, msg): def on_message(mqttc, obj, msg):
packet = msg.payload.decode() orig_packet = msg.payload.decode()
logger.debug(f"MQTT {config['name']}: on_message") logger.debug(f"MQTT {config['name']}: {orig_packet}")
if "pipelines" not in config: if "pipelines" not in config:
logger.warning(f"MQTT {config['name']}: no pipeline") logger.warning(f"MQTT {config['name']}: no pipeline")
return return
for pipeline, pipeline_plugins in config["pipelines"].items(): p = plugins["packet_filter"]
pipeline_packet = p.do_action(orig_packet)
logger.debug(f"MQTT {config['name']} pipeline {pipeline} started") for pipeline, pipeline_plugins in config["pipelines"].items():
if not packet: packet = pipeline_packet
continue
for plugin in pipeline_plugins: logger.debug(f"MQTT {config['name']} pipeline {pipeline} initiated")
for plugin_key, plugin_config in plugin.items(): if not packet:
if plugin_key not in plugins: continue
logger.error(f"No such plugin: {plugin_key}. Skipping")
for plugin in pipeline_plugins:
if not packet:
continue continue
p = plugins[plugin_key] for plugin_key, plugin_config in plugin.items():
p.configure(devices, mqtt_servers, plugin_config) if plugin_key not in plugins:
logger.error(f"No such plugin: {plugin_key}. Skipping")
continue
try: p = plugins[plugin_key]
packet = p.do_action(packet) p.configure(devices, mqtt_servers, plugin_config)
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_publish(mqttc, obj, mid): try:
logger.debug(f"MQTT {config['name']}: on_publish: {mid}") 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): def on_publish(mqttc, obj, mid):
logger.debug(f"MQTT {config['name']}: on_subscribe: {mid}") logger.debug(f"MQTT {config['name']}: on_publish: {mid}")
mqttc.on_message = on_message def on_subscribe(mqttc, obj, mid, granted_qos):
mqttc.on_connect = on_connect logger.debug(f"MQTT {config['name']}: on_subscribe: {mid}")
mqttc.on_publish = on_publish
mqttc.on_subscribe = on_subscribe
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"]: mqtt_servers[config["name"]] = mqttc
mqttc.tls_set(cert_reqs=ssl.CERT_NONE)
mqttc.tls_insecure_set(True)
mqttc.connect(config["server"], config["port"], 60) import ssl
if "topic" in config: if "insecure" in config and config["insecure"]:
mqttc.subscribe(config["topic"], 0) mqttc.tls_set(cert_reqs=ssl.CERT_NONE)
mqttc.tls_insecure_set(True)
mqttc.loop_start() try:
logger.debug(f"Connecting to MQTT {config['server']}")
mqttc.connect(config["server"], config["port"], 60)
if "topic" in config:
mqttc.subscribe(config["topic"], 0)
mqttc.loop_start()
except Exception as e:
logger.error(f"MQTT {config['name']} could not start: {e}")
pass
while True: while True:
time.sleep(1000) time.sleep(1000)
for device, instance in devices.items(): if devices:
instance.close() for device, instance in devices.items():
instance.close()
for server, instance in mqtt_servers.items(): if mqtt_servers:
instance.disconnect() for server, instance in mqtt_servers.items():
instance.disconnect()
+311 -82
View File
@@ -1,16 +1,20 @@
from haversine import haversine from haversine import haversine
from meshtastic import mesh_pb2 from meshtastic import mesh_pb2
from meshtastic.__init__ import BROADCAST_ADDR from random import randrange
import base64 import base64
import json import json
import logging import logging
import os import os
import re
import ssl
plugins = {} plugins = {}
class Plugin: class Plugin(object):
def __init__(self) -> None:
self.logger.setLevel(logging.INFO)
def configure(self, devices, mqtt_servers, config): def configure(self, devices, mqtt_servers, config):
self.config = config self.config = config
self.devices = devices self.devices = devices
@@ -29,25 +33,42 @@ class Plugin:
class PacketFilter(Plugin): class PacketFilter(Plugin):
logger = logging.getLogger(name="meshtastic.bridge.filter.packet") logger = logging.getLogger(name="meshtastic.bridge.filter.packet")
def strip_raw(self, dict_obj): def strip_raw(self, data):
if type(data) is not dict:
return data
if "raw" in data:
del data["raw"]
for k, v in data.items():
data[k] = self.strip_raw(v)
return data
def normalize(self, dict_obj):
"""
Packets are either a dict, string dict or string
"""
if type(dict_obj) is not dict: if type(dict_obj) is not dict:
return dict_obj try:
dict_obj = json.loads(dict_obj)
except:
dict_obj = {"decoded": {"text": dict_obj}}
if "raw" in dict_obj: return self.strip_raw(dict_obj)
del dict_obj["raw"]
for k, v in dict_obj.items():
dict_obj[k] = self.strip_raw(v)
return dict_obj
def do_action(self, packet): def do_action(self, packet):
packet = self.strip_raw(packet) self.logger.debug(f"Before normalization: {packet}")
packet = self.normalize(packet)
if "decoded" in packet and "payload" in packet["decoded"]: if "decoded" in packet and "payload" in packet["decoded"]:
packet["decoded"]["payload"] = base64.b64encode( if type(packet["decoded"]["payload"]) is bytes:
packet["decoded"]["payload"] text = packet["decoded"]["payload"]
).decode("utf-8") packet["decoded"]["payload"] = base64.b64encode(
packet["decoded"]["payload"]
).decode("utf-8")
self.logger.debug(f"After normalization: {packet}")
return packet return packet
@@ -74,6 +95,33 @@ class MessageFilter(Plugin):
self.logger.error("Missing packet") self.logger.error("Missing packet")
return 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 "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 = { filters = {
"app": packet["decoded"]["portnum"], "app": packet["decoded"]["portnum"],
"from": packet["fromId"], "from": packet["fromId"],
@@ -83,12 +131,15 @@ class MessageFilter(Plugin):
for filter_key, value in filters.items(): for filter_key, value in filters.items():
if filter_key in self.config: if filter_key in self.config:
filter_val = self.config[filter_key] filter_val = self.config[filter_key]
if ( if (
"allow" in filter_val "allow" in filter_val
and filter_val["allow"] and filter_val["allow"]
and value not in 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 {value} doesn't match {filter_key} allow filter"
)
return None return None
if ( if (
@@ -96,7 +147,9 @@ class MessageFilter(Plugin):
and filter_val["disallow"] and filter_val["disallow"]
and value in 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 {value} matches {filter_key} disallow filter"
)
return None return None
self.logger.debug(f"Accepted") self.logger.debug(f"Accepted")
@@ -106,21 +159,24 @@ class MessageFilter(Plugin):
plugins["message_filter"] = MessageFilter() plugins["message_filter"] = MessageFilter()
class DistanceFilter(Plugin): class LocationFilter(Plugin):
logger = logging.getLogger(name="meshtastic.bridge.filter.distance") logger = logging.getLogger(name="meshtastic.bridge.filter.distance")
def do_action(self, packet): 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 message_source_position = None
current_local_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 ( 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"] and "longitude" in packet["decoded"]["position"]
): ):
message_source_position = ( message_source_position = (
@@ -128,44 +184,49 @@ class DistanceFilter(Plugin):
packet["decoded"]["position"]["longitude"], 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 = ( current_local_position = (
nodeInfo["position"]["latitude"], self.config["compare_latitude"],
nodeInfo["position"]["longitude"], self.config["compare_longitude"],
) )
if message_source_position and current_local_position: if message_source_position and current_local_position:
distance_km = haversine(message_source_position, 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 # message originates from too far a distance
if ( if "max_distance_km" in self.config and self.config["max_distance_km"] > 0:
"max_distance_km" in self.config acceptable_distance = self.config["max_distance_km"]
and self.config["max_distance_km"] > 0
and distance_km > self.config["max_distance_km"] if comparison == "within" and distance_km > acceptable_distance:
): self.logger.debug(
logger.debug( f"Packet from too far: {distance_km} > {acceptable_distance}"
f"Packet from too far: {distance_km} > {SUPPORTED_BRIDGE_DISTANCE_KM}" )
) return None
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 return packet
plugins["distance_filter"] = DistanceFilter() plugins["location_filter"] = LocationFilter()
class WebhookPlugin(Plugin): class WebhookPlugin(Plugin):
logger = logging.getLogger(name="meshtastic.bridge.plugin.webhook") logger = logging.getLogger(name="meshtastic.bridge.plugin.webhook")
def do_action(self, packet): def do_action(self, packet):
if type(packet) is not dict:
try:
packet = json.loads(packet)
except:
self.logger.warning("Packet is not dict")
return packet
if "active" in self.config and not self.config["active"]: if "active" in self.config and not self.config["active"]:
return packet return packet
@@ -181,8 +242,8 @@ class WebhookPlugin(Plugin):
text = packet["decoded"]["text"] if "text" in packet["decoded"] else None text = packet["decoded"]["text"] if "text" in packet["decoded"] else None
macros = { macros = {
"{LAT}": position["latitude"] if position else None, "{LAT}": position["latitude"] if position else "",
"{LNG}": position["longitude"] if position else None, "{LNG}": position["longitude"] if position else "",
"{MSG}": self.config["message"] if "message" in self.config else text, "{MSG}": self.config["message"] if "message" in self.config else text,
"{FID}": packet["fromId"], "{FID}": packet["fromId"],
"{TID}": packet["toId"], "{TID}": packet["toId"],
@@ -235,24 +296,124 @@ class MQTTPlugin(Plugin):
mqtt_server = self.mqtt_servers[self.config["name"]] mqtt_server = self.mqtt_servers[self.config["name"]]
packet_payload = packet if type(packet) is str else json.dumps(packet) if not mqtt_server.is_connected():
self.logger.error("Not sent, not connected")
return
message = self.config["message"] if "message" in self.config else packet_payload packet_message = json.dumps(packet)
if "message" in self.config:
message = self.config["message"].replace("{MSG}", packet["decoded"]["text"])
else:
message = packet_message
info = mqtt_server.publish(self.config["topic"], message) info = mqtt_server.publish(self.config["topic"], message)
info.wait_for_publish() info.wait_for_publish()
self.logger.debug("Message sent") self.logger.debug("Message sent")
return packet
plugins["mqtt_plugin"] = MQTTPlugin() plugins["mqtt_plugin"] = MQTTPlugin()
class OwntracksPlugin(Plugin):
logger = logging.getLogger(name="meshtastic.bridge.plugin.Owntracks")
def do_action(self, packet):
required_options = ["tid_table", "server_name"]
for option in required_options:
if option not in self.config:
self.logger.warning(f"Missing config: {option}")
return packet
#tid_table = self.config["tid_table"]
tid_table = {}
for tid_entry in self.config["tid_table"]: # We want to check for a key with an ! and convert to string
if "!" in tid_entry:
tid_table[str(int(tid_entry[1:], 16))] = self.config["tid_table"][tid_entry]
else:
tid_table[tid_entry] = self.config["tid_table"][tid_entry]
if not "from" in packet:
self.logger.warning("Missing from: field")
return packet
if packet["from"] < 0:
packet["from"] = packet["from"] +(1 << 32)
if not str(packet["from"]) in tid_table:
self.logger.warning(f"Sender not in tid_table: {packet}")
return packet
from_str = str(packet["from"])
message = json.loads('{"_type":"location", "bs":0}')
message["tid"] = tid_table[from_str][1]
self.logger.debug(f"processing packet {packet}")
#Packet direct from radio
if (
"decoded" in packet
and "position" in packet["decoded"]
and "latitude" in packet["decoded"]["position"]
and packet["decoded"]["position"]["latitude"] != 0
):
message["lat"] = packet["decoded"]["position"]["latitude"]
message["lon"] = packet["decoded"]["position"]["longitude"]
message["tst"] = packet["decoded"]["position"]["time"]
message["created_at"] = packet["rxTime"]
if "altitude" in packet["decoded"]["position"]:
message["alt"] = packet["decoded"]["position"]["altitude"]
#packet from mqtt
elif (
"type" in packet
and packet["type"] == "position"
and "payload" in packet
and "latitude_i" in packet["payload"]
and packet["payload"]["latitude_i"] != 0
):
message["lat"] = packet["payload"]["latitude_i"]/10000000
message["lon"] = packet["payload"]["longitude_i"]/10000000
message["tst"] = packet["timestamp"]
if ("time" in packet["payload"]):
message["created_at"] = packet["payload"]["time"]
else:
message["created_at"] = packet["timestamp"]
if "altitude" in packet["payload"]:
message["alt"] = packet["payload"]["altitude"]
else:
self.logger.debug("Not a location packet")
return packet
if self.config["server_name"] not in self.mqtt_servers:
self.logger.warning(f"No server established: {self.config['server_name']}")
return packet
mqtt_server = self.mqtt_servers[self.config["server_name"]]
if not mqtt_server.is_connected():
self.logger.error("Not sent, not connected")
return
self.logger.debug("Sending owntracks message")
info = mqtt_server.publish("owntracks/user/" + tid_table[from_str][0], json.dumps(message))
#info.wait_for_publish()
self.logger.debug("Message sent")
return packet
plugins["owntracks_plugin"] = OwntracksPlugin()
class EncryptFilter(Plugin): class EncryptFilter(Plugin):
logger = logging.getLogger(name="meshtastic.bridge.filter.encrypt") logger = logging.getLogger(name="meshtastic.bridge.filter.encrypt")
def do_action(self, packet): def do_action(self, packet):
if "key" not in self.config: if "key" not in self.config:
return None return None
@@ -311,52 +472,40 @@ class DecryptFilter(Plugin):
plugins["decrypt_filter"] = DecryptFilter() plugins["decrypt_filter"] = DecryptFilter()
class SendPlugin(Plugin): class RadioMessagePlugin(Plugin):
logger = logging.getLogger(name="meshtastic.bridge.plugin.send") logger = logging.getLogger(name="meshtastic.bridge.plugin.send")
def do_action(self, packet): def do_action(self, packet):
if type(packet) is not dict:
try:
packet = json.loads(packet)
except:
self.logger.error("Packet is not a dict")
return packet
if self.config["device"] not in self.devices: if self.config["device"] not in self.devices:
self.logger.error(f"Missing interface for device {self.config['device']}") self.logger.error(f"Missing interface for device {self.config['device']}")
return packet return packet
if "to" not in packet and "toId" not in packet: destinationId = None
self.logger.debug("Not a message")
return packet
# Broadcast messages or specific
if (
"node_mapping" in self.config
and packet["to"] in self.config["node_mapping"]
):
destinationId = self.config["node_mapping"][packet["to"]]
else:
destinationId = packet["to"] if "to" in packet else packet["toId"]
if "to" in self.config: if "to" in self.config:
destinationId = self.config["to"] destinationId = self.config["to"]
elif "toId" in self.config: elif "toId" in self.config:
destinationId = self.config["toId"] destinationId = self.config["toId"]
elif "node_mapping" in self.config and "to" in packet:
destinationId = self.config["node_mapping"][packet["to"]]
elif "to" in packet:
destinationId = packet["to"]
elif "toId" in packet:
destinationId = packet["toId"]
if not destinationId:
self.logger.error("Missing 'to' property in config or packet")
return packet
device_name = self.config["device"] device_name = self.config["device"]
if device_name not in self.devices:
self.logger.warning(f"No such radio device: {device_name}")
return packet
device = self.devices[device_name] device = self.devices[device_name]
self.logger.debug(f"Sending packet to Radio {device_name}") # Not a radio packet
if "decoded" in packet and "text" in packet["decoded"] and "from" not in packet:
self.logger.debug(f"Sending text to Radio {device_name}")
device.sendText(text=packet["decoded"]["text"], destinationId=destinationId)
if "message" in self.config and self.config["message"]:
device.sendText(text=self.config["message"], destinationId=destinationId)
elif ( elif (
"lat" in self.config "lat" in self.config
and self.config["lat"] > 0 and self.config["lat"] > 0
@@ -367,13 +516,19 @@ class SendPlugin(Plugin):
lng = self.config["lng"] lng = self.config["lng"]
altitude = self.config["alt"] if "alt" in self.config else 0 altitude = self.config["alt"] if "alt" in self.config else 0
self.logger.debug(f"Sending position to Radio {device_name}")
device.sendPosition( device.sendPosition(
latitude=lat, latitude=lat,
longitude=lng, longitude=lng,
altitude=altitude, altitude=altitude,
destinationId=destinationId, destinationId=destinationId,
) )
else: elif (
"decoded" in packet
and "payload" in packet["decoded"]
and "portnum" in packet["decoded"]
):
meshPacket = mesh_pb2.MeshPacket() meshPacket = mesh_pb2.MeshPacket()
meshPacket.channel = 0 meshPacket.channel = 0
meshPacket.decoded.payload = base64.b64decode(packet["decoded"]["payload"]) meshPacket.decoded.payload = base64.b64decode(packet["decoded"]["payload"])
@@ -381,9 +536,83 @@ class SendPlugin(Plugin):
meshPacket.decoded.want_response = False meshPacket.decoded.want_response = False
meshPacket.id = device._generatePacketId() meshPacket.id = device._generatePacketId()
self.logger.debug(f"Sending packet to Radio {device_name}")
device._sendPacket(meshPacket=meshPacket, destinationId=destinationId) device._sendPacket(meshPacket=meshPacket, destinationId=destinationId)
return packet return packet
plugins["send_plugin"] = SendPlugin() plugins["radio_message_plugin"] = RadioMessagePlugin()
import time
from nostr.event import Event
from nostr.relay_manager import RelayManager
from nostr.message_type import ClientMessageType
from nostr.key import PrivateKey, PublicKey
class NoStrPlugin(Plugin):
logger = logging.getLogger(name="meshtastic.bridge.plugin.nostr_send")
def do_action(self, packet):
relays = ["wss://nostr-pub.wellorder.net", "wss://relay.damus.io"]
for config_value in ["private_key", "public_key"]:
if config_value not in self.config:
self.logger.debug(f"Missing {config_value}")
return packet
# configure relays
if "relays" in self.config:
for relay in self.config["relays"]:
relays.append(relay)
relay_manager = RelayManager()
for relay in relays:
relay_manager.add_relay(relay)
self.logger.debug(f"Opening connection to NoStr relays...")
relay_manager.open_connections(
{"cert_reqs": ssl.CERT_NONE}
) # NOTE: This disables ssl certificate verification
time.sleep(
self.config["startup_wait"] if "startup_wait" in self.config else 1.25
) # allow the connections to open
# Opportunistically use environment variable
for ek, ev in os.environ.items():
needle = "{" + ek + "}"
if needle in self.config["private_key"]:
self.config["private_key"] = self.config["private_key"].replace(
needle, ev
)
private_key = PrivateKey.from_nsec(self.config["private_key"])
public_key = PublicKey.from_npub(self.config["public_key"])
if "message" in self.config:
message = self.config["message"].replace("{MSG}", packet["decoded"]["text"])
else:
message = packet["decoded"]["text"]
event = Event(content=message, public_key=public_key.hex())
private_key.sign_event(event)
self.logger.debug(f"Sending message to NoStr ...")
relay_manager.publish_event(event)
self.logger.info(f"Sent message to NoStr")
time.sleep(
self.config["publish_wait"] if "publish_wait" in self.config else 1
) # allow the messages to send
relay_manager.close_connections()
return packet
plugins["nostr_plugin"] = NoStrPlugin()
+1
View File
@@ -4,3 +4,4 @@ requests
pyyaml pyyaml
paho-mqtt paho-mqtt
jwcrypto jwcrypto
nostr
+3 -3
View File
@@ -1,5 +1,5 @@
devices: devices:
- name: local - name: local
pipelines: pipelines:
- debugger: - debugger:
log_level: info log_level: info
+25
View File
@@ -0,0 +1,25 @@
devices:
- name: radio1
tcp: 192.168.86.27
mqtt_servers:
- name: external
server: broker.hivemq.com
port: 1883
topic: meshtastic/radio-network1
pipelines:
mqtt-to-radio:
- radio_message_plugin:
device: radio1
to: "^all"
pipelines:
pipeline1:
- debugger:
log_level: debug
radio-to-mqtt:
- message_filter:
app:
allow:
- "TEXT_MESSAGE_APP"
- mqtt_plugin:
name: external
topic: meshtastic/radio-network1
+12
View File
@@ -0,0 +1,12 @@
mqtt_servers:
- name: local
server: localhost
port: 1883
topic: msh/2/json/#
pipelines:
owntrack:
- owntracks_plugin:
server_name: local
tid_table:
"12345": ["Van", "GV"]
"-6789": ["Home", "HR"]
+18
View File
@@ -0,0 +1,18 @@
devices:
- name: radio1
tcp: 192.168.1.110
mqtt_servers:
- name: external
server: localhost
port: 1883
pipelines:
owntracks:
- owntracks_plugin:
server_name: external
tid_table:
"1234": ["Van", "GV"]
"-5678": ["Home", "HR"]
+18
View File
@@ -0,0 +1,18 @@
devices:
- name: radio1
tcp: meshtastic.local
pipelines:
pipeline1:
- debugger:
log_level: debug
radio-to-webhook:
- message_filter:
app:
allow:
- "TEXT_MESSAGE_APP"
- webhook:
active: true
body: '{"lat": "{LAT}", "lng": "{LNG}", "text_message": "{MSG}"}'
url: "https://webhook.site/452ea027-f9f1-4a62-827b-c921715fcdfb"
headers:
Content-type: application/json