mmrelay 1.2.3__tar.gz → 1.2.4__tar.gz

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.

Files changed (114) hide show
  1. {mmrelay-1.2.3/src/mmrelay.egg-info → mmrelay-1.2.4}/PKG-INFO +2 -2
  2. {mmrelay-1.2.3 → mmrelay-1.2.4}/requirements.txt +2 -2
  3. {mmrelay-1.2.3 → mmrelay-1.2.4}/setup.py +1 -1
  4. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/__init__.py +1 -1
  5. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/cli.py +19 -23
  6. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/config.py +5 -7
  7. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/constants/queue.py +4 -1
  8. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/log_utils.py +29 -8
  9. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/main.py +13 -4
  10. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/matrix_utils.py +41 -41
  11. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/message_queue.py +12 -11
  12. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/plugins/base_plugin.py +7 -7
  13. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/plugins/mesh_relay_plugin.py +16 -18
  14. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/setup_utils.py +2 -2
  15. mmrelay-1.2.4/src/mmrelay/tools/sample-docker-compose-prebuilt.yaml +30 -0
  16. mmrelay-1.2.4/src/mmrelay/tools/sample-docker-compose.yaml +30 -0
  17. {mmrelay-1.2.3 → mmrelay-1.2.4/src/mmrelay.egg-info}/PKG-INFO +2 -2
  18. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay.egg-info/requires.txt +1 -1
  19. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_auth_flow_fixes.py +5 -7
  20. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_base_plugin.py +4 -4
  21. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_e2ee_unified.py +37 -41
  22. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_log_utils.py +68 -0
  23. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_matrix_utils.py +395 -194
  24. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_matrix_utils_error_handling.py +3 -3
  25. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_mesh_relay_plugin.py +86 -20
  26. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_performance_stress.py +8 -11
  27. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_plugin_loader.py +141 -3
  28. mmrelay-1.2.3/src/mmrelay/tools/sample-docker-compose-prebuilt.yaml +0 -19
  29. mmrelay-1.2.3/src/mmrelay/tools/sample-docker-compose.yaml +0 -19
  30. {mmrelay-1.2.3 → mmrelay-1.2.4}/LICENSE +0 -0
  31. {mmrelay-1.2.3 → mmrelay-1.2.4}/MANIFEST.in +0 -0
  32. {mmrelay-1.2.3 → mmrelay-1.2.4}/README.md +0 -0
  33. {mmrelay-1.2.3 → mmrelay-1.2.4}/pyproject.toml +0 -0
  34. {mmrelay-1.2.3 → mmrelay-1.2.4}/setup.cfg +0 -0
  35. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/__main__.py +0 -0
  36. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/cli_utils.py +0 -0
  37. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/constants/__init__.py +0 -0
  38. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/constants/app.py +0 -0
  39. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/constants/config.py +0 -0
  40. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/constants/database.py +0 -0
  41. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/constants/formats.py +0 -0
  42. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/constants/messages.py +0 -0
  43. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/constants/network.py +0 -0
  44. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/db_utils.py +0 -0
  45. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/e2ee_utils.py +0 -0
  46. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/meshtastic_utils.py +0 -0
  47. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/plugin_loader.py +0 -0
  48. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/plugins/__init__.py +0 -0
  49. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/plugins/debug_plugin.py +0 -0
  50. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/plugins/drop_plugin.py +0 -0
  51. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/plugins/health_plugin.py +0 -0
  52. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/plugins/help_plugin.py +0 -0
  53. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/plugins/map_plugin.py +0 -0
  54. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/plugins/nodes_plugin.py +0 -0
  55. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/plugins/ping_plugin.py +0 -0
  56. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/plugins/telemetry_plugin.py +0 -0
  57. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/plugins/weather_plugin.py +0 -0
  58. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/runtime_utils.py +0 -0
  59. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/tools/__init__.py +0 -0
  60. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/tools/mmrelay.service +0 -0
  61. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/tools/sample.env +0 -0
  62. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/tools/sample_config.yaml +0 -0
  63. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay/windows_utils.py +0 -0
  64. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay.egg-info/SOURCES.txt +0 -0
  65. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay.egg-info/dependency_links.txt +0 -0
  66. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay.egg-info/entry_points.txt +0 -0
  67. {mmrelay-1.2.3 → mmrelay-1.2.4}/src/mmrelay.egg-info/top_level.txt +0 -0
  68. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_async_patterns.py +0 -0
  69. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_cli.py +0 -0
  70. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_cli_diagnose.py +0 -0
  71. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_cli_edge_cases.py +0 -0
  72. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_cli_utils.py +0 -0
  73. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_cli_windows_integration.py +0 -0
  74. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_config.py +0 -0
  75. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_config_checker.py +0 -0
  76. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_config_edge_cases.py +0 -0
  77. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_constants.py +0 -0
  78. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_core_utils_coverage.py +0 -0
  79. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_db_utils.py +0 -0
  80. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_db_utils_edge_cases.py +0 -0
  81. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_debug_plugin.py +0 -0
  82. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_drop_plugin.py +0 -0
  83. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_e2ee_encryption.py +0 -0
  84. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_e2ee_integration.py +0 -0
  85. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_e2ee_runner.py +0 -0
  86. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_error_boundaries.py +0 -0
  87. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_health_plugin.py +0 -0
  88. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_help_plugin.py +0 -0
  89. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_imports.py +0 -0
  90. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_integration_scenarios.py +0 -0
  91. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_main.py +0 -0
  92. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_main_entry_point.py +0 -0
  93. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_map_plugin.py +0 -0
  94. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_matrix_utils_edge_cases.py +0 -0
  95. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_meshtastic_broadcast_enabled.py +0 -0
  96. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_meshtastic_utils.py +0 -0
  97. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_meshtastic_utils_edge_cases.py +0 -0
  98. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_message_queue.py +0 -0
  99. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_message_queue_edge_cases.py +0 -0
  100. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_network_reliability.py +0 -0
  101. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_nodes_plugin.py +0 -0
  102. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_patch_coverage_improvements.py +0 -0
  103. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_ping_plugin.py +0 -0
  104. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_plugin_loader_edge_cases.py +0 -0
  105. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_prefix_customization.py +0 -0
  106. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_runtime_no_errors.py +0 -0
  107. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_setup_utils.py +0 -0
  108. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_setup_utils_edge_cases.py +0 -0
  109. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_setup_utils_execstart_improvements.py +0 -0
  110. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_setup_utils_systemctl.py +0 -0
  111. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_telemetry_plugin.py +0 -0
  112. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_tools_init.py +0 -0
  113. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_weather_plugin.py +0 -0
  114. {mmrelay-1.2.3 → mmrelay-1.2.4}/tests/test_windows_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mmrelay
