From 36a8a675437a3a0da4812010bb71accd3eb5b037 Mon Sep 17 00:00:00 2001 From: Ian Rifkin Date: Fri, 27 Feb 2026 23:48:27 -0500 Subject: [PATCH] Add transitional support for 2-byte prefixes while keeping legacy 1-byte compatibility - Update prefix command to accept BOTH legacy 2-char prefixes and configured prefix_hex_chars (e.g. 4-char) during firmware transition - Replace strict length validation with dual-length validation (2 or N) - Ensure prefix lookups work with either input length via LIKE matching - Update related SQL prefix extraction to use configured prefix length - Add fallback handling in path parsing for legacy 2-char route data Notes: - This is an interim compatibility change to support mixed networks where RF path data is still 1-byte while bot config may be 2-byte. - Needs additional testing across real multi-hop scenarios and mixed bot configurations. - Translation updates are incomplete: only English strings were updated; other translation files still need review. - Behavior and UX may need refinement after real-world testing. --- config.ini.example | 3 + modules/commands/path_command.py | 17 +++- modules/commands/prefix_command.py | 125 +++++++++++++++---------- modules/core.py | 11 +++ modules/mesh_graph.py | 20 ++-- modules/message_handler.py | 4 +- modules/transmission_tracker.py | 6 +- modules/utils.py | 4 +- modules/web_viewer/app.py | 13 ++- modules/web_viewer/templates/mesh.html | 58 +++++++----- tests/helpers.py | 8 +- translations/en.json | 4 +- 12 files changed, 168 insertions(+), 105 deletions(-) diff --git a/config.ini.example b/config.ini.example index bd41b4e..da1cb75 100644 --- a/config.ini.example +++ b/config.ini.example @@ -154,6 +154,9 @@ respond_to_dms = true # Example: channel_keywords = help,ping,test,hello # channel_keywords = +# Set a custom prefix length for the public keys to identify repeaters +prefix_bytes = 1 + [Banned_Users] # List of banned sender names (comma-separated). Matching is prefix (starts-with): # "Awful Username" also matches "Awful Username 🍆". No bot responses in channels or DMs. diff --git a/modules/commands/path_command.py b/modules/commands/path_command.py index ce2ad5d..625a0d0 100644 --- a/modules/commands/path_command.py +++ b/modules/commands/path_command.py @@ -224,8 +224,16 @@ class PathCommand(BaseCommand): path_input = path_input.replace(',', ' ').replace(':', ' ') # Extract hex values using regex - hex_pattern = r'[0-9a-fA-F]{2}' + # Try configured width first + n = getattr(self.bot, "prefix_hex_chars", 2) + hex_pattern = rf'[0-9a-fA-F]{{{n}}}' hex_matches = re.findall(hex_pattern, path_input) + + # Backward compatibility: + # if no matches and we're expecting >2 chars, try legacy 2-char paths + if not hex_matches and n > 2: + legacy_pattern = r'[0-9a-fA-F]{2}' + hex_matches = re.findall(legacy_pattern, path_input) if not hex_matches: return self.translate('commands.path.no_valid_hex') @@ -266,7 +274,7 @@ class PathCommand(BaseCommand): api_data = None # Query the database for repeaters with matching prefixes - # Node IDs are typically the first 2 characters of the public key + # Node IDs are the configured prefix of the public key (see Bot.prefix_bytes) for node_id in node_ids: # Test dependency injection: use provided lookup when available if lookup_func is not None: @@ -1313,7 +1321,8 @@ class PathCommand(BaseCommand): best_method = None for repeater in repeaters: - candidate_prefix = repeater.get('public_key', '')[:2].lower() if repeater.get('public_key') else None + pk = repeater.get('public_key') or '' + candidate_prefix = self.bot.key_prefix(pk).lower() if pk else None candidate_public_key = repeater.get('public_key', '').lower() if repeater.get('public_key') else None if not candidate_prefix: continue @@ -1712,7 +1721,7 @@ class PathCommand(BaseCommand): else: # Try to decode even single nodes (e.g., "01" should be decoded to a repeater name) # Check if path_part looks like it contains hex values - hex_pattern = r'[0-9a-fA-F]{2}' + hex_pattern = rf'[0-9a-fA-F]{{{self.bot.prefix_hex_chars}}}' if re.search(hex_pattern, path_part): # Looks like hex values, try to decode return await self._decode_path(path_part) diff --git a/modules/commands/prefix_command.py b/modules/commands/prefix_command.py index 61fc05d..11133f9 100644 --- a/modules/commands/prefix_command.py +++ b/modules/commands/prefix_command.py @@ -26,19 +26,19 @@ class PrefixCommand(BaseCommand): # Plugin metadata name = "prefix" keywords = ['prefix', 'repeater', 'lookup'] - description = "Look up repeaters by two-character prefix (e.g., 'prefix 1A')" + description = "Look up repeaters by prefix (e.g., 'prefix 1A' or 'prefix 2299')" category = "meshcore_info" requires_dm = False cooldown_seconds = 2 requires_internet = False # Will be set to True in __init__ if API is configured - + # Documentation - short_description = "Look up repeaters by two-character prefix and show their locations (if known)" - usage = "prefix " - examples = ["prefix 1A", "prefix free"] + short_description = "Look up repeaters by prefix and show their locations (if known)" + usage = "prefix " + examples = ["prefix 1A", "prefix 2299", "prefix free"] parameters = [ - {"name": "prefix", "description": "Two-character prefix (e.g., 1A, 2B)"}, - {"name": "free", "description": "Show available/unused prefixes"} + {"name": "prefix", "description": "Prefix in hex (2 chars or configured length)"}, + {"name": "free", "description": "Show available/unused prefixes (may be disabled)"}, ] def __init__(self, bot: Any): @@ -252,17 +252,18 @@ class PrefixCommand(BaseCommand): """ try: # Query all repeaters with valid coordinates - query = ''' - SELECT SUBSTR(public_key, 1, 2) as prefix, public_key, name, - latitude, longitude, - COALESCE(last_advert_timestamp, last_heard) as last_seen - FROM complete_contact_tracking - WHERE role IN ('repeater', 'roomserver') - AND latitude IS NOT NULL - AND longitude IS NOT NULL - AND latitude != 0 - AND longitude != 0 - ''' + n = int(getattr(self.bot, "prefix_hex_chars", 2)) + query = f""" + SELECT SUBSTR(public_key, 1, {n}) AS prefix, + COUNT(*) AS repeater_count, + AVG(latitude) AS avg_lat, + AVG(longitude) AS avg_lon, + MAX(COALESCE(last_advert_timestamp, last_heard)) AS most_recent + FROM complete_contact_tracking + WHERE role IN ('repeater', 'roomserver') + AND LENGTH(public_key) >= {n} + GROUP BY prefix + """ results = self.bot.db_manager.execute_query(query) @@ -380,17 +381,18 @@ class PrefixCommand(BaseCommand): """ try: # Get all known prefixes from database - query = ''' - SELECT SUBSTR(public_key, 1, 2) as prefix, - COUNT(*) as repeater_count, - AVG(latitude) as avg_lat, - AVG(longitude) as avg_lon, - MAX(COALESCE(last_advert_timestamp, last_heard)) as most_recent - FROM complete_contact_tracking - WHERE role IN ('repeater', 'roomserver') - AND LENGTH(public_key) >= 2 - GROUP BY prefix - ''' + n = int(getattr(self.bot, "prefix_hex_chars", 2)) + query = f""" + SELECT SUBSTR(public_key, 1, {n}) AS prefix, + COUNT(*) AS repeater_count, + AVG(latitude) AS avg_lat, + AVG(longitude) AS avg_lon, + MAX(COALESCE(last_advert_timestamp, last_heard)) AS most_recent + FROM complete_contact_tracking + WHERE role IN ('repeater', 'roomserver') + AND LENGTH(public_key) >= {n} + GROUP BY prefix + """ results = self.bot.db_manager.execute_query(query) @@ -451,8 +453,9 @@ class PrefixCommand(BaseCommand): # Also include free prefixes (not in database) that aren't neighbors or excluded # Generate all valid hex prefixes (01-FE, excluding 00 and FF) - for i in range(1, 255): # 1 to 254 (exclude 0 and 255) - prefix = f"{i:02X}" + max_val = (16 ** self.bot.prefix_hex_chars) + for i in range(1, max_val - 1): # still excluding all-zeros and all-FF..FF + prefix = f"{i:0{self.bot.prefix_hex_chars}X}" prefix_lower = prefix.lower() # Skip if already in database (already processed above) @@ -803,6 +806,11 @@ class PrefixCommand(BaseCommand): # Handle free/available command if command == "FREE" or command == "AVAILABLE": + if getattr(self.bot, "prefix_hex_chars", 2) > 2: + # Keep behavior consistent: send a response and return True + await self._send_prefix_response(message, "Feature disabled for multi-byte prefixes.") + return True + free_prefixes, total_free, has_data = await self.get_free_prefixes() if not has_data: response = self.translate('commands.prefix.unable_determine_free') @@ -839,10 +847,22 @@ class PrefixCommand(BaseCommand): if len(parts) >= 3 and parts[2].upper() == "ALL": include_all = True - # Validate prefix format - if len(command) != 2 or not command.isalnum(): - response = self.translate('commands.prefix.invalid_format') - return await self.send_response(message, response) + # Validate prefix format: + # - allow legacy 2-char prefixes (current mesh hop IDs) + # - allow configured N-char prefixes (e.g., 4) for pubkey-prefix lookups + n = int(getattr(self.bot, "prefix_hex_chars", 2)) + allowed_lengths = {2, n} + + if len(command) not in allowed_lengths: + # If you updated translations to mention {{prefix_hex_chars}}, great, + # but this is clearer during the transition: + response = f"Invalid prefix format. Expected 2 or {n} hex characters." + return await self.send_response(message, response) + + import re + if not re.fullmatch(r"[0-9a-fA-F]+", command): + response = f"Invalid prefix format. Expected 2 or {n} hex characters." + return await self.send_response(message, response) # Get prefix data prefix_data = await self.get_prefix_data(command, include_all=include_all) @@ -1055,7 +1075,6 @@ class PrefixCommand(BaseCommand): AND last_heard >= datetime('now', '-{self.prefix_heard_days} days') ORDER BY name ''' - # The prefix should match the first two characters of the public key prefix_pattern = f"{prefix}%" @@ -1190,23 +1209,29 @@ class PrefixCommand(BaseCommand): # When using database, use prefix_free_days to filter which prefixes are considered "used" # Only repeaters heard within prefix_free_days will be considered as using a prefix try: + # If distance filtering is enabled, we need location data to filter + n = int(getattr(self.bot, "prefix_hex_chars", 2)) + # If distance filtering is enabled, we need location data to filter if self.distance_filtering_enabled: query = f''' - SELECT DISTINCT SUBSTR(public_key, 1, 2) as prefix, latitude, longitude - FROM complete_contact_tracking - WHERE role IN ('repeater', 'roomserver') - AND LENGTH(public_key) >= 2 - AND last_heard >= datetime('now', '-{self.prefix_free_days} days') + SELECT DISTINCT SUBSTR(public_key, 1, {n}) as prefix, + latitude, + longitude + FROM complete_contact_tracking + WHERE role IN ('repeater', 'roomserver') + AND LENGTH(public_key) >= {n} + AND last_heard >= datetime('now', '-{self.prefix_free_days} days') ''' else: query = f''' - SELECT DISTINCT SUBSTR(public_key, 1, 2) as prefix - FROM complete_contact_tracking - WHERE role IN ('repeater', 'roomserver') - AND LENGTH(public_key) >= 2 - AND last_heard >= datetime('now', '-{self.prefix_free_days} days') + SELECT DISTINCT SUBSTR(public_key, 1, {n}) as prefix + FROM complete_contact_tracking + WHERE role IN ('repeater', 'roomserver') + AND LENGTH(public_key) >= {n} + AND last_heard >= datetime('now', '-{self.prefix_free_days} days') ''' + results = self.bot.db_manager.execute_query(query) for row in results: prefix = row['prefix'].upper() @@ -1236,10 +1261,12 @@ class PrefixCommand(BaseCommand): self.logger.warning("No data available for free prefixes lookup (empty cache and database)") return [], 0, False - # Generate all valid hex prefixes (01-FE, excluding 00 and FF) + # Generate all valid hex prefixes (exclude all-zeros and all-FF) all_prefixes = [] - for i in range(1, 255): # 1 to 254 (exclude 0 and 255) - prefix = f"{i:02X}" + max_val = 16 ** self.bot.prefix_hex_chars + + for i in range(1, max_val - 1): + prefix = f"{i:0{self.bot.prefix_hex_chars}X}" all_prefixes.append(prefix) # Find free prefixes diff --git a/modules/core.py b/modules/core.py index 626e0c5..0b9ef0e 100644 --- a/modules/core.py +++ b/modules/core.py @@ -79,6 +79,11 @@ class MeshCoreBot: except (OSError, ValueError, sqlite3.Error) as e: self.logger.error(f"Failed to initialize database manager: {e}") raise + + # Set length of prefix + self.prefix_bytes = self.config.getint("Bot", "prefix_bytes", fallback=1) + self.prefix_hex_chars = self.prefix_bytes * 2 + self.logger.info(f"Prefix mode: {self.prefix_bytes} bytes ({self.prefix_hex_chars} hex chars)") # Store start time in database for web viewer access try: @@ -1532,3 +1537,9 @@ long_jokes = false self.logger.error(f"Error sending startup advert: {e}") import traceback self.logger.error(traceback.format_exc()) + + def key_prefix(self, public_key: str) -> str: + return public_key[:self.prefix_hex_chars] + + def is_valid_prefix(self, prefix: str) -> bool: + return len(prefix) == self.prefix_hex_chars diff --git a/modules/mesh_graph.py b/modules/mesh_graph.py index 3833b1a..681e2e0 100644 --- a/modules/mesh_graph.py +++ b/modules/mesh_graph.py @@ -166,8 +166,8 @@ class MeshGraph: return # Normalize prefixes to lowercase - from_prefix = from_prefix.lower()[:2] - to_prefix = to_prefix.lower()[:2] + from_prefix = from_prefix.lower()[:self.bot.prefix_hex_chars] + to_prefix = to_prefix.lower()[:self.bot.prefix_hex_chars] # Intern public key strings so repeated identical keys share one object in RAM if from_public_key: @@ -797,8 +797,8 @@ class MeshGraph: Returns: bool: True if edge exists. """ - from_prefix = from_prefix.lower()[:2] - to_prefix = to_prefix.lower()[:2] + from_prefix = from_prefix.lower()[:self.bot.prefix_hex_chars] + to_prefix = to_prefix.lower()[:self.bot.prefix_hex_chars] return (from_prefix, to_prefix) in self.edges def get_edge(self, from_prefix: str, to_prefix: str) -> Optional[Dict]: @@ -811,8 +811,8 @@ class MeshGraph: Returns: Dict with edge data or None if not found. """ - from_prefix = from_prefix.lower()[:2] - to_prefix = to_prefix.lower()[:2] + from_prefix = from_prefix.lower()[:self.bot.prefix_hex_chars] + to_prefix = to_prefix.lower()[:self.bot.prefix_hex_chars] return self.edges.get((from_prefix, to_prefix)) def get_outgoing_edges(self, prefix: str) -> List[Dict]: @@ -826,7 +826,7 @@ class MeshGraph: Returns: List of edge dictionaries. """ - prefix = prefix.lower()[:2] + prefix = prefix.lower()[:self.bot.prefix_hex_chars] to_prefixes = self._outgoing_index.get(prefix) if not to_prefixes: return [] @@ -848,7 +848,7 @@ class MeshGraph: Returns: List of edge dictionaries. """ - prefix = prefix.lower()[:2] + prefix = prefix.lower()[:self.bot.prefix_hex_chars] from_prefixes = self._incoming_index.get(prefix) if not from_prefixes: return [] @@ -1052,8 +1052,8 @@ class MeshGraph: List of (candidate_prefix, score) tuples sorted by score (highest first). Score is 0.0-1.0 based on path strength. """ - from_prefix = from_prefix.lower()[:2] - to_prefix = to_prefix.lower()[:2] + from_prefix = from_prefix.lower()[:self.bot.prefix_hex_chars] + to_prefix = to_prefix.lower()[:self.bot.prefix_hex_chars] candidates: Dict[str, float] = {} diff --git a/modules/message_handler.py b/modules/message_handler.py index eece506..4a25e2a 100644 --- a/modules/message_handler.py +++ b/modules/message_handler.py @@ -2045,7 +2045,7 @@ class MessageHandler: self.logger.debug("Mesh graph: No public key in advert data, skipping graph update") return - advertiser_prefix = advertiser_key[:2].lower() + advertiser_prefix = advertiser_key[:self.bot.prefix_hex_chars].lower() # Parse path from hex string path_nodes = [] @@ -3115,7 +3115,7 @@ class MessageHandler: try: node_data = { 'public_key': public_key, - 'prefix': public_key[:2].lower() if public_key else '', + 'prefix': public_key[:self.bot.prefix_hex_chars].lower() if public_key else '', 'name': contact_name, 'role': 'repeater' } diff --git a/modules/transmission_tracker.py b/modules/transmission_tracker.py index 0dcede5..d4acb96 100644 --- a/modules/transmission_tracker.py +++ b/modules/transmission_tracker.py @@ -59,7 +59,7 @@ class TransmissionTracker: if hasattr(device_info, 'public_key'): pubkey = device_info.public_key if isinstance(pubkey, str) and len(pubkey) >= 2: - self.bot_prefix = pubkey[:2].lower() + self.bot_prefix = pubkey[:self.bot.prefix_hex_chars].lower() elif isinstance(pubkey, bytes) and len(pubkey) >= 1: self.bot_prefix = f"{pubkey[0]:02x}".lower() self.logger.debug(f"Bot prefix set to: {self.bot_prefix}") @@ -300,7 +300,7 @@ class TransmissionTracker: last_node = path_nodes[-1] if isinstance(last_node, str) and len(last_node) >= 2: # Take first 2 characters as prefix - prefix = last_node[:2].lower() + prefix = last_node[:self.bot.prefix_hex_chars].lower() # Filter out our own prefix if prefix != self.bot_prefix: return [prefix] @@ -318,7 +318,7 @@ class TransmissionTracker: if parts: last_part = parts[-1] if len(last_part) >= 2: - prefix = last_part[:2].lower() + prefix = last_part[:self.bot.prefix_hex_chars].lower() # Filter out our own prefix if prefix != self.bot_prefix: return [prefix] diff --git a/modules/utils.py b/modules/utils.py index 14f0463..7978f09 100644 --- a/modules/utils.py +++ b/modules/utils.py @@ -1567,8 +1567,8 @@ def parse_path_string(path_str: str) -> List[str]: # Replace common separators with spaces path_str = path_str.replace(',', ' ').replace(':', ' ') - # Extract hex values using regex (2-character hex pairs) - hex_pattern = r'[0-9a-fA-F]{2}' + # Extract hex values using regex (prefix_hex_chars-wide hex tokens) + hex_pattern = rf'[0-9a-fA-F]{{{prefix_hex_chars}}}' hex_matches = re.findall(hex_pattern, path_str) # Convert to uppercase for consistency diff --git a/modules/web_viewer/app.py b/modules/web_viewer/app.py index ddd4305..f92d2de 100644 --- a/modules/web_viewer/app.py +++ b/modules/web_viewer/app.py @@ -370,7 +370,7 @@ class BotDataViewer: else: # Space/comma-separated format path_input = path_input.replace(',', ' ').replace(':', ' ') - hex_pattern = r'[0-9a-fA-F]{2}' + hex_pattern = rf'[0-9a-fA-F]{{{prefix_hex_chars}}}' hex_matches = re.findall(hex_pattern, path_input) if not hex_matches: @@ -675,7 +675,7 @@ class BotDataViewer: best_method = None for repeater in repeaters: - candidate_prefix = repeater.get('public_key', '')[:2].lower() if repeater.get('public_key') else None + candidate_prefix = repeater.get('public_key', '')[:self.bot.prefix_hex_chars].lower() if repeater.get('public_key') else None candidate_public_key = repeater.get('public_key', '').lower() if repeater.get('public_key') else None if not candidate_prefix: continue @@ -1137,7 +1137,10 @@ class BotDataViewer: @self.app.route('/mesh') def mesh(): """Mesh graph visualization page""" - return render_template('mesh.html') + return render_template( + 'mesh.html', + prefix_hex_chars=self.bot.prefix_hex_chars + ) # Favicon routes @self.app.route('/apple-touch-icon.png') @@ -5167,7 +5170,7 @@ class BotDataViewer: else: # Space/comma-separated format path_input = path_hex.replace(',', ' ').replace(':', ' ') - hex_pattern = r'[0-9a-fA-F]{2}' + hex_pattern = rf'[0-9a-fA-F]{{{prefix_hex_chars}}}' hex_matches = re.findall(hex_pattern, path_input) if not hex_matches: @@ -5310,7 +5313,7 @@ class BotDataViewer: best_method = None for repeater in repeaters: - candidate_prefix = repeater.get('public_key', '')[:2].lower() if repeater.get('public_key') else None + candidate_prefix = repeater.get('public_key', '')[:self.bot.prefix_hex_chars].lower() if repeater.get('public_key') else None candidate_public_key = repeater.get('public_key', '').lower() if repeater.get('public_key') else None if not candidate_prefix: continue diff --git a/modules/web_viewer/templates/mesh.html b/modules/web_viewer/templates/mesh.html index 55846b7..a31f58f 100644 --- a/modules/web_viewer/templates/mesh.html +++ b/modules/web_viewer/templates/mesh.html @@ -378,35 +378,45 @@ let isInitialMapLoad = true; // Track if this is the first time rendering the map with nodes let highlightedPath = null; // Currently highlighted path data let pathHighlightTimeout = null; // Debounce timer for path resolution - + + const PREFIX_HEX_CHARS = {{ prefix_hex_chars|default(2) }}; + // Helper function to create unique node identifier function getNodeId(node) { return `${node.prefix}-${node.latitude.toFixed(6)}-${node.longitude.toFixed(6)}`; } - + // Detect if input is a hex path (2+ hex values) - function detectPathInput(input) { - if (!input || input.trim().length === 0) return false; - - // Normalize input: remove commas, spaces, colons - const normalized = input.replace(/[,\s:]/g, ''); - - // Check if it's a continuous hex string (e.g., "8601a5") - // If it's all hex and has 4+ characters (2+ hex pairs), it's a path - if (/^[0-9a-fA-F]{4,}$/.test(normalized)) { - return true; - } - - // Also check for space-separated hex values - const hexPattern = /\b[0-9a-fA-F]{2}\b/g; - const matches = input.match(hexPattern); - - // If we have 2+ hex values, treat as path - return matches && matches.length >= 2; - } - - // Resolve path via API - async function resolvePath(pathInput) { + function detectPathInput(input) { + if (!input || input.trim().length === 0) return false; + + // Pull from template if you can, otherwise default. + // If you already have this elsewhere on the page, reuse it. + const prefixHexChars = PREFIX_HEX_CHARS; + + // Normalize input: remove commas, spaces, colons + const normalized = input.replace(/[,\s:]/g, ''); + + // Check if it's a continuous hex string (e.g., "8601a5" or "8601A58F02") + // If it's all hex and has at least 2 tokens, treat as path. + // Optional: require it to align on token boundaries to reduce false positives. + const minChars = prefixHexChars * 2; // 2+ tokens + if (new RegExp(`^[0-9a-fA-F]{${minChars},}$`).test(normalized)) { + // If you want to be slightly stricter (still minimal), uncomment: + // if (normalized.length % prefixHexChars === 0) return true; + return true; + } + + // Also check for space-separated hex values + const hexPattern = new RegExp(`\\b[0-9a-fA-F]{${prefixHexChars}}\\b`, 'g'); + const matches = input.match(hexPattern); + + // If we have 2+ hex values, treat as path + return matches && matches.length >= 2; + } + + // Resolve path via API + async function resolvePath(pathInput) { try { const response = await fetch('/api/mesh/resolve-path', { method: 'POST', diff --git a/tests/helpers.py b/tests/helpers.py index a5379ce..a19959a 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -104,8 +104,8 @@ def create_test_edge( to_public_key = (to_prefix.lower() * 16)[:64] return { - 'from_prefix': from_prefix.lower()[:2], - 'to_prefix': to_prefix.lower()[:2], + 'from_prefix': from_prefix.lower()[:self.bot.prefix_hex_chars], + 'to_prefix': to_prefix.lower()[:self.bot.prefix_hex_chars], 'from_public_key': from_public_key, 'to_public_key': to_public_key, 'observation_count': observation_count, @@ -125,7 +125,7 @@ def create_test_path(node_ids: List[str]) -> List[str]: Returns: List of node IDs (normalized to lowercase) """ - return [node_id.lower()[:2] for node_id in node_ids] + return [node_id.lower()[:self.bot.prefix_hex_chars] for node_id in node_ids] def populate_test_graph(mesh_graph, edges: List[Dict[str, Any]]): @@ -145,7 +145,7 @@ def populate_test_graph(mesh_graph, edges: List[Dict[str, Any]]): geographic_distance=edge.get('geographic_distance') ) # Manually set observation_count and timestamps if needed - edge_key = (edge['from_prefix'].lower()[:2], edge['to_prefix'].lower()[:2]) + edge_key = (edge['from_prefix'].lower()[:self.bot.prefix_hex_chars], edge['to_prefix'].lower()[:self.bot.prefix_hex_chars]) if edge_key in mesh_graph.edges: if edge.get('observation_count', 1) > 1: mesh_graph.edges[edge_key]['observation_count'] = edge['observation_count'] diff --git a/translations/en.json b/translations/en.json index 77bce16..b8b3f7c 100644 --- a/translations/en.json +++ b/translations/en.json @@ -245,7 +245,7 @@ "path": { "description": "Decode hex path data to show which repeaters were involved in message routing", "help": "Path: path [hex] - Decode path to show repeaters. Use path alone for current message path, or path [7e,01] for specific path.", - "no_valid_hex": "❌ No valid hex values found in path data. Use format like: 11,98,a4,49,cd,5f,01", + "no_valid_hex": "❌ No valid hex values found in path data.", "no_path": "❌ No path information available in current message", "error": "Error processing path: {error}", "error_decoding": "❌ Error decoding path: {error}", @@ -273,7 +273,7 @@ "refresh_not_available": "❌ Refresh not available - no API URL configured. Using local database only.", "cache_refreshed": "🔄 Repeater prefix cache refreshed!", "unable_determine_free": "❌ Unable to determine free prefixes. Try 'prefix refresh' first.", - "invalid_format": "❌ Invalid prefix format. Use two characters (e.g., prefix 1A)", + "invalid_format": "❌ Invalid prefix format. Expected {prefix_hex_chars} hex characters.", "no_repeaters_found": "❌ No repeaters found with prefix '{prefix}'", "no_free_prefixes": "❌ No free prefixes found (all 254 valid prefixes are in use)", "available_prefixes": "Available Prefixes ({shown} of {total} free):",