mirror of
https://github.com/geoffwhittington/meshtastic-bridge.git
synced 2026-07-10 03:30:57 +02:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d4a17de70a | |||
| b23b82173c | |||
| 61116198fc | |||
| 5ce046de13 | |||
| 0eea64f2e4 | |||
| 6d186ea339 | |||
| 7d20077166 | |||
| 5a4ca73e8c | |||
| 42a4e4a76a | |||
| 170d52ef14 | |||
| 0fccf6ea9e | |||
| 461d540c30 | |||
| 6585f52f91 | |||
| 1f4027829e | |||
| 159b3b097d | |||
| dd83f29806 | |||
| 26d540fead | |||
| b72102c402 | |||
| 0930aa917e | |||
| 7ad584830a | |||
| fe5e119059 | |||
| 23c1250ccb | |||
| c5134fd5b4 | |||
| c3ff3b3a4f | |||
| 0f1e5a908d | |||
| 74114f1acc |
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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/)
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ requests
|
|||||||
pyyaml
|
pyyaml
|
||||||
paho-mqtt
|
paho-mqtt
|
||||||
jwcrypto
|
jwcrypto
|
||||||
|
nostr
|
||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
devices:
|
devices:
|
||||||
- name: local
|
- name: local
|
||||||
pipelines:
|
pipelines:
|
||||||
- debugger:
|
- debugger:
|
||||||
log_level: info
|
log_level: info
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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"]
|
||||||
@@ -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"]
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user