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.

@@ -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
- Processes incoming Meshtastic messages and relays them to Matrix rooms or plugins based on 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
@@ -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") == "TEXT_MESSAGE_APP"
514
- 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
515
615
  ):
516
- channel = 0
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") == "DETECTION_SENSOR_APP" and not config[
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
- result = asyncio.run_coroutine_threadsafe(
589
- plugin.handle_meshtastic_message(
590
- packet, formatted_message, longname, meshnet_name
591
- ),
592
- loop=loop,
593
- )
594
- found_matching_plugin = result.result()
595
- if found_matching_plugin:
596
- 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
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
- result = asyncio.run_coroutine_threadsafe(
646
- plugin.handle_meshtastic_message(
647
- packet,
648
- formatted_message=None,
649
- longname=None,
650
- meshnet_name=None,
651
- ),
652
- loop=loop,
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 connection is lost.
769
+ Periodically checks the health of the Meshtastic connection and triggers reconnection if the device becomes unresponsive.
664
770
 
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)
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["meshtastic"]["connection_type"]
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 == "ble":
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
- Send a Meshtastic text message as a reply to a specific previous message.
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
- return interface._sendPacket(
785
- mesh_packet, destinationId=destinationId, wantAck=wantAck
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
- Starts the message queue processor with the specified minimum delay between messages.
64
+ Start the message queue processor with a specified minimum delay between messages.
65
65
 
66
- Enforces a minimum delay of 2.0 seconds due to firmware requirements. If the event loop is running, the processor task is started immediately; otherwise, startup is deferred until the event loop becomes available.
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 < 2.0:
73
+ if message_delay < MINIMUM_MESSAGE_DELAY:
74
74
  logger.warning(
75
- f"Message delay {message_delay}s below firmware minimum (2.0s), using 2.0s"
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 = 2.0
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
- Stores and prunes message mapping information after a message is sent.
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): Dictionary containing mapping details such as `matrix_event_id`, `room_id`, `text`, and optional `meshnet` and `msgs_to_keep`.
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
- This method updates the message mapping database with the new mapping and prunes old mappings if configured.
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", 500)
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