mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-03-28 17:42:48 +01:00
@@ -16,6 +16,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import glob
|
||||
import ipaddress
|
||||
import re
|
||||
@@ -48,39 +49,97 @@ def _patch_meshtastic_nodeinfo_handler() -> None:
|
||||
if getattr(original, "_potato_mesh_safe_wrapper", False):
|
||||
return
|
||||
|
||||
def _safe_on_node_info_receive(iface, packet): # type: ignore[override]
|
||||
candidate_mapping: Mapping | None = None
|
||||
if isinstance(packet, Mapping):
|
||||
candidate_mapping = packet
|
||||
elif hasattr(packet, "__dict__") and isinstance(packet.__dict__, Mapping):
|
||||
candidate_mapping = packet.__dict__
|
||||
def _ensure_mapping(value) -> Mapping | None:
|
||||
"""Return ``value`` as a mapping when conversion is possible."""
|
||||
|
||||
node_id = None
|
||||
if candidate_mapping is not None:
|
||||
node_id = serialization._canonical_node_id(candidate_mapping.get("id"))
|
||||
if node_id is None:
|
||||
user_section = candidate_mapping.get("user")
|
||||
if isinstance(user_section, Mapping):
|
||||
node_id = serialization._canonical_node_id(user_section.get("id"))
|
||||
if node_id is None:
|
||||
for key in ("fromId", "from_id", "from", "num", "nodeId", "node_id"):
|
||||
node_id = serialization._canonical_node_id(
|
||||
candidate_mapping.get(key)
|
||||
)
|
||||
if node_id:
|
||||
break
|
||||
if isinstance(value, Mapping):
|
||||
return value
|
||||
if hasattr(value, "__dict__") and isinstance(value.__dict__, Mapping):
|
||||
return value.__dict__
|
||||
with contextlib.suppress(Exception):
|
||||
converted = serialization._node_to_dict(value)
|
||||
if isinstance(converted, Mapping):
|
||||
return converted
|
||||
return None
|
||||
|
||||
def _candidate_node_id(mapping: Mapping | None) -> str | None:
|
||||
"""Extract a canonical node identifier from ``mapping`` when present."""
|
||||
|
||||
if mapping is None:
|
||||
return None
|
||||
|
||||
primary_keys = (
|
||||
"id",
|
||||
"userId",
|
||||
"user_id",
|
||||
"fromId",
|
||||
"from_id",
|
||||
"from",
|
||||
"nodeId",
|
||||
"node_id",
|
||||
"nodeNum",
|
||||
"node_num",
|
||||
"num",
|
||||
)
|
||||
|
||||
for key in primary_keys:
|
||||
node_id = serialization._canonical_node_id(mapping.get(key))
|
||||
if node_id:
|
||||
if not isinstance(candidate_mapping, dict):
|
||||
try:
|
||||
candidate_mapping = dict(candidate_mapping)
|
||||
except Exception:
|
||||
candidate_mapping = {
|
||||
k: candidate_mapping[k] for k in candidate_mapping
|
||||
}
|
||||
if candidate_mapping.get("id") != node_id:
|
||||
candidate_mapping["id"] = node_id
|
||||
packet = candidate_mapping
|
||||
return node_id
|
||||
|
||||
user_section = _ensure_mapping(mapping.get("user"))
|
||||
if user_section is not None:
|
||||
for key in ("id", "userId", "user_id", "num", "nodeNum", "node_num"):
|
||||
node_id = serialization._canonical_node_id(user_section.get(key))
|
||||
if node_id:
|
||||
return node_id
|
||||
|
||||
decoded_section = _ensure_mapping(mapping.get("decoded"))
|
||||
if decoded_section is not None:
|
||||
node_id = _candidate_node_id(decoded_section)
|
||||
if node_id:
|
||||
return node_id
|
||||
|
||||
payload_section = _ensure_mapping(mapping.get("payload"))
|
||||
if payload_section is not None:
|
||||
node_id = _candidate_node_id(payload_section)
|
||||
if node_id:
|
||||
return node_id
|
||||
|
||||
for key in ("packet", "meta", "info"):
|
||||
node_id = _candidate_node_id(_ensure_mapping(mapping.get(key)))
|
||||
if node_id:
|
||||
return node_id
|
||||
|
||||
for value in mapping.values():
|
||||
if isinstance(value, (list, tuple)):
|
||||
for item in value:
|
||||
node_id = _candidate_node_id(_ensure_mapping(item))
|
||||
if node_id:
|
||||
return node_id
|
||||
else:
|
||||
node_id = _candidate_node_id(_ensure_mapping(value))
|
||||
if node_id:
|
||||
return node_id
|
||||
|
||||
return None
|
||||
|
||||
def _safe_on_node_info_receive(iface, packet): # type: ignore[override]
|
||||
candidate_mapping = _ensure_mapping(packet)
|
||||
|
||||
node_id = _candidate_node_id(candidate_mapping)
|
||||
|
||||
if node_id and candidate_mapping is not None:
|
||||
if not isinstance(candidate_mapping, dict):
|
||||
try:
|
||||
candidate_mapping = dict(candidate_mapping)
|
||||
except Exception:
|
||||
candidate_mapping = {
|
||||
k: candidate_mapping[k] for k in candidate_mapping
|
||||
}
|
||||
if candidate_mapping.get("id") != node_id:
|
||||
candidate_mapping["id"] = node_id
|
||||
packet = candidate_mapping
|
||||
|
||||
try:
|
||||
return original(iface, packet)
|
||||
|
||||
@@ -198,7 +198,6 @@ def mesh_module(monkeypatch):
|
||||
|
||||
def test_snapshot_interval_defaults_to_60_seconds(mesh_module):
|
||||
mesh = mesh_module
|
||||
|
||||
assert mesh.SNAPSHOT_SECS == 60
|
||||
|
||||
|
||||
@@ -1038,12 +1037,47 @@ def test_store_packet_dict_nodeinfo_uses_from_id_when_user_missing(
|
||||
assert captured
|
||||
_, payload, _ = captured[0]
|
||||
assert "!01020304" in payload
|
||||
node_entry = payload["!01020304"]
|
||||
assert node_entry["num"] == 0x01020304
|
||||
assert node_entry["lastHeard"] == 200
|
||||
assert node_entry["snr"] == pytest.approx(1.5)
|
||||
assert node_entry["lora_freq"] == 868
|
||||
assert node_entry["modem_preset"] == "MediumFast"
|
||||
|
||||
|
||||
def test_nodeinfo_wrapper_infers_missing_identifier(mesh_module, monkeypatch):
|
||||
"""Ensure the Meshtastic nodeinfo hook derives canonical IDs from payloads."""
|
||||
|
||||
_ = mesh_module
|
||||
import meshtastic
|
||||
from data.mesh_ingestor import interfaces
|
||||
|
||||
captured_packets: list[dict] = []
|
||||
|
||||
def _original_handler(iface, packet):
|
||||
captured_packets.append(packet)
|
||||
return packet["id"]
|
||||
|
||||
monkeypatch.setattr(
|
||||
meshtastic, "_onNodeInfoReceive", _original_handler, raising=False
|
||||
)
|
||||
interfaces._patch_meshtastic_nodeinfo_handler()
|
||||
|
||||
safe_handler = meshtastic._onNodeInfoReceive
|
||||
|
||||
class DummyUser:
|
||||
def __init__(self) -> None:
|
||||
self.num = 0x88776655
|
||||
|
||||
class DummyDecoded:
|
||||
def __init__(self) -> None:
|
||||
self.user = DummyUser()
|
||||
|
||||
class DummyPacket:
|
||||
def __init__(self) -> None:
|
||||
self.decoded = DummyDecoded()
|
||||
|
||||
iface = types.SimpleNamespace(nodes={})
|
||||
|
||||
safe_handler(iface, DummyPacket())
|
||||
|
||||
assert captured_packets, "Expected wrapper to call the original handler"
|
||||
packet = captured_packets[0]
|
||||
assert packet["id"] == "!88776655"
|
||||
|
||||
|
||||
def test_store_packet_dict_ignores_non_text(mesh_module, monkeypatch):
|
||||
|
||||
Reference in New Issue
Block a user