Skip to content

Commit 80d6dd4

Browse files
committed
Update statistics handling to use binary frames instead of JSON formatting for consistency with other companion commands. Added documentation of frame structure with code examples.
1 parent c9aa536 commit 80d6dd4

File tree

2 files changed

+257
-30
lines changed

2 files changed

+257
-30
lines changed

docs/stats_binary_frames.md

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
# Stats Binary Frame Structures
2+
3+
Binary frame structures for companion radio stats commands. All multi-byte integers use little-endian byte order.
4+
5+
## Command Codes
6+
7+
| Command | Code | Description |
8+
|---------|------|-------------|
9+
| `CMD_GET_STATS_CORE` | 56 | Get core device statistics |
10+
| `CMD_GET_STATS_RADIO` | 57 | Get radio statistics |
11+
| `CMD_GET_STATS_PACKETS` | 58 | Get packet statistics |
12+
13+
## Response Codes
14+
15+
| Response | Code | Description |
16+
|----------|------|-------------|
17+
| `RESP_CODE_STATS_CORE` | 24 | Core stats response |
18+
| `RESP_CODE_STATS_RADIO` | 25 | Radio stats response |
19+
| `RESP_CODE_STATS_PACKETS` | 26 | Packet stats response |
20+
21+
---
22+
23+
## RESP_CODE_STATS_CORE (24)
24+
25+
**Total Frame Size:** 10 bytes
26+
27+
| Offset | Size | Type | Field Name | Description | Range/Notes |
28+
|--------|------|------|------------|-------------|-------------|
29+
| 0 | 1 | uint8_t | response_code | Always `0x18` (24) | - |
30+
| 1 | 2 | uint16_t | battery_mv | Battery voltage in millivolts | 0 - 65,535 |
31+
| 3 | 4 | uint32_t | uptime_secs | Device uptime in seconds | 0 - 4,294,967,295 |
32+
| 7 | 2 | uint16_t | errors | Error flags bitmask | - |
33+
| 9 | 1 | uint8_t | queue_len | Outbound packet queue length | 0 - 255 |
34+
35+
### Example Structure (C/C++)
36+
37+
```c
38+
struct StatsCore {
39+
uint8_t response_code; // 0x18
40+
uint16_t battery_mv;
41+
uint32_t uptime_secs;
42+
uint16_t errors;
43+
uint8_t queue_len;
44+
} __attribute__((packed));
45+
```
46+
47+
---
48+
49+
## RESP_CODE_STATS_RADIO (25)
50+
51+
**Total Frame Size:** 13 bytes
52+
53+
| Offset | Size | Type | Field Name | Description | Range/Notes |
54+
|--------|------|------|------------|-------------|-------------|
55+
| 0 | 1 | uint8_t | response_code | Always `0x19` (25) | - |
56+
| 1 | 2 | int16_t | noise_floor | Radio noise floor in dBm | -140 to +10 |
57+
| 3 | 1 | int8_t | last_rssi | Last received signal strength in dBm | -128 to +127 |
58+
| 4 | 1 | int8_t | last_snr | SNR scaled by 4 | Divide by 4.0 for dB |
59+
| 5 | 4 | uint32_t | tx_air_secs | Cumulative transmit airtime in seconds | 0 - 4,294,967,295 |
60+
| 9 | 4 | uint32_t | rx_air_secs | Cumulative receive airtime in seconds | 0 - 4,294,967,295 |
61+
62+
### Example Structure (C/C++)
63+
64+
```c
65+
struct StatsRadio {
66+
uint8_t response_code; // 0x19
67+
int16_t noise_floor;
68+
int8_t last_rssi;
69+
int8_t last_snr; // Divide by 4.0 to get actual SNR in dB
70+
uint32_t tx_air_secs;
71+
uint32_t rx_air_secs;
72+
} __attribute__((packed));
73+
```
74+
75+
---
76+
77+
## RESP_CODE_STATS_PACKETS (26)
78+
79+
**Total Frame Size:** 25 bytes
80+
81+
| Offset | Size | Type | Field Name | Description | Range/Notes |
82+
|--------|------|------|------------|-------------|-------------|
83+
| 0 | 1 | uint8_t | response_code | Always `0x1A` (26) | - |
84+
| 1 | 4 | uint32_t | recv | Total packets received | 0 - 4,294,967,295 |
85+
| 5 | 4 | uint32_t | sent | Total packets sent | 0 - 4,294,967,295 |
86+
| 9 | 4 | uint32_t | flood_tx | Packets sent via flood routing | 0 - 4,294,967,295 |
87+
| 13 | 4 | uint32_t | direct_tx | Packets sent via direct routing | 0 - 4,294,967,295 |
88+
| 17 | 4 | uint32_t | flood_rx | Packets received via flood routing | 0 - 4,294,967,295 |
89+
| 21 | 4 | uint32_t | direct_rx | Packets received via direct routing | 0 - 4,294,967,295 |
90+
91+
### Notes
92+
93+
- Counters are cumulative from boot and may wrap.
94+
- `recv = flood_rx + direct_rx`
95+
- `sent = flood_tx + direct_tx`
96+
97+
### Example Structure (C/C++)
98+
99+
```c
100+
struct StatsPackets {
101+
uint8_t response_code; // 0x1A
102+
uint32_t recv;
103+
uint32_t sent;
104+
uint32_t flood_tx;
105+
uint32_t direct_tx;
106+
uint32_t flood_rx;
107+
uint32_t direct_rx;
108+
} __attribute__((packed));
109+
```
110+
111+
---
112+
113+
## Usage Example (Python)
114+
115+
```python
116+
import struct
117+
118+
def parse_stats_core(frame):
119+
"""Parse RESP_CODE_STATS_CORE frame (10 bytes)"""
120+
response_code, battery_mv, uptime_secs, errors, queue_len = \
121+
struct.unpack('<B H I H B', frame)
122+
return {
123+
'battery_mv': battery_mv,
124+
'uptime_secs': uptime_secs,
125+
'errors': errors,
126+
'queue_len': queue_len
127+
}
128+
129+
def parse_stats_radio(frame):
130+
"""Parse RESP_CODE_STATS_RADIO frame (13 bytes)"""
131+
response_code, noise_floor, last_rssi, last_snr, tx_air_secs, rx_air_secs = \
132+
struct.unpack('<B h b b I I', frame)
133+
return {
134+
'noise_floor': noise_floor,
135+
'last_rssi': last_rssi,
136+
'last_snr': last_snr / 4.0, # Unscale SNR
137+
'tx_air_secs': tx_air_secs,
138+
'rx_air_secs': rx_air_secs
139+
}
140+
141+
def parse_stats_packets(frame):
142+
"""Parse RESP_CODE_STATS_PACKETS frame (25 bytes)"""
143+
response_code, recv, sent, flood_tx, direct_tx, flood_rx, direct_rx = \
144+
struct.unpack('<B I I I I I I', frame)
145+
return {
146+
'recv': recv,
147+
'sent': sent,
148+
'flood_tx': flood_tx,
149+
'direct_tx': direct_tx,
150+
'flood_rx': flood_rx,
151+
'direct_rx': direct_rx
152+
}
153+
```
154+
155+
---
156+
157+
## Usage Example (JavaScript/TypeScript)
158+
159+
```typescript
160+
interface StatsCore {
161+
battery_mv: number;
162+
uptime_secs: number;
163+
errors: number;
164+
queue_len: number;
165+
}
166+
167+
interface StatsRadio {
168+
noise_floor: number;
169+
last_rssi: number;
170+
last_snr: number;
171+
tx_air_secs: number;
172+
rx_air_secs: number;
173+
}
174+
175+
interface StatsPackets {
176+
recv: number;
177+
sent: number;
178+
flood_tx: number;
179+
direct_tx: number;
180+
flood_rx: number;
181+
direct_rx: number;
182+
}
183+
184+
function parseStatsCore(buffer: ArrayBuffer): StatsCore {
185+
const view = new DataView(buffer);
186+
return {
187+
battery_mv: view.getUint16(1, true),
188+
uptime_secs: view.getUint32(3, true),
189+
errors: view.getUint16(7, true),
190+
queue_len: view.getUint8(9)
191+
};
192+
}
193+
194+
function parseStatsRadio(buffer: ArrayBuffer): StatsRadio {
195+
const view = new DataView(buffer);
196+
return {
197+
noise_floor: view.getInt16(1, true),
198+
last_rssi: view.getInt8(3),
199+
last_snr: view.getInt8(4) / 4.0, // Unscale SNR
200+
tx_air_secs: view.getUint32(5, true),
201+
rx_air_secs: view.getUint32(9, true)
202+
};
203+
}
204+
205+
function parseStatsPackets(buffer: ArrayBuffer): StatsPackets {
206+
const view = new DataView(buffer);
207+
return {
208+
recv: view.getUint32(1, true),
209+
sent: view.getUint32(5, true),
210+
flood_tx: view.getUint32(9, true),
211+
direct_tx: view.getUint32(13, true),
212+
flood_rx: view.getUint32(17, true),
213+
direct_rx: view.getUint32(21, true)
214+
};
215+
}
216+
```
217+
218+
---
219+
220+
## Field Size Considerations
221+
222+
- Packet counters (uint32_t): May wrap after extended high-traffic operation.
223+
- Time fields (uint32_t): Max ~136 years.
224+
- SNR (int8_t, scaled by 4): Range -32 to +31.75 dB, 0.25 dB precision.
225+

