mmrelay 1.1.1__py3-none-any.whl → 1.1.3__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/log_utils.py +40 -48
- mmrelay/main.py +49 -11
- mmrelay/matrix_utils.py +405 -144
- mmrelay/meshtastic_utils.py +91 -84
- mmrelay/message_queue.py +475 -0
- mmrelay/plugin_loader.py +2 -2
- mmrelay/plugins/base_plugin.py +92 -39
- mmrelay/tools/sample_config.yaml +26 -4
- {mmrelay-1.1.1.dist-info → mmrelay-1.1.3.dist-info}/METADATA +11 -13
- {mmrelay-1.1.1.dist-info → mmrelay-1.1.3.dist-info}/RECORD +15 -14
- mmrelay-1.1.3.dist-info/licenses/LICENSE +675 -0
- mmrelay-1.1.1.dist-info/licenses/LICENSE +0 -21
- {mmrelay-1.1.1.dist-info → mmrelay-1.1.3.dist-info}/WHEEL +0 -0
- {mmrelay-1.1.1.dist-info → mmrelay-1.1.3.dist-info}/entry_points.txt +0 -0
- {mmrelay-1.1.1.dist-info → mmrelay-1.1.3.dist-info}/top_level.txt +0 -0
mmrelay/meshtastic_utils.py
CHANGED
|
@@ -47,18 +47,17 @@ reconnecting = False
|
|
|
47
47
|
shutting_down = False
|
|
48
48
|
reconnect_task = None # To keep track of the reconnect task
|
|
49
49
|
|
|
50
|
-
#
|
|
50
|
+
# Subscription flags to prevent duplicate subscriptions
|
|
51
51
|
subscribed_to_messages = False
|
|
52
52
|
subscribed_to_connection_lost = False
|
|
53
|
-
subscribed_to_connection_established = False
|
|
54
53
|
|
|
55
54
|
|
|
56
55
|
def is_running_as_service():
|
|
57
56
|
"""
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
Determine if the application is running as a systemd service.
|
|
58
|
+
|
|
60
59
|
Returns:
|
|
61
|
-
True if
|
|
60
|
+
bool: True if running under systemd (as indicated by the INVOCATION_ID environment variable or parent process), False otherwise.
|
|
62
61
|
"""
|
|
63
62
|
# Check for INVOCATION_ID environment variable (set by systemd)
|
|
64
63
|
if os.environ.get("INVOCATION_ID"):
|
|
@@ -89,16 +88,16 @@ def serial_port_exists(port_name):
|
|
|
89
88
|
|
|
90
89
|
def connect_meshtastic(passed_config=None, force_connect=False):
|
|
91
90
|
"""
|
|
92
|
-
Establishes and manages a connection to a Meshtastic device using
|
|
93
|
-
|
|
94
|
-
If a configuration is provided, updates the global configuration and Matrix room mappings.
|
|
95
|
-
|
|
91
|
+
Establishes and manages a connection to a Meshtastic device using serial, BLE, or TCP, with automatic retries and event subscriptions.
|
|
92
|
+
|
|
93
|
+
If a configuration is provided, updates the global configuration and Matrix room mappings. If already connected and not forced, returns the existing client. Handles reconnection logic with exponential backoff, verifies serial port existence, and subscribes to message and connection lost events upon successful connection.
|
|
94
|
+
|
|
96
95
|
Parameters:
|
|
97
|
-
passed_config (dict, optional): Configuration dictionary to use for the connection.
|
|
96
|
+
passed_config (dict, optional): Configuration dictionary to use for the connection.
|
|
98
97
|
force_connect (bool, optional): If True, forces a new connection even if one already exists.
|
|
99
|
-
|
|
98
|
+
|
|
100
99
|
Returns:
|
|
101
|
-
|
|
100
|
+
meshtastic_client: The connected Meshtastic client instance, or None if connection fails or shutdown is in progress.
|
|
102
101
|
"""
|
|
103
102
|
global meshtastic_client, shutting_down, config, matrix_rooms
|
|
104
103
|
if shutting_down:
|
|
@@ -203,8 +202,8 @@ def connect_meshtastic(passed_config=None, force_connect=False):
|
|
|
203
202
|
f"Connected to {nodeInfo['user']['shortName']} / {nodeInfo['user']['hwModel']}"
|
|
204
203
|
)
|
|
205
204
|
|
|
206
|
-
# Subscribe to message and connection events (only
|
|
207
|
-
global subscribed_to_messages, subscribed_to_connection_lost
|
|
205
|
+
# Subscribe to message and connection lost events (only once per application run)
|
|
206
|
+
global subscribed_to_messages, subscribed_to_connection_lost
|
|
208
207
|
if not subscribed_to_messages:
|
|
209
208
|
pub.subscribe(on_meshtastic_message, "meshtastic.receive")
|
|
210
209
|
subscribed_to_messages = True
|
|
@@ -217,13 +216,6 @@ def connect_meshtastic(passed_config=None, force_connect=False):
|
|
|
217
216
|
subscribed_to_connection_lost = True
|
|
218
217
|
logger.debug("Subscribed to meshtastic.connection.lost")
|
|
219
218
|
|
|
220
|
-
if not subscribed_to_connection_established:
|
|
221
|
-
pub.subscribe(
|
|
222
|
-
on_established_meshtastic_connection, "meshtastic.connection.established"
|
|
223
|
-
)
|
|
224
|
-
subscribed_to_connection_established = True
|
|
225
|
-
logger.debug("Subscribed to meshtastic.connection.established")
|
|
226
|
-
|
|
227
219
|
except (
|
|
228
220
|
serial.SerialException,
|
|
229
221
|
BleakDBusError,
|
|
@@ -249,13 +241,13 @@ def connect_meshtastic(passed_config=None, force_connect=False):
|
|
|
249
241
|
return meshtastic_client
|
|
250
242
|
|
|
251
243
|
|
|
252
|
-
def on_lost_meshtastic_connection(interface=None, detection_source="
|
|
244
|
+
def on_lost_meshtastic_connection(interface=None, detection_source="unknown"):
|
|
253
245
|
"""
|
|
254
|
-
Handles Meshtastic connection
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
interface: The interface
|
|
258
|
-
detection_source
|
|
246
|
+
Handles loss of Meshtastic connection by initiating a reconnection sequence unless the system is shutting down or already reconnecting.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
interface: The Meshtastic interface (optional, for compatibility)
|
|
250
|
+
detection_source: Source that detected the connection loss (for debugging)
|
|
259
251
|
"""
|
|
260
252
|
global meshtastic_client, reconnecting, shutting_down, event_loop, reconnect_task
|
|
261
253
|
with meshtastic_lock:
|
|
@@ -290,8 +282,8 @@ def on_lost_meshtastic_connection(interface=None, detection_source="detected by
|
|
|
290
282
|
async def reconnect():
|
|
291
283
|
"""
|
|
292
284
|
Asynchronously attempts to reconnect to the Meshtastic device using exponential backoff, stopping if a shutdown is initiated.
|
|
293
|
-
|
|
294
|
-
|
|
285
|
+
|
|
286
|
+
Reconnection attempts start 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 displayed during the wait. The process stops immediately if `shutting_down` is set to True or upon successful reconnection.
|
|
295
287
|
"""
|
|
296
288
|
global meshtastic_client, reconnecting, shutting_down
|
|
297
289
|
backoff_time = 10
|
|
@@ -346,23 +338,11 @@ async def reconnect():
|
|
|
346
338
|
reconnecting = False
|
|
347
339
|
|
|
348
340
|
|
|
349
|
-
def on_established_meshtastic_connection(interface=None):
|
|
350
|
-
"""
|
|
351
|
-
Callback triggered when a Meshtastic connection is successfully established.
|
|
352
|
-
|
|
353
|
-
Clears the reconnecting flag and logs the connection event.
|
|
354
|
-
"""
|
|
355
|
-
global reconnecting
|
|
356
|
-
with meshtastic_lock:
|
|
357
|
-
logger.info("Connection established (detected by library)")
|
|
358
|
-
reconnecting = False # Clear reconnecting flag when connection is confirmed
|
|
359
|
-
|
|
360
|
-
|
|
361
341
|
def on_meshtastic_message(packet, interface):
|
|
362
342
|
"""
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
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 or disabled detection sensors are
|
|
343
|
+
Processes incoming Meshtastic messages and relays them to Matrix rooms or plugins based on message type and configuration.
|
|
344
|
+
|
|
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 or disabled detection sensors are ignored. Ensures sender information is retrieved or stored as needed.
|
|
366
346
|
"""
|
|
367
347
|
global config, matrix_rooms
|
|
368
348
|
|
|
@@ -452,11 +432,16 @@ def on_meshtastic_message(packet, interface):
|
|
|
452
432
|
else meshtastic_text
|
|
453
433
|
)
|
|
454
434
|
|
|
455
|
-
#
|
|
456
|
-
|
|
435
|
+
# Import the matrix prefix function
|
|
436
|
+
from mmrelay.matrix_utils import get_matrix_prefix
|
|
437
|
+
|
|
438
|
+
# Get the formatted prefix for the reaction
|
|
439
|
+
prefix = get_matrix_prefix(config, longname, shortname, meshnet_name)
|
|
457
440
|
|
|
458
441
|
reaction_symbol = text.strip() if (text and text.strip()) else "⚠️"
|
|
459
|
-
reaction_message =
|
|
442
|
+
reaction_message = (
|
|
443
|
+
f'\n {prefix}reacted {reaction_symbol} to "{abbreviated_text}"'
|
|
444
|
+
)
|
|
460
445
|
|
|
461
446
|
# Relay the reaction as emote to Matrix, preserving the original meshnet name
|
|
462
447
|
asyncio.run_coroutine_threadsafe(
|
|
@@ -489,9 +474,12 @@ def on_meshtastic_message(packet, interface):
|
|
|
489
474
|
# orig = (matrix_event_id, matrix_room_id, meshtastic_text, meshtastic_meshnet)
|
|
490
475
|
matrix_event_id, matrix_room_id, meshtastic_text, meshtastic_meshnet = orig
|
|
491
476
|
|
|
492
|
-
#
|
|
493
|
-
|
|
494
|
-
|
|
477
|
+
# Import the matrix prefix function
|
|
478
|
+
from mmrelay.matrix_utils import get_matrix_prefix
|
|
479
|
+
|
|
480
|
+
# Get the formatted prefix for the reply
|
|
481
|
+
prefix = get_matrix_prefix(config, longname, shortname, meshnet_name)
|
|
482
|
+
formatted_message = f"{prefix}{text}"
|
|
495
483
|
|
|
496
484
|
logger.info(f"Relaying Meshtastic reply from {longname} to Matrix")
|
|
497
485
|
|
|
@@ -582,7 +570,12 @@ def on_meshtastic_message(packet, interface):
|
|
|
582
570
|
if not shortname:
|
|
583
571
|
shortname = str(sender)
|
|
584
572
|
|
|
585
|
-
|
|
573
|
+
# Import the matrix prefix function
|
|
574
|
+
from mmrelay.matrix_utils import get_matrix_prefix
|
|
575
|
+
|
|
576
|
+
# Get the formatted prefix
|
|
577
|
+
prefix = get_matrix_prefix(config, longname, shortname, meshnet_name)
|
|
578
|
+
formatted_message = f"{prefix}{text}"
|
|
586
579
|
|
|
587
580
|
# Plugin functionality - Check if any plugin handles this message before relaying
|
|
588
581
|
from mmrelay.plugin_loader import load_plugins
|
|
@@ -667,28 +660,42 @@ def on_meshtastic_message(packet, interface):
|
|
|
667
660
|
|
|
668
661
|
async def check_connection():
|
|
669
662
|
"""
|
|
670
|
-
Periodically
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
663
|
+
Periodically checks the health of the Meshtastic connection and triggers reconnection if the connection is lost.
|
|
664
|
+
|
|
665
|
+
Health checks can be enabled/disabled via configuration. When enabled, for non-BLE connections,
|
|
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)
|
|
675
675
|
"""
|
|
676
|
-
global meshtastic_client, shutting_down, config
|
|
676
|
+
global meshtastic_client, shutting_down, config
|
|
677
677
|
|
|
678
678
|
# Check if config is available
|
|
679
679
|
if config is None:
|
|
680
680
|
logger.error("No configuration available. Cannot check connection.")
|
|
681
681
|
return
|
|
682
682
|
|
|
683
|
-
# Get heartbeat interval from config, default to 180 seconds
|
|
684
|
-
heartbeat_interval = config["meshtastic"].get("heartbeat_interval", 180)
|
|
685
683
|
connection_type = config["meshtastic"]["connection_type"]
|
|
686
684
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
)
|
|
685
|
+
# Get health check configuration
|
|
686
|
+
health_config = config["meshtastic"].get("health_check", {})
|
|
687
|
+
health_check_enabled = health_config.get("enabled", True)
|
|
688
|
+
heartbeat_interval = health_config.get("heartbeat_interval", 60)
|
|
689
|
+
|
|
690
|
+
# Support legacy heartbeat_interval configuration for backward compatibility
|
|
691
|
+
if "heartbeat_interval" in config["meshtastic"]:
|
|
692
|
+
heartbeat_interval = config["meshtastic"]["heartbeat_interval"]
|
|
693
|
+
|
|
694
|
+
# Exit early if health checks are disabled
|
|
695
|
+
if not health_check_enabled:
|
|
696
|
+
logger.info("Connection health checks are disabled in configuration")
|
|
697
|
+
return
|
|
690
698
|
|
|
691
|
-
# Track if we've logged the BLE skip message to avoid spam
|
|
692
699
|
ble_skip_logged = False
|
|
693
700
|
|
|
694
701
|
while not shutting_down:
|
|
@@ -697,13 +704,12 @@ async def check_connection():
|
|
|
697
704
|
# Skip periodic health checks to avoid duplicate reconnection attempts
|
|
698
705
|
if connection_type == "ble":
|
|
699
706
|
if not ble_skip_logged:
|
|
700
|
-
logger.info(
|
|
707
|
+
logger.info(
|
|
708
|
+
"BLE connection uses real-time disconnection detection - health checks disabled"
|
|
709
|
+
)
|
|
701
710
|
ble_skip_logged = True
|
|
702
711
|
else:
|
|
703
712
|
try:
|
|
704
|
-
logger.debug(
|
|
705
|
-
f"Checking {connection_type} connection health using getMetadata()"
|
|
706
|
-
)
|
|
707
713
|
output_capture = io.StringIO()
|
|
708
714
|
with contextlib.redirect_stdout(
|
|
709
715
|
output_capture
|
|
@@ -714,18 +720,20 @@ async def check_connection():
|
|
|
714
720
|
if "firmware_version" not in console_output:
|
|
715
721
|
raise Exception("No firmware_version in getMetadata output.")
|
|
716
722
|
|
|
717
|
-
logger.debug(f"{connection_type.capitalize()} connection healthy")
|
|
718
|
-
|
|
719
723
|
except Exception as e:
|
|
720
724
|
# Only trigger reconnection if we're not already reconnecting
|
|
721
725
|
if not reconnecting:
|
|
722
|
-
logger.
|
|
726
|
+
logger.error(
|
|
723
727
|
f"{connection_type.capitalize()} connection health check failed: {e}"
|
|
724
728
|
)
|
|
725
|
-
|
|
726
|
-
|
|
729
|
+
on_lost_meshtastic_connection(
|
|
730
|
+
interface=meshtastic_client,
|
|
731
|
+
detection_source=f"health check failed: {str(e)}",
|
|
732
|
+
)
|
|
727
733
|
else:
|
|
728
|
-
logger.debug(
|
|
734
|
+
logger.debug(
|
|
735
|
+
"Skipping reconnection trigger - already reconnecting"
|
|
736
|
+
)
|
|
729
737
|
elif reconnecting:
|
|
730
738
|
logger.debug("Skipping connection check - reconnection in progress")
|
|
731
739
|
elif not meshtastic_client:
|
|
@@ -743,21 +751,20 @@ def sendTextReply(
|
|
|
743
751
|
channelIndex: int = 0,
|
|
744
752
|
):
|
|
745
753
|
"""
|
|
746
|
-
Send a text message as a reply to a previous message.
|
|
754
|
+
Send a Meshtastic text message as a reply to a specific previous message.
|
|
747
755
|
|
|
748
|
-
|
|
749
|
-
in the Data protobuf message, which the standard sendText() method doesn't support.
|
|
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.
|
|
750
757
|
|
|
751
|
-
|
|
752
|
-
interface: The Meshtastic interface to send through
|
|
753
|
-
text: The
|
|
754
|
-
reply_id: The ID of the message
|
|
755
|
-
destinationId:
|
|
756
|
-
wantAck: Whether to request acknowledgment
|
|
757
|
-
channelIndex:
|
|
758
|
+
Parameters:
|
|
759
|
+
interface: The Meshtastic interface to send through.
|
|
760
|
+
text (str): The message content to send.
|
|
761
|
+
reply_id (int): The ID of the message being replied to.
|
|
762
|
+
destinationId: The recipient address (defaults to broadcast).
|
|
763
|
+
wantAck (bool): Whether to request acknowledgment for the message.
|
|
764
|
+
channelIndex (int): The channel index to send the message on.
|
|
758
765
|
|
|
759
766
|
Returns:
|
|
760
|
-
The sent
|
|
767
|
+
The sent MeshPacket with its ID field populated.
|
|
761
768
|
"""
|
|
762
769
|
logger.debug(f"Sending text reply: '{text}' replying to message ID {reply_id}")
|
|
763
770
|
|