mmrelay 1.1.3__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 -1
- 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 +5 -4
- mmrelay/matrix_utils.py +43 -53
- mmrelay/meshtastic_utils.py +203 -99
- mmrelay/message_queue.py +17 -17
- mmrelay/plugin_loader.py +54 -51
- mmrelay/plugins/base_plugin.py +58 -11
- 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-1.1.3.dist-info → mmrelay-1.1.4.dist-info}/METADATA +3 -3
- mmrelay-1.1.4.dist-info/RECORD +43 -0
- mmrelay-1.1.3.dist-info/RECORD +0 -35
- {mmrelay-1.1.3.dist-info → mmrelay-1.1.4.dist-info}/WHEEL +0 -0
- {mmrelay-1.1.3.dist-info → mmrelay-1.1.4.dist-info}/entry_points.txt +0 -0
- {mmrelay-1.1.3.dist-info → mmrelay-1.1.4.dist-info}/licenses/LICENSE +0 -0
- {mmrelay-1.1.3.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
|
-
Processes incoming Meshtastic
|
|
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
|
|
@@ -510,12 +609,11 @@ def on_meshtastic_message(packet, interface):
|
|
|
510
609
|
if channel is None:
|
|
511
610
|
# If channel not specified, deduce from portnum
|
|
512
611
|
if (
|
|
513
|
-
decoded.get("portnum") ==
|
|
514
|
-
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
|
|
515
615
|
):
|
|
516
|
-
channel =
|
|
517
|
-
elif decoded.get("portnum") == "DETECTION_SENSOR_APP":
|
|
518
|
-
channel = 0
|
|
616
|
+
channel = DEFAULT_CHANNEL_VALUE
|
|
519
617
|
else:
|
|
520
618
|
logger.debug(
|
|
521
619
|
f"Unknown portnum {decoded.get('portnum')}, cannot determine channel"
|
|
@@ -534,7 +632,7 @@ def on_meshtastic_message(packet, interface):
|
|
|
534
632
|
return
|
|
535
633
|
|
|
536
634
|
# If detection_sensor is disabled and this is a detection sensor packet, skip it
|
|
537
|
-
if decoded.get("portnum") ==
|
|
635
|
+
if decoded.get("portnum") == DETECTION_SENSOR_APP and not config[
|
|
538
636
|
"meshtastic"
|
|
539
637
|
].get("detection_sensor", False):
|
|
540
638
|
logger.debug(
|
|
@@ -585,15 +683,19 @@ def on_meshtastic_message(packet, interface):
|
|
|
585
683
|
found_matching_plugin = False
|
|
586
684
|
for plugin in plugins:
|
|
587
685
|
if not found_matching_plugin:
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
|
597
699
|
|
|
598
700
|
# If message is a DM or handled by plugin, do not relay further
|
|
599
701
|
if is_direct_message:
|
|
@@ -642,36 +744,31 @@ def on_meshtastic_message(packet, interface):
|
|
|
642
744
|
found_matching_plugin = False
|
|
643
745
|
for plugin in plugins:
|
|
644
746
|
if not found_matching_plugin:
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
found_matching_plugin = result.result()
|
|
655
|
-
if found_matching_plugin:
|
|
656
|
-
logger.debug(
|
|
657
|
-
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,
|
|
658
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
|
|
659
765
|
|
|
660
766
|
|
|
661
767
|
async def check_connection():
|
|
662
768
|
"""
|
|
663
|
-
Periodically checks the health of the Meshtastic connection and triggers reconnection if the
|
|
769
|
+
Periodically checks the health of the Meshtastic connection and triggers reconnection if the device becomes unresponsive.
|
|
664
770
|
|
|
665
|
-
|
|
666
|
-
invokes `localNode.getMetadata()` at configurable intervals (default 60 seconds) to verify connectivity.
|
|
667
|
-
If the check fails or the firmware version is missing, initiates reconnection logic.
|
|
668
|
-
|
|
669
|
-
BLE connections rely on real-time disconnection detection and skip periodic health checks.
|
|
670
|
-
The function runs continuously until shutdown is requested.
|
|
671
|
-
|
|
672
|
-
Configuration:
|
|
673
|
-
health_check.enabled: Enable/disable health checks (default: true)
|
|
674
|
-
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.
|
|
675
772
|
"""
|
|
676
773
|
global meshtastic_client, shutting_down, config
|
|
677
774
|
|
|
@@ -680,7 +777,7 @@ async def check_connection():
|
|
|
680
777
|
logger.error("No configuration available. Cannot check connection.")
|
|
681
778
|
return
|
|
682
779
|
|
|
683
|
-
connection_type = config[
|
|
780
|
+
connection_type = config[CONFIG_SECTION_MESHTASTIC][CONFIG_KEY_CONNECTION_TYPE]
|
|
684
781
|
|
|
685
782
|
# Get health check configuration
|
|
686
783
|
health_config = config["meshtastic"].get("health_check", {})
|
|
@@ -702,7 +799,7 @@ async def check_connection():
|
|
|
702
799
|
if meshtastic_client and not reconnecting:
|
|
703
800
|
# BLE has real-time disconnection detection in the library
|
|
704
801
|
# Skip periodic health checks to avoid duplicate reconnection attempts
|
|
705
|
-
if connection_type ==
|
|
802
|
+
if connection_type == CONNECTION_TYPE_BLE:
|
|
706
803
|
if not ble_skip_logged:
|
|
707
804
|
logger.info(
|
|
708
805
|
"BLE connection uses real-time disconnection detection - health checks disabled"
|
|
@@ -751,9 +848,7 @@ def sendTextReply(
|
|
|
751
848
|
channelIndex: int = 0,
|
|
752
849
|
):
|
|
753
850
|
"""
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
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.
|
|
757
852
|
|
|
758
853
|
Parameters:
|
|
759
854
|
interface: The Meshtastic interface to send through.
|
|
@@ -764,10 +859,15 @@ def sendTextReply(
|
|
|
764
859
|
channelIndex (int): The channel index to send the message on.
|
|
765
860
|
|
|
766
861
|
Returns:
|
|
767
|
-
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.
|
|
768
863
|
"""
|
|
769
864
|
logger.debug(f"Sending text reply: '{text}' replying to message ID {reply_id}")
|
|
770
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
|
+
|
|
771
871
|
# Create the Data protobuf message with reply_id set
|
|
772
872
|
data_msg = mesh_pb2.Data()
|
|
773
873
|
data_msg.portnum = portnums_pb2.PortNum.TEXT_MESSAGE_APP
|
|
@@ -781,9 +881,13 @@ def sendTextReply(
|
|
|
781
881
|
mesh_packet.id = interface._generatePacketId()
|
|
782
882
|
|
|
783
883
|
# Send the packet using the existing infrastructure
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
|
787
891
|
|
|
788
892
|
|
|
789
893
|
if __name__ == "__main__":
|
mmrelay/message_queue.py
CHANGED
|
@@ -13,18 +13,18 @@ from dataclasses import dataclass
|
|
|
13
13
|
from queue import Empty, Queue
|
|
14
14
|
from typing import Callable, Optional
|
|
15
15
|
|
|
16
|
+
from mmrelay.constants.database import DEFAULT_MSGS_TO_KEEP
|
|
17
|
+
from mmrelay.constants.network import MINIMUM_MESSAGE_DELAY
|
|
18
|
+
from mmrelay.constants.queue import (
|
|
19
|
+
DEFAULT_MESSAGE_DELAY,
|
|
20
|
+
MAX_QUEUE_SIZE,
|
|
21
|
+
QUEUE_HIGH_WATER_MARK,
|
|
22
|
+
QUEUE_MEDIUM_WATER_MARK,
|
|
23
|
+
)
|
|
16
24
|
from mmrelay.log_utils import get_logger
|
|
17
25
|
|
|
18
26
|
logger = get_logger(name="MessageQueue")
|
|
19
27
|
|
|
20
|
-
# Default message delay in seconds (minimum 2.0 due to firmware constraints)
|
|
21
|
-
DEFAULT_MESSAGE_DELAY = 2.2
|
|
22
|
-
|
|
23
|
-
# Queue size configuration
|
|
24
|
-
MAX_QUEUE_SIZE = 100
|
|
25
|
-
QUEUE_HIGH_WATER_MARK = 75 # 75% of MAX_QUEUE_SIZE
|
|
26
|
-
QUEUE_MEDIUM_WATER_MARK = 50 # 50% of MAX_QUEUE_SIZE
|
|
27
|
-
|
|
28
28
|
|
|
29
29
|
@dataclass
|
|
30
30
|
class QueuedMessage:
|
|
@@ -61,20 +61,20 @@ class MessageQueue:
|
|
|
61
61
|
|
|
62
62
|
def start(self, message_delay: float = DEFAULT_MESSAGE_DELAY):
|
|
63
63
|
"""
|
|
64
|
-
|
|
64
|
+
Start the message queue processor with a specified minimum delay between messages.
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
If the provided delay is below the firmware-enforced minimum, the minimum is used instead. The processor task is started immediately if the asyncio event loop is running; otherwise, startup is deferred until the event loop becomes available.
|
|
67
67
|
"""
|
|
68
68
|
with self._lock:
|
|
69
69
|
if self._running:
|
|
70
70
|
return
|
|
71
71
|
|
|
72
72
|
# Validate and enforce firmware minimum
|
|
73
|
-
if message_delay <
|
|
73
|
+
if message_delay < MINIMUM_MESSAGE_DELAY:
|
|
74
74
|
logger.warning(
|
|
75
|
-
f"Message delay {message_delay}s below firmware minimum (
|
|
75
|
+
f"Message delay {message_delay}s below firmware minimum ({MINIMUM_MESSAGE_DELAY}s), using {MINIMUM_MESSAGE_DELAY}s"
|
|
76
76
|
)
|
|
77
|
-
self._message_delay =
|
|
77
|
+
self._message_delay = MINIMUM_MESSAGE_DELAY
|
|
78
78
|
else:
|
|
79
79
|
self._message_delay = message_delay
|
|
80
80
|
self._running = True
|
|
@@ -372,13 +372,13 @@ class MessageQueue:
|
|
|
372
372
|
|
|
373
373
|
def _handle_message_mapping(self, result, mapping_info):
|
|
374
374
|
"""
|
|
375
|
-
|
|
375
|
+
Update the message mapping database with information about a sent message and prune old mappings if configured.
|
|
376
376
|
|
|
377
377
|
Parameters:
|
|
378
378
|
result: The result object from the send function, expected to have an `id` attribute.
|
|
379
|
-
mapping_info (dict):
|
|
379
|
+
mapping_info (dict): Contains mapping details such as `matrix_event_id`, `room_id`, `text`, and optionally `meshnet` and `msgs_to_keep`.
|
|
380
380
|
|
|
381
|
-
|
|
381
|
+
If required mapping fields are present, stores the mapping and prunes old entries based on the specified or default retention count.
|
|
382
382
|
"""
|
|
383
383
|
try:
|
|
384
384
|
# Import here to avoid circular imports
|
|
@@ -402,7 +402,7 @@ class MessageQueue:
|
|
|
402
402
|
logger.debug(f"Stored message map for meshtastic_id: {result.id}")
|
|
403
403
|
|
|
404
404
|
# Handle pruning if configured
|
|
405
|
-
msgs_to_keep = mapping_info.get("msgs_to_keep",
|
|
405
|
+
msgs_to_keep = mapping_info.get("msgs_to_keep", DEFAULT_MSGS_TO_KEEP)
|
|
406
406
|
if msgs_to_keep > 0:
|
|
407
407
|
prune_message_map(msgs_to_keep)
|
|
408
408
|
|