mmrelay 1.1.2__py3-none-any.whl → 1.1.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 +1 -13
- mmrelay/cli.py +124 -64
- mmrelay/config.py +63 -36
- mmrelay/config_checker.py +41 -12
- mmrelay/constants/__init__.py +54 -0
- mmrelay/constants/app.py +17 -0
- mmrelay/constants/config.py +73 -0
- mmrelay/constants/database.py +22 -0
- mmrelay/constants/formats.py +20 -0
- mmrelay/constants/messages.py +36 -0
- mmrelay/constants/network.py +35 -0
- mmrelay/constants/queue.py +17 -0
- mmrelay/db_utils.py +281 -132
- mmrelay/log_utils.py +38 -14
- mmrelay/main.py +23 -4
- mmrelay/matrix_utils.py +413 -162
- mmrelay/meshtastic_utils.py +223 -106
- mmrelay/message_queue.py +475 -0
- mmrelay/plugin_loader.py +56 -53
- mmrelay/plugins/base_plugin.py +139 -39
- mmrelay/plugins/drop_plugin.py +13 -5
- mmrelay/plugins/mesh_relay_plugin.py +7 -10
- mmrelay/plugins/weather_plugin.py +10 -1
- mmrelay/setup_utils.py +67 -30
- mmrelay/tools/sample_config.yaml +13 -3
- {mmrelay-1.1.2.dist-info → mmrelay-1.1.4.dist-info}/METADATA +12 -14
- mmrelay-1.1.4.dist-info/RECORD +43 -0
- mmrelay-1.1.4.dist-info/licenses/LICENSE +675 -0
- mmrelay-1.1.2.dist-info/RECORD +0 -34
- mmrelay-1.1.2.dist-info/licenses/LICENSE +0 -21
- {mmrelay-1.1.2.dist-info → mmrelay-1.1.4.dist-info}/WHEEL +0 -0
- {mmrelay-1.1.2.dist-info → mmrelay-1.1.4.dist-info}/entry_points.txt +0 -0
- {mmrelay-1.1.2.dist-info → mmrelay-1.1.4.dist-info}/top_level.txt +0 -0
mmrelay/plugins/base_plugin.py
CHANGED
|
@@ -7,6 +7,11 @@ import markdown
|
|
|
7
7
|
import schedule
|
|
8
8
|
|
|
9
9
|
from mmrelay.config import get_plugin_data_dir
|
|
10
|
+
from mmrelay.constants.database import (
|
|
11
|
+
DEFAULT_MAX_DATA_ROWS_PER_NODE_BASE,
|
|
12
|
+
DEFAULT_TEXT_TRUNCATION_LENGTH,
|
|
13
|
+
)
|
|
14
|
+
from mmrelay.constants.queue import DEFAULT_MESSAGE_DELAY
|
|
10
15
|
from mmrelay.db_utils import (
|
|
11
16
|
delete_plugin_data,
|
|
12
17
|
get_plugin_data,
|
|
@@ -14,10 +19,14 @@ from mmrelay.db_utils import (
|
|
|
14
19
|
store_plugin_data,
|
|
15
20
|
)
|
|
16
21
|
from mmrelay.log_utils import get_logger
|
|
22
|
+
from mmrelay.message_queue import queue_message
|
|
17
23
|
|
|
18
24
|
# Global config variable that will be set from main.py
|
|
19
25
|
config = None
|
|
20
26
|
|
|
27
|
+
# Track if we've already shown the deprecated warning
|
|
28
|
+
_deprecated_warning_shown = False
|
|
29
|
+
|
|
21
30
|
|
|
22
31
|
class BasePlugin(ABC):
|
|
23
32
|
"""Abstract base class for all mmrelay plugins.
|
|
@@ -43,7 +52,7 @@ class BasePlugin(ABC):
|
|
|
43
52
|
|
|
44
53
|
# Class-level default attributes
|
|
45
54
|
plugin_name = None # Must be overridden in subclasses
|
|
46
|
-
max_data_rows_per_node =
|
|
55
|
+
max_data_rows_per_node = DEFAULT_MAX_DATA_ROWS_PER_NODE_BASE
|
|
47
56
|
priority = 10
|
|
48
57
|
|
|
49
58
|
@property
|
|
@@ -59,20 +68,16 @@ class BasePlugin(ABC):
|
|
|
59
68
|
return ""
|
|
60
69
|
|
|
61
70
|
def __init__(self, plugin_name=None) -> None:
|
|
62
|
-
"""
|
|
71
|
+
"""
|
|
72
|
+
Initialize the plugin instance, setting its name, logger, configuration, mapped channels, and response delay.
|
|
63
73
|
|
|
64
|
-
|
|
65
|
-
plugin_name (str, optional):
|
|
66
|
-
uses class-level plugin_name attribute.
|
|
74
|
+
Parameters:
|
|
75
|
+
plugin_name (str, optional): Overrides the plugin's name. If not provided, uses the class-level `plugin_name` attribute.
|
|
67
76
|
|
|
68
77
|
Raises:
|
|
69
|
-
ValueError: If
|
|
78
|
+
ValueError: If the plugin name is not set via parameter or class attribute.
|
|
70
79
|
|
|
71
|
-
|
|
72
|
-
- Plugin-specific logger
|
|
73
|
-
- Configuration from global config
|
|
74
|
-
- Channel mapping and validation
|
|
75
|
-
- Response delay settings
|
|
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.
|
|
76
81
|
"""
|
|
77
82
|
# Allow plugin_name to be passed as a parameter for simpler initialization
|
|
78
83
|
# This maintains backward compatibility while providing a cleaner API
|
|
@@ -132,26 +137,43 @@ class BasePlugin(ABC):
|
|
|
132
137
|
f"Plugin '{self.plugin_name}': Channels {invalid_channels} are not mapped in configuration."
|
|
133
138
|
)
|
|
134
139
|
|
|
135
|
-
# Get the response delay from the meshtastic config
|
|
136
|
-
self.response_delay =
|
|
140
|
+
# Get the response delay from the meshtastic config
|
|
141
|
+
self.response_delay = DEFAULT_MESSAGE_DELAY
|
|
137
142
|
if config is not None:
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
143
|
+
meshtastic_config = config.get("meshtastic", {})
|
|
144
|
+
|
|
145
|
+
# Check for new message_delay option first, with fallback to deprecated option
|
|
146
|
+
delay = None
|
|
147
|
+
delay_key = None
|
|
148
|
+
if "message_delay" in meshtastic_config:
|
|
149
|
+
delay = meshtastic_config["message_delay"]
|
|
150
|
+
delay_key = "message_delay"
|
|
151
|
+
elif "plugin_response_delay" in meshtastic_config:
|
|
152
|
+
delay = meshtastic_config["plugin_response_delay"]
|
|
153
|
+
delay_key = "plugin_response_delay"
|
|
154
|
+
# Show deprecated warning only once globally
|
|
155
|
+
global _deprecated_warning_shown
|
|
156
|
+
if not _deprecated_warning_shown:
|
|
157
|
+
self.logger.warning(
|
|
158
|
+
"Configuration option 'plugin_response_delay' is deprecated. "
|
|
159
|
+
"Please use 'message_delay' instead. Support for 'plugin_response_delay' will be removed in a future version."
|
|
160
|
+
)
|
|
161
|
+
_deprecated_warning_shown = True
|
|
162
|
+
|
|
163
|
+
if delay is not None:
|
|
164
|
+
self.response_delay = delay
|
|
165
|
+
# Enforce minimum delay of 2 seconds due to firmware constraints
|
|
166
|
+
if self.response_delay < 2.0:
|
|
167
|
+
self.logger.warning(
|
|
168
|
+
f"{delay_key} of {self.response_delay}s is below minimum of 2.0s (firmware constraint). Using 2.0s."
|
|
169
|
+
)
|
|
170
|
+
self.response_delay = 2.0
|
|
141
171
|
|
|
142
172
|
def start(self):
|
|
143
|
-
"""
|
|
144
|
-
|
|
145
|
-
Called automatically when the plugin is loaded. Checks plugin configuration
|
|
146
|
-
for scheduling settings and sets up background jobs accordingly.
|
|
147
|
-
|
|
148
|
-
Supported schedule formats in config:
|
|
149
|
-
- schedule.hours + schedule.at: Run every N hours at specific time
|
|
150
|
-
- schedule.minutes + schedule.at: Run every N minutes at specific time
|
|
151
|
-
- schedule.hours: Run every N hours
|
|
152
|
-
- schedule.minutes: Run every N minutes
|
|
173
|
+
"""
|
|
174
|
+
Starts the plugin and configures scheduled background tasks based on plugin settings.
|
|
153
175
|
|
|
154
|
-
|
|
176
|
+
If scheduling options are present in the plugin configuration, sets up periodic execution of the `background_job` method using the specified schedule. Runs scheduled jobs in a separate daemon thread. If no scheduling is configured, the plugin starts without background tasks.
|
|
155
177
|
"""
|
|
156
178
|
if "schedule" not in self.config or (
|
|
157
179
|
"at" not in self.config["schedule"]
|
|
@@ -224,28 +246,106 @@ class BasePlugin(ABC):
|
|
|
224
246
|
return data
|
|
225
247
|
|
|
226
248
|
def get_response_delay(self):
|
|
227
|
-
"""
|
|
249
|
+
"""
|
|
250
|
+
Return the configured delay in seconds before sending a Meshtastic response.
|
|
228
251
|
|
|
229
|
-
|
|
230
|
-
int: Delay in seconds before sending responses (default: 3)
|
|
252
|
+
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.
|
|
231
253
|
|
|
232
|
-
|
|
233
|
-
|
|
254
|
+
Returns:
|
|
255
|
+
float: The response delay in seconds.
|
|
234
256
|
"""
|
|
235
257
|
return self.response_delay
|
|
236
258
|
|
|
237
|
-
def
|
|
238
|
-
"""
|
|
259
|
+
def get_my_node_id(self):
|
|
260
|
+
"""Get the relay's Meshtastic node ID.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
int: The relay's node ID, or None if unavailable
|
|
264
|
+
|
|
265
|
+
This method provides access to the relay's own node ID without requiring
|
|
266
|
+
plugins to call connect_meshtastic() directly. Useful for determining
|
|
267
|
+
if messages are direct messages or for other node identification needs.
|
|
268
|
+
|
|
269
|
+
The node ID is cached after first successful retrieval to avoid repeated
|
|
270
|
+
connection calls, as the relay's node ID is static during runtime.
|
|
271
|
+
"""
|
|
272
|
+
if hasattr(self, "_my_node_id"):
|
|
273
|
+
return self._my_node_id
|
|
274
|
+
|
|
275
|
+
from mmrelay.meshtastic_utils import connect_meshtastic
|
|
276
|
+
|
|
277
|
+
meshtastic_client = connect_meshtastic()
|
|
278
|
+
if meshtastic_client and meshtastic_client.myInfo:
|
|
279
|
+
self._my_node_id = meshtastic_client.myInfo.my_node_num
|
|
280
|
+
return self._my_node_id
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
def is_direct_message(self, packet):
|
|
284
|
+
"""Check if a Meshtastic packet is a direct message to this relay.
|
|
239
285
|
|
|
240
286
|
Args:
|
|
241
|
-
|
|
242
|
-
|
|
287
|
+
packet (dict): Meshtastic packet data
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
bool: True if the packet is a direct message to this relay, False otherwise
|
|
291
|
+
|
|
292
|
+
This method encapsulates the common pattern of checking if a message
|
|
293
|
+
is addressed directly to the relay node, eliminating the need for plugins
|
|
294
|
+
to call connect_meshtastic() directly for DM detection.
|
|
295
|
+
"""
|
|
296
|
+
toId = packet.get("to")
|
|
297
|
+
if toId is None:
|
|
298
|
+
return False
|
|
299
|
+
|
|
300
|
+
myId = self.get_my_node_id()
|
|
301
|
+
return toId == myId
|
|
302
|
+
|
|
303
|
+
def send_message(self, text: str, channel: int = 0, destination_id=None) -> bool:
|
|
304
|
+
"""
|
|
305
|
+
Send a message to the Meshtastic network using the message queue.
|
|
306
|
+
|
|
307
|
+
Queues the specified text for broadcast or direct delivery on the given channel. Returns True if the message was successfully queued, or False if the Meshtastic client is unavailable.
|
|
308
|
+
|
|
309
|
+
Parameters:
|
|
310
|
+
text (str): The message content to send.
|
|
311
|
+
channel (int, optional): The channel index for sending the message. Defaults to 0.
|
|
312
|
+
destination_id (optional): The destination node ID for direct messages. If None, the message is broadcast.
|
|
243
313
|
|
|
244
314
|
Returns:
|
|
245
|
-
bool: True if
|
|
315
|
+
bool: True if the message was queued successfully; False otherwise.
|
|
316
|
+
"""
|
|
317
|
+
from mmrelay.meshtastic_utils import connect_meshtastic
|
|
318
|
+
|
|
319
|
+
meshtastic_client = connect_meshtastic()
|
|
320
|
+
if not meshtastic_client:
|
|
321
|
+
self.logger.error("No Meshtastic client available")
|
|
322
|
+
return False
|
|
323
|
+
|
|
324
|
+
description = f"Plugin {self.plugin_name}: {text[:DEFAULT_TEXT_TRUNCATION_LENGTH]}{'...' if len(text) > DEFAULT_TEXT_TRUNCATION_LENGTH else ''}"
|
|
325
|
+
|
|
326
|
+
send_kwargs = {
|
|
327
|
+
"text": text,
|
|
328
|
+
"channelIndex": channel,
|
|
329
|
+
}
|
|
330
|
+
if destination_id:
|
|
331
|
+
send_kwargs["destinationId"] = destination_id
|
|
332
|
+
|
|
333
|
+
return queue_message(
|
|
334
|
+
meshtastic_client.sendText,
|
|
335
|
+
description=description,
|
|
336
|
+
**send_kwargs,
|
|
337
|
+
)
|
|
246
338
|
|
|
247
|
-
|
|
248
|
-
|
|
339
|
+
def is_channel_enabled(self, channel, is_direct_message=False):
|
|
340
|
+
"""
|
|
341
|
+
Determine whether the plugin should respond to a message on the specified channel or direct message.
|
|
342
|
+
|
|
343
|
+
Parameters:
|
|
344
|
+
channel: The channel identifier to check.
|
|
345
|
+
is_direct_message (bool): Set to True if the message is a direct message.
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
bool: True if the plugin should respond on the given channel or to a direct message; False otherwise.
|
|
249
349
|
"""
|
|
250
350
|
if is_direct_message:
|
|
251
351
|
return True # Always respond to DMs if the plugin is active
|
mmrelay/plugins/drop_plugin.py
CHANGED
|
@@ -2,6 +2,8 @@ import re
|
|
|
2
2
|
|
|
3
3
|
from haversine import haversine
|
|
4
4
|
|
|
5
|
+
from mmrelay.constants.database import DEFAULT_DISTANCE_KM_FALLBACK, DEFAULT_RADIUS_KM
|
|
6
|
+
from mmrelay.constants.formats import TEXT_MESSAGE_APP
|
|
5
7
|
from mmrelay.meshtastic_utils import connect_meshtastic
|
|
6
8
|
from mmrelay.plugins.base_plugin import BasePlugin
|
|
7
9
|
|
|
@@ -25,6 +27,14 @@ class Plugin(BasePlugin):
|
|
|
25
27
|
async def handle_meshtastic_message(
|
|
26
28
|
self, packet, formatted_message, longname, meshnet_name
|
|
27
29
|
):
|
|
30
|
+
"""
|
|
31
|
+
Handles incoming Meshtastic packets for the drop message plugin, delivering or storing dropped messages based on packet content and node location.
|
|
32
|
+
|
|
33
|
+
When a packet is received, attempts to deliver any stored dropped messages to the sender if they are within a configured radius of the message's location and are not the original dropper. If the packet contains a properly formatted drop command, extracts the message and stores it with the sender's current location for future delivery.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
True if a drop command was processed and stored, False otherwise.
|
|
37
|
+
"""
|
|
28
38
|
meshtastic_client = connect_meshtastic()
|
|
29
39
|
nodeInfo = meshtastic_client.getMyNodeInfo()
|
|
30
40
|
|
|
@@ -55,10 +65,8 @@ class Plugin(BasePlugin):
|
|
|
55
65
|
message["location"],
|
|
56
66
|
)
|
|
57
67
|
except (ValueError, TypeError):
|
|
58
|
-
distance_km =
|
|
59
|
-
radius_km = (
|
|
60
|
-
self.config["radius_km"] if "radius_km" in self.config else 5
|
|
61
|
-
)
|
|
68
|
+
distance_km = DEFAULT_DISTANCE_KM_FALLBACK
|
|
69
|
+
radius_km = self.config.get("radius_km", DEFAULT_RADIUS_KM)
|
|
62
70
|
if distance_km <= radius_km:
|
|
63
71
|
target_node = packet["fromId"]
|
|
64
72
|
self.logger.debug(f"Sending dropped message to {target_node}")
|
|
@@ -76,7 +84,7 @@ class Plugin(BasePlugin):
|
|
|
76
84
|
if (
|
|
77
85
|
"decoded" in packet
|
|
78
86
|
and "portnum" in packet["decoded"]
|
|
79
|
-
and packet["decoded"]["portnum"] ==
|
|
87
|
+
and packet["decoded"]["portnum"] == TEXT_MESSAGE_APP
|
|
80
88
|
):
|
|
81
89
|
text = packet["decoded"]["text"] if "text" in packet["decoded"] else None
|
|
82
90
|
if f"!{self.plugin_name}" not in text:
|
|
@@ -6,6 +6,7 @@ import re
|
|
|
6
6
|
|
|
7
7
|
from meshtastic import mesh_pb2
|
|
8
8
|
|
|
9
|
+
from mmrelay.constants.database import DEFAULT_MAX_DATA_ROWS_PER_NODE_MESH_RELAY
|
|
9
10
|
from mmrelay.plugins.base_plugin import BasePlugin, config
|
|
10
11
|
|
|
11
12
|
|
|
@@ -25,21 +26,17 @@ class Plugin(BasePlugin):
|
|
|
25
26
|
"""
|
|
26
27
|
|
|
27
28
|
plugin_name = "mesh_relay"
|
|
28
|
-
max_data_rows_per_node =
|
|
29
|
+
max_data_rows_per_node = DEFAULT_MAX_DATA_ROWS_PER_NODE_MESH_RELAY
|
|
29
30
|
|
|
30
31
|
def normalize(self, dict_obj):
|
|
31
|
-
"""
|
|
32
|
+
"""
|
|
33
|
+
Converts packet data in various formats (dict, JSON string, or plain string) into a normalized dictionary with raw data fields removed.
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
dict_obj: Packet data
|
|
35
|
+
Parameters:
|
|
36
|
+
dict_obj: Packet data as a dictionary, JSON string, or plain string.
|
|
35
37
|
|
|
36
38
|
Returns:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
Handles various packet formats:
|
|
40
|
-
- Dict objects (passed through)
|
|
41
|
-
- JSON strings (parsed)
|
|
42
|
-
- Plain strings (wrapped in TEXT_MESSAGE_APP format)
|
|
39
|
+
A dictionary representing the normalized packet with raw fields stripped.
|
|
43
40
|
"""
|
|
44
41
|
if not isinstance(dict_obj, dict):
|
|
45
42
|
try:
|
|
@@ -3,6 +3,7 @@ import asyncio
|
|
|
3
3
|
import requests
|
|
4
4
|
from meshtastic.mesh_interface import BROADCAST_NUM
|
|
5
5
|
|
|
6
|
+
from mmrelay.constants.formats import TEXT_MESSAGE_APP
|
|
6
7
|
from mmrelay.plugins.base_plugin import BasePlugin
|
|
7
8
|
|
|
8
9
|
|
|
@@ -122,10 +123,18 @@ class Plugin(BasePlugin):
|
|
|
122
123
|
async def handle_meshtastic_message(
|
|
123
124
|
self, packet, formatted_message, longname, meshnet_name
|
|
124
125
|
):
|
|
126
|
+
"""
|
|
127
|
+
Processes incoming Meshtastic text messages and responds with a weather forecast if the plugin command is detected.
|
|
128
|
+
|
|
129
|
+
Checks if the message is a valid text message on the expected port, verifies channel and command enablement, retrieves the sender's GPS location, generates a weather forecast, and sends the response either as a direct message or broadcast depending on the message type.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
bool: True if the message was handled and a response was sent; False otherwise.
|
|
133
|
+
"""
|
|
125
134
|
if (
|
|
126
135
|
"decoded" in packet
|
|
127
136
|
and "portnum" in packet["decoded"]
|
|
128
|
-
and packet["decoded"]["portnum"] ==
|
|
137
|
+
and packet["decoded"]["portnum"] == TEXT_MESSAGE_APP
|
|
129
138
|
and "text" in packet["decoded"]
|
|
130
139
|
):
|
|
131
140
|
message = packet["decoded"]["text"].strip()
|
mmrelay/setup_utils.py
CHANGED
|
@@ -14,6 +14,7 @@ import subprocess
|
|
|
14
14
|
import sys
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
|
|
17
|
+
from mmrelay.constants.database import PROGRESS_COMPLETE, PROGRESS_TOTAL_STEPS
|
|
17
18
|
from mmrelay.tools import get_service_template_path
|
|
18
19
|
|
|
19
20
|
|
|
@@ -54,7 +55,11 @@ def print_service_commands():
|
|
|
54
55
|
|
|
55
56
|
|
|
56
57
|
def wait_for_service_start():
|
|
57
|
-
"""
|
|
58
|
+
"""
|
|
59
|
+
Displays a progress spinner while waiting up to 10 seconds for the mmrelay service to become active.
|
|
60
|
+
|
|
61
|
+
The function checks the service status after 5 seconds and completes early if the service is detected as active.
|
|
62
|
+
"""
|
|
58
63
|
import time
|
|
59
64
|
|
|
60
65
|
from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
|
|
@@ -67,7 +72,7 @@ def wait_for_service_start():
|
|
|
67
72
|
transient=True,
|
|
68
73
|
) as progress:
|
|
69
74
|
# Add a task that will run for approximately 10 seconds
|
|
70
|
-
task = progress.add_task("Starting", total=
|
|
75
|
+
task = progress.add_task("Starting", total=PROGRESS_TOTAL_STEPS)
|
|
71
76
|
|
|
72
77
|
# Update progress over 10 seconds
|
|
73
78
|
for i in range(10):
|
|
@@ -76,7 +81,7 @@ def wait_for_service_start():
|
|
|
76
81
|
|
|
77
82
|
# Check if service is active after 5 seconds to potentially finish early
|
|
78
83
|
if i >= 5 and is_service_active():
|
|
79
|
-
progress.update(task, completed=
|
|
84
|
+
progress.update(task, completed=PROGRESS_COMPLETE)
|
|
80
85
|
break
|
|
81
86
|
|
|
82
87
|
|
|
@@ -372,10 +377,11 @@ def check_loginctl_available():
|
|
|
372
377
|
|
|
373
378
|
|
|
374
379
|
def check_lingering_enabled():
|
|
375
|
-
"""
|
|
380
|
+
"""
|
|
381
|
+
Determine whether user lingering is enabled for the current user.
|
|
376
382
|
|
|
377
383
|
Returns:
|
|
378
|
-
bool: True if lingering is enabled, False otherwise.
|
|
384
|
+
bool: True if user lingering is enabled, False otherwise.
|
|
379
385
|
"""
|
|
380
386
|
try:
|
|
381
387
|
username = os.environ.get("USER", os.environ.get("USERNAME"))
|
|
@@ -386,7 +392,8 @@ def check_lingering_enabled():
|
|
|
386
392
|
text=True,
|
|
387
393
|
)
|
|
388
394
|
return result.returncode == 0 and "Linger=yes" in result.stdout
|
|
389
|
-
except Exception:
|
|
395
|
+
except Exception as e:
|
|
396
|
+
print(f"Error checking lingering status: {e}")
|
|
390
397
|
return False
|
|
391
398
|
|
|
392
399
|
|
|
@@ -417,7 +424,14 @@ def enable_lingering():
|
|
|
417
424
|
|
|
418
425
|
|
|
419
426
|
def install_service():
|
|
420
|
-
"""
|
|
427
|
+
"""
|
|
428
|
+
Install or update the MMRelay systemd user service, guiding the user through creation, updating, enabling, and starting the service as needed.
|
|
429
|
+
|
|
430
|
+
Prompts the user for confirmation before updating an existing service file, enabling user lingering, enabling the service to start at boot, and starting or restarting the service. Handles user interruptions gracefully and prints a summary of the service status and management commands upon completion.
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
bool: True if the installation or update process completes successfully, False otherwise.
|
|
434
|
+
"""
|
|
421
435
|
# Check if service already exists
|
|
422
436
|
existing_service = read_service_file()
|
|
423
437
|
service_path = get_user_service_path()
|
|
@@ -431,11 +445,14 @@ def install_service():
|
|
|
431
445
|
|
|
432
446
|
if update_needed:
|
|
433
447
|
print(f"The service file needs to be updated: {reason}")
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
.lower()
|
|
437
|
-
|
|
438
|
-
|
|
448
|
+
try:
|
|
449
|
+
user_input = input("Do you want to update the service file? (y/n): ")
|
|
450
|
+
if not user_input.lower().startswith("y"):
|
|
451
|
+
print("Service update cancelled.")
|
|
452
|
+
print_service_commands()
|
|
453
|
+
return True
|
|
454
|
+
except (EOFError, KeyboardInterrupt):
|
|
455
|
+
print("\nInput cancelled. Proceeding with default behavior.")
|
|
439
456
|
print("Service update cancelled.")
|
|
440
457
|
print_service_commands()
|
|
441
458
|
return True
|
|
@@ -450,9 +467,11 @@ def install_service():
|
|
|
450
467
|
if not create_service_file():
|
|
451
468
|
return False
|
|
452
469
|
|
|
453
|
-
# Reload daemon
|
|
470
|
+
# Reload daemon (continue even if this fails)
|
|
454
471
|
if not reload_daemon():
|
|
455
|
-
|
|
472
|
+
print(
|
|
473
|
+
"Warning: Failed to reload systemd daemon. You may need to run 'systemctl --user daemon-reload' manually."
|
|
474
|
+
)
|
|
456
475
|
|
|
457
476
|
if existing_service:
|
|
458
477
|
print("Service file updated successfully")
|
|
@@ -473,13 +492,16 @@ def install_service():
|
|
|
473
492
|
print(
|
|
474
493
|
"Lingering allows user services to run even when you're not logged in."
|
|
475
494
|
)
|
|
476
|
-
|
|
477
|
-
input(
|
|
495
|
+
try:
|
|
496
|
+
user_input = input(
|
|
478
497
|
"Do you want to enable lingering for your user? (requires sudo) (y/n): "
|
|
479
498
|
)
|
|
480
|
-
.lower()
|
|
481
|
-
|
|
482
|
-
|
|
499
|
+
should_enable_lingering = user_input.lower().startswith("y")
|
|
500
|
+
except (EOFError, KeyboardInterrupt):
|
|
501
|
+
print("\nInput cancelled. Skipping lingering setup.")
|
|
502
|
+
should_enable_lingering = False
|
|
503
|
+
|
|
504
|
+
if should_enable_lingering:
|
|
483
505
|
enable_lingering()
|
|
484
506
|
|
|
485
507
|
# Check if the service is already enabled
|
|
@@ -488,11 +510,16 @@ def install_service():
|
|
|
488
510
|
print("The service is already enabled to start at boot.")
|
|
489
511
|
else:
|
|
490
512
|
print("The service is not currently enabled to start at boot.")
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
513
|
+
try:
|
|
514
|
+
user_input = input(
|
|
515
|
+
"Do you want to enable the service to start at boot? (y/n): "
|
|
516
|
+
)
|
|
517
|
+
enable_service = user_input.lower().startswith("y")
|
|
518
|
+
except (EOFError, KeyboardInterrupt):
|
|
519
|
+
print("\nInput cancelled. Skipping service enable.")
|
|
520
|
+
enable_service = False
|
|
521
|
+
|
|
522
|
+
if enable_service:
|
|
496
523
|
try:
|
|
497
524
|
subprocess.run(
|
|
498
525
|
["/usr/bin/systemctl", "--user", "enable", "mmrelay.service"],
|
|
@@ -509,7 +536,14 @@ def install_service():
|
|
|
509
536
|
service_active = is_service_active()
|
|
510
537
|
if service_active:
|
|
511
538
|
print("The service is already running.")
|
|
512
|
-
|
|
539
|
+
try:
|
|
540
|
+
user_input = input("Do you want to restart the service? (y/n): ")
|
|
541
|
+
restart_service = user_input.lower().startswith("y")
|
|
542
|
+
except (EOFError, KeyboardInterrupt):
|
|
543
|
+
print("\nInput cancelled. Skipping service restart.")
|
|
544
|
+
restart_service = False
|
|
545
|
+
|
|
546
|
+
if restart_service:
|
|
513
547
|
try:
|
|
514
548
|
subprocess.run(
|
|
515
549
|
["/usr/bin/systemctl", "--user", "restart", "mmrelay.service"],
|
|
@@ -526,11 +560,14 @@ def install_service():
|
|
|
526
560
|
print(f"Error: {e}")
|
|
527
561
|
else:
|
|
528
562
|
print("The service is not currently running.")
|
|
529
|
-
|
|
530
|
-
input("Do you want to start the service now? (y/n): ")
|
|
531
|
-
.lower()
|
|
532
|
-
|
|
533
|
-
|
|
563
|
+
try:
|
|
564
|
+
user_input = input("Do you want to start the service now? (y/n): ")
|
|
565
|
+
start_now = user_input.lower().startswith("y")
|
|
566
|
+
except (EOFError, KeyboardInterrupt):
|
|
567
|
+
print("\nInput cancelled. Skipping service start.")
|
|
568
|
+
start_now = False
|
|
569
|
+
|
|
570
|
+
if start_now:
|
|
534
571
|
if start_service():
|
|
535
572
|
# Wait for the service to start
|
|
536
573
|
wait_for_service_start()
|
mmrelay/tools/sample_config.yaml
CHANGED
|
@@ -3,6 +3,10 @@ matrix:
|
|
|
3
3
|
access_token: reaalllllyloooooongsecretttttcodeeeeeeforrrrbot # See: https://t2bot.io/docs/access_tokens/
|
|
4
4
|
bot_user_id: "@botuser:example.matrix.org"
|
|
5
5
|
|
|
6
|
+
# Message prefix customization (Meshtastic → Matrix direction)
|
|
7
|
+
#prefix_enabled: true # Enable prefixes on messages from mesh (e.g., "[Alice/MyMesh]: message")
|
|
8
|
+
#prefix_format: "[{long}/{mesh}]: " # Default format. Variables: {long1-20}, {long}, {short}, {mesh1-20}, {mesh}
|
|
9
|
+
|
|
6
10
|
matrix_rooms: # Needs at least 1 room & channel, but supports all Meshtastic channels
|
|
7
11
|
- id: "#someroomalias:example.matrix.org" # Matrix room aliases & IDs supported
|
|
8
12
|
meshtastic_channel: 0
|
|
@@ -15,9 +19,6 @@ meshtastic:
|
|
|
15
19
|
serial_port: /dev/ttyUSB0 # Only used when connection is "serial"
|
|
16
20
|
ble_address: AA:BB:CC:DD:EE:FF # Only used when connection is "ble" - Uses either an address or name from a `meshtastic --ble-scan`
|
|
17
21
|
meshnet_name: Your Meshnet Name # This is displayed in full on Matrix, but is truncated when sent to a Meshnet
|
|
18
|
-
broadcast_enabled: true # Must be set to true to enable Matrix to Meshtastic messages
|
|
19
|
-
detection_sensor: true # Must be set to true to forward messages of Meshtastic's detection sensor module
|
|
20
|
-
plugin_response_delay: 3 # Default response delay in seconds for plugins that respond on the mesh;
|
|
21
22
|
message_interactions: # Configure reactions and replies (both require message storage in database)
|
|
22
23
|
reactions: false # Enable reaction relaying between platforms
|
|
23
24
|
replies: false # Enable reply relaying between platforms
|
|
@@ -29,6 +30,15 @@ meshtastic:
|
|
|
29
30
|
# # Note: BLE connections use real-time disconnection detection and skip periodic checks
|
|
30
31
|
# # Legacy: heartbeat_interval at meshtastic level still supported but deprecated
|
|
31
32
|
|
|
33
|
+
# Additional configuration options (commented out with defaults)
|
|
34
|
+
#broadcast_enabled: true # Must be set to true to enable Matrix to Meshtastic messages
|
|
35
|
+
#detection_sensor: true # Must be set to true to forward messages of Meshtastic's detection sensor module
|
|
36
|
+
#message_delay: 2.2 # Delay in seconds between messages sent to mesh (minimum: 2.0 due to firmware)
|
|
37
|
+
|
|
38
|
+
# Message prefix customization (Matrix → Meshtastic direction)
|
|
39
|
+
#prefix_enabled: true # Enable username prefixes on messages sent to mesh (e.g., "Alice[M]: message")
|
|
40
|
+
#prefix_format: "{display5}[M]: " # Default format. See EXTRA_CONFIGURATION.md for all variables.
|
|
41
|
+
|
|
32
42
|
logging:
|
|
33
43
|
level: info
|
|
34
44
|
#log_to_file: true # Set to true to enable file logging
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mmrelay
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.4
|
|
4
4
|
Summary: Bridge between Meshtastic mesh networks and Matrix chat rooms
|
|
5
|
-
Home-page: https://github.com/
|
|
5
|
+
Home-page: https://github.com/jeremiah-k/meshtastic-matrix-relay
|
|
6
6
|
Author: Geoff Whittington, Jeremiah K., and contributors
|
|
7
7
|
Author-email: jeremiahk@gmx.com
|
|
8
|
-
Project-URL: Bug Tracker, https://github.com/
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/jeremiah-k/meshtastic-matrix-relay/issues
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Classifier: License :: OSI Approved ::
|
|
10
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
11
11
|
Classifier: Operating System :: OS Independent
|
|
12
12
|
Classifier: Development Status :: 4 - Beta
|
|
13
13
|
Classifier: Topic :: Communications
|
|
@@ -24,7 +24,7 @@ Requires-Dist: haversine==2.9.0
|
|
|
24
24
|
Requires-Dist: schedule==1.2.2
|
|
25
25
|
Requires-Dist: platformdirs==4.3.8
|
|
26
26
|
Requires-Dist: py-staticmaps>=0.4.0
|
|
27
|
-
Requires-Dist: rich==14.
|
|
27
|
+
Requires-Dist: rich==14.1.0
|
|
28
28
|
Requires-Dist: setuptools==80.9.0
|
|
29
29
|
Dynamic: author
|
|
30
30
|
Dynamic: author-email
|
|
@@ -58,11 +58,11 @@ A powerful and easy-to-use relay between Meshtastic devices and Matrix chat room
|
|
|
58
58
|
- ✨️ _Bidirectional replies and reactions support_ ✨️ **NEW!!**
|
|
59
59
|
- ✨️ _Native Docker support_ ✨️ **NEW!!**
|
|
60
60
|
|
|
61
|
-
_We would love to support [Matrix E2EE rooms](https://github.com/
|
|
61
|
+
_We would love to support [Matrix E2EE rooms](https://github.com/jeremiah-k/meshtastic-matrix-relay/issues/33), but this is currently not implemented._
|
|
62
62
|
|
|
63
63
|
## Documentation
|
|
64
64
|
|
|
65
|
-
Visit our [Wiki](https://github.com/
|
|
65
|
+
Visit our [Wiki](https://github.com/jeremiah-k/meshtastic-matrix-relay/wiki) for comprehensive guides and information.
|
|
66
66
|
|
|
67
67
|
- [Installation Instructions](docs/INSTRUCTIONS.md) - Setup and configuration guide
|
|
68
68
|
|
|
@@ -101,15 +101,13 @@ Docker provides isolated environment, easy deployment, automatic restarts, and v
|
|
|
101
101
|
|
|
102
102
|
For detailed Docker setup instructions, see the [Docker Guide](docs/DOCKER.md).
|
|
103
103
|
|
|
104
|
-
> **Note**: Docker builds currently use a temporary fork of the meshtastic library with BLE hanging fixes. PyPI releases use the upstream library. This will be resolved when the fixes are merged upstream.
|
|
105
|
-
|
|
106
104
|
---
|
|
107
105
|
|
|
108
106
|
## Windows Installer
|
|
109
107
|
|
|
110
108
|

|
|
111
109
|
|
|
112
|
-
The latest installer is available in the [releases section](https://github.com/
|
|
110
|
+
The latest installer is available in the [releases section](https://github.com/jeremiah-k/meshtastic-matrix-relay/releases).
|
|
113
111
|
|
|
114
112
|
---
|
|
115
113
|
|
|
@@ -127,7 +125,7 @@ Produce high-level details about your mesh:
|
|
|
127
125
|
|
|
128
126
|

|
|
129
127
|
|
|
130
|
-
See the full list of [core plugins](https://github.com/
|
|
128
|
+
See the full list of [core plugins](https://github.com/jeremiah-k/meshtastic-matrix-relay/wiki/Core-Plugins).
|
|
131
129
|
|
|
132
130
|
### Community & Custom Plugins
|
|
133
131
|
|
|
@@ -136,9 +134,9 @@ MMRelay's plugin system allows you to extend functionality in two ways:
|
|
|
136
134
|
- **Custom Plugins**: Create personal plugins for your own use, stored in `~/.mmrelay/plugins/custom/`
|
|
137
135
|
- **Community Plugins**: Share your creations with others or use plugins developed by the community
|
|
138
136
|
|
|
139
|
-
Check the [Community Plugins Development Guide](https://github.com/
|
|
137
|
+
Check the [Community Plugins Development Guide](https://github.com/jeremiah-k/meshtastic-matrix-relay/wiki/Community-Plugin-Development-Guide) in our wiki to get started.
|
|
140
138
|
|
|
141
|
-
✨️ Visit the [Community Plugins List](https://github.com/
|
|
139
|
+
✨️ Visit the [Community Plugins List](https://github.com/jeremiah-k/meshtastic-matrix-relay/wiki/Community-Plugin-List)!
|
|
142
140
|
|
|
143
141
|
#### Install a Community Plugin
|
|
144
142
|
|
|
@@ -164,7 +162,7 @@ Plugins make it easy to extend functionality without modifying the core program.
|
|
|
164
162
|
|
|
165
163
|
## Getting Started with Matrix
|
|
166
164
|
|
|
167
|
-
See our Wiki page [Getting Started With Matrix & MM Relay](https://github.com/
|
|
165
|
+
See our Wiki page [Getting Started With Matrix & MM Relay](https://github.com/jeremiah-k/meshtastic-matrix-relay/wiki/Getting-Started-With-Matrix-&-MM-Relay).
|
|
168
166
|
|
|
169
167
|
---
|
|
170
168
|
|