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 +1 -1
- mmrelay/log_utils.py +56 -0
- mmrelay/main.py +11 -12
- mmrelay/matrix_utils.py +82 -30
- mmrelay/meshtastic_utils.py +116 -43
- mmrelay/tools/sample-docker-compose.yaml +39 -0
- mmrelay/tools/sample.env +10 -0
- mmrelay/tools/sample_config.yaml +3 -3
- {mmrelay-1.0.11.dist-info → mmrelay-1.1.1.dist-info}/METADATA +48 -23
- {mmrelay-1.0.11.dist-info → mmrelay-1.1.1.dist-info}/RECORD +14 -12
- {mmrelay-1.0.11.dist-info → mmrelay-1.1.1.dist-info}/WHEEL +0 -0
- {mmrelay-1.0.11.dist-info → mmrelay-1.1.1.dist-info}/entry_points.txt +0 -0
- {mmrelay-1.0.11.dist-info → mmrelay-1.1.1.dist-info}/licenses/LICENSE +0 -0
- {mmrelay-1.0.11.dist-info → mmrelay-1.1.1.dist-info}/top_level.txt +0 -0
mmrelay/__init__.py
CHANGED
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
#
|
|
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
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
-
|
|
770
|
-
|
|
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
|
-
|
|
833
|
-
|
|
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
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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)
|
mmrelay/meshtastic_utils.py
CHANGED
|
@@ -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
|
-
|
|
55
|
-
|
|
59
|
+
|
|
56
60
|
Returns:
|
|
57
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
Retries until successful or
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
234
|
-
|
|
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
|
|
269
|
-
|
|
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
|
|
349
|
+
def on_established_meshtastic_connection(interface=None):
|
|
325
350
|
"""
|
|
326
|
-
|
|
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
|
-
|
|
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
|
|
634
|
-
|
|
635
|
-
|
|
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
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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
|
mmrelay/tools/sample.env
ADDED
|
@@ -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
|
mmrelay/tools/sample_config.yaml
CHANGED
|
@@ -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:
|
|
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.
|
|
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.
|
|
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
|
-
|
|
90
|
+
MMRelay includes official Docker support for easy deployment and management:
|
|
67
91
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|

|
|
86
111
|
|
|
87
|
-
The latest installer is available [
|
|
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
|

|
|
104
129
|
|
|
105
|
-
See the full list of 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:
|
|
151
|
-
- Part of the Meshtastic Community Matrix space: [#
|
|
152
|
-
- Public Relay Room: [#relay-room:
|
|
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=
|
|
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=
|
|
7
|
-
mmrelay/main.py,sha256=
|
|
8
|
-
mmrelay/matrix_utils.py,sha256=
|
|
9
|
-
mmrelay/meshtastic_utils.py,sha256=
|
|
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/
|
|
27
|
-
mmrelay
|
|
28
|
-
mmrelay
|
|
29
|
-
mmrelay-1.
|
|
30
|
-
mmrelay-1.
|
|
31
|
-
mmrelay-1.
|
|
32
|
-
mmrelay-1.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|