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.

@@ -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
- # Track pubsub subscription state to prevent duplicate subscriptions during reconnections
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
- Check if the application is running as a systemd service.
59
-
57
+ Determine if the application is running as a systemd service.
58
+
60
59
  Returns:
61
- True if the process is running under systemd, either by detecting the INVOCATION_ID environment variable or by verifying that the parent process is systemd; otherwise, False.
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 the specified connection type (serial, BLE, or TCP).
93
-
94
- If a configuration is provided, updates the global configuration and Matrix room mappings. Handles reconnection logic, including closing any existing connection if `force_connect` is True. Retries connection attempts with exponential backoff until successful or shutdown is initiated. Subscribes to message and connection events only once per process to avoid duplicate subscriptions.
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. If provided, updates the global configuration.
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
- Meshtastic interface client if the connection is successful, or None if the connection fails or shutdown is in progress.
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 if not already subscribed)
207
- global subscribed_to_messages, subscribed_to_connection_lost, subscribed_to_connection_established
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="detected by library"):
244
+ def on_lost_meshtastic_connection(interface=None, detection_source="unknown"):
253
245
  """
254
- Handles Meshtastic connection loss by initiating a reconnection sequence unless a shutdown is in progress or a reconnection is already underway.
255
-
256
- Parameters:
257
- interface: The interface that lost connection (unused; present for compatibility).
258
- detection_source (str): Description of how the disconnection was detected.
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
- The function increases the wait time between attempts up to a maximum of 5 minutes and provides a progress bar if not running as a service. The reconnection process halts immediately if the shutdown flag is set or if reconnection succeeds.
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
- Process incoming Meshtastic messages and relay them to Matrix rooms or plugins according to message type and interaction settings.
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 filtered out, and sender information is retrieved or stored as needed.
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
- # Ensure that meshnet_name is always included, using our own meshnet for accuracy.
456
- full_display_name = f"{longname}/{meshnet_name}"
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 = f'\n [{full_display_name}] reacted {reaction_symbol} to "{abbreviated_text}"'
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
- # Format the reply message for Matrix
493
- full_display_name = f"{longname}/{meshnet_name}"
494
- formatted_message = f"[{full_display_name}]: {text}"
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
- formatted_message = f"[{longname}/{meshnet_name}]: {text}"
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 verifies the health of the Meshtastic connection and triggers reconnection if needed.
671
-
672
- For non-BLE connections, this coroutine calls `localNode.getMetadata()` at a configurable interval to confirm the connection is alive. If the health check fails or does not return expected metadata, it invokes the connection lost handler to initiate reconnection. BLE connections are excluded from periodic checks, as their library provides real-time disconnection detection.
673
-
674
- This coroutine runs continuously until shutdown is requested.
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, reconnecting
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
- logger.info(
688
- f"Starting connection heartbeat monitor (interval: {heartbeat_interval}s)"
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("BLE connection uses real-time disconnection detection - health checks disabled")
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.warning(
726
+ logger.error(
723
727
  f"{connection_type.capitalize()} connection health check failed: {e}"
724
728
  )
725
- # Use existing handler with health check reason
726
- on_lost_meshtastic_connection(detection_source=f"health check failed: {str(e)}")
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("Skipping reconnection trigger - already reconnecting")
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
- This function creates a proper Meshtastic reply by setting the reply_id field
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
- Args:
752
- interface: The Meshtastic interface to send through
753
- text: The text message to send
754
- reply_id: The ID of the message this is replying to
755
- destinationId: Where to send the message (default: broadcast)
756
- wantAck: Whether to request acknowledgment
757
- channelIndex: Which channel to send on
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 packet with populated ID field
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