mmrelay 1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mmrelay might be problematic. Click here for more details.
- mmrelay/__init__.py +9 -0
- mmrelay/cli.py +384 -0
- mmrelay/config.py +218 -0
- mmrelay/config_checker.py +133 -0
- mmrelay/db_utils.py +309 -0
- mmrelay/log_utils.py +107 -0
- mmrelay/main.py +281 -0
- mmrelay/matrix_utils.py +754 -0
- mmrelay/meshtastic_utils.py +569 -0
- mmrelay/plugin_loader.py +336 -0
- mmrelay/plugins/__init__.py +3 -0
- mmrelay/plugins/base_plugin.py +212 -0
- mmrelay/plugins/debug_plugin.py +17 -0
- mmrelay/plugins/drop_plugin.py +120 -0
- mmrelay/plugins/health_plugin.py +64 -0
- mmrelay/plugins/help_plugin.py +55 -0
- mmrelay/plugins/map_plugin.py +323 -0
- mmrelay/plugins/mesh_relay_plugin.py +134 -0
- mmrelay/plugins/nodes_plugin.py +92 -0
- mmrelay/plugins/ping_plugin.py +118 -0
- mmrelay/plugins/telemetry_plugin.py +179 -0
- mmrelay/plugins/weather_plugin.py +208 -0
- mmrelay/setup_utils.py +263 -0
- mmrelay-1.0.dist-info/METADATA +160 -0
- mmrelay-1.0.dist-info/RECORD +29 -0
- mmrelay-1.0.dist-info/WHEEL +5 -0
- mmrelay-1.0.dist-info/entry_points.txt +2 -0
- mmrelay-1.0.dist-info/licenses/LICENSE +21 -0
- mmrelay-1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextlib
|
|
3
|
+
import io
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
from typing import List
|
|
7
|
+
|
|
8
|
+
import meshtastic.ble_interface
|
|
9
|
+
import meshtastic.serial_interface
|
|
10
|
+
import meshtastic.tcp_interface
|
|
11
|
+
import serial # For serial port exceptions
|
|
12
|
+
import serial.tools.list_ports # Import serial tools for port listing
|
|
13
|
+
from bleak.exc import BleakDBusError, BleakError
|
|
14
|
+
from pubsub import pub
|
|
15
|
+
|
|
16
|
+
from mmrelay.db_utils import (
|
|
17
|
+
get_longname,
|
|
18
|
+
get_message_map_by_meshtastic_id,
|
|
19
|
+
get_shortname,
|
|
20
|
+
save_longname,
|
|
21
|
+
save_shortname,
|
|
22
|
+
)
|
|
23
|
+
from mmrelay.log_utils import get_logger
|
|
24
|
+
|
|
25
|
+
# Global config variable that will be set from config.py
|
|
26
|
+
config = None
|
|
27
|
+
|
|
28
|
+
# Do not import plugin_loader here to avoid circular imports
|
|
29
|
+
|
|
30
|
+
# Initialize matrix rooms configuration
|
|
31
|
+
matrix_rooms: List[dict] = []
|
|
32
|
+
|
|
33
|
+
# Initialize logger for Meshtastic
|
|
34
|
+
logger = get_logger(name="Meshtastic")
|
|
35
|
+
|
|
36
|
+
# Global variables for the Meshtastic connection and event loop management
|
|
37
|
+
meshtastic_client = None
|
|
38
|
+
event_loop = None # Will be set from main.py
|
|
39
|
+
|
|
40
|
+
meshtastic_lock = (
|
|
41
|
+
threading.Lock()
|
|
42
|
+
) # To prevent race conditions on meshtastic_client access
|
|
43
|
+
|
|
44
|
+
reconnecting = False
|
|
45
|
+
shutting_down = False
|
|
46
|
+
reconnect_task = None # To keep track of the reconnect task
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def serial_port_exists(port_name):
|
|
50
|
+
"""
|
|
51
|
+
Check if the specified serial port exists.
|
|
52
|
+
This prevents attempting connections on non-existent ports.
|
|
53
|
+
"""
|
|
54
|
+
ports = [p.device for p in serial.tools.list_ports.comports()]
|
|
55
|
+
return port_name in ports
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def connect_meshtastic(passed_config=None, force_connect=False):
|
|
59
|
+
"""
|
|
60
|
+
Establish a connection to the Meshtastic device.
|
|
61
|
+
Attempts a connection based on connection_type (serial/ble/network).
|
|
62
|
+
Retries until successful or shutting_down is set.
|
|
63
|
+
If already connected and not force_connect, returns the existing client.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
passed_config: The configuration dictionary to use (will update global config)
|
|
67
|
+
force_connect: Whether to force a new connection even if one exists
|
|
68
|
+
"""
|
|
69
|
+
global meshtastic_client, shutting_down, config, matrix_rooms
|
|
70
|
+
if shutting_down:
|
|
71
|
+
logger.debug("Shutdown in progress. Not attempting to connect.")
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
# Update the global config if a config is passed
|
|
75
|
+
if passed_config is not None:
|
|
76
|
+
config = passed_config
|
|
77
|
+
|
|
78
|
+
# If config is valid, extract matrix_rooms
|
|
79
|
+
if config and "matrix_rooms" in config:
|
|
80
|
+
matrix_rooms = config["matrix_rooms"]
|
|
81
|
+
|
|
82
|
+
with meshtastic_lock:
|
|
83
|
+
if meshtastic_client and not force_connect:
|
|
84
|
+
return meshtastic_client
|
|
85
|
+
|
|
86
|
+
# Close previous connection if exists
|
|
87
|
+
if meshtastic_client:
|
|
88
|
+
try:
|
|
89
|
+
meshtastic_client.close()
|
|
90
|
+
except Exception as e:
|
|
91
|
+
logger.warning(f"Error closing previous connection: {e}")
|
|
92
|
+
meshtastic_client = None
|
|
93
|
+
|
|
94
|
+
# Check if config is available
|
|
95
|
+
if config is None:
|
|
96
|
+
logger.error("No configuration available. Cannot connect to Meshtastic.")
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
# Determine connection type and attempt connection
|
|
100
|
+
connection_type = config["meshtastic"]["connection_type"]
|
|
101
|
+
|
|
102
|
+
# Support legacy "network" connection type (now "tcp")
|
|
103
|
+
if connection_type == "network":
|
|
104
|
+
connection_type = "tcp"
|
|
105
|
+
logger.warning(
|
|
106
|
+
"Using 'network' connection type (legacy). 'tcp' is now the preferred name and 'network' will be deprecated in a future version."
|
|
107
|
+
)
|
|
108
|
+
retry_limit = 0 # 0 means infinite retries
|
|
109
|
+
attempts = 1
|
|
110
|
+
successful = False
|
|
111
|
+
|
|
112
|
+
while (
|
|
113
|
+
not successful
|
|
114
|
+
and (retry_limit == 0 or attempts <= retry_limit)
|
|
115
|
+
and not shutting_down
|
|
116
|
+
):
|
|
117
|
+
try:
|
|
118
|
+
if connection_type == "serial":
|
|
119
|
+
# Serial connection
|
|
120
|
+
serial_port = config["meshtastic"]["serial_port"]
|
|
121
|
+
logger.info(f"Connecting to serial port {serial_port} ...")
|
|
122
|
+
|
|
123
|
+
# Check if serial port exists before connecting
|
|
124
|
+
if not serial_port_exists(serial_port):
|
|
125
|
+
logger.warning(
|
|
126
|
+
f"Serial port {serial_port} does not exist. Waiting..."
|
|
127
|
+
)
|
|
128
|
+
time.sleep(5)
|
|
129
|
+
attempts += 1
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
meshtastic_client = meshtastic.serial_interface.SerialInterface(
|
|
133
|
+
serial_port
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
elif connection_type == "ble":
|
|
137
|
+
# BLE connection
|
|
138
|
+
ble_address = config["meshtastic"].get("ble_address")
|
|
139
|
+
if ble_address:
|
|
140
|
+
logger.info(f"Connecting to BLE address {ble_address} ...")
|
|
141
|
+
meshtastic_client = meshtastic.ble_interface.BLEInterface(
|
|
142
|
+
address=ble_address,
|
|
143
|
+
noProto=False,
|
|
144
|
+
debugOut=None,
|
|
145
|
+
noNodes=False,
|
|
146
|
+
)
|
|
147
|
+
else:
|
|
148
|
+
logger.error("No BLE address provided.")
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
elif connection_type == "tcp":
|
|
152
|
+
# TCP connection
|
|
153
|
+
target_host = config["meshtastic"]["host"]
|
|
154
|
+
logger.info(f"Connecting to host {target_host} ...")
|
|
155
|
+
meshtastic_client = meshtastic.tcp_interface.TCPInterface(
|
|
156
|
+
hostname=target_host
|
|
157
|
+
)
|
|
158
|
+
else:
|
|
159
|
+
logger.error(f"Unknown connection type: {connection_type}")
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
successful = True
|
|
163
|
+
nodeInfo = meshtastic_client.getMyNodeInfo()
|
|
164
|
+
logger.info(
|
|
165
|
+
f"Connected to {nodeInfo['user']['shortName']} / {nodeInfo['user']['hwModel']}"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Subscribe to message and connection lost events
|
|
169
|
+
pub.subscribe(on_meshtastic_message, "meshtastic.receive")
|
|
170
|
+
pub.subscribe(
|
|
171
|
+
on_lost_meshtastic_connection, "meshtastic.connection.lost"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
except (
|
|
175
|
+
serial.SerialException,
|
|
176
|
+
BleakDBusError,
|
|
177
|
+
BleakError,
|
|
178
|
+
Exception,
|
|
179
|
+
) as e:
|
|
180
|
+
if shutting_down:
|
|
181
|
+
logger.debug("Shutdown in progress. Aborting connection attempts.")
|
|
182
|
+
break
|
|
183
|
+
attempts += 1
|
|
184
|
+
if retry_limit == 0 or attempts <= retry_limit:
|
|
185
|
+
wait_time = min(
|
|
186
|
+
attempts * 2, 30
|
|
187
|
+
) # Exponential backoff capped at 30s
|
|
188
|
+
logger.warning(
|
|
189
|
+
f"Attempt #{attempts - 1} failed. Retrying in {wait_time} secs: {e}"
|
|
190
|
+
)
|
|
191
|
+
time.sleep(wait_time)
|
|
192
|
+
else:
|
|
193
|
+
logger.error(f"Could not connect after {retry_limit} attempts: {e}")
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
return meshtastic_client
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def on_lost_meshtastic_connection(interface=None):
|
|
200
|
+
"""
|
|
201
|
+
Callback invoked when the Meshtastic connection is lost.
|
|
202
|
+
Initiates a reconnect sequence unless shutting_down is True.
|
|
203
|
+
"""
|
|
204
|
+
global meshtastic_client, reconnecting, shutting_down, event_loop, reconnect_task
|
|
205
|
+
with meshtastic_lock:
|
|
206
|
+
if shutting_down:
|
|
207
|
+
logger.debug("Shutdown in progress. Not attempting to reconnect.")
|
|
208
|
+
return
|
|
209
|
+
if reconnecting:
|
|
210
|
+
logger.debug(
|
|
211
|
+
"Reconnection already in progress. Skipping additional reconnection attempt."
|
|
212
|
+
)
|
|
213
|
+
return
|
|
214
|
+
reconnecting = True
|
|
215
|
+
logger.error("Lost connection. Reconnecting...")
|
|
216
|
+
|
|
217
|
+
if meshtastic_client:
|
|
218
|
+
try:
|
|
219
|
+
meshtastic_client.close()
|
|
220
|
+
except OSError as e:
|
|
221
|
+
if e.errno == 9:
|
|
222
|
+
# Bad file descriptor, already closed
|
|
223
|
+
pass
|
|
224
|
+
else:
|
|
225
|
+
logger.warning(f"Error closing Meshtastic client: {e}")
|
|
226
|
+
except Exception as e:
|
|
227
|
+
logger.warning(f"Error closing Meshtastic client: {e}")
|
|
228
|
+
meshtastic_client = None
|
|
229
|
+
|
|
230
|
+
if event_loop:
|
|
231
|
+
reconnect_task = asyncio.run_coroutine_threadsafe(reconnect(), event_loop)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
async def reconnect():
|
|
235
|
+
"""
|
|
236
|
+
Asynchronously attempts to reconnect with exponential backoff.
|
|
237
|
+
Stops if shutting_down is set.
|
|
238
|
+
"""
|
|
239
|
+
global meshtastic_client, reconnecting, shutting_down
|
|
240
|
+
backoff_time = 10
|
|
241
|
+
try:
|
|
242
|
+
while not shutting_down:
|
|
243
|
+
try:
|
|
244
|
+
logger.info(
|
|
245
|
+
f"Reconnection attempt starting in {backoff_time} seconds..."
|
|
246
|
+
)
|
|
247
|
+
await asyncio.sleep(backoff_time)
|
|
248
|
+
if shutting_down:
|
|
249
|
+
logger.debug(
|
|
250
|
+
"Shutdown in progress. Aborting reconnection attempts."
|
|
251
|
+
)
|
|
252
|
+
break
|
|
253
|
+
meshtastic_client = connect_meshtastic(force_connect=True)
|
|
254
|
+
if meshtastic_client:
|
|
255
|
+
logger.info("Reconnected successfully.")
|
|
256
|
+
break
|
|
257
|
+
except Exception as e:
|
|
258
|
+
if shutting_down:
|
|
259
|
+
break
|
|
260
|
+
logger.error(f"Reconnection attempt failed: {e}")
|
|
261
|
+
backoff_time = min(backoff_time * 2, 300) # Cap backoff at 5 minutes
|
|
262
|
+
except asyncio.CancelledError:
|
|
263
|
+
logger.info("Reconnection task was cancelled.")
|
|
264
|
+
finally:
|
|
265
|
+
reconnecting = False
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def on_meshtastic_message(packet, interface):
|
|
269
|
+
"""
|
|
270
|
+
Handle incoming Meshtastic messages. For reaction messages, if relay_reactions is False,
|
|
271
|
+
we do not store message maps and thus won't be able to relay reactions back to Matrix.
|
|
272
|
+
If relay_reactions is True, message maps are stored inside matrix_relay().
|
|
273
|
+
"""
|
|
274
|
+
global config, matrix_rooms
|
|
275
|
+
|
|
276
|
+
# Log that we received a message (without the full packet details)
|
|
277
|
+
if packet.get("decoded", {}).get("text"):
|
|
278
|
+
logger.info(
|
|
279
|
+
f"Received Meshtastic message: {packet.get('decoded', {}).get('text')}"
|
|
280
|
+
)
|
|
281
|
+
else:
|
|
282
|
+
logger.debug("Received non-text Meshtastic message")
|
|
283
|
+
|
|
284
|
+
# Check if config is available
|
|
285
|
+
if config is None:
|
|
286
|
+
logger.error("No configuration available. Cannot process Meshtastic message.")
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
# Apply reaction filtering based on config
|
|
290
|
+
relay_reactions = config["meshtastic"].get("relay_reactions", False)
|
|
291
|
+
|
|
292
|
+
# If relay_reactions is False, filter out reaction/tapback packets to avoid complexity
|
|
293
|
+
if packet.get("decoded", {}).get("portnum") == "TEXT_MESSAGE_APP":
|
|
294
|
+
decoded = packet.get("decoded", {})
|
|
295
|
+
if not relay_reactions and ("emoji" in decoded or "replyId" in decoded):
|
|
296
|
+
logger.debug(
|
|
297
|
+
"Filtered out reaction/tapback packet due to relay_reactions=false."
|
|
298
|
+
)
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
from mmrelay.matrix_utils import matrix_relay
|
|
302
|
+
|
|
303
|
+
global event_loop
|
|
304
|
+
|
|
305
|
+
if shutting_down:
|
|
306
|
+
logger.debug("Shutdown in progress. Ignoring incoming messages.")
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
if event_loop is None:
|
|
310
|
+
logger.error("Event loop is not set. Cannot process message.")
|
|
311
|
+
return
|
|
312
|
+
|
|
313
|
+
loop = event_loop
|
|
314
|
+
|
|
315
|
+
sender = packet.get("fromId") or packet.get("from")
|
|
316
|
+
toId = packet.get("to")
|
|
317
|
+
|
|
318
|
+
decoded = packet.get("decoded", {})
|
|
319
|
+
text = decoded.get("text")
|
|
320
|
+
replyId = decoded.get("replyId")
|
|
321
|
+
emoji_flag = "emoji" in decoded and decoded["emoji"] == 1
|
|
322
|
+
|
|
323
|
+
# Determine if this is a direct message to the relay node
|
|
324
|
+
from meshtastic.mesh_interface import BROADCAST_NUM
|
|
325
|
+
|
|
326
|
+
myId = interface.myInfo.my_node_num
|
|
327
|
+
|
|
328
|
+
if toId == myId:
|
|
329
|
+
is_direct_message = True
|
|
330
|
+
elif toId == BROADCAST_NUM:
|
|
331
|
+
is_direct_message = False
|
|
332
|
+
else:
|
|
333
|
+
# Message to someone else; ignoring for broadcasting logic
|
|
334
|
+
is_direct_message = False
|
|
335
|
+
|
|
336
|
+
meshnet_name = config["meshtastic"]["meshnet_name"]
|
|
337
|
+
|
|
338
|
+
# Reaction handling (Meshtastic -> Matrix)
|
|
339
|
+
# If replyId and emoji_flag are present and relay_reactions is True, we relay as text reactions in Matrix
|
|
340
|
+
if replyId and emoji_flag and relay_reactions:
|
|
341
|
+
longname = get_longname(sender) or str(sender)
|
|
342
|
+
shortname = get_shortname(sender) or str(sender)
|
|
343
|
+
orig = get_message_map_by_meshtastic_id(replyId)
|
|
344
|
+
if orig:
|
|
345
|
+
# orig = (matrix_event_id, matrix_room_id, meshtastic_text, meshtastic_meshnet)
|
|
346
|
+
matrix_event_id, matrix_room_id, meshtastic_text, meshtastic_meshnet = orig
|
|
347
|
+
abbreviated_text = (
|
|
348
|
+
meshtastic_text[:40] + "..."
|
|
349
|
+
if len(meshtastic_text) > 40
|
|
350
|
+
else meshtastic_text
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
# Ensure that meshnet_name is always included, using our own meshnet for accuracy.
|
|
354
|
+
full_display_name = f"{longname}/{meshnet_name}"
|
|
355
|
+
|
|
356
|
+
reaction_symbol = text.strip() if (text and text.strip()) else "⚠️"
|
|
357
|
+
reaction_message = f'\n [{full_display_name}] reacted {reaction_symbol} to "{abbreviated_text}"'
|
|
358
|
+
|
|
359
|
+
# Relay the reaction as emote to Matrix, preserving the original meshnet name
|
|
360
|
+
asyncio.run_coroutine_threadsafe(
|
|
361
|
+
matrix_relay(
|
|
362
|
+
matrix_room_id,
|
|
363
|
+
reaction_message,
|
|
364
|
+
longname,
|
|
365
|
+
shortname,
|
|
366
|
+
meshnet_name,
|
|
367
|
+
decoded.get("portnum"),
|
|
368
|
+
meshtastic_id=packet.get("id"),
|
|
369
|
+
meshtastic_replyId=replyId,
|
|
370
|
+
meshtastic_text=meshtastic_text,
|
|
371
|
+
emote=True,
|
|
372
|
+
emoji=True,
|
|
373
|
+
),
|
|
374
|
+
loop=loop,
|
|
375
|
+
)
|
|
376
|
+
else:
|
|
377
|
+
logger.debug("Original message for reaction not found in DB.")
|
|
378
|
+
return
|
|
379
|
+
|
|
380
|
+
# Normal text messages or detection sensor messages
|
|
381
|
+
if text:
|
|
382
|
+
# Determine the channel for this message
|
|
383
|
+
channel = packet.get("channel")
|
|
384
|
+
if channel is None:
|
|
385
|
+
# If channel not specified, deduce from portnum
|
|
386
|
+
if (
|
|
387
|
+
decoded.get("portnum") == "TEXT_MESSAGE_APP"
|
|
388
|
+
or decoded.get("portnum") == 1
|
|
389
|
+
):
|
|
390
|
+
channel = 0
|
|
391
|
+
elif decoded.get("portnum") == "DETECTION_SENSOR_APP":
|
|
392
|
+
channel = 0
|
|
393
|
+
else:
|
|
394
|
+
logger.debug(
|
|
395
|
+
f"Unknown portnum {decoded.get('portnum')}, cannot determine channel"
|
|
396
|
+
)
|
|
397
|
+
return
|
|
398
|
+
|
|
399
|
+
# Check if channel is mapped to a Matrix room
|
|
400
|
+
channel_mapped = False
|
|
401
|
+
for room in matrix_rooms:
|
|
402
|
+
if room["meshtastic_channel"] == channel:
|
|
403
|
+
channel_mapped = True
|
|
404
|
+
break
|
|
405
|
+
|
|
406
|
+
if not channel_mapped:
|
|
407
|
+
logger.debug(f"Skipping message from unmapped channel {channel}")
|
|
408
|
+
return
|
|
409
|
+
|
|
410
|
+
# If detection_sensor is disabled and this is a detection sensor packet, skip it
|
|
411
|
+
if decoded.get("portnum") == "DETECTION_SENSOR_APP" and not config[
|
|
412
|
+
"meshtastic"
|
|
413
|
+
].get("detection_sensor", False):
|
|
414
|
+
logger.debug(
|
|
415
|
+
"Detection sensor packet received, but detection sensor processing is disabled."
|
|
416
|
+
)
|
|
417
|
+
return
|
|
418
|
+
|
|
419
|
+
# Attempt to get longname/shortname from database or nodes
|
|
420
|
+
longname = get_longname(sender)
|
|
421
|
+
shortname = get_shortname(sender)
|
|
422
|
+
|
|
423
|
+
if not longname or not shortname:
|
|
424
|
+
node = interface.nodes.get(sender)
|
|
425
|
+
if node:
|
|
426
|
+
user = node.get("user")
|
|
427
|
+
if user:
|
|
428
|
+
if not longname:
|
|
429
|
+
longname_val = user.get("longName")
|
|
430
|
+
if longname_val:
|
|
431
|
+
save_longname(sender, longname_val)
|
|
432
|
+
longname = longname_val
|
|
433
|
+
if not shortname:
|
|
434
|
+
shortname_val = user.get("shortName")
|
|
435
|
+
if shortname_val:
|
|
436
|
+
save_shortname(sender, shortname_val)
|
|
437
|
+
shortname = shortname_val
|
|
438
|
+
else:
|
|
439
|
+
logger.debug(f"Node info for sender {sender} not available yet.")
|
|
440
|
+
|
|
441
|
+
# If still not available, fallback to sender ID
|
|
442
|
+
if not longname:
|
|
443
|
+
longname = str(sender)
|
|
444
|
+
if not shortname:
|
|
445
|
+
shortname = str(sender)
|
|
446
|
+
|
|
447
|
+
formatted_message = f"[{longname}/{meshnet_name}]: {text}"
|
|
448
|
+
|
|
449
|
+
# Plugin functionality - Check if any plugin handles this message before relaying
|
|
450
|
+
from mmrelay.plugin_loader import load_plugins
|
|
451
|
+
|
|
452
|
+
plugins = load_plugins()
|
|
453
|
+
|
|
454
|
+
found_matching_plugin = False
|
|
455
|
+
for plugin in plugins:
|
|
456
|
+
if not found_matching_plugin:
|
|
457
|
+
result = asyncio.run_coroutine_threadsafe(
|
|
458
|
+
plugin.handle_meshtastic_message(
|
|
459
|
+
packet, formatted_message, longname, meshnet_name
|
|
460
|
+
),
|
|
461
|
+
loop=loop,
|
|
462
|
+
)
|
|
463
|
+
found_matching_plugin = result.result()
|
|
464
|
+
if found_matching_plugin:
|
|
465
|
+
logger.debug(f"Processed by plugin {plugin.plugin_name}")
|
|
466
|
+
|
|
467
|
+
# If message is a DM or handled by plugin, do not relay further
|
|
468
|
+
if is_direct_message:
|
|
469
|
+
logger.debug(
|
|
470
|
+
f"Received a direct message from {longname}: {text}. Not relaying to Matrix."
|
|
471
|
+
)
|
|
472
|
+
return
|
|
473
|
+
if found_matching_plugin:
|
|
474
|
+
logger.debug("Message was handled by a plugin. Not relaying to Matrix.")
|
|
475
|
+
return
|
|
476
|
+
|
|
477
|
+
# Relay the message to all Matrix rooms mapped to this channel
|
|
478
|
+
logger.info(f"Relaying Meshtastic message from {longname} to Matrix")
|
|
479
|
+
|
|
480
|
+
# Check if matrix_rooms is empty
|
|
481
|
+
if not matrix_rooms:
|
|
482
|
+
logger.error("matrix_rooms is empty. Cannot relay message to Matrix.")
|
|
483
|
+
return
|
|
484
|
+
|
|
485
|
+
for room in matrix_rooms:
|
|
486
|
+
if room["meshtastic_channel"] == channel:
|
|
487
|
+
# Storing the message_map (if enabled) occurs inside matrix_relay() now,
|
|
488
|
+
# controlled by relay_reactions.
|
|
489
|
+
try:
|
|
490
|
+
asyncio.run_coroutine_threadsafe(
|
|
491
|
+
matrix_relay(
|
|
492
|
+
room["id"],
|
|
493
|
+
formatted_message,
|
|
494
|
+
longname,
|
|
495
|
+
shortname,
|
|
496
|
+
meshnet_name,
|
|
497
|
+
decoded.get("portnum"),
|
|
498
|
+
meshtastic_id=packet.get("id"),
|
|
499
|
+
meshtastic_text=text,
|
|
500
|
+
),
|
|
501
|
+
loop=loop,
|
|
502
|
+
)
|
|
503
|
+
except Exception as e:
|
|
504
|
+
logger.error(f"Error relaying message to Matrix: {e}")
|
|
505
|
+
else:
|
|
506
|
+
# Non-text messages via plugins
|
|
507
|
+
portnum = decoded.get("portnum")
|
|
508
|
+
from mmrelay.plugin_loader import load_plugins
|
|
509
|
+
|
|
510
|
+
plugins = load_plugins()
|
|
511
|
+
found_matching_plugin = False
|
|
512
|
+
for plugin in plugins:
|
|
513
|
+
if not found_matching_plugin:
|
|
514
|
+
result = asyncio.run_coroutine_threadsafe(
|
|
515
|
+
plugin.handle_meshtastic_message(
|
|
516
|
+
packet,
|
|
517
|
+
formatted_message=None,
|
|
518
|
+
longname=None,
|
|
519
|
+
meshnet_name=None,
|
|
520
|
+
),
|
|
521
|
+
loop=loop,
|
|
522
|
+
)
|
|
523
|
+
found_matching_plugin = result.result()
|
|
524
|
+
if found_matching_plugin:
|
|
525
|
+
logger.debug(
|
|
526
|
+
f"Processed {portnum} with plugin {plugin.plugin_name}"
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
async def check_connection():
|
|
531
|
+
"""
|
|
532
|
+
Periodically checks the Meshtastic connection by calling localNode.getMetadata().
|
|
533
|
+
If it fails or doesn't return the firmware version, we assume the connection is lost
|
|
534
|
+
and attempt to reconnect.
|
|
535
|
+
"""
|
|
536
|
+
global meshtastic_client, shutting_down, config
|
|
537
|
+
|
|
538
|
+
# Check if config is available
|
|
539
|
+
if config is None:
|
|
540
|
+
logger.error("No configuration available. Cannot check connection.")
|
|
541
|
+
return
|
|
542
|
+
|
|
543
|
+
connection_type = config["meshtastic"]["connection_type"]
|
|
544
|
+
while not shutting_down:
|
|
545
|
+
if meshtastic_client:
|
|
546
|
+
try:
|
|
547
|
+
output_capture = io.StringIO()
|
|
548
|
+
with contextlib.redirect_stdout(
|
|
549
|
+
output_capture
|
|
550
|
+
), contextlib.redirect_stderr(output_capture):
|
|
551
|
+
meshtastic_client.localNode.getMetadata()
|
|
552
|
+
|
|
553
|
+
console_output = output_capture.getvalue()
|
|
554
|
+
if "firmware_version" not in console_output:
|
|
555
|
+
raise Exception("No firmware_version in getMetadata output.")
|
|
556
|
+
|
|
557
|
+
except Exception as e:
|
|
558
|
+
logger.error(f"{connection_type.capitalize()} connection lost: {e}")
|
|
559
|
+
on_lost_meshtastic_connection(meshtastic_client)
|
|
560
|
+
await asyncio.sleep(30) # Check connection every 30 seconds
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
if __name__ == "__main__":
|
|
564
|
+
# If running this standalone (normally the main.py does the loop), just try connecting and run forever.
|
|
565
|
+
meshtastic_client = connect_meshtastic()
|
|
566
|
+
loop = asyncio.get_event_loop()
|
|
567
|
+
event_loop = loop # Set the event loop for use in callbacks
|
|
568
|
+
loop.create_task(check_connection())
|
|
569
|
+
loop.run_forever()
|