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.

@@ -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
- Determine if the application is running as a systemd service.
97
+ Checks whether the application is running as a systemd service.
58
98
 
59
99
  Returns:
60
- bool: True if running under systemd (as indicated by the INVOCATION_ID environment variable or parent process), False otherwise.
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() == "systemd"
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 and manages a connection to a Meshtastic device using serial, BLE, or TCP, with automatic retries and event subscriptions.
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. 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.
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 to use for the connection.
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
- meshtastic_client: The connected Meshtastic client instance, or None if connection fails or shutdown is in progress.
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["meshtastic"]["connection_type"]
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 == "network":
137
- connection_type = "tcp"
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 = 0 # 0 means infinite retries
142
- attempts = 1
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 == "serial":
215
+ if connection_type == CONNECTION_TYPE_SERIAL:
152
216
  # Serial connection
153
- serial_port = config["meshtastic"]["serial_port"]
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 == "ble":
239
+ elif connection_type == CONNECTION_TYPE_BLE:
170
240
  # BLE connection
171
- ble_address = config["meshtastic"].get("ble_address")
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 == "tcp":
256
+ elif connection_type == CONNECTION_TYPE_TCP:
187
257
  # TCP connection
188
- target_host = config["meshtastic"]["host"]
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
- logger.info(
202
- f"Connected to {nodeInfo['user']['shortName']} / {nodeInfo['user']['hwModel']}"
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
- serial.SerialException,
221
- BleakDBusError,
222
- BleakError,
223
- Exception,
224
- ) as e:
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 * 2, 30
232
- ) # Exponential backoff capped at 30s
310
+ 2**attempts, 60
311
+ ) # Consistent exponential backoff, capped at 60s
233
312
  logger.warning(
234
- f"Attempt #{attempts - 1} failed. Retrying in {wait_time} secs: {e}"
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"Could not connect after {retry_limit} attempts: {e}")
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
- Handles loss of Meshtastic connection by initiating a reconnection sequence unless the system is shutting down or already reconnecting.
341
+ Initiate a reconnection sequence when the Meshtastic connection is lost, unless a shutdown or reconnection is already in progress.
247
342
 
248
- Args:
249
- interface: The Meshtastic interface (optional, for compatibility)
250
- detection_source: Source that detected the connection loss (for debugging)
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 == 9:
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 using exponential backoff, stopping if a shutdown is initiated.
379
+ Asynchronously attempts to reconnect to the Meshtastic device with exponential backoff, stopping if shutdown is initiated.
285
380
 
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.
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 = 10
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
- Process incoming Meshtastic messages and relay them to Matrix rooms or plugins according to message type and configuration.
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 or disabled detection sensors are ignored. Ensures sender information is retrieved or stored as needed.
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
- if packet.get("decoded", {}).get("text"):
351
- logger.info(
352
- f"Received Meshtastic message: {packet.get('decoded', {}).get('text')}"
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") == "TEXT_MESSAGE_APP":
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") == 1
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"] == 1
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["meshtastic"]["meshnet_name"]
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
- # Ensure that meshnet_name is always included, using our own meshnet for accuracy.
436
- full_display_name = f"{longname}/{meshnet_name}"
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 = f'\n [{full_display_name}] reacted {reaction_symbol} to "{abbreviated_text}"'
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
- # Format the reply message for Matrix
473
- full_display_name = f"{longname}/{meshnet_name}"
474
- formatted_message = f"[{full_display_name}]: {text}"
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") == "TEXT_MESSAGE_APP"
506
- or decoded.get("portnum") == 1
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 = 0
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") == "DETECTION_SENSOR_APP" and not config[
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
- formatted_message = f"[{longname}/{meshnet_name}]: {text}"
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
- result = asyncio.run_coroutine_threadsafe(
576
- plugin.handle_meshtastic_message(
577
- packet, formatted_message, longname, meshnet_name
578
- ),
579
- loop=loop,
580
- )
581
- found_matching_plugin = result.result()
582
- if found_matching_plugin:
583
- logger.debug(f"Processed by plugin {plugin.plugin_name}")
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
- result = asyncio.run_coroutine_threadsafe(
633
- plugin.handle_meshtastic_message(
634
- packet,
635
- formatted_message=None,
636
- longname=None,
637
- meshnet_name=None,
638
- ),
639
- loop=loop,
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 connection is lost.
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
- Configuration:
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["meshtastic"]["connection_type"]
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 == "ble":
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
- Send a Meshtastic text message as a reply to a specific previous message.
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
- return interface._sendPacket(
772
- mesh_packet, destinationId=destinationId, wantAck=wantAck
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__":