mmrelay 1.2.2__py3-none-any.whl → 1.2.4__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,4 +2,4 @@
2
2
  Meshtastic Matrix Relay - Bridge between Meshtastic mesh networks and Matrix chat rooms.
3
3
  """
4
4
 
5
- __version__ = "1.2.2"
5
+ __version__ = "1.2.4"
mmrelay/cli.py CHANGED
@@ -238,11 +238,11 @@ def print_version():
238
238
  def _validate_e2ee_dependencies():
239
239
  """
240
240
  Check whether end-to-end encryption (E2EE) is usable on the current platform.
241
-
241
+
242
242
  Returns:
243
243
  bool: True if the platform is supported and required E2EE libraries can be imported;
244
244
  False otherwise.
245
-
245
+
246
246
  Notes:
247
247
  - This function performs only local checks (platform and importability) and does not perform
248
248
  network I/O.
@@ -1499,13 +1499,13 @@ def handle_auth_logout(args):
1499
1499
  def handle_service_command(args):
1500
1500
  """
1501
1501
  Dispatch service-related subcommands.
1502
-
1502
+
1503
1503
  Currently supports the "install" subcommand which imports and runs mmrelay.setup_utils.install_service().
1504
1504
  Returns 0 on successful installation, 1 on failure or for unknown subcommands.
1505
-
1505
+
1506
1506
  Parameters:
1507
1507
  args: argparse.Namespace with a `service_command` attribute indicating the requested action.
1508
-
1508
+
1509
1509
  Returns:
1510
1510
  int: Exit code (0 on success, 1 on error).
1511
1511
  """
@@ -1524,15 +1524,15 @@ def handle_service_command(args):
1524
1524
 
1525
1525
  def _diagnose_config_paths(args):
1526
1526
  """
1527
- Print a diagnostic summary of resolved configuration file search paths and their directory accessibility.
1528
-
1529
- Uses get_config_paths(args) to compute the ordered list of candidate config file locations, then prints each path with a concise directory status icon:
1527
+ Prints a diagnostic summary of resolved configuration file search paths and their directory accessibility.
1528
+
1529
+ Each candidate config path is printed with a status icon:
1530
1530
  - ✅ directory exists and is writable
1531
1531
  - ⚠️ directory exists but is not writable
1532
1532
  - ❌ directory does not exist
1533
-
1533
+
1534
1534
  Parameters:
1535
- args (argparse.Namespace): Parsed CLI arguments used to determine the config search order.
1535
+ args (argparse.Namespace): CLI arguments used to determine the ordered list of candidate config paths (passed to get_config_paths).
1536
1536
  """
1537
1537
  print("1. Testing configuration paths...")
1538
1538
  from mmrelay.config import get_config_paths
@@ -1551,11 +1551,11 @@ def _diagnose_config_paths(args):
1551
1551
  def _diagnose_sample_config_accessibility():
1552
1552
  """
1553
1553
  Check availability of the bundled sample configuration and print a short diagnostic.
1554
-
1554
+
1555
1555
  Performs two non-destructive checks and prints human-readable results:
1556
1556
  1) Verifies whether the sample config file exists at the path returned by mmrelay.tools.get_sample_config_path().
1557
1557
  2) Attempts to read the embedded resource "sample_config.yaml" from the mmrelay.tools package via importlib.resources and reports success and the content length.
1558
-
1558
+
1559
1559
  Returns:
1560
1560
  bool: True if a filesystem sample config exists at the resolved path; False otherwise.
1561
1561
  """
@@ -1586,18 +1586,15 @@ def _diagnose_sample_config_accessibility():
1586
1586
 
1587
1587
  def _diagnose_platform_specific(args):
1588
1588
  """
1589
- Run platform-specific diagnostic checks.
1590
-
1591
- On Windows, imports and runs Windows-specific requirement checks and a configuration-generation
1592
- test from mmrelay.windows_utils, printing per-component results and any warnings. On non-Windows
1593
- platforms this reports that platform-specific tests are not required.
1594
-
1589
+ Run platform-specific diagnostic checks and print a concise report.
1590
+
1591
+ On Windows, executes Windows-specific requirement checks and a configuration-generation test using the provided CLI arguments; on non-Windows platforms, reports that platform-specific tests are not required.
1592
+
1595
1593
  Parameters:
1596
- args (argparse.Namespace): Parsed CLI arguments; passed through to the Windows
1597
- config-generation test when running on Windows.
1598
-
1594
+ args (argparse.Namespace): CLI arguments forwarded to the Windows configuration-generation test (used only when running on Windows).
1595
+
1599
1596
  Returns:
1600
- bool: True if the platform is Windows (Windows checks were executed), False otherwise.
1597
+ bool: `True` if Windows checks were executed (running on Windows), `False` otherwise.
1601
1598
  """
1602
1599
  print("3. Platform-specific diagnostics...")
1603
1600
  import sys
@@ -1704,7 +1701,7 @@ logging:
1704
1701
  def _diagnose_minimal_config_template():
1705
1702
  """
1706
1703
  Validate the built-in minimal YAML configuration template and print a concise pass/fail status.
1707
-
1704
+
1708
1705
  Attempts to parse the string returned by _get_minimal_config_template() using yaml.safe_load. Prints a single-line result showing a ✅ with the template character length when the template is valid YAML, or a ❌ with the YAML parsing error when invalid. This is a non-destructive diagnostic helper that prints output and does not return a value.
1709
1706
  """
1710
1707
  print("4. Testing minimal config template fallback...")
@@ -1720,15 +1717,15 @@ def _diagnose_minimal_config_template():
1720
1717
 
1721
1718
  def handle_config_diagnose(args):
1722
1719
  """
1723
- Run non-destructive diagnostics for the MMRelay configuration subsystem and print a human-readable report.
1724
-
1725
- Performs four checks: resolves candidate config paths and reports directory accessibility; verifies packaged sample config availability (filesystem and importlib.resources fallback); runs platform-specific diagnostics (Windows-focused where applicable); and validates the built-in minimal YAML template. Prints findings and suggested next steps to stdout/stderr.
1726
-
1720
+ Run a set of non-destructive diagnostics for the MMRelay configuration subsystem and print a concise, human-readable report.
1721
+
1722
+ Performs four checks without modifying user files: (1) resolves and reports candidate configuration file paths and their directory accessibility, (2) verifies availability and readability of the packaged sample configuration, (3) executes platform-specific diagnostics (Windows checks when applicable), and (4) validates the built-in minimal YAML configuration template. Results and actionable guidance are written to stdout/stderr; additional Windows-specific guidance may be printed to stderr on unexpected failures.
1723
+
1727
1724
  Parameters:
1728
- args (argparse.Namespace): Parsed CLI arguments used to resolve config search paths and to control platform-specific checks.
1729
-
1725
+ args (argparse.Namespace): Parsed CLI arguments used to determine configuration search paths and to control platform-specific diagnostic behavior.
1726
+
1730
1727
  Returns:
1731
- int: Exit code (0 on success, 1 on failure).
1728
+ int: Exit code where `0` indicates diagnostics completed successfully and `1` indicates a failure occurred (an error summary is printed to stderr).
1732
1729
  """
1733
1730
  print("MMRelay Configuration System Diagnostics")
1734
1731
  print("=" * 40)
mmrelay/config.py CHANGED
@@ -196,14 +196,12 @@ def get_log_dir():
196
196
 
197
197
  def get_e2ee_store_dir():
198
198
  """
199
- Return the absolute path to the E2EE data store directory, creating it if missing.
200
-
201
- On Linux and macOS this is "<base_dir>/store" where base_dir is returned by get_base_dir().
202
- On Windows this is "<custom_data_dir>/store" when module-level custom_data_dir is set; otherwise it uses
203
- platformdirs.user_data_dir(APP_NAME, APP_AUTHOR)/store.
204
-
199
+ Get the absolute path to the application's end-to-end encryption (E2EE) data store directory, creating it if necessary.
200
+
201
+ On Linux and macOS the directory is located under the application base directory; on Windows it uses the configured custom data directory when set, otherwise the platform-specific user data directory. The directory will be created if it does not exist.
202
+
205
203
  Returns:
206
- str: Absolute path to the ensured store directory. The directory is created if it does not exist.
204
+ store_dir (str): Absolute path to the ensured E2EE store directory.
207
205
  """
208
206
  if sys.platform in ["linux", "darwin"]:
209
207
  # Use ~/.mmrelay/store/ for Linux and Mac
@@ -388,12 +386,12 @@ def is_e2ee_enabled(config):
388
386
  def check_e2ee_enabled_silently(args=None):
