mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-06-11 16:54:44 +02:00
0251304407
Every MQTT-published packet has shipped with duration="0" since the PacketRecord factory was introduced. The repeater already computes LoRa time-on-air via AirtimeManager.calculate_airtime() (the canonical Semtech reference formula) for duty-cycle gating and TX delay, but the result was thrown away after each packet - never stored on the packet_record dict that flows to MQTT/SQLite/Glass/websocket. What changes - engine.py: RepeaterHandler._build_packet_record() now computes airtime_ms once per packet (Semtech formula via AirtimeManager) and stores it as packet_record['airtime_ms']. Single source of truth for every downstream consumer. - storage_utils.py: PacketRecord.from_packet_record() reads the new airtime_ms field and serializes it as a rounded integer in the 'duration' field of the published JSON. Falls back to 0 if the field is missing (backward compatibility for any older code path). - storage_collector.py: _publish_packet_to_mqtt() simplified - no recomputation, no helper. The publish path is now a passthrough. Why MQTT consumers (firmware-compatible analyzers, dashboards, the upstream meshcoretomqtt project) expect the same time-on-air value the firmware emits. Hard-coded "0" makes airtime/utilization charts derived from the mqtt stream useless and silently diverges from firmware behavior. Plumbing the value through packet_record (instead of recomputing in the publish path) means any future consumer - SQLite schema, web UI charts, Glass telemetry - reads the same number without separate calculations. Tests tests/test_packet_duration.py - 5 tests covering: - backward compat (legacy packet_record without airtime_ms => '0') - airtime_ms field flows through to duration as rounded integer string - explicit zero stays '0' - AirtimeManager output matches an independently-implemented Semtech reference for typical MeshCore EU settings (SF8/62.5kHz/CR4-8) - low-data-rate optimization branch (SF12/125kHz triggers DE=1) Co-Authored-By: Oz <oz-agent@warp.dev>
91 lines
2.8 KiB
Python
91 lines
2.8 KiB
Python
"""Storage utility classes and functions for data acquisition."""
|
|
|
|
from dataclasses import asdict, dataclass
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
|
|
@dataclass
|
|
class PacketRecord:
|
|
"""
|
|
Data class for packet record format.
|
|
Converts internal packet_record format to standardized publish format.
|
|
Reusable across MQTT and other handlers.
|
|
"""
|
|
|
|
origin: str
|
|
origin_id: str
|
|
timestamp: str
|
|
type: str
|
|
direction: str
|
|
time: str
|
|
date: str
|
|
len: str
|
|
packet_type: str
|
|
route: str
|
|
payload_len: str
|
|
raw: str
|
|
SNR: str
|
|
RSSI: str
|
|
score: str
|
|
duration: str
|
|
hash: str
|
|
|
|
@classmethod
|
|
def from_packet_record(
|
|
cls, packet_record: dict, origin: str, origin_id: str
|
|
) -> Optional["PacketRecord"]:
|
|
"""
|
|
Create PacketRecord from internal packet_record format.
|
|
|
|
The ``duration`` field is sourced from ``packet_record['airtime_ms']``,
|
|
which RepeaterHandler._build_packet_record populates using the
|
|
Semtech-reference time-on-air formula on the active radio settings.
|
|
Records produced by older code paths that pre-date that field fall
|
|
back to 0 to preserve legacy behavior.
|
|
|
|
Args:
|
|
packet_record: Internal packet record dictionary
|
|
origin: Node name
|
|
origin_id: Public key of the node
|
|
|
|
Returns:
|
|
PacketRecord instance or None if raw_packet is missing
|
|
"""
|
|
if "raw_packet" not in packet_record or not packet_record["raw_packet"]:
|
|
return None
|
|
|
|
# Extract timestamp and format date/time
|
|
timestamp = packet_record.get("timestamp", 0)
|
|
dt = datetime.fromtimestamp(timestamp)
|
|
|
|
# Format route type (1=Flood->F, 2=Direct->D, etc)
|
|
route_map = {1: "F", 2: "D"}
|
|
route = route_map.get(packet_record.get("route", 0), str(packet_record.get("route", 0)))
|
|
|
|
airtime_ms = float(packet_record.get("airtime_ms", 0.0) or 0.0)
|
|
|
|
return cls(
|
|
origin=origin,
|
|
origin_id=origin_id,
|
|
timestamp=dt.isoformat(),
|
|
type="PACKET",
|
|
direction="rx",
|
|
time=dt.strftime("%H:%M:%S"),
|
|
date=dt.strftime("%-d/%-m/%Y"),
|
|
len=str(len(packet_record["raw_packet"]) // 2),
|
|
packet_type=str(packet_record.get("type", 0)),
|
|
route=route,
|
|
payload_len=str(packet_record.get("payload_length", 0)),
|
|
raw=packet_record["raw_packet"],
|
|
SNR=str(packet_record.get("snr", 0)),
|
|
RSSI=str(packet_record.get("rssi", 0)),
|
|
score=str(int(packet_record.get("score", 0) * 1000)),
|
|
duration=str(int(round(airtime_ms))),
|
|
hash=packet_record.get("packet_hash", ""),
|
|
)
|
|
|
|
def to_dict(self) -> dict:
|
|
"""Convert to dictionary for JSON serialization"""
|
|
return asdict(self)
|