Skip to content

Commit 86f347e

Browse files
committed
initial testing channel setup
1 parent ae1721d commit 86f347e

File tree

16 files changed

+342
-90
lines changed

16 files changed

+342
-90
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,22 @@ Copy the the `.env.default` file to `.env` and view for the environment variable
1717
- INFLUXDB_V2_TOKEN
1818
- INFLUXDB_V2_URL
1919

20+
There are some other tunables as well:
21+
22+
- INFLUXDB_V2_WRITE_PRECISION
23+
- MESHTASTIC_API_CACHE_TTL: The time to cache the Meshtastic API data. Defaults to 6 hours.
24+
- MESHTASTIC_KEY: The base64 encoded encryption key for the primary channel. Defaults to the the key provided by `AQ==`
25+
- MQTT_TEST_CHANNEL
26+
- MQTT_TEST_CHANNEL_ID
27+
- DISCORD_BOT_TOKEN
28+
- DISCORD_BOT_OWNER_ID
29+
- BRIDGER_ADMIN_ROLE
30+
- EMQX_API_KEY
31+
- EMQX_SECRET_KEY
32+
- EMQX_URL
33+
- LOG_PATH: Set this to a file path to log to a file. Defaults to `logs/bridger.log`
34+
35+
2036
Then install the required packages in a Python virtual environment:
2137