389
387
  """
390
388
  Check silently whether End-to-End Encryption (E2EE) is enabled in the first readable configuration file.
391
-
389
+
392
390
  Searches candidate configuration paths returned by get_config_paths(args) in priority order, loads the first readable YAML file, and returns True if that configuration enables E2EE (via is_e2ee_enabled). I/O and YAML parsing errors are ignored and the function continues to the next candidate. On Windows this always returns False.
393
-
391
+
394
392
  Parameters:
395
393
  args (optional): Parsed command-line arguments that can influence config search order.
396
-
394
+
397
395
  Returns:
398
396
  bool: True if E2EE is enabled in the first valid configuration file found; otherwise False.
399
397
  """
@@ -493,16 +491,16 @@ def load_credentials():
493
491
  def save_credentials(credentials):
494
492
  """
495
493
  Persist a JSON-serializable credentials mapping to <base_dir>/credentials.json.
496
-
494
+
497
495
  Writes the provided credentials (a JSON-serializable mapping) to the application's
498
496
  base configuration directory as credentials.json, creating the base directory if
499
497
  necessary. On Unix-like systems the file permissions are adjusted to be
500
498
  restrictive (0o600) when possible. I/O and permission errors are caught and
501
499
  logged; the function does not raise them.
502
-
500
+
503
501
  Parameters:
504
502
  credentials (dict): JSON-serializable mapping of credentials to persist.
505
-
503
+
506
504
  Returns:
507
505
  None
508
506
  """
@@ -822,18 +820,18 @@ def load_config(config_file=None, args=None):
822
820
  def validate_yaml_syntax(config_content, config_path):
823
821
  """
824
822
  Validate YAML text for syntax and common style issues, parse it with PyYAML, and return results.
825
-
823
+
826
824
  Performs lightweight line-based checks for frequent mistakes (using '=' instead of ':'
827
825
  for mappings and non-standard boolean words like 'yes'/'no' or 'on'/'off') and then
828
826
  attempts to parse the content with yaml.safe_load. If only style warnings are found,
829
827
  parsing is considered successful and warnings are returned; if parsing fails or true
830
828
  syntax errors are detected, a detailed error message is returned that references
831
829
  config_path to identify the source.
832
-
830
+
833
831
  Parameters:
834
832
  config_content (str): Raw YAML text to validate.
835
833
  config_path (str): Path or label used in error messages to identify the source of the content.
836
-
834
+
837
835
  Returns:
838
836
  tuple:
839
837
  is_valid (bool): True if YAML parsed successfully (style warnings allowed), False on syntax/parsing error.
@@ -6,7 +6,10 @@ delays, size limits, and water marks for queue management.
6
6
  """
7
7
 
8
8
  # Message timing constants
9
- DEFAULT_MESSAGE_DELAY = 2.0 # Firmware-enforced minimum delay in seconds
9
+ DEFAULT_MESSAGE_DELAY = (
10
+ 2.5 # Set above the 2.0s firmware limit to prevent message dropping
11
+ )
12
+ MINIMUM_MESSAGE_DELAY = 2.1 # Minimum delay enforced to stay above firmware limit
10
13
 
11
14
  # Queue size management
12
15
  MAX_QUEUE_SIZE = 500
