mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-05-29 20:04:10 +00:00
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.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 <XX|free|refresh>"
|
||||
examples = ["prefix 1A", "prefix free"]
|
||||
short_description = "Look up repeaters by prefix and show their locations (if known)"
|
||||
usage = "prefix <XX|XXXX|free|refresh>"
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
+10
-10
@@ -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] = {}
|
||||
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
+4
-4
@@ -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']
|
||||
|
||||
@@ -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):",
|
||||
|
||||
Reference in New Issue
Block a user