mmrelay 1.1.2__py3-none-any.whl → 1.1.4__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 +1 -13
- mmrelay/cli.py +124 -64
- mmrelay/config.py +63 -36
- mmrelay/config_checker.py +41 -12
- mmrelay/constants/__init__.py +54 -0
- mmrelay/constants/app.py +17 -0
- mmrelay/constants/config.py +73 -0
- mmrelay/constants/database.py +22 -0
- mmrelay/constants/formats.py +20 -0
- mmrelay/constants/messages.py +36 -0
- mmrelay/constants/network.py +35 -0
- mmrelay/constants/queue.py +17 -0
- mmrelay/db_utils.py +281 -132
- mmrelay/log_utils.py +38 -14
- mmrelay/main.py +23 -4
- mmrelay/matrix_utils.py +413 -162
- mmrelay/meshtastic_utils.py +223 -106
- mmrelay/message_queue.py +475 -0
- mmrelay/plugin_loader.py +56 -53
- mmrelay/plugins/base_plugin.py +139 -39
- mmrelay/plugins/drop_plugin.py +13 -5
- mmrelay/plugins/mesh_relay_plugin.py +7 -10
- mmrelay/plugins/weather_plugin.py +10 -1
- mmrelay/setup_utils.py +67 -30
- mmrelay/tools/sample_config.yaml +13 -3
- {mmrelay-1.1.2.dist-info → mmrelay-1.1.4.dist-info}/METADATA +12 -14
- mmrelay-1.1.4.dist-info/RECORD +43 -0
- mmrelay-1.1.4.dist-info/licenses/LICENSE +675 -0
- mmrelay-1.1.2.dist-info/RECORD +0 -34
- mmrelay-1.1.2.dist-info/licenses/LICENSE +0 -21
- {mmrelay-1.1.2.dist-info → mmrelay-1.1.4.dist-info}/WHEEL +0 -0
- {mmrelay-1.1.2.dist-info → mmrelay-1.1.4.dist-info}/entry_points.txt +0 -0
- {mmrelay-1.1.2.dist-info → mmrelay-1.1.4.dist-info}/top_level.txt +0 -0
mmrelay/meshtastic_utils.py
CHANGED
|
@@ -11,10 +11,50 @@ import meshtastic.serial_interface
|
|
|
11
11
|
import meshtastic.tcp_interface
|
|
12
12
|
import serial # For serial port exceptions
|
|
13
13
|
import serial.tools.list_ports # Import serial tools for port listing
|
|
14
|
-
from bleak.exc import BleakDBusError, BleakError
|
|
15
14
|
from meshtastic.protobuf import mesh_pb2, portnums_pb2
|
|
16
15
|
from pubsub import pub
|
|
17
16
|
|
|
17
|
+
from mmrelay.constants.config import (
|
|
18
|
+
CONFIG_KEY_MESHNET_NAME,
|
|
19
|
+
CONFIG_SECTION_MESHTASTIC,
|
|
20
|
+
)
|
|
21
|
+
from mmrelay.constants.formats import (
|
|
22
|
+
DETECTION_SENSOR_APP,
|
|
23
|
+
EMOJI_FLAG_VALUE,
|
|
24
|
+
TEXT_MESSAGE_APP,
|
|
25
|
+
)
|
|
26
|
+
from mmrelay.constants.messages import (
|
|
27
|
+
DEFAULT_CHANNEL_VALUE,
|
|
28
|
+
PORTNUM_NUMERIC_VALUE,
|
|
29
|
+
)
|
|
30
|
+
from mmrelay.constants.network import (
|
|
31
|
+
CONFIG_KEY_BLE_ADDRESS,
|
|
32
|
+
CONFIG_KEY_CONNECTION_TYPE,
|
|
33
|
+
CONFIG_KEY_HOST,
|
|
34
|
+
CONFIG_KEY_SERIAL_PORT,
|
|
35
|
+
CONNECTION_TYPE_BLE,
|
|
36
|
+
CONNECTION_TYPE_NETWORK,
|
|
37
|
+
CONNECTION_TYPE_SERIAL,
|
|
38
|
+
CONNECTION_TYPE_TCP,
|
|
39
|
+
DEFAULT_BACKOFF_TIME,
|
|
40
|
+
DEFAULT_RETRY_ATTEMPTS,
|
|
41
|
+
ERRNO_BAD_FILE_DESCRIPTOR,
|
|
42
|
+
INFINITE_RETRIES,
|
|
43
|
+
SYSTEMD_INIT_SYSTEM,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Import BLE exceptions conditionally
|
|
47
|
+
try:
|
|
48
|
+
from bleak.exc import BleakDBusError, BleakError
|
|
49
|
+
except ImportError:
|
|
50
|
+
# Define dummy exception classes if bleak is not available
|
|
51
|
+
class BleakDBusError(Exception):
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
class BleakError(Exception):
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
18
58
|
from mmrelay.db_utils import (
|
|
19
59
|
get_longname,
|
|
20
60
|
get_message_map_by_meshtastic_id,
|
|
@@ -54,10 +94,10 @@ subscribed_to_connection_lost = False
|
|
|
54
94
|
|
|
55
95
|
def is_running_as_service():
|
|
56
96
|
"""
|
|
57
|
-
|
|
97
|
+
Checks whether the application is running as a systemd service.
|
|
58
98
|
|
|
59
99
|
Returns:
|
|
60
|
-
bool: True if running under systemd (
|
|
100
|
+
bool: True if running under systemd (detected via environment variable or parent process), otherwise False.
|
|
61
101
|
"""
|
|
62
102
|
# Check for INVOCATION_ID environment variable (set by systemd)
|
|
63
103
|
if os.environ.get("INVOCATION_ID"):
|
|
@@ -70,7 +110,7 @@ def is_running_as_service():
|
|
|
70
110
|
if line.startswith("PPid:"):
|
|
71
111
|
ppid = int(line.split()[1])
|
|
72
112
|
with open(f"/proc/{ppid}/comm") as p:
|
|
73
|
-
return p.read().strip() ==
|
|
113
|
+
return p.read().strip() == SYSTEMD_INIT_SYSTEM
|
|
74
114
|
except (FileNotFoundError, PermissionError, ValueError):
|
|
75
115
|
pass
|
|
76
116
|
|
|
@@ -88,22 +128,26 @@ def serial_port_exists(port_name):
|
|
|
88
128
|
|
|
89
129
|
def connect_meshtastic(passed_config=None, force_connect=False):
|
|
90
130
|
"""
|
|
91
|
-
Establishes
|
|
131
|
+
Establishes a connection to a Meshtastic device using serial, BLE, or TCP, with automatic retries and event subscriptions.
|
|
92
132
|
|
|
93
|
-
If a configuration is provided, updates the global configuration and Matrix room mappings.
|
|
133
|
+
If a configuration is provided, updates the global configuration and Matrix room mappings. Prevents concurrent or duplicate connection attempts, validates required configuration fields, and supports both legacy and current connection types. Verifies serial port existence before connecting and handles connection failures with exponential backoff. Subscribes to message and connection lost events upon successful connection.
|
|
94
134
|
|
|
95
135
|
Parameters:
|
|
96
|
-
passed_config (dict, optional): Configuration dictionary
|
|
136
|
+
passed_config (dict, optional): Configuration dictionary for the connection.
|
|
97
137
|
force_connect (bool, optional): If True, forces a new connection even if one already exists.
|
|
98
138
|
|
|
99
139
|
Returns:
|
|
100
|
-
|
|
140
|
+
The connected Meshtastic client instance, or None if connection fails or shutdown is in progress.
|
|
101
141
|
"""
|
|
102
|
-
global meshtastic_client, shutting_down, config, matrix_rooms
|
|
142
|
+
global meshtastic_client, shutting_down, reconnecting, config, matrix_rooms
|
|
103
143
|
if shutting_down:
|
|
104
144
|
logger.debug("Shutdown in progress. Not attempting to connect.")
|
|
105
145
|
return None
|
|
106
146
|
|
|
147
|
+
if reconnecting:
|
|
148
|
+
logger.debug("Reconnection already in progress. Not attempting new connection.")
|
|
149
|
+
return None
|
|
150
|
+
|
|
107
151
|
# Update the global config if a config is passed
|
|
108
152
|
if passed_config is not None:
|
|
109
153
|
config = passed_config
|
|
@@ -129,17 +173,37 @@ def connect_meshtastic(passed_config=None, force_connect=False):
|
|
|
129
173
|
logger.error("No configuration available. Cannot connect to Meshtastic.")
|
|
130
174
|
return None
|
|
131
175
|
|
|
176
|
+
# Check if meshtastic config section exists
|
|
177
|
+
if (
|
|
178
|
+
CONFIG_SECTION_MESHTASTIC not in config
|
|
179
|
+
or config[CONFIG_SECTION_MESHTASTIC] is None
|
|
180
|
+
):
|
|
181
|
+
logger.error(
|
|
182
|
+
"No Meshtastic configuration section found. Cannot connect to Meshtastic."
|
|
183
|
+
)
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
# Check if connection_type is specified
|
|
187
|
+
if (
|
|
188
|
+
CONFIG_KEY_CONNECTION_TYPE not in config[CONFIG_SECTION_MESHTASTIC]
|
|
189
|
+
or config[CONFIG_SECTION_MESHTASTIC][CONFIG_KEY_CONNECTION_TYPE] is None
|
|
190
|
+
):
|
|
191
|
+
logger.error(
|
|
192
|
+
"No connection type specified in Meshtastic configuration. Cannot connect to Meshtastic."
|
|
193
|
+
)
|
|
194
|
+
return None
|
|
195
|
+
|
|
132
196
|
# Determine connection type and attempt connection
|
|
133
|
-
connection_type = config[
|
|
197
|
+
connection_type = config[CONFIG_SECTION_MESHTASTIC][CONFIG_KEY_CONNECTION_TYPE]
|
|
134
198
|
|
|
135
199
|
# Support legacy "network" connection type (now "tcp")
|
|
136
|
-
if connection_type ==
|
|
137
|
-
connection_type =
|
|
200
|
+
if connection_type == CONNECTION_TYPE_NETWORK:
|
|
201
|
+
connection_type = CONNECTION_TYPE_TCP
|
|
138
202
|
logger.warning(
|
|
139
203
|
"Using 'network' connection type (legacy). 'tcp' is now the preferred name and 'network' will be deprecated in a future version."
|
|
140
204
|
)
|
|
141
|
-
retry_limit =
|
|
142
|
-
attempts =
|
|
205
|
+
retry_limit = INFINITE_RETRIES # 0 means infinite retries
|
|
206
|
+
attempts = DEFAULT_RETRY_ATTEMPTS
|
|
143
207
|
successful = False
|
|
144
208
|
|
|
145
209
|
while (
|
|
@@ -148,9 +212,15 @@ def connect_meshtastic(passed_config=None, force_connect=False):
|
|
|
148
212
|
and not shutting_down
|
|
149
213
|
):
|
|
150
214
|
try:
|
|
151
|
-
if connection_type ==
|
|
215
|
+
if connection_type == CONNECTION_TYPE_SERIAL:
|
|
152
216
|
# Serial connection
|
|
153
|
-
serial_port = config["meshtastic"]
|
|
217
|
+
serial_port = config["meshtastic"].get(CONFIG_KEY_SERIAL_PORT)
|
|
218
|
+
if not serial_port:
|
|
219
|
+
logger.error(
|
|
220
|
+
"No serial port specified in Meshtastic configuration."
|
|
221
|
+
)
|
|
222
|
+
return None
|
|
223
|
+
|
|
154
224
|
logger.info(f"Connecting to serial port {serial_port}")
|
|
155
225
|
|
|
156
226
|
# Check if serial port exists before connecting
|
|
@@ -166,9 +236,9 @@ def connect_meshtastic(passed_config=None, force_connect=False):
|
|
|
166
236
|
serial_port
|
|
167
237
|
)
|
|
168
238
|
|
|
169
|
-
elif connection_type ==
|
|
239
|
+
elif connection_type == CONNECTION_TYPE_BLE:
|
|
170
240
|
# BLE connection
|
|
171
|
-
ble_address = config["meshtastic"].get(
|
|
241
|
+
ble_address = config["meshtastic"].get(CONFIG_KEY_BLE_ADDRESS)
|
|
172
242
|
if ble_address:
|
|
173
243
|
logger.info(f"Connecting to BLE address {ble_address}")
|
|
174
244
|
|
|
@@ -183,9 +253,15 @@ def connect_meshtastic(passed_config=None, force_connect=False):
|
|
|
183
253
|
logger.error("No BLE address provided.")
|
|
184
254
|
return None
|
|
185
255
|
|
|
186
|
-
elif connection_type ==
|
|
256
|
+
elif connection_type == CONNECTION_TYPE_TCP:
|
|
187
257
|
# TCP connection
|
|
188
|
-
target_host = config["meshtastic"]
|
|
258
|
+
target_host = config["meshtastic"].get(CONFIG_KEY_HOST)
|
|
259
|
+
if not target_host:
|
|
260
|
+
logger.error(
|
|
261
|
+
"No host specified in Meshtastic configuration for TCP connection."
|
|
262
|
+
)
|
|
263
|
+
return None
|
|
264
|
+
|
|
189
265
|
logger.info(f"Connecting to host {target_host}")
|
|
190
266
|
|
|
191
267
|
# Connect without progress indicator
|
|
@@ -198,9 +274,12 @@ def connect_meshtastic(passed_config=None, force_connect=False):
|
|
|
198
274
|
|
|
199
275
|
successful = True
|
|
200
276
|
nodeInfo = meshtastic_client.getMyNodeInfo()
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
)
|
|
277
|
+
|
|
278
|
+
# Safely access node info fields
|
|
279
|
+
user_info = nodeInfo.get("user", {}) if nodeInfo else {}
|
|
280
|
+
short_name = user_info.get("shortName", "unknown")
|
|
281
|
+
hw_model = user_info.get("hwModel", "unknown")
|
|
282
|
+
logger.info(f"Connected to {short_name} / {hw_model}")
|
|
204
283
|
|
|
205
284
|
# Subscribe to message and connection lost events (only once per application run)
|
|
206
285
|
global subscribed_to_messages, subscribed_to_connection_lost
|
|
@@ -216,26 +295,42 @@ def connect_meshtastic(passed_config=None, force_connect=False):
|
|
|
216
295
|
subscribed_to_connection_lost = True
|
|
217
296
|
logger.debug("Subscribed to meshtastic.connection.lost")
|
|
218
297
|
|
|
219
|
-
except (
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
298
|
+
except (TimeoutError, ConnectionRefusedError, MemoryError) as e:
|
|
299
|
+
# Handle critical errors that should not be retried
|
|
300
|
+
logger.error(f"Critical connection error: {e}")
|
|
301
|
+
return None
|
|
302
|
+
except (serial.SerialException, BleakDBusError, BleakError) as e:
|
|
303
|
+
# Handle specific connection errors
|
|
225
304
|
if shutting_down:
|
|
226
305
|
logger.debug("Shutdown in progress. Aborting connection attempts.")
|
|
227
306
|
break
|
|
228
307
|
attempts += 1
|
|
229
308
|
if retry_limit == 0 or attempts <= retry_limit:
|
|
230
309
|
wait_time = min(
|
|
231
|
-
attempts
|
|
232
|
-
) #
|
|
310
|
+
2**attempts, 60
|
|
311
|
+
) # Consistent exponential backoff, capped at 60s
|
|
233
312
|
logger.warning(
|
|
234
|
-
f"
|
|
313
|
+
f"Connection attempt {attempts} failed: {e}. Retrying in {wait_time} seconds..."
|
|
235
314
|
)
|
|
236
315
|
time.sleep(wait_time)
|
|
237
316
|
else:
|
|
238
|
-
logger.error(f"
|
|
317
|
+
logger.error(f"Connection failed after {attempts} attempts: {e}")
|
|
318
|
+
return None
|
|
319
|
+
except Exception as e:
|
|
320
|
+
if shutting_down:
|
|
321
|
+
logger.debug("Shutdown in progress. Aborting connection attempts.")
|
|
322
|
+
break
|
|
323
|
+
attempts += 1
|
|
324
|
+
if retry_limit == 0 or attempts <= retry_limit:
|
|
325
|
+
wait_time = min(
|
|
326
|
+
2**attempts, 60
|
|
327
|
+
) # Consistent exponential backoff, capped at 60s
|
|
328
|
+
logger.warning(
|
|
329
|
+
f"An unexpected error occurred on attempt {attempts}: {e}. Retrying in {wait_time} seconds..."
|
|
330
|
+
)
|
|
331
|
+
time.sleep(wait_time)
|
|
332
|
+
else:
|
|
333
|
+
logger.error(f"Connection failed after {attempts} attempts: {e}")
|
|
239
334
|
return None
|
|
240
335
|
|
|
241
336
|
return meshtastic_client
|
|
@@ -243,11 +338,11 @@ def connect_meshtastic(passed_config=None, force_connect=False):
|
|
|
243
338
|
|
|
244
339
|
def on_lost_meshtastic_connection(interface=None, detection_source="unknown"):
|
|
245
340
|
"""
|
|
246
|
-
|
|
341
|
+
Initiate a reconnection sequence when the Meshtastic connection is lost, unless a shutdown or reconnection is already in progress.
|
|
247
342
|
|
|
248
|
-
|
|
249
|
-
interface:
|
|
250
|
-
detection_source:
|
|
343
|
+
Parameters:
|
|
344
|
+
interface: Optional Meshtastic interface instance, included for compatibility.
|
|
345
|
+
detection_source (str): Identifier for the source that detected the connection loss, used for debugging.
|
|
251
346
|
"""
|
|
252
347
|
global meshtastic_client, reconnecting, shutting_down, event_loop, reconnect_task
|
|
253
348
|
with meshtastic_lock:
|
|
@@ -266,7 +361,7 @@ def on_lost_meshtastic_connection(interface=None, detection_source="unknown"):
|
|
|
266
361
|
try:
|
|
267
362
|
meshtastic_client.close()
|
|
268
363
|
except OSError as e:
|
|
269
|
-
if e.errno ==
|
|
364
|
+
if e.errno == ERRNO_BAD_FILE_DESCRIPTOR:
|
|
270
365
|
# Bad file descriptor, already closed
|
|
271
366
|
pass
|
|
272
367
|
else:
|
|
@@ -281,12 +376,12 @@ def on_lost_meshtastic_connection(interface=None, detection_source="unknown"):
|
|
|
281
376
|
|
|
282
377
|
async def reconnect():
|
|
283
378
|
"""
|
|
284
|
-
Asynchronously attempts to reconnect to the Meshtastic device
|
|
379
|
+
Asynchronously attempts to reconnect to the Meshtastic device with exponential backoff, stopping if shutdown is initiated.
|
|
285
380
|
|
|
286
|
-
Reconnection
|
|
381
|
+
Reconnection starts with a 10-second delay, doubling up to a maximum of 5 minutes between attempts. If not running as a service, a progress bar is shown during the wait. The process stops immediately if shutdown is triggered or reconnection succeeds.
|
|
287
382
|
"""
|
|
288
383
|
global meshtastic_client, reconnecting, shutting_down
|
|
289
|
-
backoff_time =
|
|
384
|
+
backoff_time = DEFAULT_BACKOFF_TIME
|
|
290
385
|
try:
|
|
291
386
|
while not shutting_down:
|
|
292
387
|
try:
|
|
@@ -340,17 +435,21 @@ async def reconnect():
|
|
|
340
435
|
|
|
341
436
|
def on_meshtastic_message(packet, interface):
|
|
342
437
|
"""
|
|
343
|
-
|
|
438
|
+
Processes an incoming Meshtastic message and relays it to Matrix rooms or plugins based on message type and configuration.
|
|
344
439
|
|
|
345
|
-
Handles reactions and replies by relaying them to Matrix if enabled. Normal text messages are relayed to all mapped Matrix rooms unless handled by a plugin or directed to the relay node. Non-text messages are passed to plugins for processing. Messages from unmapped channels
|
|
440
|
+
Handles reactions and replies by relaying them to Matrix if enabled. Normal text messages are relayed to all mapped Matrix rooms unless handled by a plugin or directed to the relay node. Non-text messages are passed to plugins for processing. Messages from unmapped channels, disabled detection sensors, or during shutdown are ignored. Ensures sender information is retrieved or stored as needed.
|
|
346
441
|
"""
|
|
347
442
|
global config, matrix_rooms
|
|
348
443
|
|
|
444
|
+
# Validate packet structure
|
|
445
|
+
if not packet or not isinstance(packet, dict):
|
|
446
|
+
logger.error("Received malformed packet: packet is None or not a dict")
|
|
447
|
+
return
|
|
448
|
+
|
|
349
449
|
# Log that we received a message (without the full packet details)
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
)
|
|
450
|
+
decoded = packet.get("decoded")
|
|
451
|
+
if decoded and isinstance(decoded, dict) and decoded.get("text"):
|
|
452
|
+
logger.info(f"Received Meshtastic message: {decoded.get('text')}")
|
|
354
453
|
else:
|
|
355
454
|
logger.debug("Received non-text Meshtastic message")
|
|
356
455
|
|
|
@@ -367,13 +466,13 @@ def on_meshtastic_message(packet, interface):
|
|
|
367
466
|
message_storage_enabled(interactions)
|
|
368
467
|
|
|
369
468
|
# Filter packets based on interaction settings
|
|
370
|
-
if packet.get("decoded", {}).get("portnum") ==
|
|
469
|
+
if packet.get("decoded", {}).get("portnum") == TEXT_MESSAGE_APP:
|
|
371
470
|
decoded = packet.get("decoded", {})
|
|
372
471
|
# Filter out reactions if reactions are disabled
|
|
373
472
|
if (
|
|
374
473
|
not interactions["reactions"]
|
|
375
474
|
and "emoji" in decoded
|
|
376
|
-
and decoded.get("emoji") ==
|
|
475
|
+
and decoded.get("emoji") == EMOJI_FLAG_VALUE
|
|
377
476
|
):
|
|
378
477
|
logger.debug(
|
|
379
478
|
"Filtered out reaction packet due to reactions being disabled."
|
|
@@ -400,7 +499,7 @@ def on_meshtastic_message(packet, interface):
|
|
|
400
499
|
decoded = packet.get("decoded", {})
|
|
401
500
|
text = decoded.get("text")
|
|
402
501
|
replyId = decoded.get("replyId")
|
|
403
|
-
emoji_flag = "emoji" in decoded and decoded["emoji"] ==
|
|
502
|
+
emoji_flag = "emoji" in decoded and decoded["emoji"] == EMOJI_FLAG_VALUE
|
|
404
503
|
|
|
405
504
|
# Determine if this is a direct message to the relay node
|
|
406
505
|
from meshtastic.mesh_interface import BROADCAST_NUM
|
|
@@ -415,7 +514,7 @@ def on_meshtastic_message(packet, interface):
|
|
|
415
514
|
# Message to someone else; ignoring for broadcasting logic
|
|
416
515
|
is_direct_message = False
|
|
417
516
|
|
|
418
|
-
meshnet_name = config[
|
|
517
|
+
meshnet_name = config[CONFIG_SECTION_MESHTASTIC][CONFIG_KEY_MESHNET_NAME]
|
|
419
518
|
|
|
420
519
|
# Reaction handling (Meshtastic -> Matrix)
|
|
421
520
|
# If replyId and emoji_flag are present and reactions are enabled, we relay as text reactions in Matrix
|
|
@@ -432,11 +531,16 @@ def on_meshtastic_message(packet, interface):
|
|
|
432
531
|
else meshtastic_text
|
|
433
532
|
)
|
|
434
533
|
|
|
435
|
-
#
|
|
436
|
-
|
|
534
|
+
# Import the matrix prefix function
|
|
535
|
+
from mmrelay.matrix_utils import get_matrix_prefix
|
|
536
|
+
|
|
537
|
+
# Get the formatted prefix for the reaction
|
|
538
|
+
prefix = get_matrix_prefix(config, longname, shortname, meshnet_name)
|
|
437
539
|
|
|
438
540
|
reaction_symbol = text.strip() if (text and text.strip()) else "⚠️"
|
|
439
|
-
reaction_message =
|
|
541
|
+
reaction_message = (
|
|
542
|
+
f'\n {prefix}reacted {reaction_symbol} to "{abbreviated_text}"'
|
|
543
|
+
)
|
|
440
544
|
|
|
441
545
|
# Relay the reaction as emote to Matrix, preserving the original meshnet name
|
|
442
546
|
asyncio.run_coroutine_threadsafe(
|
|
@@ -469,9 +573,12 @@ def on_meshtastic_message(packet, interface):
|
|
|
469
573
|
# orig = (matrix_event_id, matrix_room_id, meshtastic_text, meshtastic_meshnet)
|
|
470
574
|
matrix_event_id, matrix_room_id, meshtastic_text, meshtastic_meshnet = orig
|
|
471
575
|
|
|
472
|
-
#
|
|
473
|
-
|
|
474
|
-
|
|
576
|
+
# Import the matrix prefix function
|
|
577
|
+
from mmrelay.matrix_utils import get_matrix_prefix
|
|
578
|
+
|
|
579
|
+
# Get the formatted prefix for the reply
|
|
580
|
+
prefix = get_matrix_prefix(config, longname, shortname, meshnet_name)
|
|
581
|
+
formatted_message = f"{prefix}{text}"
|
|
475
582
|
|
|
476
583
|
logger.info(f"Relaying Meshtastic reply from {longname} to Matrix")
|
|
477
584
|
|
|
@@ -502,12 +609,11 @@ def on_meshtastic_message(packet, interface):
|
|
|
502
609
|
if channel is None:
|
|
503
610
|
# If channel not specified, deduce from portnum
|
|
504
611
|
if (
|
|
505
|
-
decoded.get("portnum") ==
|
|
506
|
-
or decoded.get("portnum") ==
|
|
612
|
+
decoded.get("portnum") == TEXT_MESSAGE_APP
|
|
613
|
+
or decoded.get("portnum") == PORTNUM_NUMERIC_VALUE
|
|
614
|
+
or decoded.get("portnum") == DETECTION_SENSOR_APP
|
|
507
615
|
):
|
|
508
|
-
channel =
|
|
509
|
-
elif decoded.get("portnum") == "DETECTION_SENSOR_APP":
|
|
510
|
-
channel = 0
|
|
616
|
+
channel = DEFAULT_CHANNEL_VALUE
|
|
511
617
|
else:
|
|
512
618
|
logger.debug(
|
|
513
619
|
f"Unknown portnum {decoded.get('portnum')}, cannot determine channel"
|
|
@@ -526,7 +632,7 @@ def on_meshtastic_message(packet, interface):
|
|
|
526
632
|
return
|
|
527
633
|
|
|
528
634
|
# If detection_sensor is disabled and this is a detection sensor packet, skip it
|
|
529
|
-
if decoded.get("portnum") ==
|
|
635
|
+
if decoded.get("portnum") == DETECTION_SENSOR_APP and not config[
|
|
530
636
|
"meshtastic"
|
|
531
637
|
].get("detection_sensor", False):
|
|
532
638
|
logger.debug(
|
|
@@ -562,7 +668,12 @@ def on_meshtastic_message(packet, interface):
|
|
|
562
668
|
if not shortname:
|
|
563
669
|
shortname = str(sender)
|
|
564
670
|
|
|
565
|
-
|
|
671
|
+
# Import the matrix prefix function
|
|
672
|
+
from mmrelay.matrix_utils import get_matrix_prefix
|
|
673
|
+
|
|
674
|
+
# Get the formatted prefix
|
|
675
|
+
prefix = get_matrix_prefix(config, longname, shortname, meshnet_name)
|
|
676
|
+
formatted_message = f"{prefix}{text}"
|
|
566
677
|
|
|
567
678
|
# Plugin functionality - Check if any plugin handles this message before relaying
|
|
568
679
|
from mmrelay.plugin_loader import load_plugins
|
|
@@ -572,15 +683,19 @@ def on_meshtastic_message(packet, interface):
|
|
|
572
683
|
found_matching_plugin = False
|
|
573
684
|
for plugin in plugins:
|
|
574
685
|
if not found_matching_plugin:
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
686
|
+
try:
|
|
687
|
+
result = asyncio.run_coroutine_threadsafe(
|
|
688
|
+
plugin.handle_meshtastic_message(
|
|
689
|
+
packet, formatted_message, longname, meshnet_name
|
|
690
|
+
),
|
|
691
|
+
loop=loop,
|
|
692
|
+
)
|
|
693
|
+
found_matching_plugin = result.result()
|
|
694
|
+
if found_matching_plugin:
|
|
695
|
+
logger.debug(f"Processed by plugin {plugin.plugin_name}")
|
|
696
|
+
except Exception as e:
|
|
697
|
+
logger.error(f"Plugin {plugin.plugin_name} failed: {e}")
|
|
698
|
+
# Continue processing other plugins
|
|
584
699
|
|
|
585
700
|
# If message is a DM or handled by plugin, do not relay further
|
|
586
701
|
if is_direct_message:
|
|
@@ -629,36 +744,31 @@ def on_meshtastic_message(packet, interface):
|
|
|
629
744
|
found_matching_plugin = False
|
|
630
745
|
for plugin in plugins:
|
|
631
746
|
if not found_matching_plugin:
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
found_matching_plugin = result.result()
|
|
642
|
-
if found_matching_plugin:
|
|
643
|
-
logger.debug(
|
|
644
|
-
f"Processed {portnum} with plugin {plugin.plugin_name}"
|
|
747
|
+
try:
|
|
748
|
+
result = asyncio.run_coroutine_threadsafe(
|
|
749
|
+
plugin.handle_meshtastic_message(
|
|
750
|
+
packet,
|
|
751
|
+
formatted_message=None,
|
|
752
|
+
longname=None,
|
|
753
|
+
meshnet_name=None,
|
|
754
|
+
),
|
|
755
|
+
loop=loop,
|
|
645
756
|
)
|
|
757
|
+
found_matching_plugin = result.result()
|
|
758
|
+
if found_matching_plugin:
|
|
759
|
+
logger.debug(
|
|
760
|
+
f"Processed {portnum} with plugin {plugin.plugin_name}"
|
|
761
|
+
)
|
|
762
|
+
except Exception as e:
|
|
763
|
+
logger.error(f"Plugin {plugin.plugin_name} failed: {e}")
|
|
764
|
+
# Continue processing other plugins
|
|
646
765
|
|
|
647
766
|
|
|
648
767
|
async def check_connection():
|
|
649
768
|
"""
|
|
650
|
-
Periodically checks the health of the Meshtastic connection and triggers reconnection if the
|
|
651
|
-
|
|
652
|
-
Health checks can be enabled/disabled via configuration. When enabled, for non-BLE connections,
|
|
653
|
-
invokes `localNode.getMetadata()` at configurable intervals (default 60 seconds) to verify connectivity.
|
|
654
|
-
If the check fails or the firmware version is missing, initiates reconnection logic.
|
|
655
|
-
|
|
656
|
-
BLE connections rely on real-time disconnection detection and skip periodic health checks.
|
|
657
|
-
The function runs continuously until shutdown is requested.
|
|
769
|
+
Periodically checks the health of the Meshtastic connection and triggers reconnection if the device becomes unresponsive.
|
|
658
770
|
|
|
659
|
-
|
|
660
|
-
health_check.enabled: Enable/disable health checks (default: true)
|
|
661
|
-
health_check.heartbeat_interval: Interval between checks in seconds (default: 60)
|
|
771
|
+
For non-BLE connections, performs a metadata check at configurable intervals to verify device responsiveness. If the check fails or the firmware version is missing, initiates reconnection unless already in progress. BLE connections are excluded from periodic checks due to real-time disconnection detection. The function runs continuously until shutdown is requested, with health check behavior controlled by configuration.
|
|
662
772
|
"""
|
|
663
773
|
global meshtastic_client, shutting_down, config
|
|
664
774
|
|
|
@@ -667,7 +777,7 @@ async def check_connection():
|
|
|
667
777
|
logger.error("No configuration available. Cannot check connection.")
|
|
668
778
|
return
|
|
669
779
|
|
|
670
|
-
connection_type = config[
|
|
780
|
+
connection_type = config[CONFIG_SECTION_MESHTASTIC][CONFIG_KEY_CONNECTION_TYPE]
|
|
671
781
|
|
|
672
782
|
# Get health check configuration
|
|
673
783
|
health_config = config["meshtastic"].get("health_check", {})
|
|
@@ -689,7 +799,7 @@ async def check_connection():
|
|
|
689
799
|
if meshtastic_client and not reconnecting:
|
|
690
800
|
# BLE has real-time disconnection detection in the library
|
|
691
801
|
# Skip periodic health checks to avoid duplicate reconnection attempts
|
|
692
|
-
if connection_type ==
|
|
802
|
+
if connection_type == CONNECTION_TYPE_BLE:
|
|
693
803
|
if not ble_skip_logged:
|
|
694
804
|
logger.info(
|
|
695
805
|
"BLE connection uses real-time disconnection detection - health checks disabled"
|
|
@@ -738,9 +848,7 @@ def sendTextReply(
|
|
|
738
848
|
channelIndex: int = 0,
|
|
739
849
|
):
|
|
740
850
|
"""
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
Creates and sends a reply message by setting the `reply_id` field in the Meshtastic Data protobuf, enabling proper reply threading. Returns the sent packet with its ID populated.
|
|
851
|
+
Sends a text message as a reply to a specific previous message via the Meshtastic interface.
|
|
744
852
|
|
|
745
853
|
Parameters:
|
|
746
854
|
interface: The Meshtastic interface to send through.
|
|
@@ -751,10 +859,15 @@ def sendTextReply(
|
|
|
751
859
|
channelIndex (int): The channel index to send the message on.
|
|
752
860
|
|
|
753
861
|
Returns:
|
|
754
|
-
The sent MeshPacket with its ID field populated.
|
|
862
|
+
The sent MeshPacket with its ID field populated, or None if sending fails or the interface is unavailable.
|
|
755
863
|
"""
|
|
756
864
|
logger.debug(f"Sending text reply: '{text}' replying to message ID {reply_id}")
|
|
757
865
|
|
|
866
|
+
# Check if interface is available
|
|
867
|
+
if interface is None:
|
|
868
|
+
logger.error("No Meshtastic interface available for sending reply")
|
|
869
|
+
return None
|
|
870
|
+
|
|
758
871
|
# Create the Data protobuf message with reply_id set
|
|
759
872
|
data_msg = mesh_pb2.Data()
|
|
760
873
|
data_msg.portnum = portnums_pb2.PortNum.TEXT_MESSAGE_APP
|
|
@@ -768,9 +881,13 @@ def sendTextReply(
|
|
|
768
881
|
mesh_packet.id = interface._generatePacketId()
|
|
769
882
|
|
|
770
883
|
# Send the packet using the existing infrastructure
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
884
|
+
try:
|
|
885
|
+
return interface._sendPacket(
|
|
886
|
+
mesh_packet, destinationId=destinationId, wantAck=wantAck
|
|
887
|
+
)
|
|
888
|
+
except Exception as e:
|
|
889
|
+
logger.error(f"Failed to send text reply: {e}")
|
|
890
|
+
return None
|
|
774
891
|
|
|
775
892
|
|
|
776
893
|
if __name__ == "__main__":
|