examples/companion_radio/MyMesh.cpp

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1536,44 +1536,46 @@ void MyMesh::handleCmdFrame(size_t len) {
15361536
writeErrFrame(ERR_CODE_NOT_FOUND);
15371537
}
15381538
} else if (cmd_frame[0] == CMD_GET_STATS_CORE) {
1539-
char json_reply[160];
1540-
formatStatsReply(json_reply);
15411539
int i = 0;
15421540
out_frame[i++] = RESP_CODE_STATS_CORE;
1543-
int json_len = strlen(json_reply);
1544-
if (i + json_len <= MAX_FRAME_SIZE) {
1545-
memcpy(&out_frame[i], json_reply, json_len);
1546-
i += json_len;
1547-
_serial->writeFrame(out_frame, i);
1548-
} else {
1549-
writeErrFrame(ERR_CODE_TABLE_FULL);
1550-
}
1541+
uint16_t battery_mv = board.getBattMilliVolts();
1542+
uint32_t uptime_secs = _ms->getMillis() / 1000;
1543+
uint8_t queue_len = (uint8_t)_mgr->getOutboundCount(0xFFFFFFFF);
1544+
memcpy(&out_frame[i], &battery_mv, 2); i += 2;
1545+
memcpy(&out_frame[i], &uptime_secs, 4); i += 4;
1546+
memcpy(&out_frame[i], &_err_flags, 2); i += 2;
1547+
out_frame[i++] = queue_len;
1548+
_serial->writeFrame(out_frame, i);
15511549
} else if (cmd_frame[0] == CMD_GET_STATS_RADIO) {
1552-
char json_reply[160];
1553-
formatRadioStatsReply(json_reply);
15541550
int i = 0;
15551551
out_frame[i++] = RESP_CODE_STATS_RADIO;
1556-
int json_len = strlen(json_reply);
1557-
if (i + json_len <= MAX_FRAME_SIZE) {
1558-
memcpy(&out_frame[i], json_reply, json_len);
1559-
i += json_len;
1560-
_serial->writeFrame(out_frame, i);
1561-
} else {
1562-
writeErrFrame(ERR_CODE_TABLE_FULL);
1563-
}
1552+
int16_t noise_floor = (int16_t)_radio->getNoiseFloor();
1553+
int8_t last_rssi = (int8_t)radio_driver.getLastRSSI();
1554+
int8_t last_snr = (int8_t)(radio_driver.getLastSNR() * 4); // scaled by 4 for 0.25 dB precision
1555+
uint32_t tx_air_secs = getTotalAirTime() / 1000;
1556+
uint32_t rx_air_secs = getReceiveAirTime() / 1000;
1557+
memcpy(&out_frame[i], &noise_floor, 2); i += 2;
1558+
out_frame[i++] = last_rssi;
1559+
out_frame[i++] = last_snr;
1560+
memcpy(&out_frame[i], &tx_air_secs, 4); i += 4;
1561+
memcpy(&out_frame[i], &rx_air_secs, 4); i += 4;
1562+
_serial->writeFrame(out_frame, i);
15641563
} else if (cmd_frame[0] == CMD_GET_STATS_PACKETS) {
1565-
char json_reply[160];
1566-
formatPacketStatsReply(json_reply);
15671564
int i = 0;
15681565
out_frame[i++] = RESP_CODE_STATS_PACKETS;
1569-
int json_len = strlen(json_reply);
1570-
if (i + json_len <= MAX_FRAME_SIZE) {
1571-
memcpy(&out_frame[i], json_reply, json_len);
1572-
i += json_len;
1573-
_serial->writeFrame(out_frame, i);
1574-
} else {
1575-
writeErrFrame(ERR_CODE_TABLE_FULL);
1576-
}
1566+
uint32_t recv = radio_driver.getPacketsRecv();
1567+
uint32_t sent = radio_driver.getPacketsSent();
1568+
uint32_t n_sent_flood = getNumSentFlood();
1569+
uint32_t n_sent_direct = getNumSentDirect();
1570+
uint32_t n_recv_flood = getNumRecvFlood();
1571+
uint32_t n_recv_direct = getNumRecvDirect();
1572+
memcpy(&out_frame[i], &recv, 4); i += 4;
1573+
memcpy(&out_frame[i], &sent, 4); i += 4;
1574+
memcpy(&out_frame[i], &n_sent_flood, 4); i += 4;
1575+
memcpy(&out_frame[i], &n_sent_direct, 4); i += 4;
1576+
memcpy(&out_frame[i], &n_recv_flood, 4); i += 4;
1577+
memcpy(&out_frame[i], &n_recv_direct, 4); i += 4;
1578+
_serial->writeFrame(out_frame, i);
15771579
} else if (cmd_frame[0] == CMD_FACTORY_RESET && memcmp(&cmd_frame[1], "reset", 5) == 0) {
15781580
bool success = _store->formatFileSystem();
15791581
if (success) {

0 commit comments

Comments
 (0)