mmrelay 1.0.11__py3-none-any.whl → 1.1.1__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 CHANGED
@@ -14,4 +14,4 @@ else:
14
14
  __version__ = version("mmrelay")
15
15
  except PackageNotFoundError:
16
16
  # If all else fails, use hardcoded version
17
- __version__ = "1.0.11"
17
+ __version__ = "1.1.1"
mmrelay/log_utils.py CHANGED
@@ -30,6 +30,17 @@ log_file_path = None
30
30
 
31
31
 
32
32
  def get_logger(name):
33
+ """
34
+ Create and configure a logger with console and optional file output, supporting colorized output and log rotation.
35
+
36
+ The logger's level, color usage, and file logging behavior are determined by global configuration and command line arguments. Console output uses rich formatting if enabled. File logging supports log rotation and stores logs in a configurable or default location. The log file path is stored globally if the logger name is "M<>M Relay".
37
+
38
+ Parameters:
39
+ name (str): The name of the logger to create.
40
+
41
+ Returns:
42
+ logging.Logger: The configured logger instance.
43
+ """
33
44
  logger = logging.getLogger(name=name)
34
45
 
35
46
  # Default to INFO level if config is not available
@@ -134,3 +145,48 @@ def get_logger(name):
134
145
  logger.addHandler(file_handler)
135
146
 
136
147
  return logger
148
+
149
+
150
+ def setup_upstream_logging_capture():
151
+ """
152
+ Redirects warning and error log messages from upstream libraries and the root logger into the application's formatted logging system.
153
+
154
+ This ensures that log output from external dependencies (such as "meshtastic", "bleak", and "asyncio") appears with consistent formatting alongside the application's own logs. Only messages at WARNING level or higher are captured, and messages originating from the application's own loggers are excluded to prevent recursion.
155
+ """
156
+ # Get our main logger
157
+ main_logger = get_logger("Upstream")
158
+
159
+ # Create a custom handler that redirects root logger messages
160
+ class UpstreamLogHandler(logging.Handler):
161
+ def emit(self, record):
162
+ # Skip if this is already from our logger to avoid recursion
163
+ """
164
+ Redirects log records from external sources to the main logger, mapping their severity and prefixing with the original logger name.
165
+
166
+ Skips records originating from the application's own loggers to prevent recursion.
167
+ """
168
+ if record.name.startswith("mmrelay") or record.name == "Upstream":
169
+ return
170
+
171
+ # Map the log level and emit through our logger
172
+ if record.levelno >= logging.ERROR:
173
+ main_logger.error(f"[{record.name}] {record.getMessage()}")
174
+ elif record.levelno >= logging.WARNING:
175
+ main_logger.warning(f"[{record.name}] {record.getMessage()}")
176
+ elif record.levelno >= logging.INFO:
177
+ main_logger.info(f"[{record.name}] {record.getMessage()}")
178
+ else:
179
+ main_logger.debug(f"[{record.name}] {record.getMessage()}")
180
+
181
+ # Add our handler to the root logger
182
+ root_logger = logging.getLogger()
183
+ upstream_handler = UpstreamLogHandler()
184
+ upstream_handler.setLevel(logging.WARNING) # Only capture warnings and errors
185
+ root_logger.addHandler(upstream_handler)
186
+
187
+ # Also set up specific loggers for known upstream libraries
188
+ for logger_name in ["meshtastic", "bleak", "asyncio"]:
189
+ upstream_logger = logging.getLogger(logger_name)
190
+ upstream_logger.addHandler(upstream_handler)
191
+ upstream_logger.setLevel(logging.WARNING)
192
+ upstream_logger.propagate = False # Prevent duplicate messages via root logger
mmrelay/main.py CHANGED
@@ -20,7 +20,7 @@ from mmrelay.db_utils import (
20
20
  update_shortnames,
21
21
  wipe_message_map,
22
22
  )
23
- from mmrelay.log_utils import get_logger
23
+ from mmrelay.log_utils import get_logger, setup_upstream_logging_capture
24
24
  from mmrelay.matrix_utils import connect_matrix, join_matrix_room
25
25
  from mmrelay.matrix_utils import logger as matrix_logger
26
26
  from mmrelay.matrix_utils import on_room_member, on_room_message
@@ -50,13 +50,12 @@ def print_banner():
50
50
 
51
51
  async def main(config):
52
52
  """
53
- Main asynchronous function to set up and run the relay.
54
- Includes logic for wiping the message_map if configured in database.msg_map.wipe_on_restart
55
- or db.msg_map.wipe_on_restart (legacy format).
56
- Also updates longnames and shortnames periodically as before.
53
+ Sets up and runs the asynchronous relay between Meshtastic and Matrix, managing connections, event handling, and graceful shutdown.
57
54
 
58
- Args:
59
- config: The loaded configuration
55
+ This function initializes the database, configures logging, loads plugins, connects to both Meshtastic and Matrix, joins specified Matrix rooms, and registers event callbacks for message and membership events. It periodically updates node names from the Meshtastic network and manages the Matrix sync loop, handling reconnections and shutdown signals. If configured, it wipes the message map on startup and shutdown.
56
+
57
+ Parameters:
58
+ config: The loaded configuration dictionary containing Matrix, Meshtastic, and database settings.
60
59
  """
