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:
l5y
2025-10-13 22:35:06 +02:00
committed by GitHub
parent 63beb2ea6b
commit 7db76ec2fc
6 changed files with 410 additions and 7 deletions
+2
View File
@@ -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",
+9
View File
@@ -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",
+1
View File
@@ -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:
+52 -6
View File
@@ -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:
+167 -1
View File
@@ -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
View File
@@ -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):