mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-21 18:52:36 +10:00
Add MCP server for interacting with meshtastic devices and testing framework / TUI
This commit is contained in:
@@ -1,42 +1,77 @@
|
||||
"""Telemetry: device metrics (battery, voltage, channel util) arrive at the peer.
|
||||
"""Telemetry: device-metrics packets arrive at the peer.
|
||||
|
||||
After ~2× the telemetry interval, B's entry in A's node DB should carry a
|
||||
populated `deviceMetrics` block. This is the happy-path "my fleet is
|
||||
reporting health data" operator test.
|
||||
Two-path verification:
|
||||
1. Listen on TX's pubsub for inbound telemetry packets originating from
|
||||
RX's node_num — if one arrives within the window, telemetry works.
|
||||
2. Fall back to checking TX's node DB for a populated `deviceMetrics`
|
||||
block on the RX record (which the firmware writes on receipt).
|
||||
|
||||
Both paths prove the same invariant; path 1 gives faster failure signal,
|
||||
path 2 handles the case where the packet arrived before we subscribed.
|
||||
|
||||
Warmup note: when this test runs after `test_baked_prefs_survive_factory_reset`,
|
||||
both devices have empty node-DBs. We kick a broadcast text from RX through
|
||||
its own ReceiveCollector so TX learns RX exists and starts accepting its
|
||||
telemetry; without it, a fresh-boot pair can take 10+ min to swap NODEINFO
|
||||
before the first telemetry arrives.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from meshtastic_mcp.connection import connect
|
||||
|
||||
from ..mesh._receive import ReceiveCollector
|
||||
|
||||
@pytest.mark.timeout(360)
|
||||
def test_device_telemetry_broadcast(baked_mesh: dict[str, Any], wait_until) -> None:
|
||||
"""Wait up to 5 minutes for B's device telemetry to land in A's DB.
|
||||
|
||||
Firmware default telemetry interval is 900s; on a fresh mesh the first
|
||||
device-metrics broadcast happens within ~30-120s of boot because devices
|
||||
broadcast once on startup. We only require that some telemetry is present,
|
||||
not that we see multiple cycles.
|
||||
@pytest.mark.timeout(600)
|
||||
def test_device_telemetry_broadcast(mesh_pair: dict[str, Any]) -> None:
|
||||
"""Runs for every directed pair. Waits up to ~8 minutes for TX to see
|
||||
RX's device telemetry — either as a live inbound pubsub packet or as
|
||||
a populated deviceMetrics on RX's node-DB record.
|
||||
|
||||
Firmware default telemetry interval is 900s; after a fresh boot the
|
||||
first device-metrics broadcast happens within ~30-120s. We warm up
|
||||
the mesh first with a cross-broadcast so NODEINFO is exchanged, then
|
||||
wait up to 7 min for a telemetry packet.
|
||||
"""
|
||||
if "nrf52" not in baked_mesh or "esp32s3" not in baked_mesh:
|
||||
pytest.skip("both roles required")
|
||||
tx_port = mesh_pair["tx"]["port"]
|
||||
rx_port = mesh_pair["rx"]["port"]
|
||||
rx_node_num = mesh_pair["rx"]["my_node_num"]
|
||||
|
||||
a_port = baked_mesh["nrf52"]["port"]
|
||||
b_node_num = baked_mesh["esp32s3"]["my_node_num"]
|
||||
# Open both sides' pubsub listeners up front so we capture anything that
|
||||
# arrives during the warmup exchange.
|
||||
with ReceiveCollector(tx_port, topic="meshtastic.receive.telemetry") as tx_rx:
|
||||
with ReceiveCollector(rx_port, topic="meshtastic.receive.text") as rx_tx:
|
||||
# Warmup: send a broadcast from RX through its own collector so
|
||||
# TX learns about RX (NODEINFO rides along with TEXT_MESSAGE_APP).
|
||||
# Skipping this turns a 5-min wait into a 15-min wait on a fresh
|
||||
# factory-reset pair.
|
||||
rx_tx.send_text(f"warmup-{int(time.time())}")
|
||||
time.sleep(5.0)
|
||||
|
||||
def b_has_telemetry() -> bool:
|
||||
with connect(port=a_port) as iface:
|
||||
rec = (iface.nodesByNum or {}).get(b_node_num, {})
|
||||
metrics = rec.get("deviceMetrics") or {}
|
||||
# Any one of these being non-None is sufficient evidence that
|
||||
# telemetry arrived.
|
||||
return any(
|
||||
metrics.get(k) is not None
|
||||
for k in ("batteryLevel", "voltage", "channelUtilization", "airUtilTx")
|
||||
# Path 1: wait for a telemetry packet from RX on TX's pubsub.
|
||||
got = tx_rx.wait_for(
|
||||
lambda pkt: pkt.get("from") == rx_node_num,
|
||||
timeout=420, # 7 min — well above the 30-120s typical first broadcast
|
||||
)
|
||||
if got is not None:
|
||||
return # Path 1 confirmed delivery.
|
||||
|
||||
wait_until(b_has_telemetry, timeout=300, backoff_start=5.0, backoff_max=15.0)
|
||||
# Path 2: re-query TX's node DB for a populated deviceMetrics on RX.
|
||||
# Device may have reported telemetry before we subscribed, or the
|
||||
# pubsub delivery might race with our window — re-check nodesByNum.
|
||||
with connect(port=tx_port) as iface:
|
||||
rec = (iface.nodesByNum or {}).get(rx_node_num, {})
|
||||
metrics = rec.get("deviceMetrics") or {}
|
||||
has_any = any(
|
||||
metrics.get(k) is not None
|
||||
for k in ("batteryLevel", "voltage", "channelUtilization", "airUtilTx")
|
||||
)
|
||||
assert has_any, (
|
||||
f"no telemetry from node 0x{rx_node_num:08x} within 7 min; "
|
||||
f"deviceMetrics={metrics!r}"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user