forked from iarv/meshtastic-bridge
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4a17de70a | ||
|
|
b23b82173c | ||
|
|
61116198fc | ||
|
|
5ce046de13 | ||
|
|
0eea64f2e4 | ||
|
|
6d186ea339 | ||
|
|
7d20077166 | ||
|
|
5a4ca73e8c | ||
|
|
42a4e4a76a | ||
|
|
170d52ef14 | ||
|
|
0fccf6ea9e | ||
|
|
461d540c30 | ||
|
|
6585f52f91 | ||
|
|
1f4027829e |
@@ -1,5 +1,5 @@
|
||||
# set base image (host OS)
|
||||
FROM python:3.8
|
||||
FROM python:3.9
|
||||
|
||||
# set the working directory in the container
|
||||
WORKDIR /code
|
||||
|
||||
76
README.md
76
README.md
@@ -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`:
|
||||
|
||||
| Plugin | Description |
|
||||
| ----------------- | -------------------------------------------------------------------- |
|
||||
| `debugger` | Log the packet to the system console |
|
||||
| `message_filter` | Filters out packets from the bridge that match a specific criteria |
|
||||
| `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 |
|
||||
| `decrypt_filter` | Decrypt a packet originating from MQTT |
|
||||
| `radio_message_plugin` | Send a packet to a specified `device` |
|
||||
| Plugin | Description |
|
||||
| ---------------------- | -------------------------------------------------------------------- |
|
||||
| `debugger` | Log the packet to the system console |
|
||||
| `message_filter` | Filters out packets from the bridge that match a specific criteria |
|
||||
| `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 |
|
||||
| `decrypt_filter` | Decrypt a packet originating from MQTT |
|
||||
| `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
|
||||
|
||||
@@ -199,7 +201,7 @@ webhook:
|
||||
- **log_level** `debug` or `info`. Default `info`
|
||||
- **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
|
||||
- **message** Override the packet message with a custom value
|
||||
- **message** Override the packet message with a custom value.
|
||||
- **topic** The message topic
|
||||
|
||||
For example:
|
||||
@@ -210,6 +212,10 @@ mqtt_plugin:
|
||||
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
|
||||
|
||||
- **log_level** `debug` or `info`. Default `info`
|
||||
@@ -234,6 +240,49 @@ decrypt_filter:
|
||||
key: '/home/user/keys/key.pem'
|
||||
```
|
||||
|
||||
### 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`
|
||||
@@ -277,10 +326,13 @@ python main.py
|
||||
|
||||
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
|
||||
|
||||
- Example guidance for creating [PEM](https://www.suse.com/support/kb/doc/?id=000018152) key files.
|
||||
- Test webhooks using [Webhooks.site](https://webhook.site/)
|
||||
|
||||
71
main.py
71
main.py
@@ -31,33 +31,33 @@ class CustomTCPInterface(meshtastic.tcp_interface.TCPInterface):
|
||||
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")
|
||||
if "pipelines" in bridge_config:
|
||||
for pipeline, pipeline_plugins in bridge_config["pipelines"].items():
|
||||
logger.debug(f"Pipeline {pipeline} initiated")
|
||||
|
||||
p = plugins["packet_filter"]
|
||||
pipeline_packet = p.do_action(packet)
|
||||
p = plugins["packet_filter"]
|
||||
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}")
|
||||
for plugin in pipeline_plugins:
|
||||
if not pipeline_packet:
|
||||
logger.debug("Skipping since the packet is null")
|
||||
continue
|
||||
|
||||
if plugin_key not in plugins:
|
||||
logger.error(f"No such plugin: {plugin_key}. Skipping")
|
||||
continue
|
||||
for plugin_key, plugin_config in plugin.items():
|
||||
logger.debug(f"Processing plugin: {pipeline}/{plugin_key}")
|
||||
if not pipeline_packet:
|
||||
logger.debug("Skipping since the packet is null")
|
||||
continue
|
||||
|
||||
p = plugins[plugin_key]
|
||||
p.configure(devices, mqtt_servers, plugin_config)
|
||||
if plugin_key not in plugins:
|
||||
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(
|
||||
@@ -87,22 +87,23 @@ with open("config.yaml") as f:
|
||||
devices = {}
|
||||
mqtt_servers = {}
|
||||
|
||||
for device in bridge_config["devices"]:
|
||||
if "active" in device and not device["active"]:
|
||||
continue
|
||||
if "devices" in bridge_config:
|
||||
for device in bridge_config["devices"]:
|
||||
if "active" in device and not device["active"]:
|
||||
continue
|
||||
|
||||
if "serial" in device:
|
||||
devices[device["name"]] = meshtastic.serial_interface.SerialInterface(
|
||||
devPath=device["serial"]
|
||||
)
|
||||
elif "tcp" in device:
|
||||
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()
|
||||
if "serial" in device:
|
||||
devices[device["name"]] = meshtastic.serial_interface.SerialInterface(
|
||||
devPath=device["serial"]
|
||||
)
|
||||
elif "tcp" in device:
|
||||
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()
|
||||
|
||||
if "mqtt_servers" in bridge_config:
|
||||
for config in bridge_config["mqtt_servers"]:
|
||||
@@ -136,7 +137,6 @@ if "mqtt_servers" in bridge_config:
|
||||
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']}: {orig_packet}")
|
||||
|
||||
if "pipelines" not in config:
|
||||
@@ -147,7 +147,6 @@ if "mqtt_servers" in bridge_config:
|
||||
pipeline_packet = p.do_action(orig_packet)
|
||||
|
||||
for pipeline, pipeline_plugins in config["pipelines"].items():
|
||||
|
||||
packet = pipeline_packet
|
||||
|
||||
logger.debug(f"MQTT {config['name']} pipeline {pipeline} initiated")
|
||||
|
||||
173
plugins.py
173
plugins.py
@@ -6,7 +6,7 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
||||
import ssl
|
||||
|
||||
plugins = {}
|
||||
|
||||
@@ -242,8 +242,8 @@ class WebhookPlugin(Plugin):
|
||||
text = packet["decoded"]["text"] if "text" in packet["decoded"] else None
|
||||
|
||||
macros = {
|
||||
"{LAT}": position["latitude"] if position else None,
|
||||
"{LNG}": position["longitude"] if position else None,
|
||||
"{LAT}": position["latitude"] if position else "",
|
||||
"{LNG}": position["longitude"] if position else "",
|
||||
"{MSG}": self.config["message"] if "message" in self.config else text,
|
||||
"{FID}": packet["fromId"],
|
||||
"{TID}": packet["toId"],
|
||||
@@ -312,15 +312,108 @@ class MQTTPlugin(Plugin):
|
||||
|
||||
self.logger.debug("Message sent")
|
||||
|
||||
return packet
|
||||
|
||||
|
||||
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):
|
||||
logger = logging.getLogger(name="meshtastic.bridge.filter.encrypt")
|
||||
|
||||
def do_action(self, packet):
|
||||
|
||||
if "key" not in self.config:
|
||||
return None
|
||||
|
||||
@@ -451,3 +544,75 @@ class RadioMessagePlugin(Plugin):
|
||||
|
||||
|
||||
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
|
||||
paho-mqtt
|
||||
jwcrypto
|
||||
nostr
|
||||
12
use-cases/owntracks/config.mqtt.yaml
Normal file
12
use-cases/owntracks/config.mqtt.yaml
Normal 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
use-cases/owntracks/config.net.yaml
Normal file
18
use-cases/owntracks/config.net.yaml
Normal 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
use-cases/webhook/config.yaml
Normal file
18
use-cases/webhook/config.yaml
Normal 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
|
||||
Reference in New Issue
Block a user