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/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())