Skip to content

Commit 2e178e8

Browse files
authored
Merge pull request #82 from jkingsman/add-repeater-error-count
Add repeater error count delivery in telemetry
2 parents ff58e1c + 5cf8150 commit 2e178e8

2 files changed

Lines changed: 153 additions & 1 deletion

File tree

src/meshcore/parsing.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,5 +105,11 @@ def parse_status(data, pubkey_prefix=None, offset=0):
105105
res["direct_dups"] = int.from_bytes(data[offset+44:offset+46], byteorder="little")
106106
res["flood_dups"] = int.from_bytes(data[offset+46:offset+48], byteorder="little")
107107
res["rx_airtime"] = int.from_bytes(data[offset+48:offset+52], byteorder="little")
108-
108+
109+
# n_recv_errors: uint32_t appended in firmware v1.12.0 (56-byte frame)
110+
if len(data) >= offset + 56:
111+
res["recv_errors"] = int.from_bytes(data[offset+52:offset+56], byteorder="little")
112+
else:
113+
res["recv_errors"] = None
114+
109115
return res

tests/unit/test_parse_status.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
"""Tests for meshcore.parsing.parse_status covering the RepeaterStats binary frame.
2+
3+
The firmware struct (MeshCore/examples/simple_repeater/MyMesh.h) is:
4+
5+
struct RepeaterStats {
6+
uint16_t batt_milli_volts; // offset 0 (2)
7+
uint16_t curr_tx_queue_len; // offset 2 (2)
8+
int16_t noise_floor; // offset 4 (2)
9+
int16_t last_rssi; // offset 6 (2)
10+
uint32_t n_packets_recv; // offset 8 (4)
11+
uint32_t n_packets_sent; // offset 12 (4)
12+
uint32_t total_air_time_secs; // offset 16 (4)
13+
uint32_t total_up_time_secs; // offset 20 (4)
14+
uint32_t n_sent_flood; // offset 24 (4)
15+
uint32_t n_sent_direct; // offset 28 (4)
16+
uint32_t n_recv_flood; // offset 32 (4)
17+
uint32_t n_recv_direct; // offset 36 (4)
18+
uint16_t err_events; // offset 40 (2)
19+
int16_t last_snr; // x4 // offset 42 (2)
20+
uint16_t n_direct_dups; // offset 44 (2)
21+
uint16_t n_flood_dups; // offset 46 (2)
22+
uint32_t total_rx_air_time_secs; // offset 48 (4)
23+
uint32_t n_recv_errors; // offset 52 (4) -- 56-byte frame
24+
};
25+
"""
26+
import struct
27+
from meshcore.parsing import parse_status
28+
29+
30+
def _build_frame(
31+
*,
32+
bat=4200,
33+
tx_queue_len=3,
34+
noise_floor=-118,
35+
last_rssi=-85,
36+
nb_recv=1000,
37+
nb_sent=500,
38+
airtime=3600,
39+
uptime=86400,
40+
sent_flood=100,
41+
sent_direct=400,
42+
recv_flood=300,
43+
recv_direct=700,
44+
err_events=0,
45+
last_snr_x4=30, # 7.5 dB * 4
46+
direct_dups=5,
47+
flood_dups=10,
48+
rx_airtime=7200,
49+
recv_errors=None,
50+
):
51+
"""Build a RepeaterStats binary frame (52 or 56 bytes)."""
52+
frame = struct.pack(
53+
"<HHhhIIIIIIIIHhHHI",
54+
bat,
55+
tx_queue_len,
56+
noise_floor,
57+
last_rssi,
58+
nb_recv,
59+
nb_sent,
60+
airtime,
61+
uptime,
62+
sent_flood,
63+
sent_direct,
64+
recv_flood,
65+
recv_direct,
66+
err_events,
67+
last_snr_x4,
68+
direct_dups,
69+
flood_dups,
70+
rx_airtime,
71+
)
72+
assert len(frame) == 52
73+
if recv_errors is not None:
74+
frame += struct.pack("<I", recv_errors)
75+
return frame
76+
77+
78+
class TestParseStatusWithPrefix:
79+
"""parse_status called with an explicit pubkey_prefix (binary-request path)."""
80+
81+
def test_full_56_byte_frame(self):
82+
data = _build_frame(recv_errors=42)
83+
res = parse_status(data, pubkey_prefix="aabb11223344")
84+
85+
assert res["pubkey_pre"] == "aabb11223344"
86+
assert res["bat"] == 4200
87+
assert res["tx_queue_len"] == 3
88+
assert res["noise_floor"] == -118
89+
assert res["last_rssi"] == -85
90+
assert res["nb_recv"] == 1000
91+
assert res["nb_sent"] == 500
92+
assert res["airtime"] == 3600
93+
assert res["uptime"] == 86400
94+
assert res["sent_flood"] == 100
95+
assert res["sent_direct"] == 400
96+
assert res["recv_flood"] == 300
97+
assert res["recv_direct"] == 700
98+
assert res["full_evts"] == 0
99+
assert res["last_snr"] == 7.5
100+
assert res["direct_dups"] == 5
101+
assert res["flood_dups"] == 10
102+
assert res["rx_airtime"] == 7200
103+
assert res["recv_errors"] == 42
104+
105+
def test_legacy_52_byte_frame(self):
106+
data = _build_frame() # no recv_errors → 52 bytes
107+
res = parse_status(data, pubkey_prefix="aabb11223344")
108+
109+
assert res["bat"] == 4200
110+
assert res["recv_errors"] is None
111+
112+
def test_nonzero_recv_errors(self):
113+
data = _build_frame(recv_errors=123456)
114+
res = parse_status(data, pubkey_prefix="cc1122334455")
115+
assert res["recv_errors"] == 123456
116+
117+
118+
class TestParseStatusEmbedded:
119+
"""parse_status called without pubkey_prefix (STATUS_RESPONSE push path).
120+
121+
In this path the first 2 bytes are a header, bytes 2-8 are the pubkey
122+
prefix, and fields start at offset 8.
123+
"""
124+
125+
def test_56_byte_payload_with_header(self):
126+
header = b"\x00\x00" # 2-byte header
127+
pubkey = bytes.fromhex("aabb11223344") # 6-byte prefix
128+
body = _build_frame(recv_errors=99)
129+
data = header + pubkey + body
130+
131+
res = parse_status(data)
132+
133+
assert res["pubkey_pre"] == "aabb11223344"
134+
assert res["bat"] == 4200
135+
assert res["recv_errors"] == 99
136+
137+
def test_52_byte_payload_with_header(self):
138+
header = b"\x00\x00"
139+
pubkey = bytes.fromhex("aabb11223344")
140+
body = _build_frame() # 52 bytes, no recv_errors
141+
data = header + pubkey + body
142+
143+
res = parse_status(data)
144+
145+
assert res["pubkey_pre"] == "aabb11223344"
146+
assert res["recv_errors"] is None

0 commit comments

Comments
 (0)