mmrelay 1.0__py3-none-any.whl → 1.0.1__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 +7 -3
- mmrelay/cli.py +9 -2
- mmrelay/config.py +2 -3
- mmrelay/log_utils.py +56 -27
- mmrelay/main.py +30 -1
- mmrelay/meshtastic_utils.py +54 -4
- mmrelay/setup_utils.py +254 -91
- {mmrelay-1.0.dist-info → mmrelay-1.0.1.dist-info}/METADATA +24 -9
- {mmrelay-1.0.dist-info → mmrelay-1.0.1.dist-info}/RECORD +13 -13
- {mmrelay-1.0.dist-info → mmrelay-1.0.1.dist-info}/WHEEL +1 -1
- {mmrelay-1.0.dist-info → mmrelay-1.0.1.dist-info}/entry_points.txt +0 -0
- {mmrelay-1.0.dist-info → mmrelay-1.0.1.dist-info}/licenses/LICENSE +0 -0
- {mmrelay-1.0.dist-info → mmrelay-1.0.1.dist-info}/top_level.txt +0 -0
mmrelay/__init__.py
CHANGED
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
Meshtastic Matrix Relay - Bridge between Meshtastic mesh networks and Matrix chat rooms.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
import importlib.metadata
|
|
5
6
|
import os
|
|
6
7
|
|
|
7
|
-
#
|
|
8
|
-
|
|
9
|
-
__version__ =
|
|
8
|
+
# Try to get version from package metadata (setup.cfg)
|
|
9
|
+
try:
|
|
10
|
+
__version__ = importlib.metadata.version("mmrelay")
|
|
11
|
+
except importlib.metadata.PackageNotFoundError:
|
|
12
|
+
# If package is not installed, fall back to environment variable or default
|
|
13
|
+
__version__ = os.environ.get("GITHUB_REF_NAME", "1.0.1")
|
mmrelay/cli.py
CHANGED
|
@@ -94,6 +94,13 @@ def get_version():
|
|
|
94
94
|
return __version__
|
|
95
95
|
|
|
96
96
|
|
|
97
|
+
def print_version():
|
|
98
|
+
"""
|
|
99
|
+
Print the version in a simple format.
|
|
100
|
+
"""
|
|
101
|
+
print(f"MMRelay v{__version__}")
|
|
102
|
+
|
|
103
|
+
|
|
97
104
|
def check_config(args=None):
|
|
98
105
|
"""
|
|
99
106
|
Check if the configuration file is valid.
|
|
@@ -262,7 +269,7 @@ def main():
|
|
|
262
269
|
|
|
263
270
|
# Handle --version
|
|
264
271
|
if args.version:
|
|
265
|
-
|
|
272
|
+
print_version()
|
|
266
273
|
return 0
|
|
267
274
|
|
|
268
275
|
# If no command was specified, run the main functionality
|
|
@@ -289,7 +296,7 @@ def handle_cli_commands(args):
|
|
|
289
296
|
"""
|
|
290
297
|
# Handle --version
|
|
291
298
|
if args.version:
|
|
292
|
-
|
|
299
|
+
print_version()
|
|
293
300
|
return True
|
|
294
301
|
|
|
295
302
|
# Handle --install-service
|
mmrelay/config.py
CHANGED
|
@@ -187,7 +187,7 @@ def load_config(config_file=None, args=None):
|
|
|
187
187
|
|
|
188
188
|
# If a specific config file was provided, use it
|
|
189
189
|
if config_file and os.path.isfile(config_file):
|
|
190
|
-
|
|
190
|
+
# Store the config path but don't log it yet - will be logged by main.py
|
|
191
191
|
with open(config_file, "r") as f:
|
|
192
192
|
relay_config = yaml.load(f, Loader=SafeLoader)
|
|
193
193
|
config_path = config_file
|
|
@@ -200,10 +200,9 @@ def load_config(config_file=None, args=None):
|
|
|
200
200
|
for path in config_paths:
|
|
201
201
|
if os.path.isfile(path):
|
|
202
202
|
config_path = path
|
|
203
|
-
|
|
203
|
+
# Store the config path but don't log it yet - will be logged by main.py
|
|
204
204
|
with open(config_path, "r") as f:
|
|
205
205
|
relay_config = yaml.load(f, Loader=SafeLoader)
|
|
206
|
-
logger.info(f"Loaded configuration with keys: {list(relay_config.keys())}")
|
|
207
206
|
return relay_config
|
|
208
207
|
|
|
209
208
|
# No config file found
|
mmrelay/log_utils.py
CHANGED
|
@@ -2,44 +2,84 @@ import logging
|
|
|
2
2
|
import os
|
|
3
3
|
from logging.handlers import RotatingFileHandler
|
|
4
4
|
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.logging import RichHandler
|
|
7
|
+
|
|
5
8
|
from mmrelay.cli import parse_arguments
|
|
6
9
|
from mmrelay.config import get_log_dir
|
|
7
10
|
|
|
11
|
+
# Initialize Rich console
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
# Define custom log level styles - not used directly but kept for reference
|
|
15
|
+
# Rich 14.0.0+ supports level_styles parameter, but we're using an approach
|
|
16
|
+
# that works with older versions too
|
|
17
|
+
LOG_LEVEL_STYLES = {
|
|
18
|
+
"DEBUG": "dim blue",
|
|
19
|
+
"INFO": "green",
|
|
20
|
+
"WARNING": "yellow",
|
|
21
|
+
"ERROR": "bold red",
|
|
22
|
+
"CRITICAL": "bold white on red",
|
|
23
|
+
}
|
|
24
|
+
|
|
8
25
|
# Global config variable that will be set from main.py
|
|
9
26
|
config = None
|
|
10
27
|
|
|
28
|
+
# Global variable to store the log file path
|
|
29
|
+
log_file_path = None
|
|
30
|
+
|
|
11
31
|
|
|
12
32
|
def get_logger(name):
|
|
13
33
|
logger = logging.getLogger(name=name)
|
|
14
34
|
|
|
15
35
|
# Default to INFO level if config is not available
|
|
16
36
|
log_level = logging.INFO
|
|
37
|
+
color_enabled = True # Default to using colors
|
|
17
38
|
|
|
18
|
-
# Try to get log level from config
|
|
39
|
+
# Try to get log level and color settings from config
|
|
19
40
|
global config
|
|
20
|
-
if config is not None and "logging" in config
|
|
21
|
-
|
|
41
|
+
if config is not None and "logging" in config:
|
|
42
|
+
if "level" in config["logging"]:
|
|
43
|
+
log_level = getattr(logging, config["logging"]["level"].upper())
|
|
44
|
+
# Check if colors should be disabled
|
|
45
|
+
if "color_enabled" in config["logging"]:
|
|
46
|
+
color_enabled = config["logging"]["color_enabled"]
|
|
22
47
|
|
|
23
48
|
logger.setLevel(log_level)
|
|
24
49
|
logger.propagate = False
|
|
25
50
|
|
|
26
|
-
# Add
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
51
|
+
# Add handler for console logging (with or without colors)
|
|
52
|
+
if color_enabled:
|
|
53
|
+
# Use Rich handler with colors
|
|
54
|
+
console_handler = RichHandler(
|
|
55
|
+
rich_tracebacks=True,
|
|
56
|
+
console=console,
|
|
57
|
+
show_time=True,
|
|
58
|
+
show_level=True,
|
|
59
|
+
show_path=False,
|
|
60
|
+
markup=True,
|
|
61
|
+
log_time_format="%Y-%m-%d %H:%M:%S",
|
|
62
|
+
omit_repeated_times=False,
|
|
63
|
+
)
|
|
64
|
+
console_handler.setFormatter(logging.Formatter("%(name)s: %(message)s"))
|
|
65
|
+
else:
|
|
66
|
+
# Use standard handler without colors
|
|
67
|
+
console_handler = logging.StreamHandler()
|
|
68
|
+
console_handler.setFormatter(
|
|
69
|
+
logging.Formatter(
|
|
70
|
+
fmt="%(asctime)s %(levelname)s:%(name)s:%(message)s",
|
|
71
|
+
datefmt="%Y-%m-%d %H:%M:%S %z",
|
|
72
|
+
)
|
|
32
73
|
)
|
|
33
|
-
)
|
|
34
|
-
logger.addHandler(stream_handler)
|
|
74
|
+
logger.addHandler(console_handler)
|
|
35
75
|
|
|
36
76
|
# Check command line arguments for log file path
|
|
37
77
|
args = parse_arguments()
|
|
38
78
|
|
|
39
|
-
# Check if file logging is enabled
|
|
79
|
+
# Check if file logging is enabled (default to True for better user experience)
|
|
40
80
|
if (
|
|
41
81
|
config is not None
|
|
42
|
-
and config.get("logging", {}).get("log_to_file",
|
|
82
|
+
and config.get("logging", {}).get("log_to_file", True)
|
|
43
83
|
or args.logfile
|
|
44
84
|
):
|
|
45
85
|
# Priority: 1. Command line arg, 2. Config file, 3. Default location (~/.mmrelay/logs)
|
|
@@ -64,21 +104,10 @@ def get_logger(name):
|
|
|
64
104
|
if log_dir: # Ensure non-empty directory paths exist
|
|
65
105
|
os.makedirs(log_dir, exist_ok=True)
|
|
66
106
|
|
|
67
|
-
#
|
|
107
|
+
# Store the log file path for later use
|
|
68
108
|
if name == "M<>M Relay":
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
basic_logger = logging.getLogger("LogSetup")
|
|
72
|
-
basic_logger.setLevel(logging.INFO)
|
|
73
|
-
basic_handler = logging.StreamHandler()
|
|
74
|
-
basic_handler.setFormatter(
|
|
75
|
-
logging.Formatter(
|
|
76
|
-
fmt="%(asctime)s %(levelname)s:%(name)s:%(message)s",
|
|
77
|
-
datefmt="%Y-%m-%d %H:%M:%S %z",
|
|
78
|
-
)
|
|
79
|
-
)
|
|
80
|
-
basic_logger.addHandler(basic_handler)
|
|
81
|
-
basic_logger.info(f"Writing logs to: {log_file}")
|
|
109
|
+
global log_file_path
|
|
110
|
+
log_file_path = log_file
|
|
82
111
|
|
|
83
112
|
# Create a file handler for logging
|
|
84
113
|
try:
|
mmrelay/main.py
CHANGED
|
@@ -10,8 +10,9 @@ import sys
|
|
|
10
10
|
|
|
11
11
|
from nio import ReactionEvent, RoomMessageEmote, RoomMessageNotice, RoomMessageText
|
|
12
12
|
|
|
13
|
+
# Import version from package
|
|
13
14
|
# Import meshtastic_utils as a module to set event_loop
|
|
14
|
-
from mmrelay import meshtastic_utils
|
|
15
|
+
from mmrelay import __version__, meshtastic_utils
|
|
15
16
|
from mmrelay.db_utils import (
|
|
16
17
|
initialize_database,
|
|
17
18
|
update_longnames,
|
|
@@ -33,6 +34,18 @@ logger = get_logger(name="M<>M Relay")
|
|
|
33
34
|
logging.getLogger("nio").setLevel(logging.ERROR)
|
|
34
35
|
|
|
35
36
|
|
|
37
|
+
# Flag to track if banner has been printed
|
|
38
|
+
_banner_printed = False
|
|
39
|
+
|
|
40
|
+
def print_banner():
|
|
41
|
+
"""Print a simple startup message with version information."""
|
|
42
|
+
global _banner_printed
|
|
43
|
+
# Only print the banner once
|
|
44
|
+
if not _banner_printed:
|
|
45
|
+
logger.info(f"Starting MMRelay v{__version__}")
|
|
46
|
+
_banner_printed = True
|
|
47
|
+
|
|
48
|
+
|
|
36
49
|
async def main(config):
|
|
37
50
|
"""
|
|
38
51
|
Main asynchronous function to set up and run the relay.
|
|
@@ -202,6 +215,9 @@ def run_main(args):
|
|
|
202
215
|
Returns:
|
|
203
216
|
int: Exit code (0 for success, non-zero for failure)
|
|
204
217
|
"""
|
|
218
|
+
# Print the banner at startup
|
|
219
|
+
print_banner()
|
|
220
|
+
|
|
205
221
|
# Handle the --data-dir option
|
|
206
222
|
if args and args.data_dir:
|
|
207
223
|
import os
|
|
@@ -245,6 +261,19 @@ def run_main(args):
|
|
|
245
261
|
set_config(db_utils, config)
|
|
246
262
|
set_config(base_plugin, config)
|
|
247
263
|
|
|
264
|
+
# Get config path and log file path for logging
|
|
265
|
+
from mmrelay.config import config_path
|
|
266
|
+
from mmrelay.log_utils import log_file_path
|
|
267
|
+
|
|
268
|
+
# Create a logger with a different name to avoid conflicts with the one in config.py
|
|
269
|
+
config_rich_logger = get_logger("ConfigInfo")
|
|
270
|
+
|
|
271
|
+
# Now log the config file and log file locations with the properly formatted logger
|
|
272
|
+
if config_path:
|
|
273
|
+
config_rich_logger.info(f"Config file location: {config_path}")
|
|
274
|
+
if log_file_path:
|
|
275
|
+
config_rich_logger.info(f"Log file location: {log_file_path}")
|
|
276
|
+
|
|
248
277
|
# Check if config exists and has the required keys
|
|
249
278
|
required_keys = ["matrix", "meshtastic", "matrix_rooms"]
|
|
250
279
|
|
mmrelay/meshtastic_utils.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import contextlib
|
|
3
3
|
import io
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
4
6
|
import threading
|
|
5
7
|
import time
|
|
6
8
|
from typing import List
|
|
@@ -46,6 +48,32 @@ shutting_down = False
|
|
|
46
48
|
reconnect_task = None # To keep track of the reconnect task
|
|
47
49
|
|
|
48
50
|
|
|
51
|
+
def is_running_as_service():
|
|
52
|
+
"""
|
|
53
|
+
Check if the application is running as a systemd service.
|
|
54
|
+
This is used to determine whether to show Rich progress indicators.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
bool: True if running as a service, False otherwise
|
|
58
|
+
"""
|
|
59
|
+
# Check for INVOCATION_ID environment variable (set by systemd)
|
|
60
|
+
if os.environ.get("INVOCATION_ID"):
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
# Check if parent process is systemd
|
|
64
|
+
try:
|
|
65
|
+
with open("/proc/self/status") as f:
|
|
66
|
+
for line in f:
|
|
67
|
+
if line.startswith("PPid:"):
|
|
68
|
+
ppid = int(line.split()[1])
|
|
69
|
+
with open(f"/proc/{ppid}/comm") as p:
|
|
70
|
+
return p.read().strip() == "systemd"
|
|
71
|
+
except (FileNotFoundError, PermissionError, ValueError):
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
|
|
49
77
|
def serial_port_exists(port_name):
|
|
50
78
|
"""
|
|
51
79
|
Check if the specified serial port exists.
|
|
@@ -118,7 +146,7 @@ def connect_meshtastic(passed_config=None, force_connect=False):
|
|
|
118
146
|
if connection_type == "serial":
|
|
119
147
|
# Serial connection
|
|
120
148
|
serial_port = config["meshtastic"]["serial_port"]
|
|
121
|
-
logger.info(f"Connecting to serial port {serial_port}
|
|
149
|
+
logger.info(f"Connecting to serial port {serial_port}")
|
|
122
150
|
|
|
123
151
|
# Check if serial port exists before connecting
|
|
124
152
|
if not serial_port_exists(serial_port):
|
|
@@ -137,7 +165,9 @@ def connect_meshtastic(passed_config=None, force_connect=False):
|
|
|
137
165
|
# BLE connection
|
|
138
166
|
ble_address = config["meshtastic"].get("ble_address")
|
|
139
167
|
if ble_address:
|
|
140
|
-
logger.info(f"Connecting to BLE address {ble_address}
|
|
168
|
+
logger.info(f"Connecting to BLE address {ble_address}")
|
|
169
|
+
|
|
170
|
+
# Connect without progress indicator
|
|
141
171
|
meshtastic_client = meshtastic.ble_interface.BLEInterface(
|
|
142
172
|
address=ble_address,
|
|
143
173
|
noProto=False,
|
|
@@ -151,7 +181,9 @@ def connect_meshtastic(passed_config=None, force_connect=False):
|
|
|
151
181
|
elif connection_type == "tcp":
|
|
152
182
|
# TCP connection
|
|
153
183
|
target_host = config["meshtastic"]["host"]
|
|
154
|
-
logger.info(f"Connecting to host {target_host}
|
|
184
|
+
logger.info(f"Connecting to host {target_host}")
|
|
185
|
+
|
|
186
|
+
# Connect without progress indicator
|
|
155
187
|
meshtastic_client = meshtastic.tcp_interface.TCPInterface(
|
|
156
188
|
hostname=target_host
|
|
157
189
|
)
|
|
@@ -244,7 +276,25 @@ async def reconnect():
|
|
|
244
276
|
logger.info(
|
|
245
277
|
f"Reconnection attempt starting in {backoff_time} seconds..."
|
|
246
278
|
)
|
|
247
|
-
|
|
279
|
+
|
|
280
|
+
# Show reconnection countdown with Rich (if not in a service)
|
|
281
|
+
if not is_running_as_service():
|
|
282
|
+
from rich.progress import Progress, TextColumn, BarColumn, TimeRemainingColumn
|
|
283
|
+
with Progress(
|
|
284
|
+
TextColumn("[cyan]Meshtastic: Reconnecting in"),
|
|
285
|
+
BarColumn(),
|
|
286
|
+
TextColumn("[cyan]{task.percentage:.0f}%"),
|
|
287
|
+
TimeRemainingColumn(),
|
|
288
|
+
transient=True,
|
|
289
|
+
) as progress:
|
|
290
|
+
task = progress.add_task("Waiting", total=backoff_time)
|
|
291
|
+
for _ in range(backoff_time):
|
|
292
|
+
if shutting_down:
|
|
293
|
+
break
|
|
294
|
+
await asyncio.sleep(1)
|
|
295
|
+
progress.update(task, advance=1)
|
|
296
|
+
else:
|
|
297
|
+
await asyncio.sleep(backoff_time)
|
|
248
298
|
if shutting_down:
|
|
249
299
|
logger.debug(
|
|
250
300
|
"Shutdown in progress. Aborting reconnection attempts."
|
mmrelay/setup_utils.py
CHANGED
|
@@ -14,8 +14,20 @@ from pathlib import Path
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def get_executable_path():
|
|
17
|
-
"""Get the full path to the mmrelay executable.
|
|
18
|
-
|
|
17
|
+
"""Get the full path to the mmrelay executable.
|
|
18
|
+
|
|
19
|
+
This function tries to find the mmrelay executable in the PATH,
|
|
20
|
+
which works for both pipx and pip installations.
|
|
21
|
+
"""
|
|
22
|
+
mmrelay_path = shutil.which("mmrelay")
|
|
23
|
+
if mmrelay_path:
|
|
24
|
+
print(f"Found mmrelay executable at: {mmrelay_path}")
|
|
25
|
+
return mmrelay_path
|
|
26
|
+
else:
|
|
27
|
+
print(
|
|
28
|
+
"Warning: Could not find mmrelay executable in PATH. Using current Python interpreter."
|
|
29
|
+
)
|
|
30
|
+
return sys.executable
|
|
19
31
|
|
|
20
32
|
|
|
21
33
|
def get_user_service_path():
|
|
@@ -31,7 +43,6 @@ def service_exists():
|
|
|
31
43
|
|
|
32
44
|
def print_service_commands():
|
|
33
45
|
"""Print the commands for controlling the systemd user service."""
|
|
34
|
-
print("\nUse these commands to control the mmrelay service:")
|
|
35
46
|
print(" systemctl --user start mmrelay.service # Start the service")
|
|
36
47
|
print(" systemctl --user stop mmrelay.service # Stop the service")
|
|
37
48
|
print(" systemctl --user restart mmrelay.service # Restart the service")
|
|
@@ -39,24 +50,29 @@ def print_service_commands():
|
|
|
39
50
|
|
|
40
51
|
|
|
41
52
|
def wait_for_service_start():
|
|
42
|
-
"""Wait for the service to start with a
|
|
43
|
-
import sys
|
|
53
|
+
"""Wait for the service to start with a Rich progress indicator."""
|
|
44
54
|
import time
|
|
55
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
|
|
45
56
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
# Create a Rich progress display with spinner and elapsed time
|
|
58
|
+
with Progress(
|
|
59
|
+
SpinnerColumn(),
|
|
60
|
+
TextColumn("[bold green]Starting mmrelay service..."),
|
|
61
|
+
TimeElapsedColumn(),
|
|
62
|
+
transient=True,
|
|
63
|
+
) as progress:
|
|
64
|
+
# Add a task that will run for approximately 10 seconds
|
|
65
|
+
task = progress.add_task("Starting", total=100)
|
|
51
66
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
sys.stdout.flush()
|
|
67
|
+
# Update progress over 10 seconds
|
|
68
|
+
for i in range(10):
|
|
69
|
+
time.sleep(1)
|
|
70
|
+
progress.update(task, completed=10 * (i + 1))
|
|
57
71
|
|
|
58
|
-
|
|
59
|
-
|
|
72
|
+
# Check if service is active after 5 seconds to potentially finish early
|
|
73
|
+
if i >= 5 and is_service_active():
|
|
74
|
+
progress.update(task, completed=100)
|
|
75
|
+
break
|
|
60
76
|
|
|
61
77
|
|
|
62
78
|
def read_service_file():
|
|
@@ -67,45 +83,108 @@ def read_service_file():
|
|
|
67
83
|
return None
|
|
68
84
|
|
|
69
85
|
|
|
70
|
-
def
|
|
71
|
-
"""
|
|
72
|
-
executable_path = get_executable_path()
|
|
73
|
-
if not executable_path:
|
|
74
|
-
print("Error: Could not find mmrelay executable in PATH")
|
|
75
|
-
return False
|
|
76
|
-
|
|
77
|
-
# Create service directory if it doesn't exist
|
|
78
|
-
service_dir = get_user_service_path().parent
|
|
79
|
-
service_dir.mkdir(parents=True, exist_ok=True)
|
|
80
|
-
|
|
81
|
-
# Create logs directory if it doesn't exist
|
|
82
|
-
logs_dir = Path.home() / ".mmrelay" / "logs"
|
|
83
|
-
logs_dir.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
def get_template_service_path():
|
|
87
|
+
"""Find the path to the template service file.
|
|
84
88
|
|
|
89
|
+
Returns:
|
|
90
|
+
str: The path to the template service file, or None if not found.
|
|
91
|
+
"""
|
|
85
92
|
# Try to find the service template file
|
|
86
|
-
# First, check in the package directory
|
|
93
|
+
# First, check in the package directory (where it should be after installation)
|
|
87
94
|
package_dir = os.path.dirname(__file__)
|
|
88
95
|
template_path = os.path.join(
|
|
89
96
|
os.path.dirname(os.path.dirname(package_dir)), "tools", "mmrelay.service"
|
|
90
97
|
)
|
|
91
98
|
|
|
92
|
-
# If not found, try the repository root
|
|
99
|
+
# If not found, try the repository root (for development)
|
|
93
100
|
if not os.path.exists(template_path):
|
|
94
101
|
repo_root = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
|
95
102
|
template_path = os.path.join(repo_root, "tools", "mmrelay.service")
|
|
96
103
|
|
|
97
|
-
# If still not found, try the current directory
|
|
104
|
+
# If still not found, try the current directory (fallback)
|
|
98
105
|
if not os.path.exists(template_path):
|
|
99
106
|
template_path = os.path.join(os.getcwd(), "tools", "mmrelay.service")
|
|
100
107
|
|
|
101
108
|
if not os.path.exists(template_path):
|
|
102
|
-
|
|
103
|
-
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
return template_path
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_template_service_content():
|
|
115
|
+
"""Get the content of the template service file.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
str: The content of the template service file, or None if not found.
|
|
119
|
+
"""
|
|
120
|
+
template_path = get_template_service_path()
|
|
121
|
+
if not template_path:
|
|
122
|
+
return None
|
|
104
123
|
|
|
105
124
|
# Read the template
|
|
106
125
|
with open(template_path, "r") as f:
|
|
107
126
|
service_template = f.read()
|
|
108
127
|
|
|
128
|
+
return service_template
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def is_service_enabled():
|
|
132
|
+
"""Check if the service is enabled.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
bool: True if the service is enabled, False otherwise.
|
|
136
|
+
"""
|
|
137
|
+
try:
|
|
138
|
+
result = subprocess.run(
|
|
139
|
+
["/usr/bin/systemctl", "--user", "is-enabled", "mmrelay.service"],
|
|
140
|
+
check=False, # Don't raise an exception if the service is not enabled
|
|
141
|
+
capture_output=True,
|
|
142
|
+
text=True,
|
|
143
|
+
)
|
|
144
|
+
return result.returncode == 0 and result.stdout.strip() == "enabled"
|
|
145
|
+
except Exception:
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def is_service_active():
|
|
150
|
+
"""Check if the service is active (running).
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
bool: True if the service is active, False otherwise.
|
|
154
|
+
"""
|
|
155
|
+
try:
|
|
156
|
+
result = subprocess.run(
|
|
157
|
+
["/usr/bin/systemctl", "--user", "is-active", "mmrelay.service"],
|
|
158
|
+
check=False, # Don't raise an exception if the service is not active
|
|
159
|
+
capture_output=True,
|
|
160
|
+
text=True,
|
|
161
|
+
)
|
|
162
|
+
return result.returncode == 0 and result.stdout.strip() == "active"
|
|
163
|
+
except Exception:
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def create_service_file():
|
|
168
|
+
"""Create the systemd user service file."""
|
|
169
|
+
executable_path = get_executable_path()
|
|
170
|
+
if not executable_path:
|
|
171
|
+
print("Error: Could not find mmrelay executable in PATH")
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
# Create service directory if it doesn't exist
|
|
175
|
+
service_dir = get_user_service_path().parent
|
|
176
|
+
service_dir.mkdir(parents=True, exist_ok=True)
|
|
177
|
+
|
|
178
|
+
# Create logs directory if it doesn't exist
|
|
179
|
+
logs_dir = Path.home() / ".mmrelay" / "logs"
|
|
180
|
+
logs_dir.mkdir(parents=True, exist_ok=True)
|
|
181
|
+
|
|
182
|
+
# Get the template service content
|
|
183
|
+
service_template = get_template_service_content()
|
|
184
|
+
if not service_template:
|
|
185
|
+
print("Error: Could not find service template file")
|
|
186
|
+
return False
|
|
187
|
+
|
|
109
188
|
# Replace placeholders with actual values
|
|
110
189
|
service_content = (
|
|
111
190
|
service_template.replace(
|
|
@@ -147,74 +226,158 @@ def reload_daemon():
|
|
|
147
226
|
return False
|
|
148
227
|
|
|
149
228
|
|
|
150
|
-
def
|
|
151
|
-
"""
|
|
229
|
+
def service_needs_update():
|
|
230
|
+
"""Check if the service file needs to be updated.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
tuple: (needs_update, reason) where needs_update is a boolean and reason is a string
|
|
234
|
+
"""
|
|
152
235
|
# Check if service already exists
|
|
153
236
|
existing_service = read_service_file()
|
|
237
|
+
if not existing_service:
|
|
238
|
+
return True, "No existing service file found"
|
|
154
239
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
.lower()
|
|
160
|
-
.startswith("y")
|
|
161
|
-
):
|
|
162
|
-
print("Service installation cancelled.")
|
|
163
|
-
print_service_commands()
|
|
164
|
-
return True
|
|
240
|
+
# Get the template service path
|
|
241
|
+
template_path = get_template_service_path()
|
|
242
|
+
if not template_path:
|
|
243
|
+
return False, "Could not find template service file"
|
|
165
244
|
|
|
166
|
-
#
|
|
167
|
-
|
|
168
|
-
|
|
245
|
+
# Get the executable path
|
|
246
|
+
executable_path = get_executable_path()
|
|
247
|
+
if not executable_path:
|
|
248
|
+
return False, "Could not find mmrelay executable"
|
|
169
249
|
|
|
170
|
-
#
|
|
171
|
-
if not
|
|
172
|
-
return
|
|
250
|
+
# Check if the ExecStart line in the existing service file contains the correct executable
|
|
251
|
+
if executable_path not in existing_service:
|
|
252
|
+
return (
|
|
253
|
+
True,
|
|
254
|
+
f"Service file does not use the current executable: {executable_path}",
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# Check if the PATH environment includes pipx paths
|
|
258
|
+
if "%h/.local/pipx/venvs/mmrelay/bin" not in existing_service:
|
|
259
|
+
return True, "Service file does not include pipx paths in PATH environment"
|
|
260
|
+
|
|
261
|
+
# Check if the service file has been modified recently
|
|
262
|
+
template_mtime = os.path.getmtime(template_path)
|
|
263
|
+
service_path = get_user_service_path()
|
|
264
|
+
if os.path.exists(service_path):
|
|
265
|
+
service_mtime = os.path.getmtime(service_path)
|
|
266
|
+
if template_mtime > service_mtime:
|
|
267
|
+
return True, "Template service file is newer than installed service file"
|
|
268
|
+
|
|
269
|
+
return False, "Service file is up to date"
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def install_service():
|
|
273
|
+
"""Install or update the MMRelay user service."""
|
|
274
|
+
# Check if service already exists
|
|
275
|
+
existing_service = read_service_file()
|
|
276
|
+
service_path = get_user_service_path()
|
|
277
|
+
|
|
278
|
+
# Check if the service needs to be updated
|
|
279
|
+
update_needed, reason = service_needs_update()
|
|
173
280
|
|
|
281
|
+
# Check if the service is already installed and if it needs updating
|
|
174
282
|
if existing_service:
|
|
175
|
-
print("
|
|
283
|
+
print(f"A service file already exists at {service_path}")
|
|
284
|
+
|
|
285
|
+
if update_needed:
|
|
286
|
+
print(f"The service file needs to be updated: {reason}")
|
|
287
|
+
if (
|
|
288
|
+
not input("Do you want to update the service file? (y/n): ")
|
|
289
|
+
.lower()
|
|
290
|
+
.startswith("y")
|
|
291
|
+
):
|
|
292
|
+
print("Service update cancelled.")
|
|
293
|
+
print_service_commands()
|
|
294
|
+
return True
|
|
295
|
+
else:
|
|
296
|
+
print(f"No update needed for the service file: {reason}")
|
|
176
297
|
else:
|
|
177
|
-
print("
|
|
298
|
+
print(f"No service file found at {service_path}")
|
|
299
|
+
print("A new service file will be created.")
|
|
178
300
|
|
|
179
|
-
#
|
|
180
|
-
|
|
301
|
+
# Create or update service file if needed
|
|
302
|
+
if not existing_service or update_needed:
|
|
303
|
+
if not create_service_file():
|
|
304
|
+
return False
|
|
181
305
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
)
|
|
186
|
-
print("Please fix your configuration and then start the service manually.")
|
|
187
|
-
print_service_commands()
|
|
188
|
-
return True
|
|
306
|
+
# Reload daemon
|
|
307
|
+
if not reload_daemon():
|
|
308
|
+
return False
|
|
189
309
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
input("Do you want to enable the service to start at boot? (y/n): ")
|
|
193
|
-
.lower()
|
|
194
|
-
.startswith("y")
|
|
195
|
-
):
|
|
196
|
-
try:
|
|
197
|
-
subprocess.run(
|
|
198
|
-
["/usr/bin/systemctl", "--user", "enable", "mmrelay.service"],
|
|
199
|
-
check=True,
|
|
200
|
-
)
|
|
201
|
-
print("Service enabled successfully")
|
|
202
|
-
except subprocess.CalledProcessError as e:
|
|
203
|
-
print(f"Error enabling service: {e}")
|
|
204
|
-
except OSError as e:
|
|
205
|
-
print(f"Error: {e}")
|
|
206
|
-
|
|
207
|
-
if input("Do you want to start the service now? (y/n): ").lower().startswith("y"):
|
|
208
|
-
if start_service():
|
|
209
|
-
# Wait for the service to start
|
|
210
|
-
wait_for_service_start()
|
|
211
|
-
|
|
212
|
-
# Show service status
|
|
213
|
-
show_service_status()
|
|
214
|
-
print("Service started successfully")
|
|
310
|
+
if existing_service:
|
|
311
|
+
print("Service file updated successfully")
|
|
215
312
|
else:
|
|
216
|
-
print("
|
|
313
|
+
print("Service file created successfully")
|
|
314
|
+
|
|
315
|
+
# We don't need to validate the config here as it will be validated when the service starts
|
|
217
316
|
|
|
317
|
+
# Check if the service is already enabled
|
|
318
|
+
service_enabled = is_service_enabled()
|
|
319
|
+
if service_enabled:
|
|
320
|
+
print("The service is already enabled to start at boot.")
|
|
321
|
+
else:
|
|
322
|
+
print("The service is not currently enabled to start at boot.")
|
|
323
|
+
if (
|
|
324
|
+
input("Do you want to enable the service to start at boot? (y/n): ")
|
|
325
|
+
.lower()
|
|
326
|
+
.startswith("y")
|
|
327
|
+
):
|
|
328
|
+
try:
|
|
329
|
+
subprocess.run(
|
|
330
|
+
["/usr/bin/systemctl", "--user", "enable", "mmrelay.service"],
|
|
331
|
+
check=True,
|
|
332
|
+
)
|
|
333
|
+
print("Service enabled successfully")
|
|
334
|
+
service_enabled = True
|
|
335
|
+
except subprocess.CalledProcessError as e:
|
|
336
|
+
print(f"Error enabling service: {e}")
|
|
337
|
+
except OSError as e:
|
|
338
|
+
print(f"Error: {e}")
|
|
339
|
+
|
|
340
|
+
# Check if the service is already running
|
|
341
|
+
service_active = is_service_active()
|
|
342
|
+
if service_active:
|
|
343
|
+
print("The service is already running.")
|
|
344
|
+
if input("Do you want to restart the service? (y/n): ").lower().startswith("y"):
|
|
345
|
+
try:
|
|
346
|
+
subprocess.run(
|
|
347
|
+
["/usr/bin/systemctl", "--user", "restart", "mmrelay.service"],
|
|
348
|
+
check=True,
|
|
349
|
+
)
|
|
350
|
+
print("Service restarted successfully")
|
|
351
|
+
# Wait for the service to restart
|
|
352
|
+
wait_for_service_start()
|
|
353
|
+
# Show service status
|
|
354
|
+
show_service_status()
|
|
355
|
+
except subprocess.CalledProcessError as e:
|
|
356
|
+
print(f"Error restarting service: {e}")
|
|
357
|
+
except OSError as e:
|
|
358
|
+
print(f"Error: {e}")
|
|
359
|
+
else:
|
|
360
|
+
print("The service is not currently running.")
|
|
361
|
+
if (
|
|
362
|
+
input("Do you want to start the service now? (y/n): ")
|
|
363
|
+
.lower()
|
|
364
|
+
.startswith("y")
|
|
365
|
+
):
|
|
366
|
+
if start_service():
|
|
367
|
+
# Wait for the service to start
|
|
368
|
+
wait_for_service_start()
|
|
369
|
+
# Show service status
|
|
370
|
+
show_service_status()
|
|
371
|
+
print("Service started successfully")
|
|
372
|
+
else:
|
|
373
|
+
print("\nWarning: Failed to start the service. Please check the logs.")
|
|
374
|
+
|
|
375
|
+
# Print a summary of the service status
|
|
376
|
+
print("\nService Status Summary:")
|
|
377
|
+
print(f" Service File: {service_path}")
|
|
378
|
+
print(f" Enabled at Boot: {'Yes' if service_enabled else 'No'}")
|
|
379
|
+
print(f" Currently Running: {'Yes' if is_service_active() else 'No'}")
|
|
380
|
+
print("\nService Management Commands:")
|
|
218
381
|
print_service_commands()
|
|
219
382
|
|
|
220
383
|
return True
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mmrelay
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Bridge between Meshtastic mesh networks and Matrix chat rooms
|
|
5
5
|
Home-page: https://github.com/geoffwhittington/meshtastic-matrix-relay
|
|
6
6
|
Author: Geoff Whittington, Jeremiah K., and contributors
|
|
@@ -15,19 +15,32 @@ Requires-Python: >=3.8
|
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
License-File: LICENSE
|
|
17
17
|
Requires-Dist: meshtastic
|
|
18
|
-
Requires-Dist: Pillow==11.1
|
|
18
|
+
Requires-Dist: Pillow==11.2.1
|
|
19
19
|
Requires-Dist: matrix-nio==0.25.2
|
|
20
20
|
Requires-Dist: matplotlib==3.10.1
|
|
21
21
|
Requires-Dist: requests==2.32.3
|
|
22
|
-
Requires-Dist: markdown==3.
|
|
22
|
+
Requires-Dist: markdown==3.8
|
|
23
23
|
Requires-Dist: haversine==2.9.0
|
|
24
24
|
Requires-Dist: schedule==1.2.2
|
|
25
|
-
Requires-Dist: platformdirs==4.
|
|
25
|
+
Requires-Dist: platformdirs==4.3.7
|
|
26
26
|
Requires-Dist: py-staticmaps>=0.4.0
|
|
27
|
+
Requires-Dist: rich==14.0.0
|
|
27
28
|
Dynamic: license-file
|
|
28
29
|
|
|
29
30
|
# M<>M Relay
|
|
30
31
|
|
|
32
|
+
## ✨ Version 1.0 Released! ✨
|
|
33
|
+
|
|
34
|
+
**We're excited to announce MMRelay v1.0 with improved packaging, standardized directories, and enhanced CLI!**
|
|
35
|
+
|
|
36
|
+
**Existing users:** Version 1.0 requires a few quick migration steps:
|
|
37
|
+
|
|
38
|
+
1. Follow the [UPGRADE_TO_V1.md](UPGRADE_TO_V1.md) guide for a smooth transition
|
|
39
|
+
2. Move your configuration to the new standard location (`~/.mmrelay/config.yaml`)
|
|
40
|
+
3. See [ANNOUNCEMENT.md](ANNOUNCEMENT.md) for all the exciting new features
|
|
41
|
+
|
|
42
|
+
Not ready to upgrade yet? No problem! Run `git checkout 0.10.1` to continue using the previous version.
|
|
43
|
+
|
|
31
44
|
## (Meshtastic <=> Matrix Relay)
|
|
32
45
|
|
|
33
46
|
A powerful and easy-to-use relay between Meshtastic devices and Matrix chat rooms, allowing seamless communication across platforms. This opens the door for bridging Meshtastic devices to [many other platforms](https://matrix.org/bridges/).
|
|
@@ -41,17 +54,19 @@ MMRelay runs on Linux, macOS, and Windows.
|
|
|
41
54
|
### Quick Installation
|
|
42
55
|
|
|
43
56
|
```bash
|
|
44
|
-
# Install using
|
|
45
|
-
pip install mmrelay
|
|
46
|
-
|
|
47
|
-
# Or use pipx for isolated installation (recommended)
|
|
57
|
+
# Install using pipx for isolated installation (recommended)
|
|
48
58
|
pipx install mmrelay
|
|
59
|
+
|
|
60
|
+
# Pip will also work if you prefer
|
|
61
|
+
pip install mmrelay
|
|
49
62
|
```
|
|
50
63
|
|
|
64
|
+
For pipx installation instructions, see: [pipx installation guide](https://pipx.pypa.io/stable/installation/#on-linux)
|
|
65
|
+
|
|
51
66
|
### Resources
|
|
52
67
|
|
|
53
68
|
- **New Users**: See [INSTRUCTIONS.md](INSTRUCTIONS.md) for setup and configuration
|
|
54
|
-
- **Existing Users**:
|
|
69
|
+
- **Existing Users**: See [UPGRADE_TO_V1.md](UPGRADE_TO_V1.md) for migration guidance
|
|
55
70
|
- **Configuration**: Review [sample_config.yaml](sample_config.yaml) for examples
|
|
56
71
|
|
|
57
72
|
### Command-Line Options
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
mmrelay/__init__.py,sha256=
|
|
2
|
-
mmrelay/cli.py,sha256=
|
|
3
|
-
mmrelay/config.py,sha256=
|
|
1
|
+
mmrelay/__init__.py,sha256=rJqoGfYDKTqEyyzrpQDpwRPfFCDG90XnhGUSHzttjPo,440
|
|
2
|
+
mmrelay/cli.py,sha256=yBYOkCwGYDJ2gRKPoPltxB_PydX1CPNUerVJHcNuHTo,12355
|
|
3
|
+
mmrelay/config.py,sha256=OIDxQStuVHyWxsDfnKqt0rr26ETEoZPqHVXReEH3IG8,7006
|
|
4
4
|
mmrelay/config_checker.py,sha256=UnoHVTXzfdTfFkbmXv9r_Si76v-sxXLb5FOaQSOM45E,4909
|
|
5
5
|
mmrelay/db_utils.py,sha256=DP2YuKBZtV771wo9X-Z7Ww5txfaIR0inWh1K_oVZ7cA,11430
|
|
6
|
-
mmrelay/log_utils.py,sha256
|
|
7
|
-
mmrelay/main.py,sha256=
|
|
6
|
+
mmrelay/log_utils.py,sha256=FXhaq4WSDHwlqiG3k1BbSxiOl5be4P4Kyr1gsIThOBw,4572
|
|
7
|
+
mmrelay/main.py,sha256=Okiat89uWiOrn5wq7lvJinYP3C6wzKdlQsyLxB8cCmI,11084
|
|
8
8
|
mmrelay/matrix_utils.py,sha256=GkIVj2bbPHtx1emFMwhEhc1SWHcv4UvkuyZYdb-Wnwo,30511
|
|
9
|
-
mmrelay/meshtastic_utils.py,sha256=
|
|
9
|
+
mmrelay/meshtastic_utils.py,sha256=ha6wRYeU16wT8jkZlr7RjnsB1b9e276cgu0jw4vmdbw,23270
|
|
10
10
|
mmrelay/plugin_loader.py,sha256=QfSXu4nxxBL_pGttLJGzGki8kRtWbFdtSoD_Qnx2iqU,12758
|
|
11
|
-
mmrelay/setup_utils.py,sha256=
|
|
11
|
+
mmrelay/setup_utils.py,sha256=MWBrpf9lNlw21yOoyFhzz1DSJx2KMTzZIMcHS3MwcdM,14090
|
|
12
12
|
mmrelay/plugins/__init__.py,sha256=KVMQIXRhe0wlGj4O3IZ0vOIQRKFkfPYejHXhJL17qrc,51
|
|
13
13
|
mmrelay/plugins/base_plugin.py,sha256=7tBIARWp9Qc4iAEMcsKiUJh1cbqXHcXAk4dlu1b8SBg,7524
|
|
14
14
|
mmrelay/plugins/debug_plugin.py,sha256=Jziht9Nj_bRO6Rmy7TjfBXaYo5eM3XsenbWFxPpyUs4,443
|
|
@@ -21,9 +21,9 @@ mmrelay/plugins/nodes_plugin.py,sha256=RDabzyG5hKG5aYWecsRUcLSjMCCv6Pngmq2Qpld1A
|
|
|
21
21
|
mmrelay/plugins/ping_plugin.py,sha256=RTRdgDQUSO33lreDTmWsTlI0L1C3FJrXE0KYqfEWYO0,4017
|
|
22
22
|
mmrelay/plugins/telemetry_plugin.py,sha256=8SxWv4BLXMUTbiVaD3MjlMMdQyS7S_1OfLlVNAUMSO0,6306
|
|
23
23
|
mmrelay/plugins/weather_plugin.py,sha256=yoKA_HdFqFEhgYdXqLhvXatLphCyLJFuGUKCR7fILv0,8546
|
|
24
|
-
mmrelay-1.0.dist-info/licenses/LICENSE,sha256=yceWauM1c0-FHxVplsD7W1-AbSeRaUNlmqT4UO1msBU,1073
|
|
25
|
-
mmrelay-1.0.dist-info/METADATA,sha256=
|
|
26
|
-
mmrelay-1.0.dist-info/WHEEL,sha256=
|
|
27
|
-
mmrelay-1.0.dist-info/entry_points.txt,sha256=SJZwGUOEpQ-qx4H8UL4xKFnKeInGUaZNW1I0ddjK7Ws,45
|
|
28
|
-
mmrelay-1.0.dist-info/top_level.txt,sha256=B_ZLCRm7NYAmI3PipRUyHGymP-C-q16LSeMGzmqJfo4,8
|
|
29
|
-
mmrelay-1.0.dist-info/RECORD,,
|
|
24
|
+
mmrelay-1.0.1.dist-info/licenses/LICENSE,sha256=yceWauM1c0-FHxVplsD7W1-AbSeRaUNlmqT4UO1msBU,1073
|
|
25
|
+
mmrelay-1.0.1.dist-info/METADATA,sha256=WziSMZeJluF69VwTLtCuxGvGtFxbAcumZV4iOAEWWTg,6953
|
|
26
|
+
mmrelay-1.0.1.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
|
|
27
|
+
mmrelay-1.0.1.dist-info/entry_points.txt,sha256=SJZwGUOEpQ-qx4H8UL4xKFnKeInGUaZNW1I0ddjK7Ws,45
|
|
28
|
+
mmrelay-1.0.1.dist-info/top_level.txt,sha256=B_ZLCRm7NYAmI3PipRUyHGymP-C-q16LSeMGzmqJfo4,8
|
|
29
|
+
mmrelay-1.0.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|