mmrelay 1.1.3__py3-none-any.whl → 1.2.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/message_queue.py CHANGED
@@ -13,18 +13,18 @@ from dataclasses import dataclass
13
13
  from queue import Empty, Queue
14
14
  from typing import Callable, Optional
15
15
 
16
+ from mmrelay.constants.database import DEFAULT_MSGS_TO_KEEP
17
+ from mmrelay.constants.network import MINIMUM_MESSAGE_DELAY
18
+ from mmrelay.constants.queue import (
19
+ DEFAULT_MESSAGE_DELAY,
20
+ MAX_QUEUE_SIZE,
21
+ QUEUE_HIGH_WATER_MARK,
22
+ QUEUE_MEDIUM_WATER_MARK,
23
+ )
16
24
  from mmrelay.log_utils import get_logger
17
25
 
18
26
  logger = get_logger(name="MessageQueue")
19
27
 
20
- # Default message delay in seconds (minimum 2.0 due to firmware constraints)
21
- DEFAULT_MESSAGE_DELAY = 2.2
22
-
23
- # Queue size configuration
24
- MAX_QUEUE_SIZE = 100
25
- QUEUE_HIGH_WATER_MARK = 75 # 75% of MAX_QUEUE_SIZE
26
- QUEUE_MEDIUM_WATER_MARK = 50 # 50% of MAX_QUEUE_SIZE
27
-
28
28
 
29
29
  @dataclass
30
30
  class QueuedMessage:
@@ -61,20 +61,20 @@ class MessageQueue:
61
61
 
62
62
  def start(self, message_delay: float = DEFAULT_MESSAGE_DELAY):
63
63
  """
64
- Starts the message queue processor with the specified minimum delay between messages.
64
+ Start the message queue processor with a specified minimum delay between messages.
65
65
 
66
- Enforces a minimum delay of 2.0 seconds due to firmware requirements. If the event loop is running, the processor task is started immediately; otherwise, startup is deferred until the event loop becomes available.
66
+ If the provided delay is below the firmware-enforced minimum, the minimum is used instead. The processor task is started immediately if the asyncio event loop is running; otherwise, startup is deferred until the event loop becomes available.
67
67
  """
68
68
  with self._lock:
69
69
  if self._running:
70
70
  return
71
71
 
72
72
  # Validate and enforce firmware minimum
73
- if message_delay < 2.0:
73
+ if message_delay < MINIMUM_MESSAGE_DELAY:
74
74
  logger.warning(
75
- f"Message delay {message_delay}s below firmware minimum (2.0s), using 2.0s"
75
+ f"Message delay {message_delay}s below firmware minimum ({MINIMUM_MESSAGE_DELAY}s), using {MINIMUM_MESSAGE_DELAY}s"
76
76
  )
77
- self._message_delay = 2.0
77
+ self._message_delay = MINIMUM_MESSAGE_DELAY
78
78
  else:
79
79
  self._message_delay = message_delay
80
80
  self._running = True
@@ -372,13 +372,13 @@ class MessageQueue:
372
372
 
373
373
  def _handle_message_mapping(self, result, mapping_info):
374
374
  """
375
- Stores and prunes message mapping information after a message is sent.
375
+ Update the message mapping database with information about a sent message and prune old mappings if configured.
376
376
 
377
377
  Parameters:
378
378
  result: The result object from the send function, expected to have an `id` attribute.
379
- mapping_info (dict): Dictionary containing mapping details such as `matrix_event_id`, `room_id`, `text`, and optional `meshnet` and `msgs_to_keep`.
379
+ mapping_info (dict): Contains mapping details such as `matrix_event_id`, `room_id`, `text`, and optionally `meshnet` and `msgs_to_keep`.
380
380
 
381
- This method updates the message mapping database with the new mapping and prunes old mappings if configured.
381
+ If required mapping fields are present, stores the mapping and prunes old entries based on the specified or default retention count.
382
382
  """
383
383
  try:
384
384
  # Import here to avoid circular imports
@@ -402,7 +402,7 @@ class MessageQueue:
402
402
  logger.debug(f"Stored message map for meshtastic_id: {result.id}")
