From 7db76ec2fcea8cac5f71d7fe6f898746c6ec25b6 Mon Sep 17 00:00:00 2001 From: l5y <220195275+l5yth@users.noreply.github.com> Date: Mon, 13 Oct 2025 22:35:06 +0200 Subject: [PATCH] Capture radio metadata for ingestor payloads (#327) * Capture radio metadata and tag ingestor payloads * Log captured LoRa metadata when initializing radio config --- data/mesh_ingestor/__init__.py | 2 + data/mesh_ingestor/config.py | 9 ++ data/mesh_ingestor/daemon.py | 1 + data/mesh_ingestor/handlers.py | 58 ++++++++-- data/mesh_ingestor/interfaces.py | 168 ++++++++++++++++++++++++++++- tests/test_mesh.py | 179 +++++++++++++++++++++++++++++++ 6 files changed, 410 insertions(+), 7 deletions(-) diff --git a/data/mesh_ingestor/__init__.py b/data/mesh_ingestor/__init__.py index b2023f0..784faca 100644 --- a/data/mesh_ingestor/__init__.py +++ b/data/mesh_ingestor/__init__.py @@ -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", diff --git a/data/mesh_ingestor/config.py b/data/mesh_ingestor/config.py index 5a75c40..ab59f41 100644 --- a/data/mesh_ingestor/config.py +++ b/data/mesh_ingestor/config.py @@ -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", diff --git a/data/mesh_ingestor/daemon.py b/data/mesh_ingestor/daemon.py index 0cf56db..06f782f 100644 --- a/data/mesh_ingestor/daemon.py +++ b/data/mesh_ingestor/daemon.py @@ -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: diff --git a/data/mesh_ingestor/handlers.py b/data/mesh_ingestor/handlers.py index 54b994e..269de38 100644 --- a/data/mesh_ingestor/handlers.py +++ b/data/mesh_ingestor/handlers.py @@ -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: diff --git a/data/mesh_ingestor/interfaces.py b/data/mesh_ingestor/interfaces.py index f601b30..00f0452 100644 --- a/data/mesh_ingestor/interfaces.py +++ b/data/mesh_ingestor/interfaces.py @@ -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", diff --git a/tests/test_mesh.py b/tests/test_mesh.py index 6a485fd..731f1f0 100644 --- a/tests/test_mesh.py +++ b/tests/test_mesh.py @@ -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):