mmrelay 1.0.11__py3-none-any.whl → 1.1.0__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 +57 -20
- mmrelay/tools/sample-docker-compose.yaml +32 -0
- mmrelay/tools/sample.env +10 -0
- mmrelay/tools/sample_config.yaml +3 -3
- {mmrelay-1.0.11.dist-info → mmrelay-1.1.0.dist-info}/METADATA +47 -23
- {mmrelay-1.0.11.dist-info → mmrelay-1.1.0.dist-info}/RECORD +14 -12
- {mmrelay-1.0.11.dist-info → mmrelay-1.1.0.dist-info}/WHEEL +0 -0
- {mmrelay-1.0.11.dist-info → mmrelay-1.1.0.dist-info}/entry_points.txt +0 -0
- {mmrelay-1.0.11.dist-info → mmrelay-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {mmrelay-1.0.11.dist-info → mmrelay-1.1.0.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,17 @@ reconnecting = False
|
|
|
47
47
|
shutting_down = False
|
|
48
48
|
reconnect_task = None # To keep track of the reconnect task
|
|
49
49
|
|
|
50
|
+
# Track pubsub subscription state to prevent duplicate subscriptions during reconnections
|
|
51
|
+
subscribed_to_messages = False
|
|
52
|
+
subscribed_to_connection_lost = False
|
|
53
|
+
|
|
50
54
|
|
|
51
55
|
def is_running_as_service():
|
|
52
56
|
"""
|
|
53
|
-
|
|
54
|
-
This is used to determine whether to show Rich progress indicators.
|
|
57
|
+
Determine if the application is running as a systemd service.
|
|
55
58
|
|
|
56
59
|
Returns:
|
|
57
|
-
bool: True if running
|
|
60
|
+
bool: True if running under systemd (either via environment variable or parent process), False otherwise.
|
|
58
61
|
"""
|
|
59
62
|
# Check for INVOCATION_ID environment variable (set by systemd)
|
|
60
63
|
if os.environ.get("INVOCATION_ID"):
|
|
@@ -85,14 +88,16 @@ def serial_port_exists(port_name):
|
|
|
85
88
|
|
|
86
89
|
def connect_meshtastic(passed_config=None, force_connect=False):
|
|
87
90
|
"""
|
|
88
|
-
|
|
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.
|
|
91
|
+
Establishes and manages a connection to a Meshtastic device using the configured connection type (serial, BLE, or TCP).
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
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 lost events only once per process to avoid duplicate subscriptions.
|
|
94
|
+
|
|
95
|
+
Parameters:
|
|
96
|
+
passed_config (dict, optional): Configuration dictionary to use for the connection. If provided, updates the global configuration.
|
|
97
|
+
force_connect (bool, optional): If True, forces a new connection even if one already exists.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Meshtastic interface client if the connection is successful, or None if the connection fails or shutdown is in progress.
|
|
96
101
|
"""
|
|
97
102
|
global meshtastic_client, shutting_down, config, matrix_rooms
|
|
98
103
|
if shutting_down:
|
|
@@ -197,11 +202,19 @@ def connect_meshtastic(passed_config=None, force_connect=False):
|
|
|
197
202
|
f"Connected to {nodeInfo['user']['shortName']} / {nodeInfo['user']['hwModel']}"
|
|
198
203
|
)
|
|
199
204
|
|
|
200
|
-
# Subscribe to message and connection lost events
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
+
# Subscribe to message and connection lost events (only if not already subscribed)
|
|
206
|
+
global subscribed_to_messages, subscribed_to_connection_lost
|
|
207
|
+
if not subscribed_to_messages:
|
|
208
|
+
pub.subscribe(on_meshtastic_message, "meshtastic.receive")
|
|
209
|
+
subscribed_to_messages = True
|
|
210
|
+
logger.debug("Subscribed to meshtastic.receive")
|
|
211
|
+
|
|
212
|
+
if not subscribed_to_connection_lost:
|
|
213
|
+
pub.subscribe(
|
|
214
|
+
on_lost_meshtastic_connection, "meshtastic.connection.lost"
|
|
215
|
+
)
|
|
216
|
+
subscribed_to_connection_lost = True
|
|
217
|
+
logger.debug("Subscribed to meshtastic.connection.lost")
|
|
205
218
|
|
|
206
219
|
except (
|
|
207
220
|
serial.SerialException,
|
|
@@ -631,20 +644,31 @@ def on_meshtastic_message(packet, interface):
|
|
|
631
644
|
async def check_connection():
|
|
632
645
|
"""
|
|
633
646
|
Periodically checks the Meshtastic connection by calling localNode.getMetadata().
|
|
647
|
+
This creates an admin message to verify the connection is alive for all interface types.
|
|
634
648
|
If it fails or doesn't return the firmware version, we assume the connection is lost
|
|
635
649
|
and attempt to reconnect.
|
|
636
650
|
"""
|
|
637
|
-
global meshtastic_client, shutting_down, config
|
|
651
|
+
global meshtastic_client, shutting_down, config, reconnecting
|
|
638
652
|
|
|
639
653
|
# Check if config is available
|
|
640
654
|
if config is None:
|
|
641
655
|
logger.error("No configuration available. Cannot check connection.")
|
|
642
656
|
return
|
|
643
657
|
|
|
658
|
+
# Get heartbeat interval from config, default to 180 seconds
|
|
659
|
+
heartbeat_interval = config["meshtastic"].get("heartbeat_interval", 180)
|
|
644
660
|
connection_type = config["meshtastic"]["connection_type"]
|
|
661
|
+
|
|
662
|
+
logger.info(
|
|
663
|
+
f"Starting connection heartbeat monitor (interval: {heartbeat_interval}s)"
|
|
664
|
+
)
|
|
665
|
+
|
|
645
666
|
while not shutting_down:
|
|
646
|
-
if meshtastic_client:
|
|
667
|
+
if meshtastic_client and not reconnecting:
|
|
647
668
|
try:
|
|
669
|
+
logger.debug(
|
|
670
|
+
f"Checking {connection_type} connection health using getMetadata()"
|
|
671
|
+
)
|
|
648
672
|
output_capture = io.StringIO()
|
|
649
673
|
with contextlib.redirect_stdout(
|
|
650
674
|
output_capture
|
|
@@ -655,10 +679,23 @@ async def check_connection():
|
|
|
655
679
|
if "firmware_version" not in console_output:
|
|
656
680
|
raise Exception("No firmware_version in getMetadata output.")
|
|
657
681
|
|
|
682
|
+
logger.debug(f"{connection_type.capitalize()} connection healthy")
|
|
683
|
+
|
|
658
684
|
except Exception as e:
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
685
|
+
# Only trigger reconnection if we're not already reconnecting
|
|
686
|
+
if not reconnecting:
|
|
687
|
+
logger.warning(
|
|
688
|
+
f"{connection_type.capitalize()} connection health check failed: {e}"
|
|
689
|
+
)
|
|
690
|
+
on_lost_meshtastic_connection(meshtastic_client)
|
|
691
|
+
else:
|
|
692
|
+
logger.debug("Skipping reconnection trigger - already reconnecting")
|
|
693
|
+
elif reconnecting:
|
|
694
|
+
logger.debug("Skipping connection check - reconnection in progress")
|
|
695
|
+
elif not meshtastic_client:
|
|
696
|
+
logger.debug("Skipping connection check - no client available")
|
|
697
|
+
|
|
698
|
+
await asyncio.sleep(heartbeat_interval)
|
|
662
699
|
|
|
663
700
|
|
|
664
701
|
def sendTextReply(
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
services:
|
|
2
|
+
mmrelay:
|
|
3
|
+
build: .
|
|
4
|
+
container_name: meshtastic-matrix-relay
|
|
5
|
+
restart: unless-stopped
|
|
6
|
+
|
|
7
|
+
environment:
|
|
8
|
+
- TZ=UTC
|
|
9
|
+
- PYTHONUNBUFFERED=1
|
|
10
|
+
- MPLCONFIGDIR=/tmp/matplotlib
|
|
11
|
+
|
|
12
|
+
volumes:
|
|
13
|
+
# Configuration - uses standard ~/.mmrelay/config.yaml location
|
|
14
|
+
# Create this first with: make config
|
|
15
|
+
- ${MMRELAY_HOME}/.mmrelay/config.yaml:/app/config.yaml:ro
|
|
16
|
+
|
|
17
|
+
# Data and logs - same locations as standalone installation
|
|
18
|
+
# These directories will be created automatically
|
|
19
|
+
- ${MMRELAY_HOME}/.mmrelay/data:/app/data
|
|
20
|
+
- ${MMRELAY_HOME}/.mmrelay/logs:/app/logs
|
|
21
|
+
|
|
22
|
+
# For TCP connections (most common) - Meshtastic typically uses port 4403
|
|
23
|
+
ports:
|
|
24
|
+
- 4403:4403
|
|
25
|
+
|
|
26
|
+
# For serial connections, uncomment the device you need:
|
|
27
|
+
# devices:
|
|
28
|
+
# - /dev/ttyUSB0:/dev/ttyUSB0
|
|
29
|
+
# - /dev/ttyACM0:/dev/ttyACM0
|
|
30
|
+
|
|
31
|
+
# For BLE connections, uncomment this:
|
|
32
|
+
# privileged: true
|
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.0
|
|
3
|
+
Version: 1.1.0
|
|
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,26 @@ 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
|
+
|
|
60
|
+
_We would love to support [Matrix E2EE rooms](https://github.com/geoffwhittington/meshtastic-matrix-relay/issues/33), but this is currently not implemented._
|
|
61
|
+
|
|
37
62
|
## Documentation
|
|
38
63
|
|
|
39
64
|
Visit our [Wiki](https://github.com/geoffwhittington/meshtastic-matrix-relay/wiki) for comprehensive guides and information.
|
|
40
65
|
|
|
41
66
|
- [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
67
|
|
|
45
68
|
---
|
|
46
69
|
|
|
@@ -61,22 +84,23 @@ mmrelay --install-service
|
|
|
61
84
|
|
|
62
85
|
For detailed installation and configuration instructions, see the [Installation Guide](docs/INSTRUCTIONS.md).
|
|
63
86
|
|
|
64
|
-
|
|
87
|
+
## Docker
|
|
65
88
|
|
|
66
|
-
|
|
89
|
+
MMRelay includes official Docker support for easy deployment and management:
|
|
67
90
|
|
|
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!!**
|
|
91
|
+
```bash
|
|
92
|
+
# Quick setup with Docker
|
|
93
|
+
make setup # Copy config and open editor (first time)
|
|
94
|
+
make build # Build the Docker image
|
|
95
|
+
make run # Start the container
|
|
96
|
+
make logs # View logs
|
|
97
|
+
```
|
|
78
98
|
|
|
79
|
-
|
|
99
|
+
Docker provides isolated environment, easy deployment, automatic restarts, and volume persistence.
|
|
100
|
+
|
|
101
|
+
For detailed Docker setup instructions, see the [Docker Guide](DOCKER.md).
|
|
102
|
+
|
|
103
|
+
> **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
104
|
|
|
81
105
|
---
|
|
82
106
|
|
|
@@ -84,7 +108,7 @@ _We would love to support [Matrix E2EE rooms](https://github.com/geoffwhittingto
|
|
|
84
108
|
|
|
85
109
|

|
|
86
110
|
|
|
87
|
-
The latest installer is available [
|
|
111
|
+
The latest installer is available in the [releases section](https://github.com/geoffwhittington/meshtastic-matrix-relay/releases).
|
|
88
112
|
|
|
89
113
|
---
|
|
90
114
|
|
|
@@ -102,7 +126,7 @@ Produce high-level details about your mesh:
|
|
|
102
126
|
|
|
103
127
|

|
|
104
128
|
|
|
105
|
-
See the full list of core plugins
|
|
129
|
+
See the full list of [core plugins](https://github.com/geoffwhittington/meshtastic-matrix-relay/wiki/Core-Plugins).
|
|
106
130
|
|
|
107
131
|
### Community & Custom Plugins
|
|
108
132
|
|
|
@@ -147,6 +171,6 @@ See our Wiki page [Getting Started With Matrix & MM Relay](https://github.com/ge
|
|
|
147
171
|
|
|
148
172
|
Join us!
|
|
149
173
|
|
|
150
|
-
- Our project's room: [#mmrelay:
|
|
151
|
-
- Part of the Meshtastic Community Matrix space: [#
|
|
152
|
-
- Public Relay Room: [#relay-room:
|
|
174
|
+
- Our project's room: [#mmrelay:matrix.org](https://matrix.to/#/#mmrelay:matrix.org)
|
|
175
|
+
- Part of the Meshtastic Community Matrix space: [#meshnetclub:matrix.org](https://matrix.to/#/#meshnetclub:matrix.org)
|
|
176
|
+
- 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=wo5bAmayppAqLHI02pEu7w2K2_YUxHWbnanigSNlXa8,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=WdRiqbmCtUJrd45VZdJvfbrXct6rD09rtusi9nHMzc4,29163
|
|
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.0.
|
|
30
|
-
mmrelay-1.0.
|
|
31
|
-
mmrelay-1.0.
|
|
32
|
-
mmrelay-1.0.
|
|
26
|
+
mmrelay/tools/sample-docker-compose.yaml,sha256=eXi-7TGb4fjC4g0Q-qs55pBZ4UWNKAstSO1ozOXlrUI,939
|
|
27
|
+
mmrelay/tools/sample.env,sha256=RP-o3rX3jnEIrVG2rqCZq31O1yRXou4HcGrXWLVbKKw,311
|
|
28
|
+
mmrelay/tools/sample_config.yaml,sha256=0BKND0qbke8z9X9J9iHleu567dZt3RmHUxhZlQEEdFk,3290
|
|
29
|
+
mmrelay-1.1.0.dist-info/licenses/LICENSE,sha256=yceWauM1c0-FHxVplsD7W1-AbSeRaUNlmqT4UO1msBU,1073
|
|
30
|
+
mmrelay-1.1.0.dist-info/METADATA,sha256=IstoM9NyCmerUqL9QOEygC2BfAoxrHJLfXWY-J7kEd0,6658
|
|
31
|
+
mmrelay-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
32
|
+
mmrelay-1.1.0.dist-info/entry_points.txt,sha256=SJZwGUOEpQ-qx4H8UL4xKFnKeInGUaZNW1I0ddjK7Ws,45
|
|
33
|
+
mmrelay-1.1.0.dist-info/top_level.txt,sha256=B_ZLCRm7NYAmI3PipRUyHGymP-C-q16LSeMGzmqJfo4,8
|
|
34
|
+
mmrelay-1.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|