mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-11-04 03:52:34 +00:00
bot_prefix and graph_trace_helper's bot key read meshcore.device, which meshcore_py doesn't have, so both were always None and update_mesh_graph_from_trace_data skipped every trace. Both now come from meshcore.self_info's public_key (device stays a fallback), and bot_prefix is read lazily because the tracker is built before the radio connects. The helper names the bot at the trace's hash width, so the last hop's edge matches the hops' own width, and marks 2-byte edges with prefix_bytes=2. trace_command read update_graph_two_byte but never used it; 2-byte traces now follow it instead of update_graph_one_byte.
292 lines
13 KiB
Python
292 lines
13 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 .transmission_tracker import own_public_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
|
|
# Name the bot at the trace's own hash width, so its edges match the hops'
|
|
bot_key = own_public_key(bot)
|
|
width = len(path_hashes[-1])
|
|
bot_prefix = bot_key[:width] if bot_key and len(bot_key) >= width else 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()
|
|
# 2-byte and wider hashes confirm the link at that width
|
|
edge_width = {"prefix_bytes": 2} if width >= 4 else {}
|
|
|
|
# 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:
|
|
count_query = f"""
|
|
SELECT COUNT(DISTINCT public_key) as count
|
|
FROM complete_contact_tracking
|
|
WHERE public_key LIKE ?
|
|
AND role IN ('repeater', 'roomserver')
|
|
AND COALESCE(last_advert_timestamp, last_heard) >= datetime('now', '-{recency_days} days')
|
|
"""
|
|
prefix_pattern = f"{neighbor_prefix}%"
|
|
count_results = bot.db_manager.execute_query(count_query, (prefix_pattern,))
|
|
if count_results and count_results[0].get("count", 0) == 1:
|
|
query = f"""
|
|
SELECT public_key
|
|
FROM complete_contact_tracking
|
|
WHERE public_key LIKE ?
|
|
AND role IN ('repeater', 'roomserver')
|
|
AND COALESCE(last_advert_timestamp, last_heard) >= datetime('now', '-{recency_days} days')
|
|
ORDER BY is_starred DESC, COALESCE(last_advert_timestamp, last_heard) DESC
|
|
LIMIT 1
|
|
"""
|
|
results = bot.db_manager.execute_query(query, (prefix_pattern,))
|
|
if results and results[0].get("public_key"):
|
|
neighbor_key = results[0]["public_key"]
|
|
except Exception as e:
|
|
bot.logger.debug(f"Error checking uniqueness for immediate neighbor {neighbor_prefix}: {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,
|
|
**edge_width,
|
|
)
|
|
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,
|
|
**edge_width,
|
|
)
|
|
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:
|
|
count_query = f"""
|
|
SELECT COUNT(DISTINCT public_key) as count
|
|
FROM complete_contact_tracking
|
|
WHERE public_key LIKE ?
|
|
AND role IN ('repeater', 'roomserver')
|
|
AND COALESCE(last_advert_timestamp, last_heard) >= datetime('now', '-{recency_days} days')
|
|
"""
|
|
prefix_pattern = f"{last_node}%"
|
|
count_results = bot.db_manager.execute_query(count_query, (prefix_pattern,))
|
|
if count_results and count_results[0].get("count", 0) == 1:
|
|
query = f"""
|
|
SELECT public_key
|
|
FROM complete_contact_tracking
|
|
WHERE public_key LIKE ?
|
|
AND role IN ('repeater', 'roomserver')
|
|
AND COALESCE(last_advert_timestamp, last_heard) >= datetime('now', '-{recency_days} days')
|
|
ORDER BY is_starred DESC, COALESCE(last_advert_timestamp, last_heard) DESC
|
|
LIMIT 1
|
|
"""
|
|
results = bot.db_manager.execute_query(query, (prefix_pattern,))
|
|
if results and results[0].get("public_key"):
|
|
last_node_key = results[0]["public_key"]
|
|
except Exception as e:
|
|
bot.logger.debug(f"Error checking uniqueness for trace last_node {last_node}: {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,
|
|
**edge_width,
|
|
)
|
|
|
|
# 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:
|
|
count_query = f"""
|
|
SELECT COUNT(DISTINCT public_key) as count
|
|
FROM complete_contact_tracking
|
|
WHERE public_key LIKE ?
|
|
AND role IN ('repeater', 'roomserver')
|
|
AND COALESCE(last_advert_timestamp, last_heard) >= datetime('now', '-{recency_days} days')
|
|
"""
|
|
prefix_pattern = f"{node}%"
|
|
count_results = bot.db_manager.execute_query(count_query, (prefix_pattern,))
|
|
if count_results and count_results[0].get("count", 0) == 1:
|
|
query = f"""
|
|
SELECT public_key
|
|
FROM complete_contact_tracking
|
|
WHERE public_key LIKE ?
|
|
AND role IN ('repeater', 'roomserver')
|
|
AND COALESCE(last_advert_timestamp, last_heard) >= datetime('now', '-{recency_days} days')
|
|
ORDER BY is_starred DESC, COALESCE(last_advert_timestamp, last_heard) DESC
|
|
LIMIT 1
|
|
"""
|
|
results = bot.db_manager.execute_query(query, (prefix_pattern,))
|
|
if results and results[0].get("public_key"):
|
|
if key_var == "from_node_key":
|
|
from_node_key = results[0]["public_key"]
|
|
else:
|
|
to_node_key = results[0]["public_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,
|
|
**edge_width,
|
|
)
|