2238
```bash

bridger/__main__.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,10 @@
1-
import os
2-
31
from influxdb_client import InfluxDBClient
42
from paho.mqtt.client import CallbackAPIVersion
53

4+
from bridger.config import MQTT_BROKER, MQTT_PASS, MQTT_PORT, MQTT_USER
65
from bridger.log import logger
76
from bridger.mqtt import BridgerMQTT
87

9-
MQTT_BROKER = os.getenv("MQTT_BROKER", "192.168.1.110")
10-
MQTT_USER = os.getenv("MQTT_USER", "station")
11-
MQTT_PASS = os.getenv("MQTT_PASS")
12-
MQTT_PORT = os.getenv("MQTT_PORT", 1883)
13-
14-
158
if __name__ == "__main__":
169
try:
1710
influx_client: InfluxDBClient = InfluxDBClient.from_env_properties()

bridger/bot.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ def __init__(self, **kwargs):
2020

2121
self.initial_extensions = [
2222
"bridger.cogs.mqtt",
23+
"bridger.cogs.testmsg",
2324
]
2425

2526
async def setup_hook(self):

bridger/cogs/testmsg.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import os
2+
import re
3+
from datetime import datetime
4+
from functools import partial
5+
6+
import aiomqtt
7+
from aiocache import SimpleMemoryCache
8+
from discord import Embed, Message
9+
from discord.ext import commands
10+
from influxdb_client import InfluxDBClient
11+
from meshtastic.protobuf.mqtt_pb2 import ServiceEnvelope
12+
from meshtastic.protobuf.portnums_pb2 import TEXT_MESSAGE_APP
13+
14+
from bridger.config import MQTT_BROKER, MQTT_PASS, MQTT_PORT, MQTT_TOPIC, MQTT_USER
15+
from bridger.dataclasses import TextMessagePoint
16+
from bridger.influx.interfaces import InfluxReader
17+
from bridger.log import logger
18+
from bridger.mqtt import PBPacketProcessor
19+
20+
MQTT_TEST_CHANNEL_MESHTASTIC = os.getenv("MQTT_TEST_CHANNEL", "LongFast")
21+
MQTT_TEST_CHANNEL_DISCORD = int(os.getenv("MQTT_TEST_CHANNEL_ID", 1253788609316913265))
22+
TEST_MESSAGE_MATCHERS = [
23+
re.compile(r"^.*$", flags=re.IGNORECASE) if os.getenv("TEST_MESSAGE_MATCH_ALL", "false").lower() == "true" else None,
24+
re.compile(r"^\!\b.+$", flags=re.IGNORECASE),
25+
re.compile(r"^test\s+.+$", flags=re.IGNORECASE),
26+
]
27+
28+
29+
class TestMsg(commands.GroupCog, name="testmsg"):
30+
queue = SimpleMemoryCache()
31+
32+
def __init__(self, bot: commands.Bot, discord_channel_id: int, influx_reader: InfluxReader):
33+
self.bot = bot
34+
self.discord_channel_id = discord_channel_id
35+
self.discord_channel = None
36+
self.influx_reader = influx_reader
37+
38+
@commands.Cog.listener(name="on_ready")
39+
async def on_ready(self):
40+
self.discord_channel = self.bot.get_channel(self.discord_channel_id)
41+
logger.info(f"TestMsg cog is ready and channel is: {self.discord_channel}")
42+
43+
@staticmethod
44+
def create_embed(service_envelope: ServiceEnvelope):
45+
packet = service_envelope.packet
46+
gateway = service_envelope.gateway_id
47+
color = int(gateway[-6:], 16)
48+
snr = packet.rx_snr
49+
rssi = packet.rx_rssi
50+
hop_count = None
51+
formatted_time = datetime.fromtimestamp(packet.rx_time).strftime("%H:%M:%S")
52+
53+
if packet.hop_start > 0:
54+
hop_count = packet.hop_start - packet.hop_limit
55+
56+
embed = Embed(color=color)
57+
# embed.set_author(name=gateway)
58+
embed.description = f"Heard by **{gateway}** at {formatted_time}"
59+
embed.add_field(name="SNR", value=snr, inline=True)
60+
embed.add_field(name="RSSI", value=rssi, inline=True)
61+
62+
if hop_count == 0:
63+
embed.add_field(name="Hops", value="Direct", inline=True)
64+
elif hop_count is not None:
65+
embed.add_field(name="Hops", value=hop_count, inline=True)
66+
67+
return embed
68+
69+
async def update_message_embeds(self, message: Message, envelope: ServiceEnvelope):
70+
if len(message.embeds) >= 10:
71+
message_id = message.id
72+
logger.warning(f"Embed limit reached for message ID {message_id}, skipping update")
73+
return
74+
message.embeds.append(self.create_embed(envelope))
75+
await message.edit(embeds=message.embeds)
76+
77+
async def run_mqtt(self):
78+
topic = MQTT_TOPIC.removesuffix("/#")
79+
channel = MQTT_TEST_CHANNEL_MESHTASTIC
80+
full_topic = f"{topic}/{channel}/#"
81+
82+
async with aiomqtt.Client(
83+
MQTT_BROKER,
84+
MQTT_PORT,
85+
username=MQTT_USER,
86+
password=MQTT_PASS,
87+
clean_session=True,
88+
) as client:
89+
await client.subscribe(full_topic)
90+
logger.info(f"Subscribed to {full_topic}")
91+
await logger.complete()
92+
93+
async for message in client.messages:
94+
try:
95+
service_envelope = ServiceEnvelope.FromString(message.payload)
96+
except Exception:
97+
logger.exception("Failed to decode MQTT message")
98+
continue
99+
100+
processor = PBPacketProcessor(service_envelope=service_envelope, strip_text=False)
101+
102+
if processor.portnum == TEXT_MESSAGE_APP:
103+
data: TextMessagePoint = processor.data
104+
if not data or not data.text:
105+
continue
106+
107+
if not any(pattern.match(data.text) for pattern in TEST_MESSAGE_MATCHERS if pattern):
108+
continue
109+
110+
logger.debug(f"Test message matched: {data.text}")
111+
112+
packet = service_envelope.packet
113+
packet_id = packet.id
114+
source_node_id = getattr(packet, "from")
115+
node_info = self.influx_reader.get_node_info(source_node_id)
116+
117+
short = node_info.get("short_name") if node_info else None
118+
long = node_info.get("long_name") if node_info else None
119+
name = f"**{short}** ({long})" if short and long else f"**{source_node_id}**"
120+
message_id = await self.queue.get(packet_id)
121+
122+
extra = {
123+
"packet_id": packet_id,
124+
"source_node_id": source_node_id,
125+
"text": data.text,
126+
"gateway": service_envelope.gateway_id,
127+
"short_name": short,
128+
"long_name": long,
129+
"name": name,
130+
"node_info": node_info,
131+
}
132+
133+
logger.bind(**extra).debug(f"Message ID {message_id} for packet ID {packet_id} from {name}")
134+
135+
if message_id:
136+
try:
137+
message = await self.discord_channel.fetch_message(message_id)
138+
await self.update_message_embeds(message, service_envelope)
139+
except Exception:
140+
logger.exception("Failed to fetch or edit Discord message")
141+
else:
142+
content = f"Test message from {name} <t:{packet.rx_time}:R>\n> {data.text}"
143+
embeds = [self.create_embed(service_envelope)]
144+
try:
145+
message: Message = await self.discord_channel.send(content, embeds=embeds)
146+
await self.queue.set(packet_id, message.id, ttl=3600)
147+
except Exception:
148+
logger.exception("Failed to send Discord message")
149+
150+
151+
def restart_mqtt_on_exception(task, bot: commands.Bot):
152+
try:
153+
task.result()
154+
except Exception:
155+
logger.exception("MQTT task failed. Restarting...")
156+
new_task = bot.loop.create_task(bot.cogs["testmsg"].run_mqtt())
157+
new_task.add_done_callback(partial(restart_mqtt_on_exception, bot=bot))
158+
159+
160+
async def setup(bot: commands.Bot):
161+
influx_client = InfluxDBClient.from_env_properties()
162+
influx_reader = InfluxReader(influx_client=influx_client)
163+
await bot.add_cog(TestMsg(bot, MQTT_TEST_CHANNEL_DISCORD, influx_reader))
164+
run_mqtt_task = bot.loop.create_task(bot.cogs["testmsg"].run_mqtt())
165+
run_mqtt_task.add_done_callback(partial(restart_mqtt_on_exception, bot=bot))

bridger/config.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import os
2+
3+
MQTT_BROKER = os.getenv("MQTT_BROKER", "192.168.1.110")
4+
MQTT_USER = os.getenv("MQTT_USER", "station")
5+
MQTT_PASS = os.getenv("MQTT_PASS")
6+
MQTT_PORT = os.getenv("MQTT_PORT", 1883)
7+
MQTT_TOPIC = os.getenv("MQTT_TOPIC", "egr/home/2/e/#")
8+
INFLUXDB_V2_BUCKET = os.getenv("INFLUXDB_V2_BUCKET", "meshtastic")
9+
INFLUXDB_V2_WRITE_PRECISION = os.getenv("INFLUXDB_V2_WRITE_PRECISION", "s") # s, ms, us, or ns
10+
MESHTASTIC_API_ENDPOINT = "https://api.meshtastic.org"
11+
MESHTASTIC_API_CACHE_TTL = int(os.getenv("MESHTASTIC_API_CACHE_TTL", 3600 * 6)) # Default to 6 hours if not set

bridger/influx.py

Lines changed: 0 additions & 67 deletions
This file was deleted.

bridger/influx/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)