From df4dab8509081a08010229af207ac2eae288bab8 Mon Sep 17 00:00:00 2001 From: agessaman Date: Fri, 7 Nov 2025 22:16:48 -0800 Subject: [PATCH 1/5] Add statistics commands and response handling in MyMesh - Introduced new commands for retrieving statistics: CMD_GET_STATS_CORE, CMD_GET_STATS_RADIO, and CMD_GET_STATS_PACKETS. - Implemented corresponding response handling methods to format and send statistics data. - Updated MyMesh constructor to initialize new member variables for managing statistics. - Included StatsFormatHelper for formatting statistics replies. --- examples/companion_radio/MyMesh.cpp | 62 ++++++++++++++++++++++++++++- examples/companion_radio/MyMesh.h | 8 ++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 598d535f..fa9edc8c 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -52,6 +52,9 @@ #define CMD_SEND_PATH_DISCOVERY_REQ 52 #define CMD_SET_FLOOD_SCOPE 54 // v8+ #define CMD_SEND_CONTROL_DATA 55 // v8+ +#define CMD_GET_STATS_CORE 56 +#define CMD_GET_STATS_RADIO 57 +#define CMD_GET_STATS_PACKETS 58 #define RESP_CODE_OK 0 #define RESP_CODE_ERR 1 @@ -77,6 +80,9 @@ #define RESP_CODE_CUSTOM_VARS 21 #define RESP_CODE_ADVERT_PATH 22 #define RESP_CODE_TUNING_PARAMS 23 +#define RESP_CODE_STATS_CORE 24 +#define RESP_CODE_STATS_RADIO 25 +#define RESP_CODE_STATS_PACKETS 26 #define SEND_TIMEOUT_BASE_MILLIS 500 #define FLOOD_SEND_TIMEOUT_FACTOR 16.0f @@ -704,7 +710,7 @@ uint32_t MyMesh::calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t void MyMesh::onSendTimeout() {} MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui) - : BaseChatMesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(16), tables), + : BaseChatMesh(radio, *(_ms_clock = new ArduinoMillis()), rng, rtc, *(_pkt_mgr = new StaticPoolPacketManager(16)), tables), _serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui) { _iter_started = false; _cli_rescue = false; @@ -1529,6 +1535,45 @@ void MyMesh::handleCmdFrame(size_t len) { } else { writeErrFrame(ERR_CODE_NOT_FOUND); } + } else if (cmd_frame[0] == CMD_GET_STATS_CORE) { + char json_reply[160]; + formatStatsReply(json_reply); + int i = 0; + out_frame[i++] = RESP_CODE_STATS_CORE; + int json_len = strlen(json_reply); + if (i + json_len <= MAX_FRAME_SIZE) { + memcpy(&out_frame[i], json_reply, json_len); + i += json_len; + _serial->writeFrame(out_frame, i); + } else { + writeErrFrame(ERR_CODE_TABLE_FULL); + } + } else if (cmd_frame[0] == CMD_GET_STATS_RADIO) { + char json_reply[160]; + formatRadioStatsReply(json_reply); + int i = 0; + out_frame[i++] = RESP_CODE_STATS_RADIO; + int json_len = strlen(json_reply); + if (i + json_len <= MAX_FRAME_SIZE) { + memcpy(&out_frame[i], json_reply, json_len); + i += json_len; + _serial->writeFrame(out_frame, i); + } else { + writeErrFrame(ERR_CODE_TABLE_FULL); + } + } else if (cmd_frame[0] == CMD_GET_STATS_PACKETS) { + char json_reply[160]; + formatPacketStatsReply(json_reply); + int i = 0; + out_frame[i++] = RESP_CODE_STATS_PACKETS; + int json_len = strlen(json_reply); + if (i + json_len <= MAX_FRAME_SIZE) { + memcpy(&out_frame[i], json_reply, json_len); + i += json_len; + _serial->writeFrame(out_frame, i); + } else { + writeErrFrame(ERR_CODE_TABLE_FULL); + } } else if (cmd_frame[0] == CMD_FACTORY_RESET && memcmp(&cmd_frame[1], "reset", 5) == 0) { bool success = _store->formatFileSystem(); if (success) { @@ -1565,6 +1610,21 @@ void MyMesh::enterCLIRescue() { Serial.println("========= CLI Rescue ========="); } +void MyMesh::formatStatsReply(char *reply) { + // Use StatsFormatHelper + // Note: err_flags is private in Dispatcher, so we use 0 + StatsFormatHelper::formatCoreStats(reply, board, *_ms_clock, 0, _pkt_mgr); +} + +void MyMesh::formatRadioStatsReply(char *reply) { + StatsFormatHelper::formatRadioStats(reply, _radio, radio_driver, getTotalAirTime(), getReceiveAirTime()); +} + +void MyMesh::formatPacketStatsReply(char *reply) { + StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(), + getNumRecvFlood(), getNumRecvDirect()); +} + void MyMesh::checkCLIRescueCmd() { int len = strlen(cli_command); while (Serial.available() && len < sizeof(cli_command)-1) { diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index f2b56e5e..ef451d5d 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -69,6 +69,7 @@ #include #include +#include /* -------------------------------------------------------------------------------------- */ @@ -170,6 +171,11 @@ private: void checkCLIRescueCmd(); void checkSerialInterface(); + // Stats methods + void formatStatsReply(char *reply); + void formatRadioStatsReply(char *reply); + void formatPacketStatsReply(char *reply); + // helpers, short-cuts void savePrefs() { _store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon); } void saveChannels() { _store->saveChannels(this); } @@ -178,6 +184,8 @@ private: private: DataStore* _store; NodePrefs _prefs; + mesh::PacketManager* _pkt_mgr; // stored for stats access + mesh::MillisecondClock* _ms_clock; // stored for stats access uint32_t pending_login; uint32_t pending_status; uint32_t pending_telemetry, pending_discovery; // pending _TELEMETRY_REQ From c9aa536ca632db7f28029ba1a4a66bc4bdccda92 Mon Sep 17 00:00:00 2001 From: agessaman Date: Sat, 8 Nov 2025 20:44:42 -0800 Subject: [PATCH 2/5] Reverted MyMesh constructor for simplicity. Updated formatStatsReply method to use new member variables for statistics handling. Removed excess variable creation --- examples/companion_radio/MyMesh.cpp | 5 ++--- examples/companion_radio/MyMesh.h | 2 -- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index fa9edc8c..6fe165c5 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -710,7 +710,7 @@ uint32_t MyMesh::calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t void MyMesh::onSendTimeout() {} MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui) - : BaseChatMesh(radio, *(_ms_clock = new ArduinoMillis()), rng, rtc, *(_pkt_mgr = new StaticPoolPacketManager(16)), tables), + : BaseChatMesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(16), tables), _serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui) { _iter_started = false; _cli_rescue = false; @@ -1612,8 +1612,7 @@ void MyMesh::enterCLIRescue() { void MyMesh::formatStatsReply(char *reply) { // Use StatsFormatHelper - // Note: err_flags is private in Dispatcher, so we use 0 - StatsFormatHelper::formatCoreStats(reply, board, *_ms_clock, 0, _pkt_mgr); + StatsFormatHelper::formatCoreStats(reply, board, *_ms, _err_flags, _mgr); } void MyMesh::formatRadioStatsReply(char *reply) { diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index ef451d5d..61a25e14 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -184,8 +184,6 @@ private: private: DataStore* _store; NodePrefs _prefs; - mesh::PacketManager* _pkt_mgr; // stored for stats access - mesh::MillisecondClock* _ms_clock; // stored for stats access uint32_t pending_login; uint32_t pending_status; uint32_t pending_telemetry, pending_discovery; // pending _TELEMETRY_REQ From 80d6dd4367c2882a5b576ed1394bb4c2151b290e Mon Sep 17 00:00:00 2001 From: agessaman Date: Sun, 9 Nov 2025 11:28:11 -0800 Subject: [PATCH 3/5] 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. --- docs/stats_binary_frames.md | 225 ++++++++++++++++++++++++++++ examples/companion_radio/MyMesh.cpp | 62 ++++---- 2 files changed, 257 insertions(+), 30 deletions(-) create mode 100644 docs/stats_binary_frames.md diff --git a/docs/stats_binary_frames.md b/docs/stats_binary_frames.md new file mode 100644 index 00000000..d4327ea6 --- /dev/null +++ b/docs/stats_binary_frames.md @@ -0,0 +1,225 @@ +# Stats Binary Frame Structures + +Binary frame structures for companion radio stats commands. All multi-byte integers use little-endian byte order. + +## Command Codes + +| Command | Code | Description | +|---------|------|-------------| +| `CMD_GET_STATS_CORE` | 56 | Get core device statistics | +| `CMD_GET_STATS_RADIO` | 57 | Get radio statistics | +| `CMD_GET_STATS_PACKETS` | 58 | Get packet statistics | + +## Response Codes + +| Response | Code | Description | +|----------|------|-------------| +| `RESP_CODE_STATS_CORE` | 24 | Core stats response | +| `RESP_CODE_STATS_RADIO` | 25 | Radio stats response | +| `RESP_CODE_STATS_PACKETS` | 26 | Packet stats response | + +--- + +## RESP_CODE_STATS_CORE (24) + +**Total Frame Size:** 10 bytes + +| Offset | Size | Type | Field Name | Description | Range/Notes | +|--------|------|------|------------|-------------|-------------| +| 0 | 1 | uint8_t | response_code | Always `0x18` (24) | - | +| 1 | 2 | uint16_t | battery_mv | Battery voltage in millivolts | 0 - 65,535 | +| 3 | 4 | uint32_t | uptime_secs | Device uptime in seconds | 0 - 4,294,967,295 | +| 7 | 2 | uint16_t | errors | Error flags bitmask | - | +| 9 | 1 | uint8_t | queue_len | Outbound packet queue length | 0 - 255 | + +### Example Structure (C/C++) + +```c +struct StatsCore { + uint8_t response_code; // 0x18 + uint16_t battery_mv; + uint32_t uptime_secs; + uint16_t errors; + uint8_t queue_len; +} __attribute__((packed)); +``` + +--- + +## RESP_CODE_STATS_RADIO (25) + +**Total Frame Size:** 13 bytes + +| Offset | Size | Type | Field Name | Description | Range/Notes | +|--------|------|------|------------|-------------|-------------| +| 0 | 1 | uint8_t | response_code | Always `0x19` (25) | - | +| 1 | 2 | int16_t | noise_floor | Radio noise floor in dBm | -140 to +10 | +| 3 | 1 | int8_t | last_rssi | Last received signal strength in dBm | -128 to +127 | +| 4 | 1 | int8_t | last_snr | SNR scaled by 4 | Divide by 4.0 for dB | +| 5 | 4 | uint32_t | tx_air_secs | Cumulative transmit airtime in seconds | 0 - 4,294,967,295 | +| 9 | 4 | uint32_t | rx_air_secs | Cumulative receive airtime in seconds | 0 - 4,294,967,295 | + +### Example Structure (C/C++) + +```c +struct StatsRadio { + uint8_t response_code; // 0x19 + int16_t noise_floor; + int8_t last_rssi; + int8_t last_snr; // Divide by 4.0 to get actual SNR in dB + uint32_t tx_air_secs; + uint32_t rx_air_secs; +} __attribute__((packed)); +``` + +--- + +## RESP_CODE_STATS_PACKETS (26) + +**Total Frame Size:** 25 bytes + +| Offset | Size | Type | Field Name | Description | Range/Notes | +|--------|------|------|------------|-------------|-------------| +| 0 | 1 | uint8_t | response_code | Always `0x1A` (26) | - | +| 1 | 4 | uint32_t | recv | Total packets received | 0 - 4,294,967,295 | +| 5 | 4 | uint32_t | sent | Total packets sent | 0 - 4,294,967,295 | +| 9 | 4 | uint32_t | flood_tx | Packets sent via flood routing | 0 - 4,294,967,295 | +| 13 | 4 | uint32_t | direct_tx | Packets sent via direct routing | 0 - 4,294,967,295 | +| 17 | 4 | uint32_t | flood_rx | Packets received via flood routing | 0 - 4,294,967,295 | +| 21 | 4 | uint32_t | direct_rx | Packets received via direct routing | 0 - 4,294,967,295 | + +### Notes + +- Counters are cumulative from boot and may wrap. +- `recv = flood_rx + direct_rx` +- `sent = flood_tx + direct_tx` + +### Example Structure (C/C++) + +```c +struct StatsPackets { + uint8_t response_code; // 0x1A + uint32_t recv; + uint32_t sent; + uint32_t flood_tx; + uint32_t direct_tx; + uint32_t flood_rx; + uint32_t direct_rx; +} __attribute__((packed)); +``` + +--- + +## Usage Example (Python) + +```python +import struct + +def parse_stats_core(frame): + """Parse RESP_CODE_STATS_CORE frame (10 bytes)""" + response_code, battery_mv, uptime_secs, errors, queue_len = \ + struct.unpack('writeFrame(out_frame, i); - } else { - writeErrFrame(ERR_CODE_TABLE_FULL); - } + uint16_t battery_mv = board.getBattMilliVolts(); + uint32_t uptime_secs = _ms->getMillis() / 1000; + uint8_t queue_len = (uint8_t)_mgr->getOutboundCount(0xFFFFFFFF); + memcpy(&out_frame[i], &battery_mv, 2); i += 2; + memcpy(&out_frame[i], &uptime_secs, 4); i += 4; + memcpy(&out_frame[i], &_err_flags, 2); i += 2; + out_frame[i++] = queue_len; + _serial->writeFrame(out_frame, i); } else if (cmd_frame[0] == CMD_GET_STATS_RADIO) { - char json_reply[160]; - formatRadioStatsReply(json_reply); int i = 0; out_frame[i++] = RESP_CODE_STATS_RADIO; - int json_len = strlen(json_reply); - if (i + json_len <= MAX_FRAME_SIZE) { - memcpy(&out_frame[i], json_reply, json_len); - i += json_len; - _serial->writeFrame(out_frame, i); - } else { - writeErrFrame(ERR_CODE_TABLE_FULL); - } + int16_t noise_floor = (int16_t)_radio->getNoiseFloor(); + int8_t last_rssi = (int8_t)radio_driver.getLastRSSI(); + int8_t last_snr = (int8_t)(radio_driver.getLastSNR() * 4); // scaled by 4 for 0.25 dB precision + uint32_t tx_air_secs = getTotalAirTime() / 1000; + uint32_t rx_air_secs = getReceiveAirTime() / 1000; + memcpy(&out_frame[i], &noise_floor, 2); i += 2; + out_frame[i++] = last_rssi; + out_frame[i++] = last_snr; + memcpy(&out_frame[i], &tx_air_secs, 4); i += 4; + memcpy(&out_frame[i], &rx_air_secs, 4); i += 4; + _serial->writeFrame(out_frame, i); } else if (cmd_frame[0] == CMD_GET_STATS_PACKETS) { - char json_reply[160]; - formatPacketStatsReply(json_reply); int i = 0; out_frame[i++] = RESP_CODE_STATS_PACKETS; - int json_len = strlen(json_reply); - if (i + json_len <= MAX_FRAME_SIZE) { - memcpy(&out_frame[i], json_reply, json_len); - i += json_len; - _serial->writeFrame(out_frame, i); - } else { - writeErrFrame(ERR_CODE_TABLE_FULL); - } + uint32_t recv = radio_driver.getPacketsRecv(); + uint32_t sent = radio_driver.getPacketsSent(); + uint32_t n_sent_flood = getNumSentFlood(); + uint32_t n_sent_direct = getNumSentDirect(); + uint32_t n_recv_flood = getNumRecvFlood(); + uint32_t n_recv_direct = getNumRecvDirect(); + memcpy(&out_frame[i], &recv, 4); i += 4; + memcpy(&out_frame[i], &sent, 4); i += 4; + memcpy(&out_frame[i], &n_sent_flood, 4); i += 4; + memcpy(&out_frame[i], &n_sent_direct, 4); i += 4; + memcpy(&out_frame[i], &n_recv_flood, 4); i += 4; + memcpy(&out_frame[i], &n_recv_direct, 4); i += 4; + _serial->writeFrame(out_frame, i); } else if (cmd_frame[0] == CMD_FACTORY_RESET && memcmp(&cmd_frame[1], "reset", 5) == 0) { bool success = _store->formatFileSystem(); if (success) { From 39f83efbfe9c34129996f35eec6de916b890762c Mon Sep 17 00:00:00 2001 From: agessaman Date: Sun, 9 Nov 2025 11:39:47 -0800 Subject: [PATCH 4/5] Remove unused statistics formatting methods and associated header includes from MyMesh class. Whoops. --- examples/companion_radio/MyMesh.cpp | 14 -------------- examples/companion_radio/MyMesh.h | 6 ------ 2 files changed, 20 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 6fc9a433..140f58c6 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1612,20 +1612,6 @@ void MyMesh::enterCLIRescue() { Serial.println("========= CLI Rescue ========="); } -void MyMesh::formatStatsReply(char *reply) { - // Use StatsFormatHelper - StatsFormatHelper::formatCoreStats(reply, board, *_ms, _err_flags, _mgr); -} - -void MyMesh::formatRadioStatsReply(char *reply) { - StatsFormatHelper::formatRadioStats(reply, _radio, radio_driver, getTotalAirTime(), getReceiveAirTime()); -} - -void MyMesh::formatPacketStatsReply(char *reply) { - StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(), - getNumRecvFlood(), getNumRecvDirect()); -} - void MyMesh::checkCLIRescueCmd() { int len = strlen(cli_command); while (Serial.available() && len < sizeof(cli_command)-1) { diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 61a25e14..f2b56e5e 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -69,7 +69,6 @@ #include #include -#include /* -------------------------------------------------------------------------------------- */ @@ -171,11 +170,6 @@ private: void checkCLIRescueCmd(); void checkSerialInterface(); - // Stats methods - void formatStatsReply(char *reply); - void formatRadioStatsReply(char *reply); - void formatPacketStatsReply(char *reply); - // helpers, short-cuts void savePrefs() { _store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon); } void saveChannels() { _store->saveChannels(this); } From a3c9a07377d1895f78c776049ebd2bd99b90f4ae Mon Sep 17 00:00:00 2001 From: agessaman Date: Mon, 17 Nov 2025 09:57:36 -0800 Subject: [PATCH 5/5] Modify CMD_GET_STATS with sub-types for core, radio, and packet statistics. Consolidated to a single RESP_CODE_STATS with a second byte to identify response structure. Updated documentation and examples to reflect the new command structure and response parsing. --- docs/stats_binary_frames.md | 201 ++++++++++++++++++++-------- examples/companion_radio/MyMesh.cpp | 103 +++++++------- 2 files changed, 200 insertions(+), 104 deletions(-) diff --git a/docs/stats_binary_frames.md b/docs/stats_binary_frames.md index d4327ea6..1b409912 100644 --- a/docs/stats_binary_frames.md +++ b/docs/stats_binary_frames.md @@ -6,37 +6,53 @@ Binary frame structures for companion radio stats commands. All multi-byte integ | Command | Code | Description | |---------|------|-------------| -| `CMD_GET_STATS_CORE` | 56 | Get core device statistics | -| `CMD_GET_STATS_RADIO` | 57 | Get radio statistics | -| `CMD_GET_STATS_PACKETS` | 58 | Get packet statistics | +| `CMD_GET_STATS` | 56 | Get statistics (2-byte command: code + sub-type) | + +### Stats Sub-Types + +The `CMD_GET_STATS` command uses a 2-byte frame structure: +- **Byte 0:** `CMD_GET_STATS` (56) +- **Byte 1:** Stats sub-type: + - `STATS_TYPE_CORE` (0) - Get core device statistics + - `STATS_TYPE_RADIO` (1) - Get radio statistics + - `STATS_TYPE_PACKETS` (2) - Get packet statistics ## Response Codes | Response | Code | Description | |----------|------|-------------| -| `RESP_CODE_STATS_CORE` | 24 | Core stats response | -| `RESP_CODE_STATS_RADIO` | 25 | Radio stats response | -| `RESP_CODE_STATS_PACKETS` | 26 | Packet stats response | +| `RESP_CODE_STATS` | 24 | Statistics response (2-byte response: code + sub-type) | + +### Stats Response Sub-Types + +The `RESP_CODE_STATS` response uses a 2-byte header structure: +- **Byte 0:** `RESP_CODE_STATS` (24) +- **Byte 1:** Stats sub-type (matches command sub-type): + - `STATS_TYPE_CORE` (0) - Core device statistics response + - `STATS_TYPE_RADIO` (1) - Radio statistics response + - `STATS_TYPE_PACKETS` (2) - Packet statistics response --- -## RESP_CODE_STATS_CORE (24) +## RESP_CODE_STATS + STATS_TYPE_CORE (24, 0) -**Total Frame Size:** 10 bytes +**Total Frame Size:** 11 bytes | Offset | Size | Type | Field Name | Description | Range/Notes | |--------|------|------|------------|-------------|-------------| | 0 | 1 | uint8_t | response_code | Always `0x18` (24) | - | -| 1 | 2 | uint16_t | battery_mv | Battery voltage in millivolts | 0 - 65,535 | -| 3 | 4 | uint32_t | uptime_secs | Device uptime in seconds | 0 - 4,294,967,295 | -| 7 | 2 | uint16_t | errors | Error flags bitmask | - | -| 9 | 1 | uint8_t | queue_len | Outbound packet queue length | 0 - 255 | +| 1 | 1 | uint8_t | stats_type | Always `0x00` (STATS_TYPE_CORE) | - | +| 2 | 2 | uint16_t | battery_mv | Battery voltage in millivolts | 0 - 65,535 | +| 4 | 4 | uint32_t | uptime_secs | Device uptime in seconds | 0 - 4,294,967,295 | +| 8 | 2 | uint16_t | errors | Error flags bitmask | - | +| 10 | 1 | uint8_t | queue_len | Outbound packet queue length | 0 - 255 | ### Example Structure (C/C++) ```c struct StatsCore { uint8_t response_code; // 0x18 + uint8_t stats_type; // 0x00 (STATS_TYPE_CORE) uint16_t battery_mv; uint32_t uptime_secs; uint16_t errors; @@ -46,24 +62,26 @@ struct StatsCore { --- -## RESP_CODE_STATS_RADIO (25) +## RESP_CODE_STATS + STATS_TYPE_RADIO (24, 1) -**Total Frame Size:** 13 bytes +**Total Frame Size:** 14 bytes | Offset | Size | Type | Field Name | Description | Range/Notes | |--------|------|------|------------|-------------|-------------| -| 0 | 1 | uint8_t | response_code | Always `0x19` (25) | - | -| 1 | 2 | int16_t | noise_floor | Radio noise floor in dBm | -140 to +10 | -| 3 | 1 | int8_t | last_rssi | Last received signal strength in dBm | -128 to +127 | -| 4 | 1 | int8_t | last_snr | SNR scaled by 4 | Divide by 4.0 for dB | -| 5 | 4 | uint32_t | tx_air_secs | Cumulative transmit airtime in seconds | 0 - 4,294,967,295 | -| 9 | 4 | uint32_t | rx_air_secs | Cumulative receive airtime in seconds | 0 - 4,294,967,295 | +| 0 | 1 | uint8_t | response_code | Always `0x18` (24) | - | +| 1 | 1 | uint8_t | stats_type | Always `0x01` (STATS_TYPE_RADIO) | - | +| 2 | 2 | int16_t | noise_floor | Radio noise floor in dBm | -140 to +10 | +| 4 | 1 | int8_t | last_rssi | Last received signal strength in dBm | -128 to +127 | +| 5 | 1 | int8_t | last_snr | SNR scaled by 4 | Divide by 4.0 for dB | +| 6 | 4 | uint32_t | tx_air_secs | Cumulative transmit airtime in seconds | 0 - 4,294,967,295 | +| 10 | 4 | uint32_t | rx_air_secs | Cumulative receive airtime in seconds | 0 - 4,294,967,295 | ### Example Structure (C/C++) ```c struct StatsRadio { - uint8_t response_code; // 0x19 + uint8_t response_code; // 0x18 + uint8_t stats_type; // 0x01 (STATS_TYPE_RADIO) int16_t noise_floor; int8_t last_rssi; int8_t last_snr; // Divide by 4.0 to get actual SNR in dB @@ -74,19 +92,20 @@ struct StatsRadio { --- -## RESP_CODE_STATS_PACKETS (26) +## RESP_CODE_STATS + STATS_TYPE_PACKETS (24, 2) -**Total Frame Size:** 25 bytes +**Total Frame Size:** 26 bytes | Offset | Size | Type | Field Name | Description | Range/Notes | |--------|------|------|------------|-------------|-------------| -| 0 | 1 | uint8_t | response_code | Always `0x1A` (26) | - | -| 1 | 4 | uint32_t | recv | Total packets received | 0 - 4,294,967,295 | -| 5 | 4 | uint32_t | sent | Total packets sent | 0 - 4,294,967,295 | -| 9 | 4 | uint32_t | flood_tx | Packets sent via flood routing | 0 - 4,294,967,295 | -| 13 | 4 | uint32_t | direct_tx | Packets sent via direct routing | 0 - 4,294,967,295 | -| 17 | 4 | uint32_t | flood_rx | Packets received via flood routing | 0 - 4,294,967,295 | -| 21 | 4 | uint32_t | direct_rx | Packets received via direct routing | 0 - 4,294,967,295 | +| 0 | 1 | uint8_t | response_code | Always `0x18` (24) | - | +| 1 | 1 | uint8_t | stats_type | Always `0x02` (STATS_TYPE_PACKETS) | - | +| 2 | 4 | uint32_t | recv | Total packets received | 0 - 4,294,967,295 | +| 6 | 4 | uint32_t | sent | Total packets sent | 0 - 4,294,967,295 | +| 10 | 4 | uint32_t | flood_tx | Packets sent via flood routing | 0 - 4,294,967,295 | +| 14 | 4 | uint32_t | direct_tx | Packets sent via direct routing | 0 - 4,294,967,295 | +| 18 | 4 | uint32_t | flood_rx | Packets received via flood routing | 0 - 4,294,967,295 | +| 22 | 4 | uint32_t | direct_rx | Packets received via direct routing | 0 - 4,294,967,295 | ### Notes @@ -98,7 +117,8 @@ struct StatsRadio { ```c struct StatsPackets { - uint8_t response_code; // 0x1A + uint8_t response_code; // 0x18 + uint8_t stats_type; // 0x02 (STATS_TYPE_PACKETS) uint32_t recv; uint32_t sent; uint32_t flood_tx; @@ -110,15 +130,38 @@ struct StatsPackets { --- -## Usage Example (Python) +## Command Usage Example (Python) + +```python +# Send CMD_GET_STATS command +def send_get_stats_core(serial_interface): + """Send command to get core stats""" + cmd = bytes([56, 0]) # CMD_GET_STATS (56) + STATS_TYPE_CORE (0) + serial_interface.write(cmd) + +def send_get_stats_radio(serial_interface): + """Send command to get radio stats""" + cmd = bytes([56, 1]) # CMD_GET_STATS (56) + STATS_TYPE_RADIO (1) + serial_interface.write(cmd) + +def send_get_stats_packets(serial_interface): + """Send command to get packet stats""" + cmd = bytes([56, 2]) # CMD_GET_STATS (56) + STATS_TYPE_PACKETS (2) + serial_interface.write(cmd) +``` + +--- + +## Response Parsing Example (Python) ```python import struct def parse_stats_core(frame): - """Parse RESP_CODE_STATS_CORE frame (10 bytes)""" - response_code, battery_mv, uptime_secs, errors, queue_len = \ - struct.unpack('getMillis() / 1000; - uint8_t queue_len = (uint8_t)_mgr->getOutboundCount(0xFFFFFFFF); - memcpy(&out_frame[i], &battery_mv, 2); i += 2; - memcpy(&out_frame[i], &uptime_secs, 4); i += 4; - memcpy(&out_frame[i], &_err_flags, 2); i += 2; - out_frame[i++] = queue_len; - _serial->writeFrame(out_frame, i); - } else if (cmd_frame[0] == CMD_GET_STATS_RADIO) { - int i = 0; - out_frame[i++] = RESP_CODE_STATS_RADIO; - int16_t noise_floor = (int16_t)_radio->getNoiseFloor(); - int8_t last_rssi = (int8_t)radio_driver.getLastRSSI(); - int8_t last_snr = (int8_t)(radio_driver.getLastSNR() * 4); // scaled by 4 for 0.25 dB precision - uint32_t tx_air_secs = getTotalAirTime() / 1000; - uint32_t rx_air_secs = getReceiveAirTime() / 1000; - memcpy(&out_frame[i], &noise_floor, 2); i += 2; - out_frame[i++] = last_rssi; - out_frame[i++] = last_snr; - memcpy(&out_frame[i], &tx_air_secs, 4); i += 4; - memcpy(&out_frame[i], &rx_air_secs, 4); i += 4; - _serial->writeFrame(out_frame, i); - } else if (cmd_frame[0] == CMD_GET_STATS_PACKETS) { - int i = 0; - out_frame[i++] = RESP_CODE_STATS_PACKETS; - uint32_t recv = radio_driver.getPacketsRecv(); - uint32_t sent = radio_driver.getPacketsSent(); - uint32_t n_sent_flood = getNumSentFlood(); - uint32_t n_sent_direct = getNumSentDirect(); - uint32_t n_recv_flood = getNumRecvFlood(); - uint32_t n_recv_direct = getNumRecvDirect(); - memcpy(&out_frame[i], &recv, 4); i += 4; - memcpy(&out_frame[i], &sent, 4); i += 4; - memcpy(&out_frame[i], &n_sent_flood, 4); i += 4; - memcpy(&out_frame[i], &n_sent_direct, 4); i += 4; - memcpy(&out_frame[i], &n_recv_flood, 4); i += 4; - memcpy(&out_frame[i], &n_recv_direct, 4); i += 4; - _serial->writeFrame(out_frame, i); + } else if (cmd_frame[0] == CMD_GET_STATS && len >= 2) { + uint8_t stats_type = cmd_frame[1]; + if (stats_type == STATS_TYPE_CORE) { + int i = 0; + out_frame[i++] = RESP_CODE_STATS; + out_frame[i++] = STATS_TYPE_CORE; + uint16_t battery_mv = board.getBattMilliVolts(); + uint32_t uptime_secs = _ms->getMillis() / 1000; + uint8_t queue_len = (uint8_t)_mgr->getOutboundCount(0xFFFFFFFF); + memcpy(&out_frame[i], &battery_mv, 2); i += 2; + memcpy(&out_frame[i], &uptime_secs, 4); i += 4; + memcpy(&out_frame[i], &_err_flags, 2); i += 2; + out_frame[i++] = queue_len; + _serial->writeFrame(out_frame, i); + } else if (stats_type == STATS_TYPE_RADIO) { + int i = 0; + out_frame[i++] = RESP_CODE_STATS; + out_frame[i++] = STATS_TYPE_RADIO; + int16_t noise_floor = (int16_t)_radio->getNoiseFloor(); + int8_t last_rssi = (int8_t)radio_driver.getLastRSSI(); + int8_t last_snr = (int8_t)(radio_driver.getLastSNR() * 4); // scaled by 4 for 0.25 dB precision + uint32_t tx_air_secs = getTotalAirTime() / 1000; + uint32_t rx_air_secs = getReceiveAirTime() / 1000; + memcpy(&out_frame[i], &noise_floor, 2); i += 2; + out_frame[i++] = last_rssi; + out_frame[i++] = last_snr; + memcpy(&out_frame[i], &tx_air_secs, 4); i += 4; + memcpy(&out_frame[i], &rx_air_secs, 4); i += 4; + _serial->writeFrame(out_frame, i); + } else if (stats_type == STATS_TYPE_PACKETS) { + int i = 0; + out_frame[i++] = RESP_CODE_STATS; + out_frame[i++] = STATS_TYPE_PACKETS; + uint32_t recv = radio_driver.getPacketsRecv(); + uint32_t sent = radio_driver.getPacketsSent(); + uint32_t n_sent_flood = getNumSentFlood(); + uint32_t n_sent_direct = getNumSentDirect(); + uint32_t n_recv_flood = getNumRecvFlood(); + uint32_t n_recv_direct = getNumRecvDirect(); + memcpy(&out_frame[i], &recv, 4); i += 4; + memcpy(&out_frame[i], &sent, 4); i += 4; + memcpy(&out_frame[i], &n_sent_flood, 4); i += 4; + memcpy(&out_frame[i], &n_sent_direct, 4); i += 4; + memcpy(&out_frame[i], &n_recv_flood, 4); i += 4; + memcpy(&out_frame[i], &n_recv_direct, 4); i += 4; + _serial->writeFrame(out_frame, i); + } else { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); // invalid stats sub-type + } } else if (cmd_frame[0] == CMD_FACTORY_RESET && memcmp(&cmd_frame[1], "reset", 5) == 0) { bool success = _store->formatFileSystem(); if (success) {