3
- Version: 1.2.3
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,4 +1,4 @@
1
- meshtastic>=2.6.4
1
+ meshtastic>=2.7.3
2
2
  Pillow==11.3.0
3
3
  matrix-nio==0.25.2
4
4
  matplotlib==3.10.1
@@ -16,7 +16,7 @@ setuptools>=80.9.0 # Required for console script entry points and Windows compa
16
16
  # Testing and coverage
17
17
  coverage==7.10.7
18
18
  pytest==8.4.2
19
- pytest-cov==6.3.0
19
+ pytest-cov==7.0.0
20
20
  pytest-asyncio==1.2.0
21
21
  pytest-env==1.1.5
22
22
  pytest-timeout==2.4.0
@@ -44,7 +44,7 @@ setup(
44
44
  ],
45
45
  python_requires=">=3.9",
46
46
  install_requires=[
47
- "meshtastic>=2.6.4",
47
+ "meshtastic>=2.7.3",
48
48
  "Pillow==11.3.0",
49
49
  "matrix-nio==0.25.2",
50
50
  "matplotlib==3.10.1",
@@ -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.3"
5
+ __version__ = "1.2.4"
@@ -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
- Computes the ordered list of candidate config file locations via get_config_paths(args) and prints each path with a short 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): CLI arguments used to determine the config search order (passed to get_config_paths).
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
@@ -1586,19 +1586,15 @@ def _diagnose_sample_config_accessibility():
1586
1586
 