403
403
 
404
404
  # Handle pruning if configured
405
- msgs_to_keep = mapping_info.get("msgs_to_keep", 500)
405
+ msgs_to_keep = mapping_info.get("msgs_to_keep", DEFAULT_MSGS_TO_KEEP)
406
406
  if msgs_to_keep > 0:
407
407
  prune_message_map(msgs_to_keep)
408
408
 
mmrelay/plugin_loader.py CHANGED
@@ -16,68 +16,82 @@ sorted_active_plugins = []
16
16
  plugins_loaded = False
17
17
 
18
18
 
19
- def get_custom_plugin_dirs():
19
+ def _reset_caches_for_tests():
20
+ """
21
+ Reset the global plugin loader caches to their initial state for testing purposes.
22
+
23
+ Clears cached plugin instances and loading state to ensure test isolation and prevent interference between test runs.
20
24
  """
21
- Returns a list of directories to check for custom plugins in order of priority:
22
- 1. User directory (~/.mmrelay/plugins/custom)
23
- 2. Local directory (plugins/custom) for backward compatibility
25
+ global sorted_active_plugins, plugins_loaded
26
+ sorted_active_plugins = []
27
+ plugins_loaded = False
28
+
29
+
30
+ def _get_plugin_dirs(plugin_type):
31
+ """
32
+ Return a prioritized list of directories for the specified plugin type, including user and local plugin directories if accessible.
33
+
34
+ Parameters:
35
+ plugin_type (str): Either "custom" or "community", specifying the type of plugins.
36
+
37
+ Returns:
38
+ list: List of plugin directories to search, with the user directory first if available, followed by the local directory for backward compatibility.
24
39
  """
25
40
  dirs = []
26
41
 
27
42
  # Check user directory first (preferred location)
28
- user_dir = os.path.join(get_base_dir(), "plugins", "custom")
29
- os.makedirs(user_dir, exist_ok=True)
30
- dirs.append(user_dir)
43
+ user_dir = os.path.join(get_base_dir(), "plugins", plugin_type)
44
+ try:
45
+ os.makedirs(user_dir, exist_ok=True)
46
+ dirs.append(user_dir)
47
+ except (OSError, PermissionError) as e:
48
+ logger.warning(f"Cannot create user plugin directory {user_dir}: {e}")
31
49
 
32
50
  # Check local directory (backward compatibility)
33
- local_dir = os.path.join(get_app_path(), "plugins", "custom")
34
- dirs.append(local_dir)
51
+ local_dir = os.path.join(get_app_path(), "plugins", plugin_type)
52
+ try:
53
+ os.makedirs(local_dir, exist_ok=True)
54
+ dirs.append(local_dir)
55
+ except (OSError, PermissionError):
56
+ # Skip local directory if we can't create it (e.g., in Docker)
57
+ logger.debug(f"Cannot create local plugin directory {local_dir}, skipping")
35
58
 
36
59
  return dirs
37
60
 
38
61
 
39
- def get_community_plugin_dirs():
62
+ def get_custom_plugin_dirs():
40
63
  """
41
- Returns a list of directories to check for community plugins in order of priority:
42
- 1. User directory (~/.mmrelay/plugins/community)
43
- 2. Local directory (plugins/community) for backward compatibility
64
+ Return the list of directories to search for custom plugins, ordered by priority.
65
+
66
+ The directories include the user-specific custom plugins directory and a local directory for backward compatibility.
44
67
  """
45
- dirs = []
68
+ return _get_plugin_dirs("custom")
46
69
 
47
- # Check user directory first (preferred location)
48
- user_dir = os.path.join(get_base_dir(), "plugins", "community")
49
- os.makedirs(user_dir, exist_ok=True)
50
- dirs.append(user_dir)
51
70
 
52
- # Check local directory (backward compatibility)
53
- local_dir = os.path.join(get_app_path(), "plugins", "community")
54
- dirs.append(local_dir)
71
+ def get_community_plugin_dirs():
72
+ """
73
+ Return the list of directories to search for community plugins, ordered by priority.
55
74
 
56
- return dirs
75
+ The directories include the user-specific community plugins directory and a local directory for backward compatibility.
76
+ """
77
+ return _get_plugin_dirs("community")
57
78
 
