Files
meshcore-bot/modules/graph_trace_helper.py
T
agessaman b20e20925d refactor: one query for the unique recent repeater behind a path prefix
Seven places in message_handler and graph_trace_helper ran the same pair of queries, byte for byte: count the repeaters and room servers whose key starts with the prefix and were heard within the recency window, and if exactly one was, fetch its key (starred first, then most recent). contacts_repo.unique_recent_repeater_key now runs it and returns (count, key); each caller keeps its own logging, including the two that report the match count. Same queries, same order, same handling of execute_query's empty result on a database error.
2026-10-01 11:02:57 -07:00

252 lines
10 KiB
Python

#!/usr/bin/env python3
"""
Graph update helper for trace data.
Shared by message_handler (on RX) and trace command (when TRACE_DATA is received).
"""
import time
from typing import Any, Optional
from .contacts_repo import unique_recent_repeater_key
def update_mesh_graph_from_trace_data(
bot: Any,
path_hashes: list[str],
packet_info: dict[str, Any],
*,
is_our_trace: Optional[bool] = None,
) -> None:
"""Update mesh graph with edges from a trace packet's pathHashes.
When the bot receives a trace packet, it's the destination, so we can confirm
the edges in the path. The pathHashes represent the routing path the packet took.
Special case: If this is a trace we sent that came back through an immediate neighbor,
we can trust both directions (Bot -> Neighbor and Neighbor -> Bot).
Args:
bot: MeshCoreBot instance (mesh_graph, transmission_tracker, config, db_manager, meshcore).
path_hashes: Per-hop hash strings from the trace payload (uppercase hex). Length in nibbles is
``2 * (1 << (flags & 3))`` per hop (1, 2, 4, or 8 bytes), not always 2 hex chars.
packet_info: Packet information dictionary (packet_hash optional; used when is_our_trace is None).
is_our_trace: If None, derived from packet_info['packet_hash'] and transmission_tracker.
If True/False, use that value (e.g. trace command sets True when TRACE_DATA matches our tag).
"""
if not path_hashes or len(path_hashes) == 0:
bot.logger.debug("Mesh graph: Trace packet has no pathHashes, skipping graph update")
return
if not hasattr(bot, "mesh_graph") or not bot.mesh_graph:
bot.logger.debug("Mesh graph: Graph not initialized, skipping trace update")
return
if not hasattr(bot, "transmission_tracker") or not bot.transmission_tracker:
bot.logger.debug("Mesh graph: Cannot get bot prefix, skipping trace update")
return
mesh_graph = bot.mesh_graph
bot_prefix = bot.transmission_tracker.bot_prefix
if not bot_prefix:
bot.logger.debug("Mesh graph: Bot prefix not available, skipping trace update")
return
bot_prefix = bot_prefix.lower()
# Resolve is_our_trace
if is_our_trace is None:
is_our_trace = False
packet_hash = packet_info.get("packet_hash")
if packet_hash and hasattr(bot, "transmission_tracker"):
record = bot.transmission_tracker.match_packet_hash(packet_hash, time.time())
if record:
is_our_trace = True
bot.logger.debug("Mesh graph: Trace packet is one we sent (matched transmission record)")
# Check if this came back through an immediate neighbor
is_immediate_neighbor = False
if is_our_trace and len(path_hashes) == 1:
is_immediate_neighbor = True
bot.logger.info(
f"Mesh graph: Trace came back through immediate neighbor {path_hashes[0]} - trusting both directions"
)
bot.logger.debug(
f"Mesh graph: Updating graph from trace pathHashes: {path_hashes} "
f"(bot is destination: {bot_prefix}, is_our_trace: {is_our_trace}, immediate_neighbor: {is_immediate_neighbor})"
)
recency_days = bot.config.getint("Path_Command", "graph_edge_expiration_days", fallback=7)
from .utils import _get_node_location_from_db, calculate_distance
bot_location = None
try:
bot_location_result = _get_node_location_from_db(bot, bot_prefix, None, recency_days)
if bot_location_result:
bot_location, _ = bot_location_result
except Exception as e:
bot.logger.debug(f"Could not get bot location: {e}")
if len(path_hashes) == 0:
return
last_node = path_hashes[-1].lower()
if is_immediate_neighbor:
neighbor_prefix = path_hashes[0].lower()
neighbor_key = None
try:
_, unique_key = unique_recent_repeater_key(bot.db_manager, neighbor_prefix, recency_days)
if unique_key:
neighbor_key = unique_key
except Exception as e:
bot.logger.debug(f"Error checking uniqueness for immediate neighbor {neighbor_prefix}: {e}")
bot_key = None
if hasattr(bot, "meshcore") and bot.meshcore and getattr(bot.meshcore, "device", None):
try:
device_info = bot.meshcore.device
if hasattr(device_info, "public_key"):
pubkey = device_info.public_key
if isinstance(pubkey, str):
bot_key = pubkey
elif isinstance(pubkey, bytes):
bot_key = pubkey.hex()
except Exception as e:
bot.logger.debug(f"Could not get bot public key: {e}")
geographic_distance = None
try:
if bot_location:
neighbor_result = _get_node_location_from_db(bot, neighbor_prefix, bot_location, recency_days)
if neighbor_result:
neighbor_location, selected_neighbor_key = neighbor_result
if not neighbor_key and selected_neighbor_key:
neighbor_key = selected_neighbor_key
if neighbor_location and bot_location:
geographic_distance = calculate_distance(
neighbor_location[0], neighbor_location[1],
bot_location[0], bot_location[1],
)
except Exception as e:
bot.logger.debug(f"Could not calculate distance for immediate neighbor edge: {e}")
mesh_graph.add_edge(
from_prefix=bot_prefix,
to_prefix=neighbor_prefix,
from_public_key=bot_key,
to_public_key=neighbor_key,
hop_position=1,
geographic_distance=geographic_distance,
)
mesh_graph.add_edge(
from_prefix=neighbor_prefix,
to_prefix=bot_prefix,
from_public_key=neighbor_key,
to_public_key=bot_key,
hop_position=1,
geographic_distance=geographic_distance,
)
bot.logger.info(f"Mesh graph: Created trusted bidirectional edge with immediate neighbor {neighbor_prefix}")
return
# Regular case: trace from elsewhere, bot is destination
geographic_distance = None
last_node_key = None
try:
_, unique_key = unique_recent_repeater_key(bot.db_manager, last_node, recency_days)
if unique_key:
last_node_key = unique_key
except Exception as e:
bot.logger.debug(f"Error checking uniqueness for trace last_node {last_node}: {e}")
bot_key = None
if hasattr(bot, "meshcore") and bot.meshcore and getattr(bot.meshcore, "device", None):
try:
device_info = bot.meshcore.device
if hasattr(device_info, "public_key"):
pubkey = device_info.public_key
if isinstance(pubkey, str):
bot_key = pubkey
elif isinstance(pubkey, bytes):
bot_key = pubkey.hex()
except Exception as e:
bot.logger.debug(f"Could not get bot public key: {e}")
try:
if bot_location:
last_node_result = _get_node_location_from_db(bot, last_node, bot_location, recency_days)
if last_node_result:
last_node_location, selected_key = last_node_result
if not last_node_key and selected_key:
last_node_key = selected_key
if last_node_location and bot_location:
geographic_distance = calculate_distance(
last_node_location[0], last_node_location[1],
bot_location[0], bot_location[1],
)
except Exception as e:
bot.logger.debug(f"Could not calculate distance for trace edge {last_node}->{bot_prefix}: {e}")
mesh_graph.add_edge(
from_prefix=last_node,
to_prefix=bot_prefix,
from_public_key=last_node_key,
to_public_key=bot_key,
hop_position=len(path_hashes),
geographic_distance=geographic_distance,
)
# Create edges between nodes in the pathHashes (if more than one)
previous_location = bot_location
for i in range(len(path_hashes) - 1, 0, -1):
from_node = path_hashes[i - 1].lower()
to_node = path_hashes[i].lower()
hop_position = len(path_hashes) - i
from_node_key = None
to_node_key = None
for node, key_var in [(from_node, "from_node_key"), (to_node, "to_node_key")]:
try:
_, unique_key = unique_recent_repeater_key(bot.db_manager, node, recency_days)
if unique_key:
if key_var == "from_node_key":
from_node_key = unique_key
else:
to_node_key = unique_key
except Exception as e:
bot.logger.debug(f"Error checking uniqueness for trace node {node}: {e}")
geographic_distance = None
try:
if previous_location:
from_result = _get_node_location_from_db(bot, from_node, previous_location, recency_days)
if from_result:
from_location, selected_from_key = from_result
if not from_node_key and selected_from_key:
from_node_key = selected_from_key
to_result = _get_node_location_from_db(bot, to_node, from_location, recency_days)
if to_result:
to_location, selected_to_key = to_result
if not to_node_key and selected_to_key:
to_node_key = selected_to_key
if from_location and to_location:
geographic_distance = calculate_distance(
from_location[0], from_location[1],
to_location[0], to_location[1],
)
previous_location = from_location
except Exception as e:
bot.logger.debug(f"Could not calculate distance for trace edge {from_node}->{to_node}: {e}")
mesh_graph.add_edge(
from_prefix=from_node,
to_prefix=to_node,
from_public_key=from_node_key,
to_public_key=to_node_key,
hop_position=hop_position,
geographic_distance=geographic_distance,
)