1587
1587
  def _diagnose_platform_specific(args):
1588
1588
  """
1589
- Run platform-specific diagnostic checks and report results.
1590
-
1591
- On Windows, attempts to import and run Windows-specific requirement checks and a
1592
- configuration-generation test, printing per-component statuses and any warnings.
1593
- On non-Windows platforms this reports that platform-specific tests are not
1594
- required.
1595
-
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
+
1596
1593
  Parameters:
1597
- args (argparse.Namespace): CLI arguments passed through to the Windows
1598
- configuration-generation test (only used when running on Windows).
1599
-
1594
+ args (argparse.Namespace): CLI arguments forwarded to the Windows configuration-generation test (used only when running on Windows).
1595
+
1600
1596
  Returns:
1601
- bool: True if Windows checks were executed (running on Windows), False otherwise.
1597
+ bool: `True` if Windows checks were executed (running on Windows), `False` otherwise.
1602
1598
  """
1603
1599
  print("3. Platform-specific diagnostics...")
1604
1600
  import sys
@@ -1721,15 +1717,15 @@ def _diagnose_minimal_config_template():
1721
1717
 
1722
1718
  def handle_config_diagnose(args):
1723
1719
  """
1724
- Run non-destructive diagnostics for the MMRelay configuration subsystem and print a human-readable report.
1725
-
1726
- Performs four checks without modifying user files: (1) resolves and reports candidate configuration file paths and their directory accessibility, (2) verifies availability of the packaged sample configuration, (3) runs 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.
1727
-
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
+
1728
1724
  Parameters:
1729
- args (argparse.Namespace): Parsed CLI arguments used to resolve configuration search paths and to control platform-specific checks.
1730
-
1725
+ args (argparse.Namespace): Parsed CLI arguments used to determine configuration search paths and to control platform-specific diagnostic behavior.
1726
+
1731
1727
  Returns:
1732
- int: Exit code (0 on success, 1 on failure). On failure an error summary is printed to stderr.
1728
+ int: Exit code where `0` indicates diagnostics completed successfully and `1` indicates a failure occurred (an error summary is printed to stderr).
1733
1729
  """
1734
1730
  print("MMRelay Configuration System Diagnostics")
1735
1731
  print("=" * 40)
@@ -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 application's end-to-end-encryption (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 is
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.
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
@@ -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
@@ -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
@@ -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
@@ -155,8 +164,8 @@ async def main(config):
155
164
  async def shutdown():
