mmrelay 1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mmrelay might be problematic. Click here for more details.
- mmrelay/__init__.py +9 -0
- mmrelay/cli.py +384 -0
- mmrelay/config.py +218 -0
- mmrelay/config_checker.py +133 -0
- mmrelay/db_utils.py +309 -0
- mmrelay/log_utils.py +107 -0
- mmrelay/main.py +281 -0
- mmrelay/matrix_utils.py +754 -0
- mmrelay/meshtastic_utils.py +569 -0
- mmrelay/plugin_loader.py +336 -0
- mmrelay/plugins/__init__.py +3 -0
- mmrelay/plugins/base_plugin.py +212 -0
- mmrelay/plugins/debug_plugin.py +17 -0
- mmrelay/plugins/drop_plugin.py +120 -0
- mmrelay/plugins/health_plugin.py +64 -0
- mmrelay/plugins/help_plugin.py +55 -0
- mmrelay/plugins/map_plugin.py +323 -0
- mmrelay/plugins/mesh_relay_plugin.py +134 -0
- mmrelay/plugins/nodes_plugin.py +92 -0
- mmrelay/plugins/ping_plugin.py +118 -0
- mmrelay/plugins/telemetry_plugin.py +179 -0
- mmrelay/plugins/weather_plugin.py +208 -0
- mmrelay/setup_utils.py +263 -0
- mmrelay-1.0.dist-info/METADATA +160 -0
- mmrelay-1.0.dist-info/RECORD +29 -0
- mmrelay-1.0.dist-info/WHEEL +5 -0
- mmrelay-1.0.dist-info/entry_points.txt +2 -0
- mmrelay-1.0.dist-info/licenses/LICENSE +21 -0
- mmrelay-1.0.dist-info/top_level.txt +1 -0
mmrelay/main.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This script connects a Meshtastic mesh network to Matrix chat rooms by relaying messages between them.
|
|
3
|
+
It uses Meshtastic-python and Matrix nio client library to interface with the radio and the Matrix server respectively.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import logging
|
|
8
|
+
import signal
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from nio import ReactionEvent, RoomMessageEmote, RoomMessageNotice, RoomMessageText
|
|
12
|
+
|
|
13
|
+
# Import meshtastic_utils as a module to set event_loop
|
|
14
|
+
from mmrelay import meshtastic_utils
|
|
15
|
+
from mmrelay.db_utils import (
|
|
16
|
+
initialize_database,
|
|
17
|
+
update_longnames,
|
|
18
|
+
update_shortnames,
|
|
19
|
+
wipe_message_map,
|
|
20
|
+
)
|
|
21
|
+
from mmrelay.log_utils import get_logger
|
|
22
|
+
from mmrelay.matrix_utils import connect_matrix, join_matrix_room
|
|
23
|
+
from mmrelay.matrix_utils import logger as matrix_logger
|
|
24
|
+
from mmrelay.matrix_utils import on_room_message
|
|
25
|
+
from mmrelay.meshtastic_utils import connect_meshtastic
|
|
26
|
+
from mmrelay.meshtastic_utils import logger as meshtastic_logger
|
|
27
|
+
from mmrelay.plugin_loader import load_plugins
|
|
28
|
+
|
|
29
|
+
# Initialize logger
|
|
30
|
+
logger = get_logger(name="M<>M Relay")
|
|
31
|
+
|
|
32
|
+
# Set the logging level for 'nio' to ERROR to suppress warnings
|
|
33
|
+
logging.getLogger("nio").setLevel(logging.ERROR)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def main(config):
|
|
37
|
+
"""
|
|
38
|
+
Main asynchronous function to set up and run the relay.
|
|
39
|
+
Includes logic for wiping the message_map if configured in database.msg_map.wipe_on_restart
|
|
40
|
+
or db.msg_map.wipe_on_restart (legacy format).
|
|
41
|
+
Also updates longnames and shortnames periodically as before.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
config: The loaded configuration
|
|
45
|
+
"""
|
|
46
|
+
# Extract Matrix configuration
|
|
47
|
+
from typing import List
|
|
48
|
+
|
|
49
|
+
matrix_rooms: List[dict] = config["matrix_rooms"]
|
|
50
|
+
|
|
51
|
+
# Set the event loop in meshtastic_utils
|
|
52
|
+
meshtastic_utils.event_loop = asyncio.get_event_loop()
|
|
53
|
+
|
|
54
|
+
# Initialize the SQLite database
|
|
55
|
+
initialize_database()
|
|
56
|
+
|
|
57
|
+
# Check database config for wipe_on_restart (preferred format)
|
|
58
|
+
database_config = config.get("database", {})
|
|
59
|
+
msg_map_config = database_config.get("msg_map", {})
|
|
60
|
+
wipe_on_restart = msg_map_config.get("wipe_on_restart", False)
|
|
61
|
+
|
|
62
|
+
# If not found in database config, check legacy db config
|
|
63
|
+
if not wipe_on_restart:
|
|
64
|
+
db_config = config.get("db", {})
|
|
65
|
+
legacy_msg_map_config = db_config.get("msg_map", {})
|
|
66
|
+
legacy_wipe_on_restart = legacy_msg_map_config.get("wipe_on_restart", False)
|
|
67
|
+
|
|
68
|
+
if legacy_wipe_on_restart:
|
|
69
|
+
wipe_on_restart = legacy_wipe_on_restart
|
|
70
|
+
logger.warning(
|
|
71
|
+
"Using 'db.msg_map' configuration (legacy). 'database.msg_map' is now the preferred format and 'db.msg_map' will be deprecated in a future version."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if wipe_on_restart:
|
|
75
|
+
logger.debug("wipe_on_restart enabled. Wiping message_map now (startup).")
|
|
76
|
+
wipe_message_map()
|
|
77
|
+
|
|
78
|
+
# Load plugins early
|
|
79
|
+
load_plugins(passed_config=config)
|
|
80
|
+
|
|
81
|
+
# Connect to Meshtastic
|
|
82
|
+
meshtastic_utils.meshtastic_client = connect_meshtastic(passed_config=config)
|
|
83
|
+
|
|
84
|
+
# Connect to Matrix
|
|
85
|
+
matrix_client = await connect_matrix(passed_config=config)
|
|
86
|
+
|
|
87
|
+
# Join the rooms specified in the config.yaml
|
|
88
|
+
for room in matrix_rooms:
|
|
89
|
+
await join_matrix_room(matrix_client, room["id"])
|
|
90
|
+
|
|
91
|
+
# Register the message callback for Matrix
|
|
92
|
+
matrix_logger.info("Listening for inbound Matrix messages...")
|
|
93
|
+
matrix_client.add_event_callback(
|
|
94
|
+
on_room_message, (RoomMessageText, RoomMessageNotice, RoomMessageEmote)
|
|
95
|
+
)
|
|
96
|
+
# Add ReactionEvent callback so we can handle matrix reactions
|
|
97
|
+
matrix_client.add_event_callback(on_room_message, ReactionEvent)
|
|
98
|
+
|
|
99
|
+
# Set up shutdown event
|
|
100
|
+
shutdown_event = asyncio.Event()
|
|
101
|
+
|
|
102
|
+
async def shutdown():
|
|
103
|
+
matrix_logger.info("Shutdown signal received. Closing down...")
|
|
104
|
+
meshtastic_utils.shutting_down = True # Set the shutting_down flag
|
|
105
|
+
shutdown_event.set()
|
|
106
|
+
|
|
107
|
+
loop = asyncio.get_running_loop()
|
|
108
|
+
|
|
109
|
+
# Handle signals differently based on the platform
|
|
110
|
+
if sys.platform != "win32":
|
|
111
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
112
|
+
loop.add_signal_handler(sig, lambda: asyncio.create_task(shutdown()))
|
|
113
|
+
else:
|
|
114
|
+
# On Windows, we can't use add_signal_handler, so we'll handle KeyboardInterrupt
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
# -------------------------------------------------------------------
|
|
118
|
+
# IMPORTANT: We create a task to run the meshtastic_utils.check_connection()
|
|
119
|
+
# so its while loop runs in parallel with the matrix sync loop
|
|
120
|
+
# Use "_" to avoid trunk's "assigned but unused variable" warning
|
|
121
|
+
# -------------------------------------------------------------------
|
|
122
|
+
_ = asyncio.create_task(meshtastic_utils.check_connection())
|
|
123
|
+
|
|
124
|
+
# Start the Matrix client sync loop
|
|
125
|
+
try:
|
|
126
|
+
while not shutdown_event.is_set():
|
|
127
|
+
try:
|
|
128
|
+
if meshtastic_utils.meshtastic_client:
|
|
129
|
+
# Update longnames & shortnames
|
|
130
|
+
update_longnames(meshtastic_utils.meshtastic_client.nodes)
|
|
131
|
+
update_shortnames(meshtastic_utils.meshtastic_client.nodes)
|
|
132
|
+
else:
|
|
133
|
+
meshtastic_logger.warning("Meshtastic client is not connected.")
|
|
134
|
+
|
|
135
|
+
matrix_logger.info("Starting Matrix sync loop...")
|
|
136
|
+
sync_task = asyncio.create_task(
|
|
137
|
+
matrix_client.sync_forever(timeout=30000)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
shutdown_task = asyncio.create_task(shutdown_event.wait())
|
|
141
|
+
|
|
142
|
+
# Wait for either the matrix sync to fail, or for a shutdown
|
|
143
|
+
done, pending = await asyncio.wait(
|
|
144
|
+
[sync_task, shutdown_task],
|
|
145
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
146
|
+
)
|
|
147
|
+
if shutdown_event.is_set():
|
|
148
|
+
matrix_logger.info("Shutdown event detected. Stopping sync loop...")
|
|
149
|
+
sync_task.cancel()
|
|
150
|
+
try:
|
|
151
|
+
await sync_task
|
|
152
|
+
except asyncio.CancelledError:
|
|
153
|
+
pass
|
|
154
|
+
break
|
|
155
|
+
|
|
156
|
+
except Exception as e:
|
|
157
|
+
if shutdown_event.is_set():
|
|
158
|
+
break
|
|
159
|
+
matrix_logger.error(f"Error syncing with Matrix server: {e}")
|
|
160
|
+
await asyncio.sleep(5) # Wait briefly before retrying
|
|
161
|
+
except KeyboardInterrupt:
|
|
162
|
+
await shutdown()
|
|
163
|
+
finally:
|
|
164
|
+
# Cleanup
|
|
165
|
+
matrix_logger.info("Closing Matrix client...")
|
|
166
|
+
await matrix_client.close()
|
|
167
|
+
if meshtastic_utils.meshtastic_client:
|
|
168
|
+
meshtastic_logger.info("Closing Meshtastic client...")
|
|
169
|
+
try:
|
|
170
|
+
meshtastic_utils.meshtastic_client.close()
|
|
171
|
+
except Exception as e:
|
|
172
|
+
meshtastic_logger.warning(f"Error closing Meshtastic client: {e}")
|
|
173
|
+
|
|
174
|
+
# Attempt to wipe message_map on shutdown if enabled
|
|
175
|
+
if wipe_on_restart:
|
|
176
|
+
logger.debug("wipe_on_restart enabled. Wiping message_map now (shutdown).")
|
|
177
|
+
wipe_message_map()
|
|
178
|
+
|
|
179
|
+
# Cancel the reconnect task if it exists
|
|
180
|
+
if meshtastic_utils.reconnect_task:
|
|
181
|
+
meshtastic_utils.reconnect_task.cancel()
|
|
182
|
+
meshtastic_logger.info("Cancelled Meshtastic reconnect task.")
|
|
183
|
+
|
|
184
|
+
# Cancel any remaining tasks (including the check_conn_task)
|
|
185
|
+
tasks = [t for t in asyncio.all_tasks(loop) if not t.done()]
|
|
186
|
+
for task in tasks:
|
|
187
|
+
task.cancel()
|
|
188
|
+
try:
|
|
189
|
+
await task
|
|
190
|
+
except asyncio.CancelledError:
|
|
191
|
+
pass
|
|
192
|
+
|
|
193
|
+
matrix_logger.info("Shutdown complete.")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def run_main(args):
|
|
197
|
+
"""Run the main functionality of the application.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
args: The parsed command-line arguments
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
int: Exit code (0 for success, non-zero for failure)
|
|
204
|
+
"""
|
|
205
|
+
# Handle the --data-dir option
|
|
206
|
+
if args and args.data_dir:
|
|
207
|
+
import os
|
|
208
|
+
|
|
209
|
+
import mmrelay.config
|
|
210
|
+
|
|
211
|
+
# Set the global custom_data_dir variable
|
|
212
|
+
mmrelay.config.custom_data_dir = os.path.abspath(args.data_dir)
|
|
213
|
+
# Create the directory if it doesn't exist
|
|
214
|
+
os.makedirs(mmrelay.config.custom_data_dir, exist_ok=True)
|
|
215
|
+
|
|
216
|
+
# Load configuration
|
|
217
|
+
from mmrelay.config import load_config
|
|
218
|
+
|
|
219
|
+
# Load configuration with args
|
|
220
|
+
config = load_config(args=args)
|
|
221
|
+
|
|
222
|
+
# Handle the --log-level option
|
|
223
|
+
if args and args.log_level:
|
|
224
|
+
# Override the log level from config
|
|
225
|
+
if "logging" not in config:
|
|
226
|
+
config["logging"] = {}
|
|
227
|
+
config["logging"]["level"] = args.log_level
|
|
228
|
+
|
|
229
|
+
# Set the global config variables in each module
|
|
230
|
+
from mmrelay import (
|
|
231
|
+
db_utils,
|
|
232
|
+
log_utils,
|
|
233
|
+
matrix_utils,
|
|
234
|
+
meshtastic_utils,
|
|
235
|
+
plugin_loader,
|
|
236
|
+
)
|
|
237
|
+
from mmrelay.config import set_config
|
|
238
|
+
from mmrelay.plugins import base_plugin
|
|
239
|
+
|
|
240
|
+
# Use the centralized set_config function to set up the configuration for all modules
|
|
241
|
+
set_config(matrix_utils, config)
|
|
242
|
+
set_config(meshtastic_utils, config)
|
|
243
|
+
set_config(plugin_loader, config)
|
|
244
|
+
set_config(log_utils, config)
|
|
245
|
+
set_config(db_utils, config)
|
|
246
|
+
set_config(base_plugin, config)
|
|
247
|
+
|
|
248
|
+
# Check if config exists and has the required keys
|
|
249
|
+
required_keys = ["matrix", "meshtastic", "matrix_rooms"]
|
|
250
|
+
|
|
251
|
+
# Check each key individually for better debugging
|
|
252
|
+
for key in required_keys:
|
|
253
|
+
if key not in config:
|
|
254
|
+
logger.error(f"Required key '{key}' is missing from config")
|
|
255
|
+
|
|
256
|
+
if not config or not all(key in config for key in required_keys):
|
|
257
|
+
# Exit with error if no config exists
|
|
258
|
+
missing_keys = [key for key in required_keys if key not in config]
|
|
259
|
+
logger.error(
|
|
260
|
+
f"Configuration is missing required keys: {missing_keys}. "
|
|
261
|
+
"Please create a valid config.yaml file or use --generate-config to create one."
|
|
262
|
+
)
|
|
263
|
+
return 1
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
asyncio.run(main(config))
|
|
267
|
+
return 0
|
|
268
|
+
except KeyboardInterrupt:
|
|
269
|
+
logger.info("Interrupted by user. Exiting.")
|
|
270
|
+
return 0
|
|
271
|
+
except Exception as e:
|
|
272
|
+
logger.error(f"Error running main functionality: {e}")
|
|
273
|
+
return 1
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
if __name__ == "__main__":
|
|
277
|
+
import sys
|
|
278
|
+
|
|
279
|
+
from mmrelay.cli import main
|
|
280
|
+
|
|
281
|
+
sys.exit(main())
|