61
60
  # Extract Matrix configuration
62
61
  from typing import List
@@ -69,6 +68,9 @@ async def main(config):
69
68
  # Initialize the SQLite database
70
69
  initialize_database()
71
70
 
71
+ # Set up upstream logging capture to format library messages consistently
72
+ setup_upstream_logging_capture()
73
+
72
74
  # Check database config for wipe_on_restart (preferred format)
73
75
  database_config = config.get("database", {})
74
76
  msg_map_config = database_config.get("msg_map", {})
@@ -131,11 +133,8 @@ async def main(config):
131
133
  # On Windows, we can't use add_signal_handler, so we'll handle KeyboardInterrupt
132
134
  pass
133
135
 
134
- # -------------------------------------------------------------------
135
- # IMPORTANT: We create a task to run the meshtastic_utils.check_connection()
136
- # so its while loop runs in parallel with the matrix sync loop
137
- # Use "_" to avoid trunk's "assigned but unused variable" warning
138
- # -------------------------------------------------------------------
136
+ # Start connection health monitoring using getMetadata() heartbeat
137
+ # This provides proactive connection detection for all interface types
139
138
  _ = asyncio.create_task(meshtastic_utils.check_connection())
140
139
 
141
140
  # Start the Matrix client sync loop
mmrelay/matrix_utils.py CHANGED
@@ -207,9 +207,9 @@ async def join_matrix_room(matrix_client, room_id_or_alias: str) -> None:
207
207
  if room_id_or_alias.startswith("#"):
208
208
  # If it's a room alias, resolve it to a room ID
209
209
  response = await matrix_client.room_resolve_alias(room_id_or_alias)
210
- if not response.room_id:
210
+ if not hasattr(response, "room_id") or not response.room_id:
211
211
  logger.error(
212
- f"Failed to resolve room alias '{room_id_or_alias}': {response.message}"
212
+ f"Failed to resolve room alias '{room_id_or_alias}': {getattr(response, 'message', str(response))}"
213
213
  )
214
214
  return
215
215
  room_id = response.room_id