156
165
  """
157
166
  Signal the application to begin shutdown.
158
-
159
- Sets the Meshtastic shutdown flag and triggers the local shutdown event so any coroutines waiting on that event can start their cleanup. This coroutine only signals shutdown; it does not perform client shutdown or resource cleanup itself.
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
@@ -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,
@@ -789,16 +788,16 @@ 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.
793
-
794
- Attempts credential selection in this order: credentials.json (preferred), automatic login using username/password from config (saved to credentials.json), then direct token values from config. Optionally enables End-to-End Encryption (E2EE) when configured and dependencies are available. Performs an initial full-state sync, resolves configured room aliases to room IDs, sets module-level connection state (matrix_client, bot_user_name, matrix_homeserver, matrix_rooms, matrix_access_token, bot_user_id) and returns the ready AsyncClient.
795
-
791
+ Initialize and return a configured matrix-nio AsyncClient connected to the configured Matrix homeserver.
792
+
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.
794
+
796
795
  Parameters:
797
- passed_config (dict | None): Optional configuration to use for this connection attempt; if provided, it overrides the module-level config for this call.
798
-
796
+ passed_config (dict | None): Optional configuration to use for this connection attempt; when provided it overrides the module-level config for this call.
797
+
799
798
  Returns:
800
- AsyncClient | None: An initialized matrix-nio AsyncClient on success, or None if connection/credentials are unavailable.
801
-
799
+ AsyncClient | None: A ready-to-use matrix-nio AsyncClient on success, or `None` if connection or credentials are unavailable.
800
+
802
801
  Raises:
803
802
  ValueError: If the required top-level "matrix_rooms" configuration is missing.
804
803
  ConnectionError: If the initial Matrix sync fails or times out.
@@ -2318,17 +2317,20 @@ async def send_reply_to_meshtastic(
2318
2317
  reply_id=None,
2319
2318
  ):
2320
2319
  """
2321
- Enqueue a Matrix reply to be sent over Meshtastic, either as a structured reply targeting an existing Meshtastic message or as a regular broadcast.
2322
-
2323
- If Meshtastic broadcasting is disabled this is a no-op. 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 for later reply/reaction correlation. Failures are logged; the function does not raise exceptions.
2324
-
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
+
2325
2324
  Parameters:
2326
- room_config (dict): Room-specific configuration must include "meshtastic_channel" (integer channel index).
2327
- room: Matrix room object; room.room_id is used to build mapping metadata.
2328
- event: Matrix event object; event.event_id is used to build mapping metadata.
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.
2329
2331
  storage_enabled (bool): If True, create and attach a message-mapping record to the queued Meshtastic message.
2330
- reply_id (int | None): If provided, send as a structured Meshtastic reply targeting this message ID; otherwise send a regular text broadcast.
2331
- local_meshnet_name (str | None): Name of the local meshnet used in mapping metadata.
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
- Handle a Matrix reply by forwarding it to Meshtastic when the replied-to Matrix event maps to a Meshtastic message.
2437
-
2438
- If the Matrix event identified by reply_to_event_id has an associated Meshtastic mapping, this function formats a Meshtastic reply that preserves sender attribution and enqueues it referencing the original Meshtastic message ID. If no mapping exists, it returns 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:
2443
+ room: Matrix room object where the reply originated.
2444
+ event: Matrix event object representing the reply.
2441
2445
  reply_to_event_id (str): Matrix event ID being replied to; used to locate the Meshtastic mapping.
2442
- storage_enabled (bool): Whether message mappings/storage are enabled (affects created mapping behavior when sending).
2443
- local_meshnet_name (str): Local meshnet name used to determine formatting when replying across meshnets.
2444
- config (dict): Relay configuration passed to formatting routines.
2445
- mesh_text_override (str | None): Optional text to send instead of derived text.
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.
2446
2452
  longname (str | None): Sender long display name used for prefixing.
2447
2453
  shortname (str | None): Sender short display name used for prefixing.
2448
2454
  meshnet_name (str | None): Remote meshnet name associated with the original mapping, if any.
2449
-
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.
2554
+ Handle an incoming Matrix room event and relay it to Meshtastic when applicable.
2549
2555
 
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.
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
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"),
@@ -473,11 +473,12 @@ class MessageQueue:
473
473
 
474
474
  def _should_send_message(self) -> bool:
475
475
  """
476
- Return True when it is safe to send a Meshtastic message; otherwise False.
477
-
478
- Performs runtime checks: verifies the module-level `reconnecting` flag is False, a Meshtastic client object exists, and if the client exposes `is_connected` (callable or boolean) that it reports connected. If any check fails the method returns False.
479
-
480
- If importing the Meshtastic utilities raises ImportError, the queue will be stopped asynchronously and the method returns 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,17 +518,17 @@ class MessageQueue:
517
518
 
518
519
  def _handle_message_mapping(self, result, mapping_info):
519
520
  """
520
- Persist a sent mesh-to-Matrix message mapping and optionally prune old mappings.
521
-
522
- If mapping_info contains 'matrix_event_id', 'room_id', and 'text', this 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: An object returned by the send function 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
528
  - matrix_event_id (str): Matrix event ID to map to.
528
529
  - room_id (str): Matrix room ID where the event was sent.
529
530
  - text (str): Message text to associate with the mapping.
530
- - meshnet (optional): Mesh network identifier passed through to storage.
531
+ - meshnet (optional): Mesh network identifier to pass to storage.
531
532
  - msgs_to_keep (optional, int): Number of mappings to retain when pruning.
532
533
  """
533
534
  try:
@@ -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,21 +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 matches the bridged-packet marker.
143
-
144
- Checks event.source["content"]["body"] (when it is a string) against the anchored
145
- pattern "^Processed (.+) radio packet$". Returns True when the body matches this
146
- pattern, otherwise False.
147
-
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
+
148
146
  Parameters:
149
- event: Matrix event object whose .source mapping is expected to contain a "content" dict.
150
-
147
+ event: Matrix event object whose `.source` mapping is expected to contain a `"content"` dict with a `"body"` string.
148
+
151
149
  Returns:
152
- bool: True if the content body matches the relayed-packet pattern, False otherwise.
150
+ True if the content body matches `^Processed (.+) radio packet$`, False otherwise.
153
151
  """
154
152
  # Check for the presence of necessary keys in the event
155
153
  content = event.source.get("content", {})
@@ -182,7 +180,7 @@ class Plugin(BasePlugin):
182
180
  """
183
181
  # Use the event for matching instead of full_message
184
182
  if not self.matches(event):
185
- return None
183
+ return False
186
184
 
187
185
  channel = None
188
186
  if config is not None:
@@ -193,18 +191,18 @@ class Plugin(BasePlugin):
193
191
 
194
192
  if channel is None:
195
193
  self.logger.debug(f"Skipping message from unmapped channel {channel}")
196
- return None
194
+ return False
197
195
 
198
196
  packet_json = event.source["content"].get("meshtastic_packet")
199
197
  if not packet_json:
200
198
  self.logger.debug("Missing embedded packet")
201
- return None
199
+ return False
202
200
 
203
201
  try:
204
202
  packet = json.loads(packet_json)
205
- except (json.JSONDecodeError, TypeError) as e:
206
- self.logger.exception(f"Error processing embedded packet: {e}")
207
- return None
203
+ except (json.JSONDecodeError, TypeError):
204
+ self.logger.exception("Error processing embedded packet")
205
+ return False
208
206
 
209
207
  from mmrelay.meshtastic_utils import connect_meshtastic
210
208
 
@@ -221,4 +219,4 @@ class Plugin(BasePlugin):
221
219
  meshtastic_client._sendPacket(
222
220
  meshPacket=meshPacket, destinationId=packet["toId"]
223
221
  )
224
- return None
222
+ return True
@@ -359,9 +359,9 @@ 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
-
362
+
363
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
-
364
+
365
365
  Returns:
366
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
  """
@@ -0,0 +1,30 @@
1
+ services:
2
+ mmrelay:
3
+ image: ghcr.io/jeremiah-k/mmrelay:latest
4
+ container_name: meshtastic-matrix-relay
5
+ restart: unless-stopped
6
+ stop_grace_period: 30s
7
+ user: "${UID:-1000}:${GID:-1000}"
8
+ environment:
9
+ - TZ=UTC # Set timezone (PYTHONUNBUFFERED and MPLCONFIGDIR are set in Dockerfile)
10
+
11
+ volumes:
12
+ # Mount your config directory - create ~/.mmrelay/config.yaml first
13
+ # See docs/DOCKER.md for setup instructions
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