From 0ced52b2a2380e62ba891f45ea738f09faa4bc9c Mon Sep 17 00:00:00 2001 From: agessaman Date: Sat, 4 Oct 2025 09:47:40 -0700 Subject: [PATCH] Refactor Path Command to enhance path decoding and response handling --- modules/commands/path_command.py | 186 ++++++++++++++++++++----------- 1 file changed, 123 insertions(+), 63 deletions(-) diff --git a/modules/commands/path_command.py b/modules/commands/path_command.py index da88d60..da945ca 100644 --- a/modules/commands/path_command.py +++ b/modules/commands/path_command.py @@ -6,6 +6,7 @@ Decodes hex path data to show which repeaters were involved in message routing import re import time +import asyncio from typing import List, Optional, Dict, Any from .base_command import BaseCommand from ..models import MeshMessage @@ -36,7 +37,8 @@ class PathCommand(BaseCommand): # Check if message starts with any of our keywords content_lower = content.lower() for keyword in self.keywords: - if content_lower.startswith(keyword + ' '): + # Check for exact match or keyword followed by space + if content_lower == keyword or content_lower.startswith(keyword + ' '): return True return False @@ -44,19 +46,23 @@ class PathCommand(BaseCommand): """Execute path decode command""" self.logger.info(f"Path command executed with content: {message.content}") + # Store the current message for use in _extract_path_from_recent_messages + self._current_message = message + # Parse the message content to extract path data content = message.content.strip() parts = content.split() if len(parts) < 2: - response = self.get_help() + # No arguments provided - try to extract path from current message + response = await self._extract_path_from_recent_messages() else: # Extract path data from the command path_input = " ".join(parts[1:]) response = await self._decode_path(path_input) - # Send the response - await self.bot.command_manager.send_response(message, response) + # Send the response (may be split into multiple messages if long) + await self._send_path_response(message, response) return True async def _decode_path(self, path_input: str) -> str: @@ -74,11 +80,12 @@ class PathCommand(BaseCommand): return "❌ No valid hex values found in path data. Use format like: 11,98,a4,49,cd,5f,01" # Convert to uppercase for consistency + # hex_matches preserves the order from the original path node_ids = [match.upper() for match in hex_matches] self.logger.info(f"Decoding path with {len(node_ids)} nodes: {','.join(node_ids)}") - # Look up repeater names for each node ID + # Look up repeater names for each node ID (order preserved) repeater_info = await self._lookup_repeater_names(node_ids) # Format the response @@ -255,70 +262,123 @@ class PathCommand(BaseCommand): return None def _format_path_response(self, node_ids: List[str], repeater_info: Dict[str, Dict[str, Any]]) -> str: - """Format the path decode response (max 130 chars per line)""" - # Group results by found/not found/collision - found_repeaters = [] - collision_nodes = [] - unknown_nodes = [] + """Format the path decode response (max 130 chars per line) - for node_id in node_ids: - info = repeater_info.get(node_id, {}) - if info.get('found', False): - if info.get('collision', False): - collision_nodes.append((node_id, info)) - else: - found_repeaters.append((node_id, info)) - else: - unknown_nodes.append(node_id) - - # Build response lines (each max 130 chars) + Maintains the order of repeaters as they appear in the path (first to last) + """ + # Build response lines in path order (first to last as message traveled) lines = [] - # Show found repeaters (compact format) - if found_repeaters: - for node_id, info in found_repeaters: - name = info['name'] - - # Truncate name if too long - if len(name) > 27: - name = name[:24] + "..." - - line = f"{node_id}: {name}" - if len(line) > 130: - line = line[:127] + "..." - lines.append(line) - - # Show collision nodes (compact format) - if collision_nodes: - for node_id, info in collision_nodes: - matches = info.get('matches', 0) - line = f"{node_id}: {matches} repeaters" - if len(line) > 130: - line = line[:127] + "..." - lines.append(line) - - # Show unknown nodes (compact format) - if unknown_nodes: - unknown_str = f"Unknown: {','.join(unknown_nodes)}" - if len(unknown_str) > 130: - # Split if too long - for node_id in unknown_nodes: - lines.append(f"Unknown: {node_id}") + # Process nodes in path order (first to last as message traveled) + for node_id in node_ids: + info = repeater_info.get(node_id, {}) + + if info.get('found', False): + if info.get('collision', False): + # Multiple repeaters with same prefix + matches = info.get('matches', 0) + line = f"{node_id}: {matches} repeaters" + else: + # Single repeater found + name = info['name'] + + # Truncate name if too long + if len(name) > 27: + name = name[:24] + "..." + + line = f"{node_id}: {name}" else: - lines.append(unknown_str) + # Unknown repeater + line = f"{node_id}: Unknown" + + # Ensure line fits within 130 character limit + if len(line) > 130: + line = line[:127] + "..." + + lines.append(line) + # Return all lines - let _send_path_response handle the splitting return "\n".join(lines) + async def _send_path_response(self, message: MeshMessage, response: str): + """Send path response, splitting into multiple messages if necessary""" + if len(response) <= 130: + # Single message is fine + await self.bot.command_manager.send_response(message, response) + else: + # Split into multiple messages + lines = response.split('\n') + current_message = "" + message_count = 0 + + for i, line in enumerate(lines): + # Check if adding this line would exceed 130 characters + if len(current_message) + len(line) + 1 > 130: # +1 for newline + # Send current message and start new one + if current_message: + # Add ellipsis on new line to end of continued message (if not the last message) + if i < len(lines): + current_message += "\n..." + await self.bot.command_manager.send_response(message, current_message.rstrip()) + await asyncio.sleep(2.0) # Delay between messages (same as other commands) + message_count += 1 + + # Start new message with ellipsis on new line at beginning (if not first message) + if message_count > 0: + current_message = f"...\n{line}" + else: + current_message = line + else: + # Add line to current message + if current_message: + current_message += f"\n{line}" + else: + current_message = line + + # Send the last message if there's content + if current_message: + await self.bot.command_manager.send_response(message, current_message) + + async def _extract_path_from_recent_messages(self) -> str: + """Extract path from the current message's path information (same as test command)""" + try: + # Use the path information from the current message being processed + # This is the same reliable source that the test command uses + if hasattr(self, '_current_message') and self._current_message and self._current_message.path: + path_string = self._current_message.path + self.logger.info(f"Using path from current message: {path_string}") + + # Check if it's a direct connection + if "Direct" in path_string or "0 hops" in path_string: + return "📡 Direct connection (0 hops)" + + # Try to extract path nodes from the path string + # Path strings are typically in format: "node1,node2,node3 via ROUTE_TYPE_*" + if " via ROUTE_TYPE_" in path_string: + # Extract just the path part before the route type + path_part = path_string.split(" via ROUTE_TYPE_")[0] + else: + path_part = path_string + + # Check if it looks like a comma-separated path + if ',' in path_part: + path_input = path_part + self.logger.info(f"Found path from current message: {path_input}") + return await self._decode_path(path_input) + else: + # Single node or unknown format + return f"📡 Path: {path_string}" + else: + return "❌ No path information available in current message" + + except Exception as e: + self.logger.error(f"Error extracting path from current message: {e}") + return f"❌ Error extracting path from current message: {e}" + def get_help(self) -> str: """Get help text for the path command""" - return """Path Decode: !path - -Decode hex path to show repeaters involved in routing. - -Examples: -• !path 11,98,a4,49,cd,5f,01 -• !path 11 98 a4 49 cd 5f 01 -• !path 1198a449cd5f01 - -Shows repeater names, collisions, and unknown nodes. -Uses local database - run !repeater scan to update.""" + return """Path: !path [hex] - Decode path to show repeaters. Use !path alone for recent message path, or !path [7e,01] for specific path.""" + + def get_help_text(self) -> str: + """Get help text for the path command (used by help system)""" + return self.get_help()