58
79
 
59
80
  def clone_or_update_repo(repo_url, ref, plugins_dir):
60
- """Clone or update a Git repository for community plugins.
81
+ """
82
+ Clone or update a community plugin Git repository and ensure its dependencies are installed.
83
+
84
+ Attempts to clone the repository at the specified branch or tag, or update it if it already exists. Handles switching between branches and tags, falls back to default branches if needed, and installs Python dependencies from `requirements.txt` using either pip or pipx. Logs errors and warnings for any issues encountered.
61
85
 
62
- Args:
63
- repo_url (str): Git repository URL to clone/update
86
+ Parameters:
87
+ repo_url (str): The URL of the Git repository to clone or update.
64
88
  ref (dict): Reference specification with keys:
65
- - type: "tag" or "branch"
66
- - value: tag name or branch name
67
- plugins_dir (str): Directory where the repository should be cloned
89
+ - type: "tag" or "branch"
90
+ - value: The tag or branch name to use.
91
+ plugins_dir (str): Directory where the repository should be cloned or updated.
68
92
 
69
93
  Returns:
70
- bool: True if successful, False if clone/update failed
71
-
72
- Handles complex Git operations including:
73
- - Cloning new repositories with specific tags/branches
74
- - Updating existing repositories and switching refs
75
- - Installing requirements.txt dependencies via pip or pipx
76
- - Fallback to default branches (main/master) when specified ref fails
77
- - Robust error handling and logging
78
-
79
- The function automatically installs Python dependencies if a requirements.txt
80
- file is found in the repository root.
94
+ bool: True if the repository was successfully cloned or updated and dependencies were handled; False if any critical error occurred.
81
95
  """
82
96
  # Extract the repository name from the URL
83
97
  repo_name = os.path.splitext(os.path.basename(repo_url.rstrip("/")))[0]
@@ -326,7 +340,13 @@ def clone_or_update_repo(repo_url, ref, plugins_dir):
326
340
  # Repository doesn't exist yet, clone it
327
341
  try:
328
342
  os.makedirs(plugins_dir, exist_ok=True)
343
+ except (OSError, PermissionError) as e:
344
+ logger.error(f"Cannot create plugin directory {plugins_dir}: {e}")
345
+ logger.error(f"Skipping repository {repo_name} due to permission error")
346
+ return False
329
347
 
348
+ # Now try to clone the repository
349
+ try:
330
350
  # If it's a default branch, just clone it directly
331
351
  if is_default_branch:
332
352
  try:
@@ -519,26 +539,17 @@ def clone_or_update_repo(repo_url, ref, plugins_dir):
519
539
 
520
540
 
521
541
  def load_plugins_from_directory(directory, recursive=False):
522
- """Load plugin classes from Python files in a directory.
523
-
524
- Args:
525
- directory (str): Directory path to search for plugin files
526
- recursive (bool): Whether to search subdirectories recursively
527
-
528
- Returns:
529
- list: List of instantiated plugin objects found in the directory
542
+ """
543
+ Dynamically loads and instantiates plugin classes from Python files in a specified directory.
530
544
 
531
- Scans for .py files and attempts to import each as a module. Looks for
532
- a 'Plugin' class in each module and instantiates it if found.
545
+ Scans the given directory (and subdirectories if `recursive` is True) for `.py` files, importing each as a module and instantiating its `Plugin` class if present. Automatically attempts to install missing dependencies when a `ModuleNotFoundError` occurs, supporting both pip and pipx environments. Provides compatibility for plugins importing from either `plugins` or `mmrelay.plugins`. Skips files without a `Plugin` class or with unresolved import errors.
533
546
 
534
- Features:
535
- - Automatic dependency installation for missing imports (via pip/pipx)
536
- - Compatibility layer for import paths (plugins vs mmrelay.plugins)
537
- - Proper sys.path management for plugin directory imports
538
- - Comprehensive error handling and logging
547
+ Parameters:
548
+ directory (str): Path to the directory containing plugin files.
549
+ recursive (bool): If True, searches subdirectories recursively.
539
550
 
540
- Skips files that don't define a Plugin class or have import errors
541
- that can't be automatically resolved.
551
+ Returns:
552
+ list: Instantiated plugin objects found in the directory.
542
553
  """
