mmrelay 1.1.1__py3-none-any.whl → 1.1.3__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 -13
- mmrelay/log_utils.py +40 -48
- mmrelay/main.py +49 -11
- mmrelay/matrix_utils.py +405 -144
- mmrelay/meshtastic_utils.py +91 -84
- mmrelay/message_queue.py +475 -0
- mmrelay/plugin_loader.py +2 -2
- mmrelay/plugins/base_plugin.py +92 -39
- mmrelay/tools/sample_config.yaml +26 -4
- {mmrelay-1.1.1.dist-info → mmrelay-1.1.3.dist-info}/METADATA +11 -13
- {mmrelay-1.1.1.dist-info → mmrelay-1.1.3.dist-info}/RECORD +15 -14
- mmrelay-1.1.3.dist-info/licenses/LICENSE +675 -0
- mmrelay-1.1.1.dist-info/licenses/LICENSE +0 -21
- {mmrelay-1.1.1.dist-info → mmrelay-1.1.3.dist-info}/WHEEL +0 -0
- {mmrelay-1.1.1.dist-info → mmrelay-1.1.3.dist-info}/entry_points.txt +0 -0
- {mmrelay-1.1.1.dist-info → mmrelay-1.1.3.dist-info}/top_level.txt +0 -0
mmrelay/__init__.py
CHANGED
|
@@ -2,16 +2,4 @@
|
|
|
2
2
|
Meshtastic Matrix Relay - Bridge between Meshtastic mesh networks and Matrix chat rooms.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
from importlib.metadata import PackageNotFoundError, version
|
|
7
|
-
|
|
8
|
-
# First try to get version from environment variable (GitHub tag)
|
|
9
|
-
if "GITHUB_REF_NAME" in os.environ:
|
|
10
|
-
__version__ = os.environ.get("GITHUB_REF_NAME")
|
|
11
|
-
else:
|
|
12
|
-
# Fall back to package metadata using importlib.metadata (modern replacement for pkg_resources)
|
|
13
|
-
try:
|
|
14
|
-
__version__ = version("mmrelay")
|
|
15
|
-
except PackageNotFoundError:
|
|
16
|
-
# If all else fails, use hardcoded version
|
|
17
|
-
__version__ = "1.1.1"
|
|
5
|
+
__version__ = "1.1.3"
|
mmrelay/log_utils.py
CHANGED
|
@@ -28,15 +28,52 @@ config = None
|
|
|
28
28
|
# Global variable to store the log file path
|
|
29
29
|
log_file_path = None
|
|
30
30
|
|
|
31
|
+
# Track if component debug logging has been configured
|
|
32
|
+
_component_debug_configured = False
|
|
33
|
+
|
|
34
|
+
# Component logger mapping for data-driven configuration
|
|
35
|
+
_COMPONENT_LOGGERS = {
|
|
36
|
+
"matrix_nio": ["nio", "nio.client", "nio.http", "nio.crypto"],
|
|
37
|
+
"bleak": ["bleak", "bleak.backends"],
|
|
38
|
+
"meshtastic": [
|
|
39
|
+
"meshtastic",
|
|
40
|
+
"meshtastic.serial_interface",
|
|
41
|
+
"meshtastic.tcp_interface",
|
|
42
|
+
"meshtastic.ble_interface",
|
|
43
|
+
],
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def configure_component_debug_logging():
|
|
48
|
+
"""
|
|
49
|
+
Enables debug-level logging for selected external components based on configuration settings.
|
|
50
|
+
|
|
51
|
+
This function sets the log level to DEBUG for specific libraries (matrix_nio, bleak, meshtastic) if enabled in the global configuration under `logging.debug`. It ensures that component debug logging is configured only once per application run.
|
|
52
|
+
"""
|
|
53
|
+
global _component_debug_configured, config
|
|
54
|
+
|
|
55
|
+
# Only configure once
|
|
56
|
+
if _component_debug_configured or config is None:
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
debug_config = config.get("logging", {}).get("debug", {})
|
|
60
|
+
|
|
61
|
+
for component, loggers in _COMPONENT_LOGGERS.items():
|
|
62
|
+
if debug_config.get(component, False):
|
|
63
|
+
for logger_name in loggers:
|
|
64
|
+
logging.getLogger(logger_name).setLevel(logging.DEBUG)
|
|
65
|
+
|
|
66
|
+
_component_debug_configured = True
|
|
67
|
+
|
|
31
68
|
|
|
32
69
|
def get_logger(name):
|
|
33
70
|
"""
|
|
34
|
-
Create and configure a logger with console and optional file output,
|
|
71
|
+
Create and configure a logger with console and optional rotating file output, using global configuration and command-line arguments.
|
|
35
72
|
|
|
36
|
-
The logger
|
|
73
|
+
The logger supports colorized console output via Rich if enabled, and writes logs to a rotating file if configured or requested via command-line arguments. Log file location and rotation parameters are determined by priority: command-line argument, configuration file, or a default directory. The function ensures the log directory exists and stores the log file path globally if the logger name is "M<>M Relay".
|
|
37
74
|
|
|
38
75
|
Parameters:
|
|
39
|
-
name (str): The name of the logger to create.
|
|
76
|
+
name (str): The name of the logger to create and configure.
|
|
40
77
|
|
|
41
78
|
Returns:
|
|
42
79
|
logging.Logger: The configured logger instance.
|
|
@@ -145,48 +182,3 @@ def get_logger(name):
|
|
|
145
182
|
logger.addHandler(file_handler)
|
|
146
183
|
|
|
147
184
|
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
|
@@ -4,6 +4,7 @@ It uses Meshtastic-python and Matrix nio client library to interface with the ra
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import asyncio
|
|
7
|
+
import concurrent.futures
|
|
7
8
|
import logging
|
|
8
9
|
import signal
|
|
9
10
|
import sys
|
|
@@ -20,12 +21,18 @@ from mmrelay.db_utils import (
|
|
|
20
21
|
update_shortnames,
|
|
21
22
|
wipe_message_map,
|
|
22
23
|
)
|
|
23
|
-
from mmrelay.log_utils import get_logger
|
|
24
|
+
from mmrelay.log_utils import get_logger
|
|
24
25
|
from mmrelay.matrix_utils import connect_matrix, join_matrix_room
|
|
25
26
|
from mmrelay.matrix_utils import logger as matrix_logger
|
|
26
27
|
from mmrelay.matrix_utils import on_room_member, on_room_message
|
|
27
28
|
from mmrelay.meshtastic_utils import connect_meshtastic
|
|
28
29
|
from mmrelay.meshtastic_utils import logger as meshtastic_logger
|
|
30
|
+
from mmrelay.message_queue import (
|
|
31
|
+
DEFAULT_MESSAGE_DELAY,
|
|
32
|
+
get_message_queue,
|
|
33
|
+
start_message_queue,
|
|
34
|
+
stop_message_queue,
|
|
35
|
+
)
|
|
29
36
|
from mmrelay.plugin_loader import load_plugins
|
|
30
37
|
|
|
31
38
|
# Initialize logger
|
|
@@ -50,12 +57,9 @@ def print_banner():
|
|
|
50
57
|
|
|
51
58
|
async def main(config):
|
|
52
59
|
"""
|
|
53
|
-
|
|
54
|
-
|
|
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.
|
|
60
|
+
Runs the main asynchronous relay loop, managing the lifecycle and coordination between Meshtastic and Matrix clients.
|
|
56
61
|
|
|
57
|
-
|
|
58
|
-
config: The loaded configuration dictionary containing Matrix, Meshtastic, and database settings.
|
|
62
|
+
Initializes the database, loads plugins, starts the message queue, and establishes connections to both Meshtastic and Matrix. Joins configured Matrix rooms, registers event callbacks for message and membership events, and periodically updates node names from the Meshtastic network. Monitors connection health, manages the Matrix sync loop with reconnection and shutdown handling, and ensures graceful shutdown of all components, including optional message map wiping on startup and shutdown if configured.
|
|
59
63
|
"""
|
|
60
64
|
# Extract Matrix configuration
|
|
61
65
|
from typing import List
|
|
@@ -68,9 +72,6 @@ async def main(config):
|
|
|
68
72
|
# Initialize the SQLite database
|
|
69
73
|
initialize_database()
|
|
70
74
|
|
|
71
|
-
# Set up upstream logging capture to format library messages consistently
|
|
72
|
-
setup_upstream_logging_capture()
|
|
73
|
-
|
|
74
75
|
# Check database config for wipe_on_restart (preferred format)
|
|
75
76
|
database_config = config.get("database", {})
|
|
76
77
|
msg_map_config = database_config.get("msg_map", {})
|
|
@@ -95,6 +96,12 @@ async def main(config):
|
|
|
95
96
|
# Load plugins early
|
|
96
97
|
load_plugins(passed_config=config)
|
|
97
98
|
|
|
99
|
+
# Start message queue with configured message delay
|
|
100
|
+
message_delay = config.get("meshtastic", {}).get(
|
|
101
|
+
"message_delay", DEFAULT_MESSAGE_DELAY
|
|
102
|
+
)
|
|
103
|
+
start_message_queue(message_delay=message_delay)
|
|
104
|
+
|
|
98
105
|
# Connect to Meshtastic
|
|
99
106
|
meshtastic_utils.meshtastic_client = connect_meshtastic(passed_config=config)
|
|
100
107
|
|
|
@@ -137,6 +144,9 @@ async def main(config):
|
|
|
137
144
|
# This provides proactive connection detection for all interface types
|
|
138
145
|
_ = asyncio.create_task(meshtastic_utils.check_connection())
|
|
139
146
|
|
|
147
|
+
# Ensure message queue processor is started now that event loop is running
|
|
148
|
+
get_message_queue().ensure_processor_started()
|
|
149
|
+
|
|
140
150
|
# Start the Matrix client sync loop
|
|
141
151
|
try:
|
|
142
152
|
while not shutdown_event.is_set():
|
|
@@ -178,14 +188,39 @@ async def main(config):
|
|
|
178
188
|
await shutdown()
|
|
179
189
|
finally:
|
|
180
190
|
# Cleanup
|
|
191
|
+
matrix_logger.info("Stopping message queue...")
|
|
192
|
+
stop_message_queue()
|
|
193
|
+
|
|
181
194
|
matrix_logger.info("Closing Matrix client...")
|
|
182
195
|
await matrix_client.close()
|
|
183
196
|
if meshtastic_utils.meshtastic_client:
|
|
184
197
|
meshtastic_logger.info("Closing Meshtastic client...")
|
|
185
198
|
try:
|
|
186
|
-
|
|
199
|
+
# Timeout wrapper to prevent infinite hanging during shutdown
|
|
200
|
+
# The meshtastic library can sometimes hang indefinitely during close()
|
|
201
|
+
# operations, especially with BLE connections. This timeout ensures
|
|
202
|
+
# the application can shut down gracefully within 10 seconds.
|
|
203
|
+
|
|
204
|
+
def _close_meshtastic():
|
|
205
|
+
"""
|
|
206
|
+
Closes the Meshtastic client connection synchronously.
|
|
207
|
+
"""
|
|
208
|
+
meshtastic_utils.meshtastic_client.close()
|
|
209
|
+
|
|
210
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
|
211
|
+
future = executor.submit(_close_meshtastic)
|
|
212
|
+
future.result(timeout=10.0) # 10-second timeout
|
|
213
|
+
|
|
214
|
+
meshtastic_logger.info("Meshtastic client closed successfully")
|
|
215
|
+
except concurrent.futures.TimeoutError:
|
|
216
|
+
meshtastic_logger.warning(
|
|
217
|
+
"Meshtastic client close timed out - forcing shutdown"
|
|
218
|
+
)
|
|
187
219
|
except Exception as e:
|
|
188
|
-
meshtastic_logger.
|
|
220
|
+
meshtastic_logger.error(
|
|
221
|
+
f"Unexpected error during Meshtastic client close: {e}",
|
|
222
|
+
exc_info=True,
|
|
223
|
+
)
|
|
189
224
|
|
|
190
225
|
# Attempt to wipe message_map on shutdown if enabled
|
|
191
226
|
if wipe_on_restart:
|
|
@@ -264,6 +299,9 @@ def run_main(args):
|
|
|
264
299
|
set_config(db_utils, config)
|
|
265
300
|
set_config(base_plugin, config)
|
|
266
301
|
|
|
302
|
+
# Configure component debug logging now that config is available
|
|
303
|
+
log_utils.configure_component_debug_logging()
|
|
304
|
+
|
|
267
305
|
# Get config path and log file path for logging
|
|
268
306
|
from mmrelay.config import config_path
|
|
269
307
|
from mmrelay.log_utils import log_file_path
|