@@ -510,23 +510,42 @@ async def send_reply_to_meshtastic(
510
510
  try:
511
511
  if reply_id is not None:
512
512
  # Send as a structured reply using our custom function
513
- sent_packet = sendTextReply(
514
- meshtastic_interface,
515
- text=reply_message,
516
- reply_id=reply_id,
517
- channelIndex=meshtastic_channel,
518
- )
519
- meshtastic_logger.info(
520
- f"Relaying Matrix reply from {full_display_name} to radio broadcast as structured reply to message {reply_id}"
521
- )
513
+ try:
514
+ sent_packet = sendTextReply(
515
+ meshtastic_interface,
516
+ text=reply_message,
517
+ reply_id=reply_id,
518
+ channelIndex=meshtastic_channel,
519
+ )
520
+ meshtastic_logger.info(
521
+ f"Relaying Matrix reply from {full_display_name} to radio broadcast as structured reply to message {reply_id}"
522
+ )
523
+ meshtastic_logger.debug(
524
+ f"sendTextReply returned packet: {sent_packet}"
525
+ )
526
+ except Exception as e:
527
+ meshtastic_logger.error(
528
+ f"Error sending structured reply to Meshtastic: {e}"
529
+ )
530
+ return
522
531
  else:
523
532
  # Send as regular message (fallback for when no reply_id is available)
524
- sent_packet = meshtastic_interface.sendText(
525
- text=reply_message, channelIndex=meshtastic_channel
526
- )
527
- meshtastic_logger.info(
528
- f"Relaying Matrix reply from {full_display_name} to radio broadcast as regular message"
529
- )
533
+ try:
534
+ meshtastic_logger.debug(
535
+ f"Attempting to send text to Meshtastic: '{reply_message}' on channel {meshtastic_channel}"
536
+ )
537
+ sent_packet = meshtastic_interface.sendText(
538
+ text=reply_message, channelIndex=meshtastic_channel
539
+ )
540
+ meshtastic_logger.info(
541
+ f"Relaying Matrix reply from {full_display_name} to radio broadcast as regular message"
542
+ )
543
+ meshtastic_logger.debug(f"sendText returned packet: {sent_packet}")
544
+ except Exception as e:
545
+ meshtastic_logger.error(
546
+ f"Error sending reply message to Meshtastic: {e}"
547
+ )
548
+ return
530
549
 
531
550
  # Store the reply in message map if storage is enabled
532
551
  if storage_enabled and sent_packet and hasattr(sent_packet, "id"):
@@ -766,9 +785,16 @@ async def on_room_message(
766
785
  logger.debug(
767
786
  f"Sending reaction to Meshtastic with meshnet={local_meshnet_name}: {reaction_message}"
768
787
  )
769
- meshtastic_interface.sendText(
770
- text=reaction_message, channelIndex=meshtastic_channel
771
- )
788
+ try:
789
+ sent_packet = meshtastic_interface.sendText(
790
+ text=reaction_message, channelIndex=meshtastic_channel
791
+ )
792
+ logger.debug(
793
+ f"Remote reaction sendText returned packet: {sent_packet}"
794
+ )
795
+ except Exception as e:
796
+ logger.error(f"Error sending remote reaction to Meshtastic: {e}")
797
+ return
772
798
  # We've relayed the remote reaction to our local mesh, so we're done.
773
799
  return
774
800
 
@@ -829,9 +855,16 @@ async def on_room_message(
829
855
  logger.debug(
830
856
  f"Sending reaction to Meshtastic with meshnet={local_meshnet_name}: {reaction_message}"
831
857
  )
832
- meshtastic_interface.sendText(
833
- text=reaction_message, channelIndex=meshtastic_channel
834
- )
858
+ try:
859
+ sent_packet = meshtastic_interface.sendText(
860
+ text=reaction_message, channelIndex=meshtastic_channel
861
+ )
862
+ logger.debug(
863
+ f"Local reaction sendText returned packet: {sent_packet}"
864
+ )
865
+ except Exception as e:
866
+ logger.error(f"Error sending local reaction to Meshtastic: {e}")
867
+ return
835
868
  return
836
869
 
837
870
  # Handle Matrix replies to Meshtastic messages (only if replies are enabled)
@@ -938,13 +971,25 @@ async def on_room_message(
938
971
  if portnum == "DETECTION_SENSOR_APP":
939
972
  # If detection_sensor is enabled, forward this data as detection sensor data
940
973
  if config["meshtastic"].get("detection_sensor", False):
941
- sent_packet = meshtastic_interface.sendData(
942
- data=full_message.encode("utf-8"),
943
- channelIndex=meshtastic_channel,
944
- portNum=meshtastic.protobuf.portnums_pb2.PortNum.DETECTION_SENSOR_APP,
945
- )
946
- # Note: Detection sensor messages are not stored in message_map because they are never replied to
947
- # Only TEXT_MESSAGE_APP messages need to be stored for reaction handling
974
+ try:
975
+ meshtastic_logger.debug(
976
+ f"Attempting to send detection sensor data to Meshtastic: '{full_message}' on channel {meshtastic_channel}"
977
+ )
978
+ sent_packet = meshtastic_interface.sendData(
979
+ data=full_message.encode("utf-8"),
980
+ channelIndex=meshtastic_channel,
981
+ portNum=meshtastic.protobuf.portnums_pb2.PortNum.DETECTION_SENSOR_APP,
982
+ )
983
+ meshtastic_logger.debug(
984
+ f"sendData returned packet: {sent_packet}"
985
+ )
986
+ # Note: Detection sensor messages are not stored in message_map because they are never replied to
987
+ # Only TEXT_MESSAGE_APP messages need to be stored for reaction handling
988
+ except Exception as e:
989
+ meshtastic_logger.error(
990
+ f"Error sending detection sensor data to Meshtastic: {e}"
991
+ )
992
+ return
948
993
  else:
949
994
  meshtastic_logger.debug(
950
995
  f"Detection sensor packet received from {full_display_name}, but detection sensor processing is disabled."
@@ -958,8 +1003,15 @@ async def on_room_message(
958
1003
  sent_packet = meshtastic_interface.sendText(
959
1004
  text=full_message, channelIndex=meshtastic_channel
960
1005
  )
1006
+ if not sent_packet:
1007
+ meshtastic_logger.warning(
1008
+ "sendText returned None - message may not have been sent"
1009
+ )
961
1010
  except Exception as e:
962
1011
  meshtastic_logger.error(f"Error sending message to Meshtastic: {e}")
1012
+ import traceback
1013
+
1014
+ meshtastic_logger.error(f"Full traceback: {traceback.format_exc()}")
963
1015
  return
964
1016
  # Store message_map only if storage is enabled and only for TEXT_MESSAGE_APP
965
1017
  # (these are the only messages that can be replied to and thus need reaction handling)
@@ -47,14 +47,18 @@ 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
51
+ subscribed_to_messages = False
52
+ subscribed_to_connection_lost = False
53
+ subscribed_to_connection_established = False
54
+
50
55
 
51
56
  def is_running_as_service():
52
57
  """
53
58
  Check if the application is running as a systemd service.
54
- This is used to determine whether to show Rich progress indicators.
55
-
59
+
56
60
  Returns:
57
- bool: True if running as a service, False otherwise
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.
58
62
  """
59
63
  # Check for INVOCATION_ID environment variable (set by systemd)
60
64
  if os.environ.get("INVOCATION_ID"):
@@ -85,14 +89,16 @@ def serial_port_exists(port_name):
85
89
 
86
90
  def connect_meshtastic(passed_config=None, force_connect=False):
87
91
  """
88
- Establish a connection to the Meshtastic device.
89
- Attempts a connection based on connection_type (serial/ble/network).
90
- Retries until successful or shutting_down is set.
91
- If already connected and not force_connect, returns the existing client.
92
-
93
- Args:
94
- passed_config: The configuration dictionary to use (will update global config)
95
- force_connect: Whether to force a new connection even if one exists
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
+
96
+ Parameters:
97
+ passed_config (dict, optional): Configuration dictionary to use for the connection. If provided, updates the global configuration.
98
+ force_connect (bool, optional): If True, forces a new connection even if one already exists.
99
+
100
+ Returns:
101
+ Meshtastic interface client if the connection is successful, or None if the connection fails or shutdown is in progress.
96
102
  """
97
103
  global meshtastic_client, shutting_down, config, matrix_rooms
98
104
  if shutting_down:
@@ -197,11 +203,26 @@ def connect_meshtastic(passed_config=None, force_connect=False):
197
203
  f"Connected to {nodeInfo['user']['shortName']} / {nodeInfo['user']['hwModel']}"
198
204
  )
199
205
 
200
- # Subscribe to message and connection lost events
201
- pub.subscribe(on_meshtastic_message, "meshtastic.receive")
202
- pub.subscribe(
203
- on_lost_meshtastic_connection, "meshtastic.connection.lost"
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
208
+ if not subscribed_to_messages:
209
+ pub.subscribe(on_meshtastic_message, "meshtastic.receive")
210
+ subscribed_to_messages = True
211
+ logger.debug("Subscribed to meshtastic.receive")
212
+
213
+ if not subscribed_to_connection_lost:
214
+ pub.subscribe(
215
+ on_lost_meshtastic_connection, "meshtastic.connection.lost"
216
+ )
217
+ subscribed_to_connection_lost = True
218
+ logger.debug("Subscribed to meshtastic.connection.lost")
219
+
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")
205
226
 
206
227
  except (
207
228
  serial.SerialException,
@@ -228,10 +249,13 @@ def connect_meshtastic(passed_config=None, force_connect=False):
228
249
  return meshtastic_client
229
250
 
230
251
 
231
- def on_lost_meshtastic_connection(interface=None):
252
+ def on_lost_meshtastic_connection(interface=None, detection_source="detected by library"):
232
253
  """
233
- Callback invoked when the Meshtastic connection is lost.
234
- Initiates a reconnect sequence unless shutting_down is True.
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.
235
259
  """
236
260
  global meshtastic_client, reconnecting, shutting_down, event_loop, reconnect_task
237
261
  with meshtastic_lock:
@@ -244,7 +268,7 @@ def on_lost_meshtastic_connection(interface=None):
244
268
  )
245
269
  return
246
270
  reconnecting = True
247
- logger.error("Lost connection. Reconnecting...")
271
+ logger.error(f"Lost connection ({detection_source}). Reconnecting...")
248
272
 
249
273
  if meshtastic_client:
250
274
  try:
@@ -265,8 +289,9 @@ def on_lost_meshtastic_connection(interface=None):
265
289
 
266
290
  async def reconnect():
267
291
  """
268
- Asynchronously attempts to reconnect with exponential backoff.
269
- Stops if shutting_down is set.
292
+ 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.
270
295
  """
271
296
  global meshtastic_client, reconnecting, shutting_down
272
297
  backoff_time = 10
@@ -321,11 +346,23 @@ async def reconnect():
321
346
  reconnecting = False
322
347
 
323
348
 
324
- def on_meshtastic_message(packet, interface):
349
+ def on_established_meshtastic_connection(interface=None):
325
350
  """
326
- Processes incoming Meshtastic messages and relays them to Matrix rooms or plugins based on message type and interaction settings.
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
327
359
 
328
- Handles reactions and replies by relaying them to Matrix if enabled in the interaction settings. 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. Filters out messages from unmapped channels or disabled detection sensors, and ensures sender information is retrieved or stored as needed.
360
+
361
+ def on_meshtastic_message(packet, interface):
362
+ """
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.
329
366
  """
330
367
  global config, matrix_rooms
331
368
 
@@ -630,35 +667,71 @@ def on_meshtastic_message(packet, interface):
630
667
 
631
668
  async def check_connection():
632
669
  """
633
- Periodically checks the Meshtastic connection by calling localNode.getMetadata().
634
- If it fails or doesn't return the firmware version, we assume the connection is lost
635
- and attempt to reconnect.
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.
636
675
  """
637
- global meshtastic_client, shutting_down, config
676
+ global meshtastic_client, shutting_down, config, reconnecting
638
677
 
639
678
  # Check if config is available
640
679
  if config is None:
641
680
  logger.error("No configuration available. Cannot check connection.")
642
681
  return
643
682
 
683
+ # Get heartbeat interval from config, default to 180 seconds
684
+ heartbeat_interval = config["meshtastic"].get("heartbeat_interval", 180)
644
685
  connection_type = config["meshtastic"]["connection_type"]
686
+
687
+ logger.info(
688
+ f"Starting connection heartbeat monitor (interval: {heartbeat_interval}s)"
689
+ )
690
+
691
+ # Track if we've logged the BLE skip message to avoid spam
692
+ ble_skip_logged = False
693
+
645
694
  while not shutting_down:
646
- if meshtastic_client:
647
- try:
648
- output_capture = io.StringIO()
649
- with contextlib.redirect_stdout(
650
- output_capture
651
- ), contextlib.redirect_stderr(output_capture):
652
- meshtastic_client.localNode.getMetadata()
695
+ if meshtastic_client and not reconnecting:
696
+ # BLE has real-time disconnection detection in the library
697
+ # Skip periodic health checks to avoid duplicate reconnection attempts
698
+ if connection_type == "ble":
699
+ if not ble_skip_logged:
700
+ logger.info("BLE connection uses real-time disconnection detection - health checks disabled")
701
+ ble_skip_logged = True
702
+ else:
703
+ try:
704
+ logger.debug(
705
+ f"Checking {connection_type} connection health using getMetadata()"
706
+ )
707
+ output_capture = io.StringIO()
708
+ with contextlib.redirect_stdout(
709
+ output_capture
710
+ ), contextlib.redirect_stderr(output_capture):
711
+ meshtastic_client.localNode.getMetadata()
653
712
 
654
- console_output = output_capture.getvalue()
655
- if "firmware_version" not in console_output:
656
- raise Exception("No firmware_version in getMetadata output.")
713
+ console_output = output_capture.getvalue()
714
+ if "firmware_version" not in console_output:
715
+ raise Exception("No firmware_version in getMetadata output.")
657
716
 
658
- except Exception as e:
659
- logger.error(f"{connection_type.capitalize()} connection lost: {e}")
660
- on_lost_meshtastic_connection(meshtastic_client)
661
- await asyncio.sleep(30) # Check connection every 30 seconds
717
+ logger.debug(f"{connection_type.capitalize()} connection healthy")
718
+
719
+ except Exception as e:
720
+ # Only trigger reconnection if we're not already reconnecting
721
+ if not reconnecting:
722
+ logger.warning(
723
+ f"{connection_type.capitalize()} connection health check failed: {e}"
724
+ )
725
+ # Use existing handler with health check reason
726
+ on_lost_meshtastic_connection(detection_source=f"health check failed: {str(e)}")
727
+ else:
728
+ logger.debug("Skipping reconnection trigger - already reconnecting")
729
+ elif reconnecting:
730
+ logger.debug("Skipping connection check - reconnection in progress")
731
+ elif not meshtastic_client:
732
+ logger.debug("Skipping connection check - no client available")
733
+
734
+ await asyncio.sleep(heartbeat_interval)
662
735
 
663
736
 
664
737
  def sendTextReply(
@@ -0,0 +1,39 @@
1
+ services:
2
+ mmrelay:
3
+ build: .
4
+ container_name: meshtastic-matrix-relay
5
+ restart: unless-stopped
6
+ user: "${UID:-1000}:${GID:-1000}"
7
+
8
+ environment:
9
+ - TZ=UTC
10
+ - PYTHONUNBUFFERED=1
11
+ - MPLCONFIGDIR=/tmp/matplotlib
12
+
13
+ volumes:
14
+ # Configuration - uses standard ~/.mmrelay/config.yaml location
15
+ # Create this first with: make config
16
+ - ${MMRELAY_HOME}/.mmrelay/config.yaml:/app/config.yaml:ro
17
+
18
+ # Data and logs - same locations as standalone installation
19
+ # These directories will be created automatically
20
+ - ${MMRELAY_HOME}/.mmrelay/data:/app/data
21
+ - ${MMRELAY_HOME}/.mmrelay/logs:/app/logs
22
+
23
+ # For TCP connections (most common) - Meshtastic typically uses port 4403
24
+ ports:
25
+ - 4403:4403
26
+
27
+ # For serial connections, uncomment the device you need:
28
+ # devices:
29
+ # - /dev/ttyUSB0:/dev/ttyUSB0
30
+ # - /dev/ttyACM0:/dev/ttyACM0
31
+
32
+ # For BLE connections, uncomment these:
33
+ # privileged: true
34
+ # network_mode: host
35
+ # Additional volumes for BLE (add to existing volumes section above):
36
+ # - /var/run/dbus:/var/run/dbus:ro
37
+ # - /sys/bus/usb:/sys/bus/usb:ro
38
+ # - /sys/class/bluetooth:/sys/class/bluetooth:ro
39
+ # - /sys/devices:/sys/devices:ro
@@ -0,0 +1,10 @@
1
+ # Docker Compose environment variables
2
+ # Customize these paths if needed
3
+
4
+ # Base directory for mmrelay data
5
+ # This will be expanded by your shell when docker compose runs
6
+ MMRELAY_HOME=$HOME
7
+
8
+ # Preferred editor for config editing
9
+ # Will be set automatically when you select an editor via 'make edit'
10
+ EDITOR=nano
@@ -10,18 +10,18 @@ matrix_rooms: # Needs at least 1 room & channel, but supports all Meshtastic cha
10
10
  meshtastic_channel: 2
11
11
 
12
12
  meshtastic:
13
- connection_type: serial # Choose either "tcp", "serial", or "ble"
14
- serial_port: /dev/ttyUSB0 # Only used when connection is "serial"
13
+ connection_type: tcp # Choose either "tcp", "serial", or "ble"
15
14
  host: meshtastic.local # Only used when connection is "tcp"
15
+ serial_port: /dev/ttyUSB0 # Only used when connection is "serial"
16
16
  ble_address: AA:BB:CC:DD:EE:FF # Only used when connection is "ble" - Uses either an address or name from a `meshtastic --ble-scan`
17
17
  meshnet_name: Your Meshnet Name # This is displayed in full on Matrix, but is truncated when sent to a Meshnet
18
18
  broadcast_enabled: true # Must be set to true to enable Matrix to Meshtastic messages
19
19
  detection_sensor: true # Must be set to true to forward messages of Meshtastic's detection sensor module
20
20
  plugin_response_delay: 3 # Default response delay in seconds for plugins that respond on the mesh;
21
+ # heartbeat_interval: 180 # Interval in seconds to check connection health using getMetadata() (default: 180)
21
22
  message_interactions: # Configure reactions and replies (both require message storage in database)
22
23
  reactions: false # Enable reaction relaying between platforms
23
24
  replies: false # Enable reply relaying between platforms
24
- # Note: Legacy 'relay_reactions' setting is deprecated but still supported
25
25
 
26
26
  logging:
27
27
  level: info
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mmrelay
3
- Version: 1.0.11
3
+ Version: 1.1.1
4
4
  Summary: Bridge between Meshtastic mesh networks and Matrix chat rooms
5
5
  Home-page: https://github.com/geoffwhittington/meshtastic-matrix-relay
6
6
  Author: Geoff Whittington, Jeremiah K., and contributors
@@ -14,8 +14,8 @@ Classifier: Topic :: Communications
14
14
  Requires-Python: >=3.8
15
15
  Description-Content-Type: text/markdown
16
16
  License-File: LICENSE
17
- Requires-Dist: meshtastic
18
- Requires-Dist: Pillow==11.2.1
17
+ Requires-Dist: meshtastic>=2.6.4
18
+ Requires-Dist: Pillow==11.3.0
19
19
  Requires-Dist: matrix-nio==0.25.2
20
20
  Requires-Dist: matplotlib==3.10.1
21
21
  Requires-Dist: requests==2.32.4
@@ -26,7 +26,17 @@ Requires-Dist: platformdirs==4.3.8
26
26
  Requires-Dist: py-staticmaps>=0.4.0
27
27
  Requires-Dist: rich==14.0.0
28
28
  Requires-Dist: setuptools==80.9.0
29
+ Dynamic: author
30
+ Dynamic: author-email
31
+ Dynamic: classifier
32
+ Dynamic: description
33
+ Dynamic: description-content-type
34
+ Dynamic: home-page
29
35
  Dynamic: license-file
36
+ Dynamic: project-url
37
+ Dynamic: requires-dist
38
+ Dynamic: requires-python
39
+ Dynamic: summary
30
40
 
31
41
  # M<>M Relay
32
42
 
@@ -34,13 +44,27 @@ Dynamic: license-file
34
44
 
35
45
  A powerful and easy-to-use relay between Meshtastic devices and Matrix chat rooms, allowing seamless communication across platforms. This opens the door for bridging Meshtastic devices to [many other platforms](https://matrix.org/bridges/).
36
46
 
47
+ ## Features
48
+
49
+ - Bidirectional message relay between Meshtastic devices and Matrix chat rooms, capable of supporting multiple meshnets
50
+ - Supports serial, network, and **_BLE (now too!)_** connections for Meshtastic devices
51
+ - Custom fields are embedded in Matrix messages for relaying messages between multiple meshnets
52
+ - Truncates long messages to fit within Meshtastic's payload size
53
+ - SQLite database to store node information for improved functionality
54
+ - Customizable logging level for easy debugging
55
+ - Configurable through a simple YAML file
56
+ - Supports mapping multiple rooms and channels 1:1
57
+ - Relays messages to/from an MQTT broker, if configured in the Meshtastic firmware
58
+ - ✨️ _Bidirectional replies and reactions support_ ✨️ **NEW!!**
59
+ - ✨️ _Native Docker support_ ✨️ **NEW!!**
60
+
61
+ _We would love to support [Matrix E2EE rooms](https://github.com/geoffwhittington/meshtastic-matrix-relay/issues/33), but this is currently not implemented._
62
+
37
63
  ## Documentation
38
64
 
39
65
  Visit our [Wiki](https://github.com/geoffwhittington/meshtastic-matrix-relay/wiki) for comprehensive guides and information.
40
66
 
41
67
  - [Installation Instructions](docs/INSTRUCTIONS.md) - Setup and configuration guide
42
- - [v1.0 Release Announcement](docs/ANNOUNCEMENT.md) - New changes in v1.0
43
- - [Upgrade Guide](docs/UPGRADE_TO_V1.md) - Migration guidance for existing users
44
68
 
45
69
  ---
46
70
 
@@ -61,22 +85,23 @@ mmrelay --install-service
61
85
 
62
86
  For detailed installation and configuration instructions, see the [Installation Guide](docs/INSTRUCTIONS.md).
63
87
 
64
- ---
88
+ ## Docker
65
89
 
66
- ## Features
90
+ MMRelay includes official Docker support for easy deployment and management:
67
91
 
68
- - Bidirectional message relay between Meshtastic devices and Matrix chat rooms, capable of supporting multiple meshnets
69
- - Supports serial, network, and **_BLE (now too!)_** connections for Meshtastic devices
70
- - Custom fields are embedded in Matrix messages for relaying messages between multiple meshnets
71
- - Truncates long messages to fit within Meshtastic's payload size
72
- - SQLite database to store node information for improved functionality
73
- - Customizable logging level for easy debugging
74
- - Configurable through a simple YAML file
75
- - Supports mapping multiple rooms and channels 1:1
76
- - Relays messages to/from an MQTT broker, if configured in the Meshtastic firmware
77
- - ✨️ _Cross-platform reactions support_ ✨️ **NEW!!**
92
+ ```bash
93
+ # Quick setup with Docker
94
+ make setup # Copy config and open editor (first time)
95
+ make build # Build the Docker image
96
+ make run # Start the container
97
+ make logs # View logs
98
+ ```
78
99
 
79
- _We would love to support [Matrix E2EE rooms](https://github.com/geoffwhittington/meshtastic-matrix-relay/issues/33), but this is currently not implemented._
100
+ Docker provides isolated environment, easy deployment, automatic restarts, and volume persistence.
101
+
102
+ For detailed Docker setup instructions, see the [Docker Guide](docs/DOCKER.md).
103
+
104
+ > **Note**: Docker builds currently use a temporary fork of the meshtastic library with BLE hanging fixes. PyPI releases use the upstream library. This will be resolved when the fixes are merged upstream.
80
105
 
81
106
  ---
82
107
 
@@ -84,7 +109,7 @@ _We would love to support [Matrix E2EE rooms](https://github.com/geoffwhittingto
84
109
 
85
110
  ![Windows Installer Screenshot](https://user-images.githubusercontent.com/1770544/235249050-8c79107a-50cc-4803-b989-39e58100342d.png)
86
111
 
87
- The latest installer is available [here](https://github.com/geoffwhittington/meshtastic-matrix-relay/releases).
112
+ The latest installer is available in the [releases section](https://github.com/geoffwhittington/meshtastic-matrix-relay/releases).
88
113
 
89
114
  ---
90
115
 
@@ -102,7 +127,7 @@ Produce high-level details about your mesh:
102
127
 
103
128
  ![Mesh Details Screenshot](https://user-images.githubusercontent.com/1770544/235245873-1ddc773b-a4cd-4c67-b0a5-b55a29504b73.png)
104
129
 
105
- See the full list of core plugins [here](https://github.com/geoffwhittington/meshtastic-matrix-relay/wiki/Core-Plugins).
130
+ See the full list of [core plugins](https://github.com/geoffwhittington/meshtastic-matrix-relay/wiki/Core-Plugins).
106
131
 
107
132
  ### Community & Custom Plugins
108
133
 
@@ -147,6 +172,6 @@ See our Wiki page [Getting Started With Matrix & MM Relay](https://github.com/ge
147
172
 
148
173
  Join us!
149
174
 
150
- - Our project's room: [#mmrelay:meshnet.club](https://matrix.to/#/#mmrelay:meshnet.club)
151
- - Part of the Meshtastic Community Matrix space: [#meshtastic-community:meshnet.club](https://matrix.to/#/#meshtastic-community:meshnet.club)
152
- - Public Relay Room: [#relay-room:meshnet.club](https://matrix.to/#/#relay-room:meshnet.club) - Where we bridge multiple meshnets. Feel free to join us, with or without a relay!
175
+ - Our project's room: [#mmrelay:matrix.org](https://matrix.to/#/#mmrelay:matrix.org)
176
+ - Part of the Meshtastic Community Matrix space: [#meshnetclub:matrix.org](https://matrix.to/#/#meshnetclub:matrix.org)
177
+ - Public Relay Room: [#mmrelay-relay-room:matrix.org](https://matrix.to/#/#mmrelay-relay-room:matrix.org) - Where we bridge multiple meshnets. Feel free to join us, with or without a relay!
@@ -1,12 +1,12 @@
1
- mmrelay/__init__.py,sha256=XeHKtvrDgq8NM54pZypZ-29wpSYfW2ZBq4vBqTw3FQM,595
1
+ mmrelay/__init__.py,sha256=v-xcK3JoA0m2eBmIudme82RPgp5PQInzRSs5mitHB28,594
2
2
  mmrelay/cli.py,sha256=hdPTlcGsXTJC9GEUiScG7b3IFp02B3lwhqgwFpU3NsM,13835
3
3
  mmrelay/config.py,sha256=5VZag8iSc5yLQgvwI76bbpizbtqag74cHnfXCrWHNyA,7910
4
4
  mmrelay/config_checker.py,sha256=UnoHVTXzfdTfFkbmXv9r_Si76v-sxXLb5FOaQSOM45E,4909
5
5
  mmrelay/db_utils.py,sha256=eTMMkYVWsmO_DkrBfnZMw4ohg_xa0S9TXJoBjRFTwzo,13590
6
- mmrelay/log_utils.py,sha256=FXhaq4WSDHwlqiG3k1BbSxiOl5be4P4Kyr1gsIThOBw,4572
7
- mmrelay/main.py,sha256=88hmJzv-Fw3CSjcMSlQWKjlZlIlZBjOujMfi85r5uCk,11301
8
- mmrelay/matrix_utils.py,sha256=dtmj6yFUax6xR5NTpbgK_6WYHIcg8saFhU05_tviN7A,43172
9
- mmrelay/meshtastic_utils.py,sha256=7r8FnZiwlmLZbj3PtVuMO2Plt1reYeGnclxFyMq7hBU,26953
6
+ mmrelay/log_utils.py,sha256=ot0GpYppyNuPOWY8d3EXdLPojoJYEqfmUjVlcYm8neY,7532
7
+ mmrelay/main.py,sha256=TL5xWFXIGwAQKau-hN4sRB0wxtNWzc629ry_qPbovv0,11585
8
+ mmrelay/matrix_utils.py,sha256=XSOHztRrdIjFGyYt8QhGBL6nMv_6iodeYL288pG9voA,45813
9
+ mmrelay/meshtastic_utils.py,sha256=MgCNGSufQDddncVKJVgmp_6DDZbcHSvOvW72vuNDLeg,31596
10
10
  mmrelay/plugin_loader.py,sha256=NRiXF6Ty1WD9jNXXKvzJh7kE0ba5oICXNVAfMaTPqH4,39247
11
11
  mmrelay/setup_utils.py,sha256=N6qdScHKHEMFKDmT1l7dcLDPNTusZXPkyxrLXjFLhRI,19910
12
12
  mmrelay/plugins/__init__.py,sha256=KVMQIXRhe0wlGj4O3IZ0vOIQRKFkfPYejHXhJL17qrc,51
@@ -23,10 +23,12 @@ mmrelay/plugins/telemetry_plugin.py,sha256=8SxWv4BLXMUTbiVaD3MjlMMdQyS7S_1OfLlVN
23
23
  mmrelay/plugins/weather_plugin.py,sha256=1bQhmiX-enNphzGoFVprU0LcZQX9BvGxWAJAG8Wekg0,8596
24
24
  mmrelay/tools/__init__.py,sha256=WFjDQjdevgg19_zT6iEoL29rvb1JPqYSd8708Jn5D7A,838
25
25
  mmrelay/tools/mmrelay.service,sha256=3vqK1VbfXvVftkTrTEOan77aTHeOT36hIAL7HqJsmTg,567
26
- mmrelay/tools/sample_config.yaml,sha256=HVEUYawNILwMukAnQL4Eh_2PlSvt2581eyfkxl8OxsA,3258
27
- mmrelay-1.0.11.dist-info/licenses/LICENSE,sha256=yceWauM1c0-FHxVplsD7W1-AbSeRaUNlmqT4UO1msBU,1073
28
- mmrelay-1.0.11.dist-info/METADATA,sha256=qWciy6Gpa4aa0pToMg4wDSgQUqM0HMZhhUaIA2fr8jM,5919
29
- mmrelay-1.0.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
30
- mmrelay-1.0.11.dist-info/entry_points.txt,sha256=SJZwGUOEpQ-qx4H8UL4xKFnKeInGUaZNW1I0ddjK7Ws,45
31
- mmrelay-1.0.11.dist-info/top_level.txt,sha256=B_ZLCRm7NYAmI3PipRUyHGymP-C-q16LSeMGzmqJfo4,8
32
- mmrelay-1.0.11.dist-info/RECORD,,
26
+ mmrelay/tools/sample-docker-compose.yaml,sha256=vVgJrh-6l48hkj5F-52JA5tpDWPBjiPQ36CE9Pkqn44,1251
27
+ mmrelay/tools/sample.env,sha256=RP-o3rX3jnEIrVG2rqCZq31O1yRXou4HcGrXWLVbKKw,311
28
+ mmrelay/tools/sample_config.yaml,sha256=0BKND0qbke8z9X9J9iHleu567dZt3RmHUxhZlQEEdFk,3290
29
+ mmrelay-1.1.1.dist-info/licenses/LICENSE,sha256=yceWauM1c0-FHxVplsD7W1-AbSeRaUNlmqT4UO1msBU,1073
30
+ mmrelay-1.1.1.dist-info/METADATA,sha256=ltSYRsnKQvdwsKd0CqAqMoSMWLsqZq8Pcpmfy_OjqI8,6713
31
+ mmrelay-1.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
32
+ mmrelay-1.1.1.dist-info/entry_points.txt,sha256=SJZwGUOEpQ-qx4H8UL4xKFnKeInGUaZNW1I0ddjK7Ws,45
33
+ mmrelay-1.1.1.dist-info/top_level.txt,sha256=B_ZLCRm7NYAmI3PipRUyHGymP-C-q16LSeMGzmqJfo4,8
34
+ mmrelay-1.1.1.dist-info/RECORD,,