543
554
  plugins = []
544
555
  if os.path.isdir(directory):
@@ -626,13 +637,23 @@ def load_plugins_from_directory(directory, recursive=False):
626
637
  )
627
638
 
628
639
  # Try to load the module again
629
- spec.loader.exec_module(plugin_module)
630
-
631
- if hasattr(plugin_module, "Plugin"):
632
- plugins.append(plugin_module.Plugin())
633
- else:
634
- logger.warning(
635
- f"{plugin_path} does not define a Plugin class."
640
+ try:
641
+ spec.loader.exec_module(plugin_module)
642
+
643
+ if hasattr(plugin_module, "Plugin"):
644
+ plugins.append(plugin_module.Plugin())
645
+ else:
646
+ logger.warning(
647
+ f"{plugin_path} does not define a Plugin class."
648
+ )
649
+ except ModuleNotFoundError:
650
+ logger.error(
651
+ f"Module {missing_module} still not available after installation. "
652
+ f"The package name might be different from the import name."
653
+ )
654
+ except Exception as retry_error:
655
+ logger.error(
656
+ f"Error loading plugin {plugin_path} after dependency installation: {retry_error}"
636
657
  )
637
658
 
638
659
  except subprocess.CalledProcessError:
@@ -660,25 +681,23 @@ def load_plugins_from_directory(directory, recursive=False):
660
681
 
661
682
 
662
683
  def load_plugins(passed_config=None):
663
- """Load and initialize all active plugins based on configuration.
684
+ """
685
+ Discovers, loads, and initializes all active plugins based on the provided or global configuration.
664
686
 
665
- Args:
666
- passed_config (dict, optional): Configuration dictionary to use.
667
- If None, uses global config variable.
687
+ This function orchestrates the full plugin lifecycle, including:
688
+ - Loading core, custom, and community plugins as specified in the configuration.
689
+ - Cloning or updating community plugin repositories and installing their dependencies.
690
+ - Dynamically loading plugin classes from discovered directories.
691
+ - Filtering and sorting plugins by their configured priority.
692
+ - Starting each active plugin.
693
+
694
+ If plugins have already been loaded, returns the cached sorted list.
695
+
696
+ Parameters:
697
+ passed_config (dict, optional): Configuration dictionary to use instead of the global configuration.
668
698
 
669
699
  Returns:
670
- list: List of active plugin instances sorted by priority
671
-
672
- This is the main plugin loading function that:
673
- - Loads core plugins from mmrelay.plugins package
674
- - Processes custom plugins from ~/.mmrelay/plugins/custom and plugins/custom
675
- - Downloads and loads community plugins from configured Git repositories
676
- - Filters plugins based on active status in configuration
677
- - Sorts active plugins by priority and calls their start() method
678
- - Sets up proper plugin configuration and channel mapping
679
-
680
- Only plugins explicitly marked as active=true in config are loaded.
681
- Custom and community plugins are cloned/updated automatically.
700
+ list: Active plugin instances, sorted by priority.
682
701
  """
683
702
  global sorted_active_plugins
684
703
  global plugins_loaded
@@ -750,11 +769,15 @@ def load_plugins(passed_config=None):
750
769
  plugin_path = os.path.join(custom_dir, plugin_name)
751
770
  if os.path.exists(plugin_path):
752
771
  logger.debug(f"Loading custom plugin from: {plugin_path}")
753
- plugins.extend(
754
- load_plugins_from_directory(plugin_path, recursive=False)
755
- )
756
- plugin_found = True
757
- break
772
+ try:
773
+ plugins.extend(
774
+ load_plugins_from_directory(plugin_path, recursive=False)
775
+ )
776
+ plugin_found = True
777
+ break
778
+ except Exception as e:
779
+ logger.error(f"Failed to load custom plugin {plugin_name}: {e}")
780
+ continue
758
781
 
759
782
  if not plugin_found:
760
783
  logger.warning(
@@ -780,7 +803,12 @@ def load_plugins(passed_config=None):
780
803
  if active_community_plugins:
781
804
  # Ensure all community plugin directories exist
782
805
  for dir_path in community_plugin_dirs:
783
- os.makedirs(dir_path, exist_ok=True)
806
+ try:
807
+ os.makedirs(dir_path, exist_ok=True)
808
+ except (OSError, PermissionError) as e:
809
+ logger.warning(
810
+ f"Cannot create community plugin directory {dir_path}: {e}"
811
+ )
784
812
 
785
813
  logger.debug(
786
814
  f"Loading active community plugins: {', '.join(active_community_plugins)}"
@@ -842,11 +870,17 @@ def load_plugins(passed_config=None):
842
870
  plugin_path = os.path.join(dir_path, repo_name)
843
871
  if os.path.exists(plugin_path):
844
872
  logger.info(f"Loading community plugin from: {plugin_path}")
845
- plugins.extend(
846
- load_plugins_from_directory(plugin_path, recursive=True)
847
- )
848
- plugin_found = True
849
- break
873
+ try:
874
+ plugins.extend(
875
+ load_plugins_from_directory(plugin_path, recursive=True)
876
+ )
877
+ plugin_found = True
878
+ break
879
+ except Exception as e:
880
+ logger.error(
881
+ f"Failed to load community plugin {repo_name}: {e}"
882
+ )
883
+ continue
850
884
 
851
885
  if not plugin_found:
852
886
  logger.warning(
@@ -900,4 +934,5 @@ def load_plugins(passed_config=None):
900
934
  else:
901
935
  logger.info("Loaded: none")
902
936
 
903
- plugins_loaded = True # Set the flag to indicate that plugins have been load
937
+ plugins_loaded = True # Set the flag to indicate that plugins have been loaded
938
+ return sorted_active_plugins
@@ -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,7 +19,7 @@ from mmrelay.db_utils import (
14
19
  store_plugin_data,
15
20
  )
16
21
  from mmrelay.log_utils import get_logger
17
- from mmrelay.message_queue import DEFAULT_MESSAGE_DELAY, queue_message
22
+ from mmrelay.message_queue import queue_message
18
23
 
19
24
  # Global config variable that will be set from main.py
20
25
  config = None
@@ -47,7 +52,7 @@ class BasePlugin(ABC):
47
52
 
48
53
  # Class-level default attributes
49
54
  plugin_name = None # Must be overridden in subclasses
50
- max_data_rows_per_node = 100
55
+ max_data_rows_per_node = DEFAULT_MAX_DATA_ROWS_PER_NODE_BASE
51
56
  priority = 10
52
57
 
53
58
  @property
@@ -109,10 +114,22 @@ class BasePlugin(ABC):
109
114
  break
110
115
 
111
116
  # Get the list of mapped channels
112
- self.mapped_channels = [
113
- room.get("meshtastic_channel")
114
- for room in config.get("matrix_rooms", [])
115
- ]
117
+ # Handle both list format and dict format for matrix_rooms
118
+ matrix_rooms = config.get("matrix_rooms", [])
119
+ if isinstance(matrix_rooms, dict):
120
+ # Dict format: {"room_name": {"id": "...", "meshtastic_channel": 0}}
121
+ self.mapped_channels = [
122
+ room_config.get("meshtastic_channel")
123
+ for room_config in matrix_rooms.values()
124
+ if isinstance(room_config, dict)
125
+ ]
126
+ else:
127
+ # List format: [{"id": "...", "meshtastic_channel": 0}]
128
+ self.mapped_channels = [
129
+ room.get("meshtastic_channel")
130
+ for room in matrix_rooms
131
+ if isinstance(room, dict)
132
+ ]
116
133
  else:
117
134
  self.mapped_channels = []
118
135
 
@@ -251,19 +268,63 @@ class BasePlugin(ABC):
251
268
  """
252
269
  return self.response_delay
253
270
 
271
+ def get_my_node_id(self):
272
+ """Get the relay's Meshtastic node ID.
273
+
274
+ Returns:
275
+ int: The relay's node ID, or None if unavailable
276
+
277
+ This method provides access to the relay's own node ID without requiring
278
+ plugins to call connect_meshtastic() directly. Useful for determining
279
+ if messages are direct messages or for other node identification needs.
280
+
281
+ The node ID is cached after first successful retrieval to avoid repeated
282
+ connection calls, as the relay's node ID is static during runtime.
283
+ """
284
+ if hasattr(self, "_my_node_id"):
285
+ return self._my_node_id
286
+
287
+ from mmrelay.meshtastic_utils import connect_meshtastic
288
+
289
+ meshtastic_client = connect_meshtastic()
290
+ if meshtastic_client and meshtastic_client.myInfo:
291
+ self._my_node_id = meshtastic_client.myInfo.my_node_num
292
+ return self._my_node_id
293
+ return None
294
+
295
+ def is_direct_message(self, packet):
296
+ """Check if a Meshtastic packet is a direct message to this relay.
297
+
298
+ Args:
299
+ packet (dict): Meshtastic packet data
300
+
301
+ Returns:
302
+ bool: True if the packet is a direct message to this relay, False otherwise
303
+
304
+ This method encapsulates the common pattern of checking if a message
305
+ is addressed directly to the relay node, eliminating the need for plugins
306
+ to call connect_meshtastic() directly for DM detection.
307
+ """
308
+ toId = packet.get("to")
309
+ if toId is None:
310
+ return False
311
+
312
+ myId = self.get_my_node_id()
313
+ return toId == myId
314
+
254
315
  def send_message(self, text: str, channel: int = 0, destination_id=None) -> bool:
255
316
  """
256
- Send a message to the Meshtastic network via the message queue.
317
+ Send a message to the Meshtastic network using the message queue.
257
318
 
258
- Automatically queues the message for broadcast or direct delivery, applying rate limiting as configured. Returns True if the message was successfully queued, or False if the Meshtastic client is unavailable.
319
+ 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.
259
320
 
260
321
  Parameters:
261
- text (str): The message text to send.
262
- channel (int, optional): Channel index to send the message on. Defaults to 0.
263
- destination_id (optional): Destination node ID for direct messages; if None, the message is broadcast.
322
+ text (str): The message content to send.
323
+ channel (int, optional): The channel index for sending the message. Defaults to 0.
324
+ destination_id (optional): The destination node ID for direct messages. If None, the message is broadcast.
264
325
 
265
326
  Returns:
266
- bool: True if the message was queued successfully, False otherwise.
327
+ bool: True if the message was queued successfully; False otherwise.
267
328
  """
268
329
  from mmrelay.meshtastic_utils import connect_meshtastic
269
330
 
@@ -272,9 +333,7 @@ class BasePlugin(ABC):
272
333
  self.logger.error("No Meshtastic client available")
273
334
  return False
274
335
 
275
- description = (
276
- f"Plugin {self.plugin_name}: {text[:50]}{'...' if len(text) > 50 else ''}"
277
- )
336
+ description = f"Plugin {self.plugin_name}: {text[:DEFAULT_TEXT_TRUNCATION_LENGTH]}{'...' if len(text) > DEFAULT_TEXT_TRUNCATION_LENGTH else ''}"
278
337
 
279
338
  send_kwargs = {
280
339
  "text": text,
@@ -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 = 1000
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"] == "TEXT_MESSAGE_APP"
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 = 50
29
+ max_data_rows_per_node = DEFAULT_MAX_DATA_ROWS_PER_NODE_MESH_RELAY
29
30
 
30
31
  def normalize(self, dict_obj):
31
- """Normalize packet data to consistent dictionary format.
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
- Args:
34
- dict_obj: Packet data (dict, JSON string, or plain string)
35
+ Parameters:
36
+ dict_obj: Packet data as a dictionary, JSON string, or plain string.
35
37
 
36
38
  Returns:
37
- dict: Normalized packet dictionary with raw data stripped
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: