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 CHANGED
@@ -2,16 +2,4 @@
2
2
  Meshtastic Matrix Relay - Bridge between Meshtastic mesh networks and Matrix chat rooms.
3
3
  """
4
4
 
5
- import os
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, supporting colorized output and log rotation.
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'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".
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, setup_upstream_logging_capture
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
- Sets up and runs the asynchronous relay between Meshtastic and Matrix, managing connections, event handling, and graceful shutdown.
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
- Parameters:
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
- meshtastic_utils.meshtastic_client.close()
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.warning(f"Error closing Meshtastic client: {e}")
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