fix(correlation): tie channel messages to their own RF packet

{packet_hash} came back empty on a channel, and so did {path} and
{connection_info}. message.routing_info is only attached when the RF row is
correlated, and a channel message can never correlate: MeshCore's CHAN event
carries neither raw_hex nor a pubkey prefix, so every prefix strategy in
find_recent_rf_data is skipped and the message lands on the most-recent-packet
fallback, which #80 rightly refuses to take a route from.

The fallback is in fact almost always the right packet—the firmware emits the
RF log row and the decoded CHAN event for one reception back to back—but
"almost always" is a guess, and #80 is what guessing costs. The CHAN payload
does restate three things the RF row records independently: payload type,
path length and SNR. Requiring all three to agree, on a row no older than the
correlation window, turns the fallback into a checked hypothesis and earns a
real match kind rather than a fallback tag.

SNR carries the weight: it is one reception's measured value, quantised to
0.25 dB, so an unrelated packet agreeing on all three is improbable rather
than merely unlikely. Across the 55 channel messages in my logs the
immediately preceding RF row agreed on all three every time, always within a
second, while a window-wide search on the same fields was ambiguous for half
of them—so this verifies the ordering the firmware already gives us instead
of searching for a match.

Because these messages now correlate, they also satisfy the '*' gate by the
primary route, resolve their scope properly when flood_scopes is regional,
and contribute their path to the mesh graph, which they never did before.
This commit is contained in:
agessaman committed 2026-08-25 09:28:19 -07:00
1 parent d2b83481f7
commit ba23899c31
3 files changed
+169

No files matched your search

+69
View File
@@ -32,6 +32,10 @@ RF_MATCH_KEY = "_rf_match"
RF_MATCH_EXACT = "exact"
RF_MATCH_PUBKEY = "pubkey"
RF_MATCH_PARTIAL = "partial"
# Verified against the decoded message payload's own fields rather than a packet
# prefix. Channel messages have no prefix to match on, so this is the only positive
# correlation available to them (see _rf_data_matches_chan_payload).
RF_MATCH_PAYLOAD = "payload"
RF_MATCH_FALLBACK = "fallback"
@@ -1534,8 +1538,73 @@ class MessageHandler:
scope_eligible_only=scope_eligible_only,
)
# A channel message has no packet prefix or pubkey to match on, so everything
# above lands on the most-recent-packet fallback. The decoded payload carries
# its own copies of fields the RF row also has, though, so the fallback can be
# checked rather than assumed.
if recent_rf_data is not None and not rf_data_is_correlated(recent_rf_data):
if self._rf_data_matches_chan_payload(recent_rf_data, payload):
recent_rf_data = {**recent_rf_data, RF_MATCH_KEY: RF_MATCH_PAYLOAD}
self.logger.debug(
"Verified RF row %s against the channel payload (GRP_TXT, path_len=%s, "
"SNR=%s); treating it as this message's packet",
(recent_rf_data.get("packet_prefix") or "?")[:16],
payload.get("path_len"),
payload.get("SNR"),
)
return recent_rf_data
def _rf_data_matches_chan_payload(
self, rf_data: dict[str, Any] | None, payload: dict[str, Any] | None
) -> bool:
"""True when a cached RF row is confirmed to be this channel message's packet.
MeshCore's CHAN event carries neither raw_hex nor a pubkey prefix, so the
prefix strategies in find_recent_rf_data cannot fire and every channel
message falls through to the most recent packet. That fallback is normally
right — the firmware emits the RF log row and the decoded CHAN event for the
same reception back to back — but "normally right" is not evidence, and #80
is what happens when a guess is treated as one.
The CHAN payload does, however, restate three things the RF row records
independently: payload type, path length and SNR. Requiring all three to
agree on a row heard within the correlation window turns the fallback into
a checked hypothesis. SNR is the discriminating one: it is the measured
value of a single reception, quantised to 0.25 dB, so an unrelated packet
matching all three is improbable rather than merely unlikely.
"""
if not rf_data or not payload:
return False
if rf_data.get("payload_type_int") != self._grp_txt_payload_type_int():
return False
# Bound the age: the RF row and its CHAN event arrive together, so an old row
# that happens to agree is a coincidence, not this message.
timestamp = rf_data.get("timestamp")
if not isinstance(timestamp, (int, float)):
return False
if time.time() - timestamp > self.message_timeout:
return False
payload_path_len = payload.get("path_len")
rf_path_len = (rf_data.get("routing_info") or {}).get("path_length")
if payload_path_len is None or rf_path_len is None:
return False
if int(payload_path_len) != int(rf_path_len):
return False
payload_snr = payload.get("SNR")
rf_snr = rf_data.get("snr")
if payload_snr is None or rf_snr is None:
return False
try:
# Tolerance covers float representation only; SNR is quantised to 0.25 dB.
return abs(float(payload_snr) - float(rf_snr)) < 1e-6
except (TypeError, ValueError):
return False
def find_recent_rf_data(
self,
correlation_key: str | None = None,