mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-08-07 09:22:53 +02:00
Capture radio metadata for ingestor payloads (#327)
* Capture radio metadata and tag ingestor payloads * Log captured LoRa metadata when initializing radio config
This commit is contained in:
@@ -52,6 +52,8 @@ _CONFIG_ATTRS = {
|
||||
"DEBUG",
|
||||
"INSTANCE",
|
||||
"API_TOKEN",
|
||||
"LORA_FREQ",
|
||||
"MODEM_PRESET",
|
||||
"_RECONNECT_INITIAL_DELAY_SECS",
|
||||
"_RECONNECT_MAX_DELAY_SECS",
|
||||
"_CLOSE_TIMEOUT_SECS",
|
||||
|
||||
@@ -64,6 +64,13 @@ DEBUG = os.environ.get("DEBUG") == "1"
|
||||
INSTANCE = os.environ.get("POTATOMESH_INSTANCE", "").rstrip("/")
|
||||
API_TOKEN = os.environ.get("API_TOKEN", "")
|
||||
ENERGY_SAVING = os.environ.get("ENERGY_SAVING") == "1"
|
||||
"""When ``True``, enables the ingestor's energy saving mode."""
|
||||
|
||||
LORA_FREQ: int | None = None
|
||||
"""Frequency of the local node's configured LoRa region in MHz."""
|
||||
|
||||
MODEM_PRESET: str | None = None
|
||||
"""CamelCase modem preset name reported by the local node."""
|
||||
|
||||
_RECONNECT_INITIAL_DELAY_SECS = DEFAULT_RECONNECT_INITIAL_DELAY_SECS
|
||||
_RECONNECT_MAX_DELAY_SECS = DEFAULT_RECONNECT_MAX_DELAY_SECS
|
||||
@@ -118,6 +125,8 @@ __all__ = [
|
||||
"INSTANCE",
|
||||
"API_TOKEN",
|
||||
"ENERGY_SAVING",
|
||||
"LORA_FREQ",
|
||||
"MODEM_PRESET",
|
||||
"_RECONNECT_INITIAL_DELAY_SECS",
|
||||
"_RECONNECT_MAX_DELAY_SECS",
|
||||
"_CLOSE_TIMEOUT_SECS",
|
||||
|
||||
@@ -241,6 +241,7 @@ def main() -> None:
|
||||
else:
|
||||
iface, resolved_target = interfaces._create_default_interface()
|
||||
active_candidate = resolved_target
|
||||
interfaces._ensure_radio_metadata(iface)
|
||||
retry_delay = max(0.0, config._RECONNECT_INITIAL_DELAY_SECS)
|
||||
initial_snapshot_sent = False
|
||||
if not announced_target and resolved_target:
|
||||
|
||||
@@ -42,6 +42,40 @@ from .serialization import (
|
||||
)
|
||||
|
||||
|
||||
def _radio_metadata_fields() -> dict[str, object]:
|
||||
"""Return the shared radio metadata fields for payload enrichment."""
|
||||
|
||||
metadata: dict[str, object] = {}
|
||||
freq = getattr(config, "LORA_FREQ", None)
|
||||
if freq is not None:
|
||||
metadata["lora_freq"] = freq
|
||||
preset = getattr(config, "MODEM_PRESET", None)
|
||||
if preset is not None:
|
||||
metadata["modem_preset"] = preset
|
||||
return metadata
|
||||
|
||||
|
||||
def _apply_radio_metadata(payload: dict) -> dict:
|
||||
"""Augment ``payload`` with radio metadata when available."""
|
||||
|
||||
metadata = _radio_metadata_fields()
|
||||
if metadata:
|
||||
payload.update(metadata)
|
||||
return payload
|
||||
|
||||
|
||||
def _apply_radio_metadata_to_nodes(payload: dict) -> dict:
|
||||
"""Attach radio metadata to each node entry stored in ``payload``."""
|
||||
|
||||
metadata = _radio_metadata_fields()
|
||||
if not metadata:
|
||||
return payload
|
||||
for value in payload.values():
|
||||
if isinstance(value, dict):
|
||||
value.update(metadata)
|
||||
return payload
|
||||
|
||||
|
||||
def upsert_node(node_id, node) -> None:
|
||||
"""Schedule an upsert for a single node.
|
||||
|
||||
@@ -53,7 +87,7 @@ def upsert_node(node_id, node) -> None:
|
||||
``None``. The payload is forwarded to the shared HTTP queue.
|
||||
"""
|
||||
|
||||
payload = upsert_payload(node_id, node)
|
||||
payload = _apply_radio_metadata_to_nodes(upsert_payload(node_id, node))
|
||||
_queue_post_json("/api/nodes", payload, priority=queue._NODE_POST_PRIORITY)
|
||||
|
||||
if config.DEBUG:
|
||||
@@ -243,7 +277,9 @@ def store_position_packet(packet: Mapping, decoded: Mapping) -> None:
|
||||
position_payload["raw"] = raw_payload
|
||||
|
||||
_queue_post_json(
|
||||
"/api/positions", position_payload, priority=queue._POSITION_POST_PRIORITY
|
||||
"/api/positions",
|
||||
_apply_radio_metadata(position_payload),
|
||||
priority=queue._POSITION_POST_PRIORITY,
|
||||
)
|
||||
|
||||
if config.DEBUG:
|
||||
@@ -445,7 +481,9 @@ def store_telemetry_packet(packet: Mapping, decoded: Mapping) -> None:
|
||||
telemetry_payload["barometric_pressure"] = barometric_pressure
|
||||
|
||||
_queue_post_json(
|
||||
"/api/telemetry", telemetry_payload, priority=queue._TELEMETRY_POST_PRIORITY
|
||||
"/api/telemetry",
|
||||
_apply_radio_metadata(telemetry_payload),
|
||||
priority=queue._TELEMETRY_POST_PRIORITY,
|
||||
)
|
||||
|
||||
if config.DEBUG:
|
||||
@@ -607,7 +645,9 @@ def store_nodeinfo_packet(packet: Mapping, decoded: Mapping) -> None:
|
||||
pass
|
||||
|
||||
_queue_post_json(
|
||||
"/api/nodes", {node_id: node_payload}, priority=queue._NODE_POST_PRIORITY
|
||||
"/api/nodes",
|
||||
_apply_radio_metadata_to_nodes({node_id: node_payload}),
|
||||
priority=queue._NODE_POST_PRIORITY,
|
||||
)
|
||||
|
||||
if config.DEBUG:
|
||||
@@ -720,7 +760,11 @@ def store_neighborinfo_packet(packet: Mapping, decoded: Mapping) -> None:
|
||||
if last_sent_by_id is not None:
|
||||
payload["last_sent_by_id"] = last_sent_by_id
|
||||
|
||||
_queue_post_json("/api/neighbors", payload, priority=queue._NEIGHBOR_POST_PRIORITY)
|
||||
_queue_post_json(
|
||||
"/api/neighbors",
|
||||
_apply_radio_metadata(payload),
|
||||
priority=queue._NEIGHBOR_POST_PRIORITY,
|
||||
)
|
||||
|
||||
if config.DEBUG:
|
||||
config._debug_log(
|
||||
@@ -828,7 +872,9 @@ def store_packet_dict(packet: Mapping) -> None:
|
||||
"hop_limit": int(hop) if hop is not None else None,
|
||||
}
|
||||
_queue_post_json(
|
||||
"/api/messages", message_payload, priority=queue._MESSAGE_POST_PRIORITY
|
||||
"/api/messages",
|
||||
_apply_radio_metadata(message_payload),
|
||||
priority=queue._MESSAGE_POST_PRIORITY,
|
||||
)
|
||||
|
||||
if config.DEBUG:
|
||||
|
||||
@@ -21,7 +21,7 @@ import ipaddress
|
||||
import re
|
||||
import urllib.parse
|
||||
from collections.abc import Mapping
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from meshtastic.serial_interface import SerialInterface
|
||||
from meshtastic.tcp_interface import TCPInterface
|
||||
@@ -169,6 +169,171 @@ def _patch_meshtastic_ble_receive_loop() -> None:
|
||||
|
||||
_patch_meshtastic_ble_receive_loop()
|
||||
|
||||
|
||||
def _has_field(message: Any, field_name: str) -> bool:
|
||||
"""Return ``True`` when ``message`` advertises ``field_name`` via ``HasField``."""
|
||||
|
||||
if message is None:
|
||||
return False
|
||||
has_field = getattr(message, "HasField", None)
|
||||
if callable(has_field):
|
||||
try:
|
||||
return bool(has_field(field_name))
|
||||
except Exception: # pragma: no cover - defensive guard
|
||||
return False
|
||||
return hasattr(message, field_name)
|
||||
|
||||
|
||||
def _enum_name_from_field(message: Any, field_name: str, value: Any) -> str | None:
|
||||
"""Return the enum name for ``value`` using ``message`` descriptors."""
|
||||
|
||||
descriptor = getattr(message, "DESCRIPTOR", None)
|
||||
if descriptor is None:
|
||||
return None
|
||||
fields_by_name = getattr(descriptor, "fields_by_name", {})
|
||||
field_desc = fields_by_name.get(field_name)
|
||||
if field_desc is None:
|
||||
return None
|
||||
enum_type = getattr(field_desc, "enum_type", None)
|
||||
if enum_type is None:
|
||||
return None
|
||||
enum_values = getattr(enum_type, "values_by_number", {})
|
||||
enum_value = enum_values.get(value)
|
||||
if enum_value is None:
|
||||
return None
|
||||
return getattr(enum_value, "name", None)
|
||||
|
||||
|
||||
def _resolve_lora_message(local_config: Any) -> Any | None:
|
||||
"""Return the LoRa configuration sub-message from ``local_config``."""
|
||||
|
||||
if local_config is None:
|
||||
return None
|
||||
if _has_field(local_config, "lora"):
|
||||
candidate = getattr(local_config, "lora", None)
|
||||
if candidate is not None:
|
||||
return candidate
|
||||
radio_section = getattr(local_config, "radio", None)
|
||||
if radio_section is not None:
|
||||
if _has_field(radio_section, "lora"):
|
||||
return getattr(radio_section, "lora", None)
|
||||
if hasattr(radio_section, "lora"):
|
||||
return getattr(radio_section, "lora")
|
||||
if hasattr(local_config, "lora"):
|
||||
return getattr(local_config, "lora")
|
||||
return None
|
||||
|
||||
|
||||
def _region_frequency(lora_message: Any) -> int | None:
|
||||
"""Derive the LoRa region frequency in MHz from ``lora_message``."""
|
||||
|
||||
if lora_message is None:
|
||||
return None
|
||||
region_value = getattr(lora_message, "region", None)
|
||||
if region_value is None:
|
||||
return None
|
||||
enum_name = _enum_name_from_field(lora_message, "region", region_value)
|
||||
if enum_name:
|
||||
digits = re.findall(r"\d+", enum_name)
|
||||
for token in digits:
|
||||
try:
|
||||
freq = int(token)
|
||||
except ValueError: # pragma: no cover - regex guarantees digits
|
||||
continue
|
||||
if freq >= 100:
|
||||
return freq
|
||||
for token in reversed(digits):
|
||||
try:
|
||||
return int(token)
|
||||
except ValueError: # pragma: no cover - defensive only
|
||||
continue
|
||||
if isinstance(region_value, int) and region_value >= 100:
|
||||
return region_value
|
||||
return None
|
||||
|
||||
|
||||
def _camelcase_enum_name(name: str | None) -> str | None:
|
||||
"""Convert ``name`` from ``SCREAMING_SNAKE`` to ``CamelCase``."""
|
||||
|
||||
if not name:
|
||||
return None
|
||||
parts = re.split(r"[^0-9A-Za-z]+", name.strip())
|
||||
camel_parts = [part.capitalize() for part in parts if part]
|
||||
if not camel_parts:
|
||||
return None
|
||||
return "".join(camel_parts)
|
||||
|
||||
|
||||
def _modem_preset(lora_message: Any) -> str | None:
|
||||
"""Return the CamelCase modem preset configured on ``lora_message``."""
|
||||
|
||||
if lora_message is None:
|
||||
return None
|
||||
descriptor = getattr(lora_message, "DESCRIPTOR", None)
|
||||
fields_by_name = getattr(descriptor, "fields_by_name", {}) if descriptor else {}
|
||||
if "modem_preset" in fields_by_name:
|
||||
preset_field = "modem_preset"
|
||||
elif "preset" in fields_by_name:
|
||||
preset_field = "preset"
|
||||
elif hasattr(lora_message, "modem_preset"):
|
||||
preset_field = "modem_preset"
|
||||
elif hasattr(lora_message, "preset"):
|
||||
preset_field = "preset"
|
||||
else:
|
||||
return None
|
||||
|
||||
preset_value = getattr(lora_message, preset_field, None)
|
||||
if preset_value is None:
|
||||
return None
|
||||
enum_name = _enum_name_from_field(lora_message, preset_field, preset_value)
|
||||
if isinstance(enum_name, str) and enum_name:
|
||||
return _camelcase_enum_name(enum_name)
|
||||
if isinstance(preset_value, str) and preset_value:
|
||||
return _camelcase_enum_name(preset_value)
|
||||
return None
|
||||
|
||||
|
||||
def _ensure_radio_metadata(iface: Any) -> None:
|
||||
"""Populate cached LoRa metadata by inspecting ``iface`` when available."""
|
||||
|
||||
if iface is None:
|
||||
return
|
||||
|
||||
try:
|
||||
wait_for_config = getattr(iface, "waitForConfig", None)
|
||||
if callable(wait_for_config):
|
||||
wait_for_config()
|
||||
except Exception: # pragma: no cover - hardware dependent guard
|
||||
pass
|
||||
|
||||
local_node = getattr(iface, "localNode", None)
|
||||
local_config = getattr(local_node, "localConfig", None) if local_node else None
|
||||
lora_message = _resolve_lora_message(local_config)
|
||||
if lora_message is None:
|
||||
return
|
||||
|
||||
frequency = _region_frequency(lora_message)
|
||||
preset = _modem_preset(lora_message)
|
||||
|
||||
updated = False
|
||||
if frequency is not None and getattr(config, "LORA_FREQ", None) is None:
|
||||
config.LORA_FREQ = frequency
|
||||
updated = True
|
||||
if preset is not None and getattr(config, "MODEM_PRESET", None) is None:
|
||||
config.MODEM_PRESET = preset
|
||||
updated = True
|
||||
|
||||
if updated:
|
||||
config._debug_log(
|
||||
"Captured LoRa radio metadata",
|
||||
context="interfaces.ensure_radio_metadata",
|
||||
severity="info",
|
||||
always=True,
|
||||
lora_freq=frequency,
|
||||
modem_preset=preset,
|
||||
)
|
||||
|
||||
|
||||
_DEFAULT_TCP_PORT = 4403
|
||||
_DEFAULT_TCP_TARGET = "http://127.0.0.1"
|
||||
|
||||
@@ -416,6 +581,7 @@ def _create_default_interface() -> tuple[object, str]:
|
||||
__all__ = [
|
||||
"BLEInterface",
|
||||
"NoAvailableMeshInterface",
|
||||
"_ensure_radio_metadata",
|
||||
"_DummySerialInterface",
|
||||
"_DEFAULT_TCP_PORT",
|
||||
"_DEFAULT_TCP_TARGET",
|
||||
|
||||
@@ -179,6 +179,13 @@ def mesh_module(monkeypatch):
|
||||
if hasattr(module, "_clear_post_queue"):
|
||||
module._clear_post_queue()
|
||||
|
||||
# Ensure radio metadata starts unset for each test run.
|
||||
module.config.LORA_FREQ = None
|
||||
module.config.MODEM_PRESET = None
|
||||
for attr in ("LORA_FREQ", "MODEM_PRESET"):
|
||||
if attr in module.__dict__:
|
||||
delattr(module, attr)
|
||||
|
||||
yield module
|
||||
|
||||
# Ensure a clean import for the next test
|
||||
@@ -293,6 +300,108 @@ def test_create_serial_interface_ble(mesh_module, monkeypatch):
|
||||
assert iface.nodes == {}
|
||||
|
||||
|
||||
def test_ensure_radio_metadata_extracts_config(mesh_module, capsys):
|
||||
mesh = mesh_module
|
||||
|
||||
class DummyEnumValue:
|
||||
def __init__(self, name: str) -> None:
|
||||
self.name = name
|
||||
|
||||
class DummyEnum:
|
||||
def __init__(self, mapping: dict[int, str]) -> None:
|
||||
self.values_by_number = {
|
||||
number: DummyEnumValue(name) for number, name in mapping.items()
|
||||
}
|
||||
|
||||
class DummyField:
|
||||
def __init__(self, enum_type=None) -> None:
|
||||
self.enum_type = enum_type
|
||||
|
||||
class DummyDescriptor:
|
||||
def __init__(self, fields: dict[str, DummyField]) -> None:
|
||||
self.fields_by_name = fields
|
||||
|
||||
def make_lora(
|
||||
region_value: int,
|
||||
region_name: str,
|
||||
preset_value: int,
|
||||
preset_name: str,
|
||||
*,
|
||||
preset_field: str = "modem_preset",
|
||||
):
|
||||
descriptor = DummyDescriptor(
|
||||
{
|
||||
"region": DummyField(DummyEnum({region_value: region_name})),
|
||||
preset_field: DummyField(DummyEnum({preset_value: preset_name})),
|
||||
}
|
||||
)
|
||||
|
||||
class DummyLora:
|
||||
DESCRIPTOR = descriptor
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.region = region_value
|
||||
setattr(self, preset_field, preset_value)
|
||||
|
||||
def HasField(self, name: str) -> bool: # noqa: D401 - simple proxy
|
||||
return hasattr(self, name)
|
||||
|
||||
return DummyLora()
|
||||
|
||||
class DummyRadio:
|
||||
def __init__(self, lora) -> None:
|
||||
self.lora = lora
|
||||
|
||||
def HasField(self, name: str) -> bool:
|
||||
return hasattr(self, name)
|
||||
|
||||
class DummyConfig:
|
||||
def __init__(self, lora, *, expose_direct: bool) -> None:
|
||||
if expose_direct:
|
||||
self.lora = lora
|
||||
else:
|
||||
self.radio = DummyRadio(lora)
|
||||
|
||||
def HasField(self, name: str) -> bool: # noqa: D401 - mimics protobuf API
|
||||
return hasattr(self, name)
|
||||
|
||||
class DummyLocalNode:
|
||||
def __init__(self, config) -> None:
|
||||
self.localConfig = config
|
||||
|
||||
class DummyInterface:
|
||||
def __init__(self, local_config) -> None:
|
||||
self.localNode = DummyLocalNode(local_config)
|
||||
self.wait_calls = 0
|
||||
|
||||
def waitForConfig(self) -> None: # noqa: D401 - matches Meshtastic API
|
||||
self.wait_calls += 1
|
||||
|
||||
primary_lora = make_lora(3, "EU_868", 4, "MEDIUM_FAST")
|
||||
iface = DummyInterface(DummyConfig(primary_lora, expose_direct=False))
|
||||
|
||||
mesh._ensure_radio_metadata(iface)
|
||||
first_log = capsys.readouterr().out
|
||||
|
||||
assert iface.wait_calls == 1
|
||||
assert mesh.config.LORA_FREQ == 868
|
||||
assert mesh.config.MODEM_PRESET == "MediumFast"
|
||||
assert "Captured LoRa radio metadata" in first_log
|
||||
assert "lora_freq=868" in first_log
|
||||
assert "modem_preset='MediumFast'" in first_log
|
||||
|
||||
secondary_lora = make_lora(7, "US_915", 2, "LONG_FAST", preset_field="preset")
|
||||
second_iface = DummyInterface(DummyConfig(secondary_lora, expose_direct=True))
|
||||
|
||||
mesh._ensure_radio_metadata(second_iface)
|
||||
second_log = capsys.readouterr().out
|
||||
|
||||
assert second_iface.wait_calls == 1
|
||||
assert mesh.config.LORA_FREQ == 868
|
||||
assert mesh.config.MODEM_PRESET == "MediumFast"
|
||||
assert second_log == ""
|
||||
|
||||
|
||||
def test_create_default_interface_falls_back_to_tcp(mesh_module, monkeypatch):
|
||||
mesh = mesh_module
|
||||
attempts = []
|
||||
@@ -371,6 +480,9 @@ def test_store_packet_dict_posts_text_message(mesh_module, monkeypatch):
|
||||
lambda path, payload, *, priority: captured.append((path, payload, priority)),
|
||||
)
|
||||
|
||||
mesh.config.LORA_FREQ = 868
|
||||
mesh.config.MODEM_PRESET = "MediumFast"
|
||||
|
||||
packet = {
|
||||
"id": 123,
|
||||
"rxTime": 1_700_000_000,
|
||||
@@ -403,6 +515,8 @@ def test_store_packet_dict_posts_text_message(mesh_module, monkeypatch):
|
||||
assert payload["hop_limit"] == 3
|
||||
assert payload["snr"] == pytest.approx(1.25)
|
||||
assert payload["rssi"] == -70
|
||||
assert payload["lora_freq"] == 868
|
||||
assert payload["modem_preset"] == "MediumFast"
|
||||
assert priority == mesh._MESSAGE_POST_PRIORITY
|
||||
|
||||
|
||||
@@ -415,6 +529,9 @@ def test_store_packet_dict_posts_position(mesh_module, monkeypatch):
|
||||
lambda path, payload, *, priority: captured.append((path, payload, priority)),
|
||||
)
|
||||
|
||||
mesh.config.LORA_FREQ = 868
|
||||
mesh.config.MODEM_PRESET = "MediumFast"
|
||||
|
||||
packet = {
|
||||
"id": 200498337,
|
||||
"rxTime": 1_758_624_186,
|
||||
@@ -479,6 +596,8 @@ def test_store_packet_dict_posts_position(mesh_module, monkeypatch):
|
||||
payload["payload_b64"]
|
||||
== "DQDATR8VAMATCBjw//////////8BJb150mgoAljTAXgCgAEAmAEHuAER"
|
||||
)
|
||||
assert payload["lora_freq"] == 868
|
||||
assert payload["modem_preset"] == "MediumFast"
|
||||
assert payload["raw"]["time"] == 1_758_624_189
|
||||
|
||||
|
||||
@@ -491,6 +610,9 @@ def test_store_packet_dict_posts_neighborinfo(mesh_module, monkeypatch):
|
||||
lambda path, payload, *, priority: captured.append((path, payload, priority)),
|
||||
)
|
||||
|
||||
mesh.config.LORA_FREQ = 868
|
||||
mesh.config.MODEM_PRESET = "MediumFast"
|
||||
|
||||
packet = {
|
||||
"id": 2049886869,
|
||||
"rxTime": 1_758_884_186,
|
||||
@@ -532,6 +654,8 @@ def test_store_packet_dict_posts_neighborinfo(mesh_module, monkeypatch):
|
||||
assert neighbors[1]["snr"] == pytest.approx(-2.75)
|
||||
assert neighbors[2]["neighbor_id"] == "!0badc0de"
|
||||
assert neighbors[2]["neighbor_num"] == 0x0BAD_C0DE
|
||||
assert payload["lora_freq"] == 868
|
||||
assert payload["modem_preset"] == "MediumFast"
|
||||
|
||||
|
||||
def test_store_packet_dict_handles_nodeinfo_packet(mesh_module, monkeypatch):
|
||||
@@ -543,6 +667,9 @@ def test_store_packet_dict_handles_nodeinfo_packet(mesh_module, monkeypatch):
|
||||
lambda path, payload, *, priority: captured.append((path, payload, priority)),
|
||||
)
|
||||
|
||||
mesh.config.LORA_FREQ = 868
|
||||
mesh.config.MODEM_PRESET = "MediumFast"
|
||||
|
||||
from meshtastic.protobuf import config_pb2, mesh_pb2
|
||||
|
||||
node_info = mesh_pb2.NodeInfo()
|
||||
@@ -602,6 +729,8 @@ def test_store_packet_dict_handles_nodeinfo_packet(mesh_module, monkeypatch):
|
||||
assert node_entry["position"]["latitude"] == pytest.approx(52.5)
|
||||
assert node_entry["position"]["longitude"] == pytest.approx(13.4)
|
||||
assert node_entry["position"]["time"] == 1_700_000_050
|
||||
assert node_entry["lora_freq"] == 868
|
||||
assert node_entry["modem_preset"] == "MediumFast"
|
||||
|
||||
|
||||
def test_store_packet_dict_handles_user_only_nodeinfo(mesh_module, monkeypatch):
|
||||
@@ -613,6 +742,9 @@ def test_store_packet_dict_handles_user_only_nodeinfo(mesh_module, monkeypatch):
|
||||
lambda path, payload, *, priority: captured.append((path, payload, priority)),
|
||||
)
|
||||
|
||||
mesh.config.LORA_FREQ = 868
|
||||
mesh.config.MODEM_PRESET = "MediumFast"
|
||||
|
||||
from meshtastic.protobuf import mesh_pb2
|
||||
|
||||
user_msg = mesh_pb2.User()
|
||||
@@ -645,6 +777,8 @@ def test_store_packet_dict_handles_user_only_nodeinfo(mesh_module, monkeypatch):
|
||||
assert node_entry["lastHeard"] == 1_234
|
||||
assert node_entry["user"]["longName"] == "Test Node"
|
||||
assert "deviceMetrics" not in node_entry
|
||||
assert node_entry["lora_freq"] == 868
|
||||
assert node_entry["modem_preset"] == "MediumFast"
|
||||
|
||||
|
||||
def test_store_packet_dict_nodeinfo_merges_proto_user(mesh_module, monkeypatch):
|
||||
@@ -656,6 +790,9 @@ def test_store_packet_dict_nodeinfo_merges_proto_user(mesh_module, monkeypatch):
|
||||
lambda path, payload, *, priority: captured.append((path, payload, priority)),
|
||||
)
|
||||
|
||||
mesh.config.LORA_FREQ = 868
|
||||
mesh.config.MODEM_PRESET = "MediumFast"
|
||||
|
||||
from meshtastic.protobuf import mesh_pb2
|
||||
|
||||
user_msg = mesh_pb2.User()
|
||||
@@ -686,6 +823,8 @@ def test_store_packet_dict_nodeinfo_merges_proto_user(mesh_module, monkeypatch):
|
||||
assert node_entry["lastHeard"] == 5_000
|
||||
assert node_entry["user"]["shortName"] == "Proto"
|
||||
assert node_entry["user"]["longName"] == "Proto User"
|
||||
assert node_entry["lora_freq"] == 868
|
||||
assert node_entry["modem_preset"] == "MediumFast"
|
||||
|
||||
|
||||
def test_store_packet_dict_nodeinfo_sanitizes_nested_proto(mesh_module, monkeypatch):
|
||||
@@ -697,6 +836,9 @@ def test_store_packet_dict_nodeinfo_sanitizes_nested_proto(mesh_module, monkeypa
|
||||
lambda path, payload, *, priority: captured.append((path, payload, priority)),
|
||||
)
|
||||
|
||||
mesh.config.LORA_FREQ = 868
|
||||
mesh.config.MODEM_PRESET = "MediumFast"
|
||||
|
||||
from meshtastic.protobuf import mesh_pb2
|
||||
|
||||
user_msg = mesh_pb2.User()
|
||||
@@ -730,6 +872,8 @@ def test_store_packet_dict_nodeinfo_sanitizes_nested_proto(mesh_module, monkeypa
|
||||
assert node_entry["user"]["shortName"] == "Nested"
|
||||
assert isinstance(node_entry["user"]["raw"], dict)
|
||||
assert node_entry["user"]["raw"]["id"] == "!55667788"
|
||||
assert node_entry["lora_freq"] == 868
|
||||
assert node_entry["modem_preset"] == "MediumFast"
|
||||
|
||||
|
||||
def test_store_packet_dict_nodeinfo_uses_from_id_when_user_missing(
|
||||
@@ -743,6 +887,9 @@ def test_store_packet_dict_nodeinfo_uses_from_id_when_user_missing(
|
||||
lambda path, payload, *, priority: captured.append((path, payload, priority)),
|
||||
)
|
||||
|
||||
mesh.config.LORA_FREQ = 868
|
||||
mesh.config.MODEM_PRESET = "MediumFast"
|
||||
|
||||
from meshtastic.protobuf import mesh_pb2
|
||||
|
||||
node_info = mesh_pb2.NodeInfo()
|
||||
@@ -766,6 +913,8 @@ def test_store_packet_dict_nodeinfo_uses_from_id_when_user_missing(
|
||||
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_store_packet_dict_ignores_non_text(mesh_module, monkeypatch):
|
||||
@@ -1081,6 +1230,9 @@ def test_store_packet_dict_uses_top_level_channel(mesh_module, monkeypatch):
|
||||
lambda path, payload, *, priority: captured.append((path, payload, priority)),
|
||||
)
|
||||
|
||||
mesh.config.LORA_FREQ = 868
|
||||
mesh.config.MODEM_PRESET = "MediumFast"
|
||||
|
||||
packet = {
|
||||
"id": "789",
|
||||
"rxTime": 123456,
|
||||
@@ -1100,6 +1252,8 @@ def test_store_packet_dict_uses_top_level_channel(mesh_module, monkeypatch):
|
||||
assert payload["text"] == "hi"
|
||||
assert payload["encrypted"] is None
|
||||
assert payload["snr"] is None and payload["rssi"] is None
|
||||
assert payload["lora_freq"] == 868
|
||||
assert payload["modem_preset"] == "MediumFast"
|
||||
assert priority == mesh._MESSAGE_POST_PRIORITY
|
||||
|
||||
|
||||
@@ -1112,6 +1266,9 @@ def test_store_packet_dict_handles_invalid_channel(mesh_module, monkeypatch):
|
||||
lambda path, payload, *, priority: captured.append((path, payload, priority)),
|
||||
)
|
||||
|
||||
mesh.config.LORA_FREQ = 868
|
||||
mesh.config.MODEM_PRESET = "MediumFast"
|
||||
|
||||
packet = {
|
||||
"id": 321,
|
||||
"rxTime": 999,
|
||||
@@ -1130,6 +1287,8 @@ def test_store_packet_dict_handles_invalid_channel(mesh_module, monkeypatch):
|
||||
assert path == "/api/messages"
|
||||
assert payload["channel"] == 0
|
||||
assert payload["encrypted"] is None
|
||||
assert payload["lora_freq"] == 868
|
||||
assert payload["modem_preset"] == "MediumFast"
|
||||
assert priority == mesh._MESSAGE_POST_PRIORITY
|
||||
|
||||
|
||||
@@ -1142,6 +1301,9 @@ def test_store_packet_dict_includes_encrypted_payload(mesh_module, monkeypatch):
|
||||
lambda path, payload, *, priority: captured.append((path, payload, priority)),
|
||||
)
|
||||
|
||||
mesh.config.LORA_FREQ = 868
|
||||
mesh.config.MODEM_PRESET = "MediumFast"
|
||||
|
||||
packet = {
|
||||
"id": 555,
|
||||
"rxTime": 111,
|
||||
@@ -1160,6 +1322,8 @@ def test_store_packet_dict_includes_encrypted_payload(mesh_module, monkeypatch):
|
||||
assert payload["text"] is None
|
||||
assert payload["from_id"] == 2988082812
|
||||
assert payload["to_id"] == "!receiver"
|
||||
assert payload["lora_freq"] == 868
|
||||
assert payload["modem_preset"] == "MediumFast"
|
||||
assert priority == mesh._MESSAGE_POST_PRIORITY
|
||||
|
||||
|
||||
@@ -1172,6 +1336,9 @@ def test_store_packet_dict_handles_telemetry_packet(mesh_module, monkeypatch):
|
||||
lambda path, payload, *, priority: captured.append((path, payload, priority)),
|
||||
)
|
||||
|
||||
mesh.config.LORA_FREQ = 868
|
||||
mesh.config.MODEM_PRESET = "MediumFast"
|
||||
|
||||
packet = {
|
||||
"id": 1_256_091_342,
|
||||
"rxTime": 1_758_024_300,
|
||||
@@ -1219,6 +1386,8 @@ def test_store_packet_dict_handles_telemetry_packet(mesh_module, monkeypatch):
|
||||
assert payload["channel_utilization"] == pytest.approx(0.59666663)
|
||||
assert payload["air_util_tx"] == pytest.approx(0.03908333)
|
||||
assert payload["uptime_seconds"] == 305044
|
||||
assert payload["lora_freq"] == 868
|
||||
assert payload["modem_preset"] == "MediumFast"
|
||||
|
||||
|
||||
def test_store_packet_dict_handles_environment_telemetry(mesh_module, monkeypatch):
|
||||
@@ -1230,6 +1399,9 @@ def test_store_packet_dict_handles_environment_telemetry(mesh_module, monkeypatc
|
||||
lambda path, payload, *, priority: captured.append((path, payload, priority)),
|
||||
)
|
||||
|
||||
mesh.config.LORA_FREQ = 868
|
||||
mesh.config.MODEM_PRESET = "MediumFast"
|
||||
|
||||
packet = {
|
||||
"id": 2_817_720_548,
|
||||
"rxTime": 1_758_024_400,
|
||||
@@ -1259,6 +1431,8 @@ def test_store_packet_dict_handles_environment_telemetry(mesh_module, monkeypatc
|
||||
assert payload["temperature"] == pytest.approx(21.98)
|
||||
assert payload["relative_humidity"] == pytest.approx(39.475586)
|
||||
assert payload["barometric_pressure"] == pytest.approx(1017.8353)
|
||||
assert payload["lora_freq"] == 868
|
||||
assert payload["modem_preset"] == "MediumFast"
|
||||
|
||||
|
||||
def test_post_queue_prioritises_messages(mesh_module, monkeypatch):
|
||||
@@ -1771,6 +1945,9 @@ def test_store_position_packet_defaults(mesh_module, monkeypatch):
|
||||
lambda path, payload, *, priority: captured.append((path, payload, priority)),
|
||||
)
|
||||
|
||||
mesh.config.LORA_FREQ = 868
|
||||
mesh.config.MODEM_PRESET = "MediumFast"
|
||||
|
||||
packet = {"id": "7", "rxTime": "", "from": "!abcd", "to": "", "decoded": {}}
|
||||
|
||||
mesh.store_position_packet(packet, {})
|
||||
@@ -1782,6 +1959,8 @@ def test_store_position_packet_defaults(mesh_module, monkeypatch):
|
||||
assert payload["to_id"] is None
|
||||
assert payload["latitude"] is None
|
||||
assert payload["longitude"] is None
|
||||
assert payload["lora_freq"] == 868
|
||||
assert payload["modem_preset"] == "MediumFast"
|
||||
|
||||
|
||||
def test_store_nodeinfo_packet_debug(mesh_module, monkeypatch, capsys):
|
||||
|
||||
Reference in New Issue
Block a user