mmrelay/e2ee_utils.py CHANGED
@@ -80,8 +80,13 @@ def get_e2ee_status(
80
80
  importlib.import_module("olm")
81
81
 
82
82
  if os.getenv("MMRELAY_TESTING") != "1":
83
- importlib.import_module("nio.crypto").OlmDevice
84
- importlib.import_module("nio.store").SqliteStore
83
+ nio_crypto = importlib.import_module("nio.crypto")
84
+ if not hasattr(nio_crypto, "OlmDevice"):
85
+ raise ImportError("nio.crypto.OlmDevice is unavailable")
86
+
87
+ nio_store = importlib.import_module("nio.store")
88
+ if not hasattr(nio_store, "SqliteStore"):
89
+ raise ImportError("nio.store.SqliteStore is unavailable")
85
90
 
86
91
  status["dependencies_installed"] = True
87
92
  except ImportError:
mmrelay/log_utils.py CHANGED
@@ -69,14 +69,13 @@ _COMPONENT_LOGGERS = {
69
69
 
70
70
  def configure_component_debug_logging():
71
71
  """
72
- Configure log levels for external component loggers based on config["logging"]["debug"].
72
+ Configure log levels and handlers for external component loggers based on config.
73
73
 
74
- Reads per-component entries under `config["logging"]["debug"]` and applies one of:
75
- - falsy or missing: silence the component by setting its loggers to CRITICAL+1
76
- - boolean True: enable DEBUG for the component's loggers
77
- - string: interpret as a logging level name (case-insensitive); invalid names fall back to DEBUG
74
+ Reads `config["logging"]["debug"]` and for each component:
75
+ - If enabled (True or a valid log level string), sets the component's loggers to the specified level and attaches the main application's handlers to them. This makes component logs appear in the console and log file.
76
+ - If disabled (falsy or missing), silences the component by setting its loggers to a level higher than CRITICAL.
78
77
 
79
- This function mutates the levels of loggers listed in _COMPONENT_LOGGERS and runs only once per process; no-op if called again or if global `config` is None.
78
+ This function runs only once. It is not thread-safe and should be called early in the application startup, after the main logger is configured but before other modules are imported.
80
79
  """
81
80
  global _component_debug_configured, config
82
81
 
@@ -84,7 +83,22 @@ def configure_component_debug_logging():
84
83
  if _component_debug_configured or config is None:
85
84
  return
86
85
 
87
- debug_config = config.get("logging", {}).get("debug", {})
86
+ # Get the main application logger and its handlers to attach to component loggers
87
+ main_logger = logging.getLogger(APP_DISPLAY_NAME)
88
+ main_handlers = main_logger.handlers
89
+ debug_settings = config.get("logging", {}).get("debug")
90
+
91
+ # Ensure debug_config is a dictionary, handling malformed configs gracefully
92
+ if isinstance(debug_settings, dict):
93
+ debug_config = debug_settings
94
+ else:
95
+ if debug_settings is not None:
96
+ main_logger.warning(
97
+ "Debug logging section is not a dictionary. "
98
+ "All component debug logging will be disabled. "
99
+ "Check your config.yaml debug section formatting."
100
+ )
101
+ debug_config = {}
88
102
 
89
103
  for component, loggers in _COMPONENT_LOGGERS.items():
90
104
  component_config = debug_config.get(component)
@@ -105,8 +119,15 @@ def configure_component_debug_logging():
105
119
  # Invalid config, fall back to DEBUG
106
120
  log_level = logging.DEBUG
107
121
 
122
+ # Configure all loggers for this component
108
123
  for logger_name in loggers:
109
- logging.getLogger(logger_name).setLevel(log_level)
124
+ component_logger = logging.getLogger(logger_name)
125
+ component_logger.setLevel(log_level)
126
+ component_logger.propagate = False # Prevent duplicate logging
127
+ # Attach main handlers to the component logger
128
+ for handler in main_handlers:
129
+ if handler not in component_logger.handlers:
130
+ component_logger.addHandler(handler)
110
131
  else:
111
132
  # Component debug is disabled - completely suppress external library logging
112
133
  # Use a level higher than CRITICAL to effectively disable all messages
mmrelay/main.py CHANGED
@@ -73,9 +73,18 @@ def print_banner():
73
73
 
74
74
  async def main(config):
75
75
  """
76
- Coordinates the main asynchronous relay loop between Meshtastic and Matrix clients.
76
+ Coordinate the asynchronous relay loop between Meshtastic and Matrix clients.
77
77
 
78
- 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. Optionally wipes the message map on startup and shutdown if configured.
78
+ Initializes the database and plugins, starts the message queue, connects to Meshtastic and Matrix, joins configured Matrix rooms, registers event callbacks, monitors connection health, runs the Matrix sync loop with automatic retries, and ensures an orderly shutdown of all components (including optional message map wiping on startup and shutdown).
79
+
80
+ Parameters:
81
+ config (dict): Application configuration mapping. Expected keys used by this function include:
82
+ - "matrix_rooms": list of room dicts with at least an "id" entry,
83
+ - "meshtastic": optional dict with "message_delay",
84
+ - "database" (preferred) or legacy "db": optional dict containing "msg_map" with "wipe_on_restart" boolean.
85
+
86
+ Raises:
87
+ ConnectionError: If connecting to Matrix fails and no Matrix client can be obtained.
79
88
  """
80
89
  # Extract Matrix configuration
81
90
  from typing import List
@@ -154,9 +163,9 @@ async def main(config):
154
163
 
155
164
  async def shutdown():
156
165
  """
157
- Signal the application to shut down.
158
-
159
- Sets the Meshtastic shutdown flag and triggers the local shutdown event so tasks waiting on that event can begin shutdown/cleanup. This coroutine only signals shutdown; it does not close clients or perform cleanup itself.
166
+ Signal the application to begin shutdown.
167
+
168
+ Set the Meshtastic shutdown flag and set the local shutdown event so any coroutines waiting on that event can start cleanup.
160
169
  """
161
170
  matrix_logger.info("Shutdown signal received. Closing down...")
162
171
  meshtastic_utils.shutting_down = True # Set the shutting_down flag
mmrelay/matrix_utils.py CHANGED
@@ -13,7 +13,6 @@ import time
13
13
  from typing import Any, Dict, Optional, Union
14
14
  from urllib.parse import urlparse
15
15
 
16
- import meshtastic.protobuf.portnums_pb2
17
16
  from nio import (
18
17
  AsyncClient,
19
18
  AsyncClientConfig,
@@ -113,15 +112,15 @@ def _is_room_alias(value: Any) -> bool:
113
112
  def _iter_room_alias_entries(mapping):
114
113
  """
115
114
  Yield (alias_or_id, setter) pairs for entries in a Matrix room mapping.
116
-
115
+
117
116
  Each yielded tuple contains:
118
117
  - alias_or_id (str): the room alias or room ID found in the entry (may be an alias starting with '#' or a canonical room ID). If a dict entry has no `"id"` key, an empty string is yielded.
119
118
  - setter (callable): a function accepting a single argument `new_id` which updates the underlying mapping in-place to replace the alias with the resolved room ID.
120
-
119
+
121
120
  Supports two mapping shapes:
122
121
  - list: items may be strings (alias/ID) or dicts with an `"id"` key.
123
122
  - dict: values may be strings (alias/ID) or dicts with an `"id"` key.
124
-
123
+
125
124
  The setter updates the original collection (list element or dict value) so callers can resolve aliases and persist resolved IDs back into the provided mapping.
126
125
  """
127
126
 
@@ -150,13 +149,13 @@ def _iter_room_alias_entries(mapping):
150
149
  async def _resolve_aliases_in_mapping(mapping, resolver):
151
150
  """
152
151
  Resolve Matrix room alias entries found in a mapping (list or dict) by calling an async resolver and replacing aliases with resolved room IDs in-place.
153
-
152
+
154
153
  This function iterates entries produced by _iter_room_alias_entries(mapping). For each entry whose key/value looks like a Matrix room alias (a string starting with '#'), it awaits the provided resolver coroutine with the alias; if the resolver returns a truthy room ID, the corresponding entry in the original mapping is updated via the entry's setter. If mapping is not a list or dict, the function logs a warning and returns without modifying anything.
155
-
154
+
156
155
  Parameters:
157
156
  mapping (list|dict): A mapping of Matrix rooms where some entries may be aliases (e.g., "#room:example.org").
158
157
  resolver (Callable[[str], Awaitable[Optional[str]]]): Async callable that accepts an alias and returns a resolved room ID (or falsy on failure).
159
-
158
+
160
159
  Returns:
161
160
  None
162
161
  """
@@ -178,12 +177,12 @@ async def _resolve_aliases_in_mapping(mapping, resolver):
178
177
  def _update_room_id_in_mapping(mapping, alias, resolved_id) -> bool:
179
178
  """
180
179
  Replace a room alias with its resolved room ID in a mapping.
181
-
180
+
182
181
  Parameters:
183
182
  mapping (list|dict): A matrix_rooms mapping represented as a list of aliases or a dict of entries; only list and dict types are supported.
184
183
  alias (str): The room alias to replace (e.g., "#room:server").
185
184
  resolved_id (str): The canonical room ID to substitute for the alias (e.g., "!abcdef:server").
186
-
185
+
187
186
  Returns:
188
187
  bool: True if the alias was found and replaced with resolved_id; False if the mapping type is unsupported or the alias was not present.
189
188
  """
@@ -789,29 +788,18 @@ def bot_command(command, event):
789
788
 
790
789
  async def connect_matrix(passed_config=None):
791
790
  """
792
- Create and initialize a matrix-nio AsyncClient connected to the configured Matrix homeserver, optionally enabling End-to-End Encryption (E2EE).
793
-
794
- This routine selects credentials in the following order:
795
- 1. credentials.json (preferred; restores full session including device_id and E2EE store).
796
- 2. Automatic login using username/password from the provided config (saved to credentials.json on success).
797
- 3. Direct token-based values from the config matrix section.
791
+ Initialize and return a configured matrix-nio AsyncClient connected to the configured Matrix homeserver.
798
792
 
799
- Behavior summary:
800
- - Validates presence of a top-level "matrix_rooms" configuration and raises ValueError if missing.
801
- - Builds an AsyncClient with a certifi-backed SSL context when available.
802
- - When E2EE is enabled in configuration and dependencies are present, prepares the encryption store, restores session state, and uploads device keys if needed.
803
- - Performs an initial full_state sync and resolves room aliases configured as aliases to room IDs.
804
- - Populates module-level globals used elsewhere (e.g., matrix_client, bot_user_name, matrix_homeserver, matrix_rooms, matrix_access_token, bot_user_id).
805
- - Returns the initialized AsyncClient instance ready for use.
793
+ Creates or restores client credentials (prefers credentials.json, falls back to automatic login using username/password from config, then to direct tokens in config), optionally enables End-to-End Encryption when configured and dependencies are available, performs an initial full-state sync to populate rooms, resolves room aliases found in configuration, and sets module-level connection state used by other functions.
806
794
 
807
795
  Parameters:
808
- passed_config (dict | None): Optional configuration to use for this connection attempt. If provided, it replaces the module-level config for this call.
796
+ passed_config (dict | None): Optional configuration to use for this connection attempt; when provided it overrides the module-level config for this call.
809
797
 
810
798
  Returns:
811
- AsyncClient | None: An initialized matrix-nio AsyncClient on success, or None if connection cannot be established due to missing configuration/credentials.
799
+ AsyncClient | None: A ready-to-use matrix-nio AsyncClient on success, or `None` if connection or credentials are unavailable.
812
800
 
813
801
  Raises:
814
- ValueError: If the top-level "matrix_rooms" configuration is missing from the (effective) config.
802
+ ValueError: If the required top-level "matrix_rooms" configuration is missing.
815
803
  ConnectionError: If the initial Matrix sync fails or times out.
816
804
  """
817
805
  global matrix_client, bot_user_name, matrix_homeserver, matrix_rooms, matrix_access_token, bot_user_id, config
@@ -1010,8 +998,15 @@ async def connect_matrix(passed_config=None):
1010
998
  # Also check for other required E2EE dependencies unless tests skip them
1011
999
  if os.getenv("MMRELAY_TESTING") != "1":
1012
1000
  try:
1013
- from nio.crypto import OlmDevice as _OlmDevice
1014
- from nio.store import SqliteStore as _SqliteStore
1001
+ nio_crypto = importlib.import_module("nio.crypto")
1002
+ if not hasattr(nio_crypto, "OlmDevice"):
1003
+ raise ImportError("nio.crypto.OlmDevice is unavailable")
1004
+
1005
+ nio_store = importlib.import_module("nio.store")
1006
+ if not hasattr(nio_store, "SqliteStore"):
1007
+ raise ImportError(
1008
+ "nio.store.SqliteStore is unavailable"
1009
+ )
1015
1010
 
1016
1011
  logger.debug("All E2EE dependencies are available")
1017
1012
  except ImportError:
@@ -1034,6 +1029,10 @@ async def connect_matrix(passed_config=None):
1034
1029
  "Skipping additional E2EE dependency imports in test mode"
1035
1030
  )
1036
1031
 
1032
+ if e2ee_enabled:
1033
+ # Ensure nio still receives a store path even when dependency
1034
+ # checks are skipped (e.g. production runs without MMRELAY_TESTING);
1035
+ # without this the client will not load encryption state.
1037
1036
  # Get store path from config or use default
1038
1037
  if (
1039
1038
  "encryption" in config["matrix"]
@@ -1050,8 +1049,6 @@ async def connect_matrix(passed_config=None):
1050
1049
  config["matrix"]["e2ee"]["store_path"]
1051
1050
  )
1052
1051
  else:
1053
- from mmrelay.config import get_e2ee_store_dir
1054
-
1055
1052
  e2ee_store_path = get_e2ee_store_dir()
1056
1053
 
1057
1054
  # Create store directory if it doesn't exist
@@ -1215,7 +1212,7 @@ async def connect_matrix(passed_config=None):
1215
1212
  async def _resolve_alias(alias: str) -> Optional[str]:
1216
1213
  """
1217
1214
  Resolve a Matrix room alias to its canonical room ID.
1218
-
1215
+
1219
1216
  Attempts to resolve the provided room alias using the module's Matrix client. Returns the resolved room ID string on success; returns None if the alias cannot be resolved or if an error/timeout occurs (errors from the underlying nio client are caught and handled internally).
1220
1217
  """
1221
1218
  logger.debug(f"Resolving alias from config: {alias}")
@@ -1778,17 +1775,17 @@ async def login_matrix_bot(
1778
1775
  async def join_matrix_room(matrix_client, room_id_or_alias: str) -> None:
1779
1776
  """
1780
1777
  Join the bot to a Matrix room by ID or alias.
1781
-
1778
+
1782
1779
  Resolves a room alias (e.g. "#room:server") to its canonical room ID, updates the in-memory
1783
1780
  matrix_rooms mapping with the resolved ID (if available), and attempts to join the resolved
1784
1781
  room ID. No-op if the client is already joined to the room. Errors during alias resolution
1785
1782
  or join are caught and logged; the function does not raise exceptions.
1786
-
1783
+
1787
1784
  Parameters documented only where meaning is not obvious:
1788
1785
  room_id_or_alias (str): A Matrix room identifier, either a canonical room ID (e.g. "!abc:server")
1789
1786
  or a room alias (starts with '#'). When an alias is provided, it will be resolved and
1790
1787
  the resolved room ID will be used for joining and recorded in the module's matrix_rooms mapping.
1791
-
1788
+
1792
1789
  Returns:
1793
1790
  None
1794
1791
  """
@@ -2221,12 +2218,12 @@ def strip_quoted_lines(text: str) -> str:
2221
2218
  async def get_user_display_name(room, event):
2222
2219
  """
2223
2220
  Return the display name for the event sender, preferring a room-specific name.
2224
-
2221
+
2225
2222
  If the room provides a per-room display name for the sender, that name is returned.
2226
2223
  Otherwise the function performs an asynchronous lookup against the homeserver for the
2227
2224
  user's global display name and returns it if present. If no display name is available,
2228
2225
  the sender's Matrix ID (MXID) is returned.
2229
-
2226
+
2230
2227
  Returns:
2231
2228
  str: A human-readable display name or the sender's MXID.
2232
2229
  """
@@ -2320,15 +2317,20 @@ async def send_reply_to_meshtastic(
2320
2317
  reply_id=None,
2321
2318
  ):
2322
2319
  """
2323
- Enqueue a Matrix reply for transmission over Meshtastic, either as a structured reply or a regular broadcast.
2324
-
2325
- If Meshtastic broadcasting is disabled the function returns without action. When storage_enabled is True the function will create a mapping entry (using event.event_id and room.room_id) and attach it to the queued message when possible. If reply_id is provided the message is sent as a structured reply targeting that Meshtastic message ID; otherwise it is sent as a regular text broadcast. Failures are logged; the function does not raise on enqueue errors.
2326
- Parameters that add non-obvious context:
2327
- room_config (dict): Room-specific configuration — must include "meshtastic_channel" (an integer channel index).
2328
- room: Matrix room object (room.room_id is used for mapping metadata).
2329
- event: Matrix event object (event.event_id is used for mapping metadata).
2330
- storage_enabled (bool): When True, attempt to create and attach a message-mapping record to the queued item.
2331
- reply_id (int | None): If provided, send as a structured reply targeting this Meshtastic message ID.
2320
+ Enqueue a Matrix reply to be delivered over Meshtastic as either a structured reply or a regular broadcast.
2321
+
2322
+ If broadcasting is disabled this function does nothing. When storage_enabled is True, it constructs a mapping record that links the originating Matrix event to the Meshtastic message and attaches it to the queued message so replies and reactions can be correlated later. Errors are logged; the function does not raise.
2323
+
2324
+ Parameters:
2325
+ reply_message (str): Text payload already formatted for Meshtastic.
2326
+ full_display_name (str): Sender display name used in queue descriptions and logs.
2327
+ room_config (dict): Room-specific configuration; must contain "meshtastic_channel" (integer channel index).
2328
+ room: Matrix room object; its room_id is used for mapping metadata.
2329
+ event: Matrix event object; its event_id is used for mapping metadata.
2330
+ text (str): Original Matrix message text used when building mapping metadata.
2331
+ storage_enabled (bool): If True, create and attach a message-mapping record to the queued Meshtastic message.
2332
+ local_meshnet_name (str | None): Local meshnet name included in mapping metadata when present.
2333
+ reply_id (int | None): If provided, send as a structured Meshtastic reply targeting this Meshtastic message ID; otherwise send a regular broadcast.
2332
2334
  """
2333
2335
  loop = asyncio.get_running_loop()
2334
2336
  meshtastic_interface = await loop.run_in_executor(None, connect_meshtastic)
@@ -2433,22 +2435,26 @@ async def handle_matrix_reply(
2433
2435
  meshnet_name=None,
2434
2436
  ):
2435
2437
  """
2436
- Relay a Matrix reply back to Meshtastic when the replied-to Matrix event maps to a Meshtastic message.
2437
-
2438
- If the replied-to Matrix event has a stored Meshtastic mapping, format a Meshtastic reply preserving sender attribution and queue it as a reply that references the original Meshtastic message ID. If no mapping exists, do nothing and return False so normal Matrix processing can continue.
2439
-
2438
+ Forward a Matrix reply to Meshtastic when the replied-to Matrix event maps to a Meshtastic message.
2439
+
2440
+ If the Matrix event identified by reply_to_event_id has an associated Meshtastic mapping, format a Meshtastic reply that preserves sender attribution and enqueue it referencing the original Meshtastic message ID. If no mapping exists, do nothing.
2441
+
2440
2442
  Parameters:
2441
- reply_to_event_id (str): Matrix event ID being replied to; used to locate the corresponding Meshtastic mapping.
2442
- storage_enabled (bool): If True, message mappings may be created/updated when sending the reply.
2443
- local_meshnet_name (str): Local meshnet name used to determine cross-meshnet reply formatting.
2444
- config (dict): Relay configuration passed to formatting routines.
2445
- mesh_text_override (str | None): Optional override text to send to Meshtastic instead of derived text.
2446
- longname (str | None): Sender long display name used for prefixing in the Meshtastic message.
2447
- shortname (str | None): Sender short display name used for prefixing in the Meshtastic message.
2448
- meshnet_name (str | None): Remote meshnet name of the original Matrix/meshtastic mapping (if any).
2449
-
2443
+ room: Matrix room object where the reply originated.
2444
+ event: Matrix event object representing the reply.
2445
+ reply_to_event_id (str): Matrix event ID being replied to; used to locate the Meshtastic mapping.
2446
+ text (str): The reply text from Matrix.
2447
+ room_config (dict): Per-room relay configuration used when sending to Meshtastic.
2448
+ storage_enabled (bool): Whether message mapping/storage is enabled.
2449
+ local_meshnet_name (str): Local meshnet name used to determine cross-meshnet formatting.
2450
+ config (dict): Global relay configuration passed to formatting routines.
2451
+ mesh_text_override (str | None): Optional override text to send instead of the derived text.
2452
+ longname (str | None): Sender long display name used for prefixing.
2453
+ shortname (str | None): Sender short display name used for prefixing.
2454
+ meshnet_name (str | None): Remote meshnet name associated with the original mapping, if any.
2455
+
2450
2456
  Returns:
2451
- bool: True if a mapping was found and the reply was queued to Meshtastic; False if no mapping existed and nothing was sent.
2457
+ bool: `True` if a mapping was found and the reply was queued to Meshtastic, `False` otherwise.
2452
2458
  """
2453
2459
  # Look up the original message in the message map
2454
2460
  loop = asyncio.get_running_loop()
@@ -2545,23 +2551,14 @@ async def on_room_message(
2545
2551
  ],
2546
2552
  ) -> None:
2547
2553
  """
2548
- Handle an incoming Matrix room event and, when applicable, relay it to Meshtastic.
2549
-
2550
- Processes text, notice, emote, and reaction events (including replies and messages relayed from other meshnets) for configured rooms. Behavior highlights:
2551
- - Ignores events from before the bot started and events sent by the bot itself.
2552
- - Uses per-room configuration and global interaction settings to decide whether to process or ignore the event.
2553
- - Routes reactions back to the originating Meshtastic message when a mapping exists (supports local and remote-meshnet reaction handling).
2554
- - Bridges Matrix replies to Meshtastic replies when a corresponding Meshtastic mapping is found and replies are enabled.
2555
- - Relays regular Matrix messages to Meshtastic using configured prefix/truncation rules; handles special detection-sensor port forwarding.
2556
- - Integrates with the plugin system; plugins may consume or modify messages. Messages identified as bot commands are not relayed to Meshtastic.
2557
-
2554
+ Handle an incoming Matrix room event and relay it to Meshtastic when applicable.
2555
+
2556
+ Processes text, notice, emote, and reaction events for configured rooms: ignores events from before the bot started and events sent by the bot itself; respects per-room configuration and global interaction settings; routes reactions back to the originating Meshtastic message when a mapping exists (including forwarding remote-meshnet emote reactions as radio text); bridges Matrix replies to Meshtastic replies when a corresponding mapping is found and replies are enabled; relays regular Matrix messages to Meshtastic using configured prefix and truncation rules; and honours detection-sensor forwarding when enabled. Integrates with the plugin system and treats recognized bot commands as non-relayed.
2557
+
2558
2558
  Side effects:
2559
2559
  - May enqueue Meshtastic send operations (text or data) via the internal queue.
2560
- - May read/write persistent message mappings to support reaction/reply bridging.
2561
- - May call Matrix APIs (e.g., to fetch display names).
2562
-
2563
- Returns:
2564
- - None
2560
+ - May read and write persistent message mappings to support reply/reaction bridging.
2561
+ - May call Matrix APIs (e.g., to fetch display names) and connect to Meshtastic.
2565
2562
  """
2566
2563
  # DEBUG: Log all Matrix message events to trace reception
2567
2564
  logger.debug(
@@ -2971,6 +2968,9 @@ async def on_room_message(
2971
2968
  if get_meshtastic_config_value(
2972
2969
  config, "detection_sensor", DEFAULT_DETECTION_SENSOR
2973
2970
  ):
2971
+ # Import meshtastic protobuf only when needed to delay logger creation
2972
+ import meshtastic.protobuf.portnums_pb2
2973
+
2974
2974
  success = queue_message(
2975
2975
  meshtastic_interface.sendData,
2976
2976
  data=full_message.encode("utf-8"),
@@ -44,8 +44,17 @@ from mmrelay.constants.network import (
44
44
  CONNECTION_TYPE_TCP,
45
45
  DEFAULT_BACKOFF_TIME,
46
46
  ERRNO_BAD_FILE_DESCRIPTOR,
47
- INFINITE_RETRIES,
47
+ INFINITE_RETRIES,
48
48
  )
49
+ from mmrelay.db_utils import (
50
+ get_longname,
51
+ get_message_map_by_meshtastic_id,
52
+ get_shortname,
53
+ save_longname,
54
+ save_shortname,
55
+ )
56
+ from mmrelay.log_utils import get_logger
57
+ from mmrelay.runtime_utils import is_running_as_service
49
58
 
50
59
  # Maximum number of timeout retries when retries are configured as infinite.
51
60
  MAX_TIMEOUT_RETRIES_INFINITE = 5
@@ -62,16 +71,6 @@ except ImportError:
62
71
  pass
63
72
 
64
73
 
65
- from mmrelay.db_utils import (
66
- get_longname,
67
- get_message_map_by_meshtastic_id,
68
- get_shortname,
69
- save_longname,
70
- save_shortname,
71
- )
72
- from mmrelay.log_utils import get_logger
73
- from mmrelay.runtime_utils import is_running_as_service
74
-
75
74
  # Global config variable that will be set from config.py
76
75
  config = None
77
76
 
@@ -153,15 +152,15 @@ def _submit_coro(coro, loop=None):
153
152
  def _resolve_plugin_timeout(cfg: dict | None, default: float = 5.0) -> float:
154
153
  """
155
154
  Resolve and return a positive plugin timeout value from the given configuration.
156
-
155
+
157
156
  Attempts to read meshtastic.plugin_timeout from cfg and convert it to a positive float.
158
157
  If the value is missing, non-numeric, or not greater than 0, the provided default is returned.
159
158
  Invalid or non-positive values will cause a warning to be logged.
160
-
159
+
161
160
  Parameters:
162
161
  cfg (dict | None): Configuration mapping that may contain a "meshtastic" section.
163
162
  default (float): Fallback timeout (seconds) used when cfg does not provide a valid value.
164
-
163
+
165
164
  Returns:
166
165
  float: A positive timeout in seconds.
167
166
  """
@@ -270,13 +269,13 @@ def serial_port_exists(port_name):
270
269
  def connect_meshtastic(passed_config=None, force_connect=False):
271
270
  """
272
271
  Establish and return a Meshtastic client connection (serial, BLE, or TCP), with configurable retries, exponential backoff, and single-time event subscription.
273
-
272
+
274
273
  Attempts to (re)connect using the module configuration and updates module-level state on success (meshtastic_client, matrix_rooms, and event subscriptions). Supports the legacy "network" alias for TCP, verifies serial port presence before connecting, and honors a retry limit (or infinite retries when unspecified). On successful connection the client's node info and firmware metadata are probed and message/connection-lost handlers are subscribed once for the process lifetime.
275
-
274
+
276
275
  Parameters:
277
276
  passed_config (dict, optional): If provided, replaces the module-level configuration (and may update matrix_rooms).
278
277
  force_connect (bool, optional): When True, forces creating a new connection even if one already exists.
279
-
278
+
280
279
  Returns:
281
280
  The connected Meshtastic client instance on success, or None if connection cannot be established or shutdown is in progress.
282
281
  """
@@ -464,7 +463,7 @@ def connect_meshtastic(passed_config=None, force_connect=False):
464
463
  subscribed_to_connection_lost = True
465
464
  logger.debug("Subscribed to meshtastic.connection.lost")
466
465
 
467
- except (ConnectionRefusedError, MemoryError) as e:
466
+ except (ConnectionRefusedError, MemoryError):
468
467
  # Handle critical errors that should not be retried
469
468
  logger.exception("Critical connection error")
470
469
  return None
@@ -1142,17 +1141,17 @@ def sendTextReply(
1142
1141
  ):
1143
1142
  """
1144
1143
  Send a Meshtastic text reply that references a previous Meshtastic message.
1145
-
1144
+
1146
1145
  Builds a Data payload containing `text` and `reply_id`, wraps it in a MeshPacket on `channelIndex`,
1147
1146
  and sends it using the provided Meshtastic interface.
1148
-
1147
+
1149
1148
  Parameters:
1150
1149
  text (str): UTF-8 text to send.
1151
1150
  reply_id (int): ID of the Meshtastic message being replied to.
1152
1151
  destinationId (int | str, optional): Recipient address or node ID (defaults to broadcast).
1153
1152
  wantAck (bool, optional): If True, request an acknowledgement for the packet.
1154
1153
  channelIndex (int, optional): Channel index to send the packet on.
1155
-
1154
+
1156
1155
  Returns:
1157
1156
  The result returned by the interface's _sendPacket call (typically the sent MeshPacket), or
1158
1157
  None if the interface is not available or sending fails.
mmrelay/message_queue.py CHANGED
@@ -473,11 +473,12 @@ class MessageQueue:
473
473
 
474
474
  def _should_send_message(self) -> bool:
475
475
  """
476
- Return True if it is currently safe to send a message via Meshtastic.
477
-
478
- Performs runtime checks: ensures the global reconnecting flag is not set, a Meshtastic client object is available, and if the client exposes `is_connected` (callable or boolean) that it reports connected. Returns False if any check fails.
479
-
480
- If importing the Meshtastic utilities raises ImportError, the method will asynchronously stop this MessageQueue and return False.
476
+ Check whether the queue may send a Meshtastic message.
477
+
478
+ Performs runtime checks: returns True only if the reconnection flag is not set, a Meshtastic client object exists, and—if the client exposes `is_connected`—that check indicates the client is connected. If importing Meshtastic utilities raises ImportError, a critical log is emitted and the queue is stopped asynchronously.
479
+
480
+ Returns:
481
+ `True` if not reconnecting, a Meshtastic client exists, and the client is connected when checkable; `False` otherwise.
481
482
  """
482
483
  # Import here to avoid circular imports
483
484
  try:
@@ -517,18 +518,18 @@ class MessageQueue:
517
518
 
518
519
  def _handle_message_mapping(self, result, mapping_info):
519
520
  """
520
- Persist a sent message mapping (mesh message id Matrix event) and optionally prune old mappings.
521
-
522
- If mapping_info contains 'matrix_event_id', 'room_id', and 'text', stores a mapping using result.id as the mesh message id. If 'msgs_to_keep' is present and > 0 it prunes older mappings to retain that many entries; otherwise DEFAULT_MSGS_TO_KEEP is used.
523
-
521
+ Persist a mapping from a sent Meshtastic message to a Matrix event and optionally prune old mappings.
522
+
523
+ Stores a mapping when `mapping_info` contains `matrix_event_id`, `room_id`, and `text`, using `result.id` as the Meshtastic message id. If `mapping_info["msgs_to_keep"]` is present and greater than 0, prunes older mappings to retain that many entries; otherwise uses DEFAULT_MSGS_TO_KEEP.
524
+
524
525
  Parameters:
525
- result: Send function result object with an `id` attribute (the mesh message id).
526
+ result: An object returned by the send function with an `id` attribute representing the Meshtastic message id.
526
527
  mapping_info (dict): Mapping details. Relevant keys:
527
- - matrix_event_id (str)
528
- - room_id (str)
529
- - text (str)
530
- - meshnet (optional): passed to the store operation
531
- - msgs_to_keep (optional, int): number of mappings to retain for pruning
528
+ - matrix_event_id (str): Matrix event ID to map to.
529
+ - room_id (str): Matrix room ID where the event was sent.
530
+ - text (str): Message text to associate with the mapping.
531
+ - meshnet (optional): Mesh network identifier to pass to storage.
532
+ - msgs_to_keep (optional, int): Number of mappings to retain when pruning.
532
533
  """
533
534
  try:
534
535
  # Import here to avoid circular imports
@@ -11,7 +11,7 @@ from mmrelay.constants.database import (
11
11
  DEFAULT_MAX_DATA_ROWS_PER_NODE_BASE,
12
12
  DEFAULT_TEXT_TRUNCATION_LENGTH,
13
13
  )
14
- from mmrelay.constants.queue import DEFAULT_MESSAGE_DELAY
14
+ from mmrelay.constants.queue import DEFAULT_MESSAGE_DELAY, MINIMUM_MESSAGE_DELAY
15
15
  from mmrelay.db_utils import (
16
16
  delete_plugin_data,
17
17
  get_plugin_data,
@@ -77,7 +77,7 @@ class BasePlugin(ABC):
77
77
  Raises:
78
78
  ValueError: If the plugin name is not set via parameter or class attribute.
79
79
 
80
- Loads plugin-specific configuration from the global config, validates assigned channels, and determines the response delay, enforcing a minimum of 2.0 seconds. Logs a warning if deprecated configuration options are used or if channels are not mapped.
80
+ Loads plugin-specific configuration from the global config, validates assigned channels, and determines the response delay, enforcing a minimum of 2.1 seconds. Logs a warning if deprecated configuration options are used or if channels are not mapped.
81
81
  """
82
82
  # Allow plugin_name to be passed as a parameter for simpler initialization
83
83
  # This maintains backward compatibility while providing a cleaner API
@@ -174,12 +174,12 @@ class BasePlugin(ABC):
174
174
 
175
175
  if delay is not None:
176
176
  self.response_delay = delay
177
- # Enforce minimum delay of 2 seconds due to firmware constraints
178
- if self.response_delay < 2.0:
177
+ # Enforce minimum delay above firmware limit to prevent message dropping
178
+ if self.response_delay < MINIMUM_MESSAGE_DELAY:
179
179
  self.logger.warning(
180
- f"{delay_key} of {self.response_delay}s is below minimum of 2.0s (firmware constraint). Using 2.0s."
180
+ f"{delay_key} of {self.response_delay}s is below minimum of {MINIMUM_MESSAGE_DELAY}s (above firmware limit). Using {MINIMUM_MESSAGE_DELAY}s."
181
181
  )
182
- self.response_delay = 2.0
182
+ self.response_delay = MINIMUM_MESSAGE_DELAY
183
183
 
184
184
  def start(self):
185
185
  """
@@ -261,7 +261,7 @@ class BasePlugin(ABC):
261
261
  """
262
262
  Return the configured delay in seconds before sending a Meshtastic response.
263
263
 
264
- The delay is determined by the `meshtastic.message_delay` configuration option, defaulting to 2.2 seconds with a minimum of 2.0 seconds. The deprecated `plugin_response_delay` option is also supported for backward compatibility.
264
+ The delay is determined by the `meshtastic.message_delay` configuration option, defaulting to 2.5 seconds with a minimum of 2.1 seconds. The deprecated `plugin_response_delay` option is also supported for backward compatibility.
265
265
 
266
266
  Returns:
267
267
  float: The response delay in seconds.
@@ -122,7 +122,7 @@ class Plugin(BasePlugin):
122
122
 
123
123
  if not channel_mapped:
124
124
  self.logger.debug(f"Skipping message from unmapped channel {channel}")
125
- return None
125
+ return False
126
126
 
127
127
  await matrix_client.room_send(
128
128
  room_id=room["id"],
@@ -135,20 +135,19 @@ class Plugin(BasePlugin):
135
135
  },
136
136
  )
137
137
 
138
- return None
138
+ return True
139
139
 
140
140
  def matches(self, event):
141
141
  """
142
- Return True if the Matrix event's content body exactly matches the relayed-packet marker.
143
-
144
- Checks event.source["content"]["body"] (when a string) against the anchored pattern
145
- "Processed <anything> radio packet" and returns True on match, otherwise False.
146
-
142
+ Determine whether a Matrix event's message body contains the bridged-packet marker.
143
+
144
+ Checks event.source["content"]["body"] (when it is a string) against the anchored pattern `^Processed (.+) radio packet$`.
145
+
147
146
  Parameters:
148
- event: Matrix event object with a .source mapping expected to contain a "content" dict.
149
-
147
+ event: Matrix event object whose `.source` mapping is expected to contain a `"content"` dict with a `"body"` string.
148
+
150
149
  Returns:
151
- bool: True if the body matches the relayed-packet pattern, False otherwise.
150
+ True if the content body matches `^Processed (.+) radio packet$`, False otherwise.
152
151
  """
153
152
  # Check for the presence of necessary keys in the event
154
153
  content = event.source.get("content", {})
@@ -181,7 +180,7 @@ class Plugin(BasePlugin):
181
180
  """
182
181
  # Use the event for matching instead of full_message
183
182
  if not self.matches(event):
184
- return None
183
+ return False
185
184
 
186
185
  channel = None
187
186
  if config is not None:
@@ -192,18 +191,18 @@ class Plugin(BasePlugin):
192
191
 
193
192
  if channel is None:
194
193
  self.logger.debug(f"Skipping message from unmapped channel {channel}")
195
- return None
194
+ return False
196
195
 
197
196
  packet_json = event.source["content"].get("meshtastic_packet")
198
197
  if not packet_json:
199
198
  self.logger.debug("Missing embedded packet")
200
- return None
199
+ return False
201
200
 
202
201
  try:
203
202
  packet = json.loads(packet_json)
204
- except (json.JSONDecodeError, TypeError) as e:
205
- self.logger.exception(f"Error processing embedded packet: {e}")
206
- return None
203
+ except (json.JSONDecodeError, TypeError):
204
+ self.logger.exception("Error processing embedded packet")
205
+ return False
207
206
 
208
207
  from mmrelay.meshtastic_utils import connect_meshtastic
209
208
 
@@ -220,4 +219,4 @@ class Plugin(BasePlugin):
220
219
  meshtastic_client._sendPacket(
221
220
  meshPacket=meshPacket, destinationId=packet["toId"]
222
221
  )
223
- return None
222
+ return True
mmrelay/setup_utils.py CHANGED
@@ -359,11 +359,11 @@ def is_service_active():
359
359
  def create_service_file():
360
360
  """
361
361
  Create or update the per-user systemd unit file for MMRelay.
362
-
363
- Ensures the user systemd directory (~/.config/systemd/user) and the MMRelay logs directory (~/.mmrelay/logs) exist, obtains a service unit template (using the module's template-loading fallbacks), substitutes known placeholders (working directory, packaged launcher, and config path), and normalizes the Unit's ExecStart to the resolved MMRelay invocation (an mmrelay executable on PATH or a Python `-m mmrelay` fallback) while preserving any trailing arguments. The final unit is written to ~/.config/systemd/user/mmrelay.service.
364
-
362
+
363
+ Ensures the user systemd directory (~/.config/systemd/user) and the MMRelay logs directory (~/.mmrelay/logs) exist, obtains a service unit template using the module's template-loading fallbacks, substitutes known placeholders (working directory, packaged launcher, and config path), normalizes the Unit's ExecStart to the resolved MMRelay invocation (an mmrelay executable on PATH or a Python `-m mmrelay` fallback) while preserving any trailing arguments, and writes the resulting unit to ~/.config/systemd/user/mmrelay.service.
364
+
365
365
  Returns:
366
- bool: True if the service file was written successfully; False if no template could be obtained or writing the file failed.
366
+ bool: True if the service file was written successfully; False if a template could not be obtained or writing the file failed.
367
367
  """
368
368
  # Get executable paths once to avoid duplicate calls and output
369
369
  executable_path = get_executable_path()
@@ -581,7 +581,7 @@ def check_lingering_enabled():
581
581
  def enable_lingering():
582
582
  """
583
583
  Enable systemd "lingering" for the current user by running `sudo loginctl enable-linger <user>`.
584
-
584
+
585
585
  Determines the username from environment variables or getpass.getuser(), invokes the privileged `loginctl` command to enable lingering, and returns True if the command exits successfully. On failure (non-zero exit, missing username, or subprocess/OSError), returns False and prints an error message to stderr.
586
586
  """
587
587
  try:
@@ -7,13 +7,24 @@ services:
7
7
  user: "${UID:-1000}:${GID:-1000}"
8
8
  environment:
9
9
  - TZ=UTC # Set timezone (PYTHONUNBUFFERED and MPLCONFIGDIR are set in Dockerfile)
10
+
10
11
  volumes:
11
12
  # Mount your config directory - create ~/.mmrelay/config.yaml first
12
13
  # See docs/DOCKER.md for setup instructions
13
- # For SELinux systems (RHEL/CentOS/Fedora), add :Z flag to prevent permission denied errors
14
- - ${MMRELAY_HOME:-$HOME}/.mmrelay/config.yaml:/app/config.yaml:ro,Z
15
- - ${MMRELAY_HOME:-$HOME}/.mmrelay:/app/data:Z
16
- # For non-SELinux systems, you can use:
17
- # - ${MMRELAY_HOME:-$HOME}/.mmrelay/config.yaml:/app/config.yaml:ro
18
- # - ${MMRELAY_HOME:-$HOME}/.mmrelay:/app/data
19
- # Tip: For correct permissions and paths, ensure UID, GID, and MMRELAY_HOME are set in a .env file or exported
14
+ # For non-SELinux systems (most common):
15
+ - ${MMRELAY_HOME:-$HOME}/.mmrelay/config.yaml:/app/config.yaml:ro
16
+ - ${MMRELAY_HOME:-$HOME}/.mmrelay:/app/data
17
+ # For SELinux systems (RHEL/CentOS/Fedora), add :Z flag to prevent permission denied errors:
18
+ # - ${MMRELAY_HOME:-$HOME}/.mmrelay/config.yaml:/app/config.yaml:ro,Z
19
+ # - ${MMRELAY_HOME:-$HOME}/.mmrelay:/app/data:Z
20
+
21
+ # For BLE connections, uncomment these lines (Linux only):
22
+ # - /var/run/dbus:/var/run/dbus:ro
23
+
24
+ # For BLE connections, uncomment these options (Linux only). See DOCKER.md for alternatives.
25
+ # network_mode: host
26
+ # security_opt:
27
+ # - apparmor=unconfined # Recommended for BLE to allow DBus communication
28
+ # privileged: true # Alternative if apparmor=unconfined is not acceptable
29
+
30
+ # Tip: For correct permissions and paths, ensure UID, GID, and MMRELAY_HOME are set in a .env file or exported
@@ -7,13 +7,24 @@ services:
7
7
  user: "${UID:-1000}:${GID:-1000}"
8
8
  environment:
9
9
  - TZ=UTC # Set timezone (PYTHONUNBUFFERED and MPLCONFIGDIR are set in Dockerfile)
10
+
10
11
  volumes:
11
12
  # Mount your config directory - create ~/.mmrelay/config.yaml first
12
13
  # Run 'make config' to set up the files
13
- # For SELinux systems (RHEL/CentOS/Fedora), add :Z flag to prevent permission denied errors
14
- - ${MMRELAY_HOME:-$HOME}/.mmrelay/config.yaml:/app/config.yaml:ro,Z
15
- - ${MMRELAY_HOME:-$HOME}/.mmrelay:/app/data:Z
16
- # For non-SELinux systems, you can use:
17
- # - ${MMRELAY_HOME:-$HOME}/.mmrelay/config.yaml:/app/config.yaml:ro
18
- # - ${MMRELAY_HOME:-$HOME}/.mmrelay:/app/data
19
- # Tip: For correct permissions and paths, ensure UID, GID, and MMRELAY_HOME are set in a .env file or exported
14
+ # For non-SELinux systems (most common):
15
+ - ${MMRELAY_HOME:-$HOME}/.mmrelay/config.yaml:/app/config.yaml:ro
16
+ - ${MMRELAY_HOME:-$HOME}/.mmrelay:/app/data
17
+ # For SELinux systems (RHEL/CentOS/Fedora), add :Z flag to prevent permission denied errors:
18
+ # - ${MMRELAY_HOME:-$HOME}/.mmrelay/config.yaml:/app/config.yaml:ro,Z
19
+ # - ${MMRELAY_HOME:-$HOME}/.mmrelay:/app/data:Z
20
+
21
+ # For BLE connections, uncomment these lines (Linux only):
22
+ # - /var/run/dbus:/var/run/dbus:ro
23
+
24
+ # For BLE connections, uncomment these options (Linux only). See DOCKER.md for alternatives.
25
+ # network_mode: host
26
+ # security_opt:
27
+ # - apparmor=unconfined # Recommended for BLE to allow DBus communication
28
+ # privileged: true # Alternative if apparmor=unconfined is not acceptable
29
+
30
+ # Tip: For correct permissions and paths, ensure UID, GID, and MMRELAY_HOME are set in a .env file or exported
mmrelay/windows_utils.py CHANGED
@@ -28,7 +28,7 @@ def is_windows() -> bool:
28
28
  def setup_windows_console() -> None:
29
29
  """
30
30
  Configure the Windows console for UTF-8 output and ANSI (VT100) color support.
31
-
31
+
32
32
  Best-effort operation: on Windows this attempts to set stdout/stderr encoding to UTF-8
33
33
  (if supported) and enable Virtual Terminal Processing so ANSI color sequences and
34
34
  Unicode render correctly. No-op on non-Windows platforms; failures are silently ignored.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mmrelay
3
- Version: 1.2.2
3
+ Version: 1.2.4
4
4
  Summary: Bridge between Meshtastic mesh networks and Matrix chat rooms
5
5
  Home-page: https://github.com/jeremiah-k/meshtastic-matrix-relay
6
6
  Author: Geoff Whittington, Jeremiah K., and contributors
@@ -18,7 +18,7 @@ Classifier: Topic :: Communications
18
18
  Requires-Python: >=3.9
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
- Requires-Dist: meshtastic>=2.6.4
21
+ Requires-Dist: meshtastic>=2.7.3
22
22
  Requires-Dist: Pillow==11.3.0
23
23
  Requires-Dist: matrix-nio==0.25.2
24
24
  Requires-Dist: matplotlib==3.10.1
@@ -1,19 +1,19 @@
1
- mmrelay/__init__.py,sha256=vnKf5ECNyF96YnmUUlBJS4Xwn1kTGb2VbI75clrcZV4,120
1
+ mmrelay/__init__.py,sha256=DMVdCPD3oN7x0SkEuR6Pq5etPc622-e-kkxsSN2rl2A,120
2
2
  mmrelay/__main__.py,sha256=Z839i5jxZmhdoPvR1-reWoQL4Xpmty81p8TsTKsJLC8,814
3
- mmrelay/cli.py,sha256=PNUnt1K83QI6vtPwc6hsL5Iaw9OaNDh5aS1Rlid64KE,79403
3
+ mmrelay/cli.py,sha256=T4apzc8aS7smXh0bpqoMSDHpHOOxcjls9-vSB-ue5Nk,79543
4
4
  mmrelay/cli_utils.py,sha256=h0JKhU_sToO2_Orf9ddO8x07_L1k201V61yXydfU2es,28887
5
- mmrelay/config.py,sha256=XOmQomLuQRl6MnvKOn8tSjU8jaDp6rurjtgacRkhFaw,38330
5
+ mmrelay/config.py,sha256=H93dRHAsZOtPQmeO5RZd-S7qx5zsoAn7bt1GgXdOPqw,38285
6
6
  mmrelay/db_utils.py,sha256=utt254MipaIRTaGqSAe7N9L_S7eIPSnQsOQj6Gk2E3U,22502
7
- mmrelay/e2ee_utils.py,sha256=krRXJzeo_tpW9QfcmpZe0A3kPoQ14tUrDQW7dxsx2sc,16656
8
- mmrelay/log_utils.py,sha256=psTzfRUTp1aVIE8U-7bzS7qc1ulU9EavqAEbDt8hUQA,9126
9
- mmrelay/main.py,sha256=mF1D7Psbk46CIOfGEtG3onG3Yn5Zk-3oZF_t2pTv1JY,16785
10
- mmrelay/matrix_utils.py,sha256=56VTa1ptf5eY-mcwUMTaAw09fztkoMs4j-UTi6nRUsY,134552
11
- mmrelay/meshtastic_utils.py,sha256=IJp0xD6HSqNEmOymVtgJaExtP83KhZrCM_SWDPqEIvU,48821
12
- mmrelay/message_queue.py,sha256=Ze9l_vDQBiWUX68jFvBbuMTJzyfcU4sW1jRyrN2tjC8,28005
7
+ mmrelay/e2ee_utils.py,sha256=7cSb3F-FYqFcibSmb7eJzQJoRdq4Xk0_L5oppza3PHE,16914
8
+ mmrelay/log_utils.py,sha256=CGTsHkeviCUKbe_SArYqOjPE5xRTslikMwzrag9LSFE,10290
9
+ mmrelay/main.py,sha256=TlINxwGgk5pVe8gUuxzv29tYlZPuHYdqLcYsmrkUurI,17015
10
+ mmrelay/matrix_utils.py,sha256=QPjwgkEKPgGLNFzeTXgfVusyfran7U_2NDs9UayTLWg,134586
11
+ mmrelay/meshtastic_utils.py,sha256=wbVa8HDjbYNOTDLttVMGeRwTsYt8zJilUv_63FnmHoo,48783
12
+ mmrelay/message_queue.py,sha256=aSitms4pr34VZ7nycpXuPw5ioA6x5xEdnwZ7_nh1Npw,28243
13
13
  mmrelay/plugin_loader.py,sha256=iXUpLGyjir7sfxtnL1REHdg5i5ZGWaHO_6sRQqf_JCE,59052
14
14
  mmrelay/runtime_utils.py,sha256=u8DtpfI-U7Ip3SAwjn214WfMTzc-xr5wffjN0i7WBZc,1261
15
- mmrelay/setup_utils.py,sha256=TNB2lRewKjt5aPFwVvQQeYZuE0eWvz3krn2pn-GZJvg,32738
16
- mmrelay/windows_utils.py,sha256=cXqXIszPAva-GLSY0Xxxai7PO7UBryAb9aPhdHdDo0I,13064
15
+ mmrelay/setup_utils.py,sha256=SCFGx9e1wHZt87u2JMAhPUYOy-Ui4eV3irmzy0Zh9lE,32727
16
+ mmrelay/windows_utils.py,sha256=FqjjsrCWhYxvEH5TcfVBuONCpA19VX9kcx7jHKsRWbM,13060
17
17
  mmrelay/constants/__init__.py,sha256=M8AXeIcS1JuS8OwmfTmhcCOkAz5XmWlNQ53GBxYOx94,1494
18
18
  mmrelay/constants/app.py,sha256=iNKqQMp5WZSfzsuRXYZ00znVm_ZyfwqiWMQgAblG6rY,725
19
19
  mmrelay/constants/config.py,sha256=B1A_OZLLNXHM9rHOwc01ZaJBvFugR2eXlhYYl4nUxlY,2479
@@ -21,28 +21,28 @@ mmrelay/constants/database.py,sha256=4cHfYfBePDUUtVSflrWyStcxKSQv7VE-jSrb1IzAjls
21
21
  mmrelay/constants/formats.py,sha256=cjbrfNNFCKoGSFsFHR1QQDEQudiGquA9MUapfm0_ZNI,494
22
22
  mmrelay/constants/messages.py,sha256=Reu_-6gZGGZQVP6BuqBm01QBhVTFjHVRQSPTUcQJG2Q,1531
23
23
  mmrelay/constants/network.py,sha256=QjROOAMxcP1pA1F_gUtuXzm2tORCqo5koqZbwrZRSns,1186
24
- mmrelay/constants/queue.py,sha256=yyWSrtq06b5GWzZwdl6IFtrMvxEuF9PdKSNPh8DdL2M,565
24
+ mmrelay/constants/queue.py,sha256=cNK0cLsgBfQ6nVJm9_Scxj-ICv4RU6DU_PYsUwNyU_E,675
25
25
  mmrelay/plugins/__init__.py,sha256=KVMQIXRhe0wlGj4O3IZ0vOIQRKFkfPYejHXhJL17qrc,51
26
- mmrelay/plugins/base_plugin.py,sha256=Nh_zz-Z49qYatS4i38C2bLOl1sNDUgWsqpD8NLJdnl0,20807
26
+ mmrelay/plugins/base_plugin.py,sha256=lcgKwrAMAIT9RjiC1Nyfs3fgdzUu3OQhCTZEjqh1_wg,20915
27
27
  mmrelay/plugins/debug_plugin.py,sha256=adX0cRJHUEDLldajybPfiRDDlvytkZe5aN_dSgNKP2Y,870
28
28
  mmrelay/plugins/drop_plugin.py,sha256=x4S-e0Muun2Dy1H2qwRMTBB1ptLmy7ZZJhgPu-KefGs,5394
29
29
  mmrelay/plugins/health_plugin.py,sha256=svV_GfpAVL0QhiVzi3PVZ1mNpsOL1NHSmkRF-Mn_ExE,2250
30
30
  mmrelay/plugins/help_plugin.py,sha256=S7nBhsANK46Zv9wPHOVegPGcuYGMErBsxAnrRlSSCwg,2149
31
31
  mmrelay/plugins/map_plugin.py,sha256=eHV_t3TFcypBD4xT_OQx0hD6_iGkLJOADjwYVny0PvE,11292
32
- mmrelay/plugins/mesh_relay_plugin.py,sha256=O5I_5H4Y8CSonuLoS4lGhlNee1F7RVmSv551WCSsufM,8477
32
+ mmrelay/plugins/mesh_relay_plugin.py,sha256=OE07Kz5NvmyjXH3hneK5_eTPQ41mriJGZgsPcg-gpOQ,8428
33
33
  mmrelay/plugins/nodes_plugin.py,sha256=RDabzyG5hKG5aYWecsRUcLSjMCCv6Pngmq2Qpld1A1U,2903
34
34
  mmrelay/plugins/ping_plugin.py,sha256=8uFnT3qfO3RBaTUOx348voIfKpzXB3zTfcT6Gtfc8kM,4070
35
35
  mmrelay/plugins/telemetry_plugin.py,sha256=8SxWv4BLXMUTbiVaD3MjlMMdQyS7S_1OfLlVNAUMSO0,6306
36
36
  mmrelay/plugins/weather_plugin.py,sha256=ZmTNlkEjeCvZWHlO7VMend3vnj4h2qWeeU4RUQJY2vM,14183
37
37
  mmrelay/tools/__init__.py,sha256=WFjDQjdevgg19_zT6iEoL29rvb1JPqYSd8708Jn5D7A,838
38
38
  mmrelay/tools/mmrelay.service,sha256=6_TAskmTh9pXQSDRDeh0EXl2BfsUgm9Q3W9ob5tWCS8,600
39
- mmrelay/tools/sample-docker-compose-prebuilt.yaml,sha256=RT3IWA4DuLfIB5C7fUT1cDaYhOAxXiUjBan1M6PlvKc,958
40
- mmrelay/tools/sample-docker-compose.yaml,sha256=pbE9nXSA9DcEbdwhCQ0mZzzmhYRi3L9y8crx3a_YrTE,940
39
+ mmrelay/tools/sample-docker-compose-prebuilt.yaml,sha256=ytQRW882VAecbWO1LsRsU6UbudybNhC-iv2w0Fta1Ls,1368
40
+ mmrelay/tools/sample-docker-compose.yaml,sha256=1UqZFvnJuhVwfNJ92C0qQ5KSdNRVjk8YbRNZgD8yF_M,1350
41
41
  mmrelay/tools/sample.env,sha256=RP-o3rX3jnEIrVG2rqCZq31O1yRXou4HcGrXWLVbKKw,311
42
42
  mmrelay/tools/sample_config.yaml,sha256=odAeiU-wesWuwnZGyXzVjxaXVYc26p8QI1HOEA-Tvco,6396
43
- mmrelay-1.2.2.dist-info/licenses/LICENSE,sha256=aB_07MhnK-bL5WLI1ucXLUSdW_yBVoepPRYB0kaAOl8,35204
44
- mmrelay-1.2.2.dist-info/METADATA,sha256=JKh3iEvvM3Tgr01iAyWuhPPxOkMcpxgPgXQFXgsLtyY,6167
45
- mmrelay-1.2.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
46
- mmrelay-1.2.2.dist-info/entry_points.txt,sha256=SJZwGUOEpQ-qx4H8UL4xKFnKeInGUaZNW1I0ddjK7Ws,45
47
- mmrelay-1.2.2.dist-info/top_level.txt,sha256=B_ZLCRm7NYAmI3PipRUyHGymP-C-q16LSeMGzmqJfo4,8
48
- mmrelay-1.2.2.dist-info/RECORD,,
43
+ mmrelay-1.2.4.dist-info/licenses/LICENSE,sha256=aB_07MhnK-bL5WLI1ucXLUSdW_yBVoepPRYB0kaAOl8,35204
44
+ mmrelay-1.2.4.dist-info/METADATA,sha256=SOYGlz1SUe7pRR-VESGmmpy_jO49fwctDcQFvvkjBHo,6167
45
+ mmrelay-1.2.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
46
+ mmrelay-1.2.4.dist-info/entry_points.txt,sha256=SJZwGUOEpQ-qx4H8UL4xKFnKeInGUaZNW1I0ddjK7Ws,45
47
+ mmrelay-1.2.4.dist-info/top_level.txt,sha256=B_ZLCRm7NYAmI3PipRUyHGymP-C-q16LSeMGzmqJfo4,8
48
+ mmrelay-1.2.4.dist-info/RECORD,,