New info example, overall testing helpers. Improved readme
This commit is contained in:
@@ -27,6 +27,10 @@ target_include_directories(meshcore_companion PUBLIC src)
|
||||
add_executable(meshcore_tty examples-linux/tty_bridge/meshcore_tty.c)
|
||||
target_link_libraries(meshcore_tty PRIVATE meshcore_companion)
|
||||
|
||||
# Linux example: one-shot dump of everything the radio reports, then exit.
|
||||
add_executable(meshcore_info examples-linux/info/meshcore_info.c)
|
||||
target_link_libraries(meshcore_info PRIVATE meshcore_companion)
|
||||
|
||||
# Host unit test for the codec (no hardware required).
|
||||
enable_testing()
|
||||
add_executable(test_codec test/test_codec.c)
|
||||
|
||||
@@ -75,6 +75,44 @@ void loop() {
|
||||
}
|
||||
```
|
||||
|
||||
## Linux / host build (CMake)
|
||||
|
||||
The portable C core needs no Arduino and no hardware — it builds and runs on any
|
||||
desktop. A `CMakeLists.txt` at the repo root builds the core, the host unit test,
|
||||
and the Linux examples (the Arduino C++ wrapper is skipped here — it needs
|
||||
`<Arduino.h>`):
|
||||
|
||||
```sh
|
||||
cmake -B build && cmake --build build
|
||||
ctest --test-dir build --output-on-failure # run the codec unit test
|
||||
```
|
||||
|
||||
### Linux examples (`examples-linux/`)
|
||||
|
||||
Both talk to a companion radio over **any tty** — a USB-CDC device
|
||||
(`/dev/ttyACM0`), a USB-serial adapter (`/dev/ttyUSB0`), or a raw UART
|
||||
(`/dev/serial0`):
|
||||
|
||||
- **`tty_bridge`** — live: receive/echo channel traffic and send messages.
|
||||
```sh
|
||||
./build/meshcore_tty /dev/ttyACM0
|
||||
```
|
||||
- **`info`** — one-shot: dump everything the radio reports (model, firmware,
|
||||
radio params, device time, channels + PSKs, stats), then exit.
|
||||
```sh
|
||||
./build/meshcore_info /dev/ttyACM0
|
||||
```
|
||||
|
||||
No CMake? Compile a single example directly:
|
||||
|
||||
```sh
|
||||
cc -std=c99 -Wall -Wextra -Isrc \
|
||||
examples-linux/info/meshcore_info.c src/meshcore_companion.c -o meshcore_info
|
||||
```
|
||||
|
||||
The same C core also ships starter examples for other toolchains in
|
||||
`examples-esp-idf/` (ESP-IDF UART driver) and `examples-stm32/` (STM32 HAL).
|
||||
|
||||
## What's covered
|
||||
|
||||
| Need | API |
|
||||
@@ -94,13 +132,24 @@ new command is one builder plus one `case`.
|
||||
|
||||
## Testing the core (no hardware)
|
||||
|
||||
Run everything at once with `./testall.sh` — it runs the CMake/ctest, PlatformIO
|
||||
native, and Arduino-compile suites, and auto-skips whichever tools aren't
|
||||
installed (so it works on a cmake-only or PlatformIO-only box):
|
||||
|
||||
```sh
|
||||
cd test
|
||||
cc -std=c99 -Wall -Wextra -I../src test_codec.c ../src/meshcore_companion.c -o t && ./t
|
||||
./testall.sh
|
||||
```
|
||||
|
||||
Exercises command encoding, frame reassembly across split reads, every parser,
|
||||
and resync past line garbage.
|
||||
Or run a suite individually:
|
||||
|
||||
```sh
|
||||
ctest --test-dir build --output-on-failure # via CMake (see above)
|
||||
pio test -e native # via PlatformIO's native env
|
||||
cd test && cc -std=c99 -Wall -Wextra -I../src test_codec.c ../src/meshcore_companion.c -o t && ./t
|
||||
```
|
||||
|
||||
The unit test exercises command encoding, frame reassembly across split reads,
|
||||
every parser, and resync past line garbage.
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
@@ -1,335 +1,154 @@
|
||||
# meshcore_c vs meshcore_py — Feature Gap Analysis
|
||||
|
||||
Date: 2026-06-08
|
||||
Date: 2026-06-08 (Phase 1 complete)
|
||||
|
||||
## Summary
|
||||
|
||||
`meshcore_c` is a clean, well-structured portable C99 core + Arduino C++ wrapper for
|
||||
the MeshCore Companion Radio **serial** protocol. It correctly implements the
|
||||
wire-level framing (0x3C/0x3E, LE length), handshake, channel text/data
|
||||
send/receive, PSK programming, and basic radio parameters — the minimum needed
|
||||
for a display board or sensor hub to talk to a companion radio. The code is
|
||||
concise, has zero external dependencies in the C core, and ships with four
|
||||
platform examples (Linux POSIX, ESP-IDF, STM32 HAL, Arduino).
|
||||
`meshcore_c` is a clean, portable C99 core + Arduino C++ wrapper for the MeshCore
|
||||
Companion Radio **serial** protocol. It implements the wire framing (0x3C/0x3E,
|
||||
LE length), handshake, channel text/data send/receive (incl. the V3 SNR
|
||||
variants), direct/contact + CLI message sending, channel PSK programming, full
|
||||
device/self/stats parsing, and basic radio parameters. Zero external deps in the
|
||||
core; ships examples for Linux (`tty_bridge`, `info`), ESP-IDF, STM32 HAL, and
|
||||
Arduino.
|
||||
|
||||
Compared to the reference Python library `meshcore_py`, this C implementation
|
||||
covers roughly **30%** of the protocol surface. The gaps fall naturally into
|
||||
three categories below.
|
||||
Against the reference `meshcore_py`, coverage is now roughly **45%**. The two
|
||||
remaining **major** areas are contact management and binary (telemetry/status)
|
||||
requests. Everything else is feature-completeness or convenience.
|
||||
|
||||
---
|
||||
|
||||
## MAJOR GAPS (core protocol missing — things that prevent important use cases)
|
||||
## Already implemented (baseline)
|
||||
|
||||
### 1. Contact Management — ENTIRELY MISSING
|
||||
*meshcore_py* has an extensive contacts subsystem: fetch with delta sync
|
||||
(`lastmod`), add/update, remove, share, import/export (meshcore:// URI), get by
|
||||
public key, change path, change flags, autoadd config. None of this exists in
|
||||
`meshcore_c`.
|
||||
Framing/RX assembler + resync · `app_start` · `device_query` → **DeviceInfo**
|
||||
(incl. `ver`, `repeat` fw≥9, `path_hash_mode` fw≥10) · **SelfInfo** (incl.
|
||||
`multi_acks`, `adv_loc_policy`, `telemetry_mode` base/loc/env) · get/set device
|
||||
time · **CurrTime** · battery voltage (2-byte only) · channel get/set + PSK →
|
||||
**ChannelInfo** · send channel text · **ChannelMsgRecv / ChannelDataRecv** ·
|
||||
**ContactMsgRecv** (+ signature extraction for `txt_type=2`) · **V3** channel +
|
||||
contact recv (SNR) · `send_txt_msg` / `send_cmd` (direct + CLI, CMD 2) ·
|
||||
**MsgSent** parse (ack tag + suggested timeout) · `set_radio_params` ·
|
||||
`get_stats` → **Stats** (core/radio/packets) · self-advert · auto-drain on
|
||||
`MsgWaiting` (Arduino wrapper).
|
||||
|
||||
| Feature | meshcore\_py | meshcore\_c |
|
||||
|---|---|---|
|
||||
| GET\_CONTACTS (with lastmod sync) | ✅ | ❌ |
|
||||
| ADD\_UPDATE\_CONTACT | ✅ | ❌ |
|
||||
| REMOVE\_CONTACT | ✅ | ❌ |
|
||||
| SHARE\_CONTACT | ✅ | ❌ |
|
||||
| EXPORT\_CONTACT / IMPORT\_CONTACT | ✅ | ❌ |
|
||||
| RESET\_PATH | ✅ | ❌ |
|
||||
| GET\_CONTACT\_BY\_KEY | ✅ | ❌ |
|
||||
| SET\_ADVERT\_NAME | ✅ | ❌ |
|
||||
| SET\_ADVERT\_LATLON | ✅ | ❌ |
|
||||
| GET\_ADVERT\_PATH | ✅ | ❌ |
|
||||
| SET\_AUTOADD\_CONFIG / GET\_AUTOADD\_CONFIG | ✅ | ❌ |
|
||||
| GET\_ALLOWED\_REPEAT\_FREQ | ✅ | ❌ |
|
||||
| SET\_PATH\_HASH\_MODE / GET\_PATH\_HASH\_MODE | ✅ | ❌ |
|
||||
|
||||
**Missing commands:** 9, 8, 14, 15, 16, 17, 18, 30, 42, 58, 59, 60, 61
|
||||
**Missing response parsing:** CONTACT_START, CONTACT, CONTACT_END, CONTACT_URI,
|
||||
CONTACT_DELETED (0x8F), CONTACTS_FULL (0x90), ADVERT_PATH, AUTOADD_CONFIG,
|
||||
ALLOWED_REPEAT_FREQ, DEFAULT_FLOOD_SCOPE
|
||||
|
||||
**Impact:** No way to manage the mesh node list from C. Critical for any
|
||||
application that needs to discover and maintain contacts (repeaters, gateways).
|
||||
|
||||
### 2. MSG\_SENT Response — NOT PARSED
|
||||
`meshcore_py` parses `MSG_SENT` (response code 6) which carries:
|
||||
- `expected_ack` (4 bytes) — tag used to match the ACK confirmation
|
||||
- `suggested_timeout` (4 bytes) — how long to wait for the ACK
|
||||
|
||||
`meshcore_c` has `MC_RESP_SENT = 6` in the enum but **no parsing case** in
|
||||
`mc_parse()` and **no `send_msg` command builder** (only `send_channel_text`).
|
||||
The Arduino wrapper's `sendChannelText` sends fire-and-forget.
|
||||
|
||||
**Impact:** No way to confirm message delivery. No timeout-driven retry.
|
||||
|
||||
### 3. Binary Requests — ENTIRELY MISSING
|
||||
`meshcore_py` supports `BINARY_REQ` (CMD 50) with subtypes:
|
||||
- STATUS (node health: battery, uptime, RSSI, SNR, airtime, packet counters)
|
||||
- TELEMETRY (Cayenne LPP sensor data from remote nodes)
|
||||
- MMA (min/max/avg telemetry over a time range)
|
||||
- ACL (access control list — node permissions)
|
||||
- NEIGHBOURS (list of nodes heard recently, with SNR/age)
|
||||
|
||||
And anonymous requests via `SEND_ANON_REQ` (CMD 57):
|
||||
- REGIONS, OWNER, BASIC (remote clock)
|
||||
|
||||
**Missing:**
|
||||
- `mc_cmd_send_binary_req` builder
|
||||
- `mc_cmd_send_anon_req` builder
|
||||
- `MC_RESP_BINARY_RESPONSE` (0x8C) parsing
|
||||
- Tracking infrastructure (`expected_ack` → `BINARY_RESPONSE` tag matching)
|
||||
|
||||
**Impact:** No telemetry interrogation, no remote health checks, no neighbour
|
||||
discovery. A major gap for monitoring/gateway applications.
|
||||
|
||||
### 4. STATS Response — ENUM EXISTS, PARSING MISSING
|
||||
`mc_cmd_get_stats` builds the command, and `MC_RESP_STATS = 24` is in the enum.
|
||||
But `mc_parse()` has **no case** for `MC_RESP_STATS`, so requesting stats
|
||||
returns a raw event code with no parsed data.
|
||||
|
||||
`meshcore_py` parses three stat subtypes:
|
||||
- **Core stats:** battery_mv, uptime_secs, errors, queue_len
|
||||
- **Radio stats:** noise_floor, last_rssi, last_snr, tx_air_secs, rx_air_secs
|
||||
- **Packet stats:** recv, sent, flood_tx, direct_tx, flood_rx, direct_rx, recv_errors
|
||||
|
||||
**Impact:** Stats command is wired but returns unparseable data. The C struct
|
||||
definitions for stats need to be added to `mc_event_t`.
|
||||
|
||||
### 5. SELF\_INFO Parsing — INCOMPLETE
|
||||
`meshcore_c`'s `mc_parse` for `MC_RESP_SELF_INFO` reads 55 bytes minimum but
|
||||
the byte offsets are from an older firmware version. Compared to `meshcore_py`:
|
||||
|
||||
| Field | meshcore\_py (current firmware) | meshcore\_c |
|
||||
|---|---|---|
|
||||
| `multi_acks` | offset 43 (1 byte) | ❌ treats as reserved |
|
||||
| `adv_loc_policy` | offset 44 (1 byte) | ❌ treats as reserved |
|
||||
| `telemetry_mode` | offset 45 (1 byte, decoded to 3×2-bit fields) | ❌ treats as reserved |
|
||||
| `manual_add_contacts` | offset 46 (1 byte) | ✅ |
|
||||
| `radio_freq` / `radio_bw` | offsets 47-54 | ✅ |
|
||||
| `radio_sf` / `radio_cr` | offsets 55-56 | ✅ (conditional guard) |
|
||||
|
||||
The C struct `mc_self_info_t` has a `manual_add_contacts` field but **not**
|
||||
`multi_acks`, `adv_loc_policy`, or the three `telemetry_mode_*` sub-fields.
|
||||
|
||||
**Impact:** Self-info data is incomplete. Cannot read/write advanced device
|
||||
settings through the C API.
|
||||
|
||||
### 6. DEVICE\_INFO — MISSING FIELDS
|
||||
`meshcore_py` parses additional fields for firmware v9+ and v10+:
|
||||
- `repeat` (repeater mode, fw ≥ 9)
|
||||
- `path_hash_mode` (fw ≥ 10)
|
||||
- `ver` string (20-byte version field)
|
||||
- `max_contacts` correctly applied as `×2` on the wire value
|
||||
|
||||
`meshcore_c` applies `×2` to `max_contacts` but does not parse:
|
||||
- `repeat` flag
|
||||
- `path_hash_mode`
|
||||
- `ver` string
|
||||
|
||||
**Impact:** Cannot detect repeater mode or path hash configuration from the
|
||||
device query response.
|
||||
|
||||
### 7. CHANNEL\_MSG\_RECV — V3 FORMAT MISSING
|
||||
Newer MeshCore firmware (v3+) sends `CHANNEL_MSG_RECV_V3` (response 17) which
|
||||
includes an SNR byte at the start. `meshcore_c` only parses `MC_RESP_CHANNEL_MSG_RECV`
|
||||
(response 8). The same gap exists for `CONTACT_MSG_RECV_V3` (response 16).
|
||||
|
||||
**Impact:** On newer firmware, V3 messages pass through as unknown events with
|
||||
no SNR or RSSI data available.
|
||||
Host unit tests (`test/test_codec.c`) cover all of the above parsers/builders.
|
||||
|
||||
---
|
||||
|
||||
## MINOR GAPS (feature-completeness items)
|
||||
## Remaining gaps
|
||||
|
||||
### 8. Direct/Contact Message Sending
|
||||
- **Missing command builders:** `mc_cmd_send_txt_msg` (CMD 2), `mc_cmd_send_cmd` (CMD 2 with txt_type=1)
|
||||
- **Missing command:** `SEND_LOGIN` (26), `SEND_LOGOUT` (29), `SEND_STATUS_REQ` (27)
|
||||
- `meshcore_py` has the full message retry flow with `send_msg_with_retry` including automatic path-reset-to-flood fallback.
|
||||
### MAJOR
|
||||
|
||||
### 9. Device Configuration Commands
|
||||
- `SET_DEVICE_PIN` (37) — for BLE PIN pairing
|
||||
- `SET_OTHER_PARAMS` (38) — telemetry modes, multi_acks, advert policy
|
||||
- `REBOOT` (19) — simple but essential
|
||||
- `FACTORY_RESET` (51) — with two-step safety pattern
|
||||
- `SET_TUNING_PARAMS` (21) / `GET_TUNING_PARAMS` (43) — rx_delay, airtime_factor
|
||||
- `HAS_CONNECTION` (28) — check connectivity
|
||||
#### 1. Contact management — entirely missing
|
||||
No way to enumerate or manage the mesh node list from C.
|
||||
|
||||
### 10. Crypto & Security
|
||||
- `EXPORT_PRIVATE_KEY` (23) / `IMPORT_PRIVATE_KEY` (24)
|
||||
- `SIGN_START` (33) / `SIGN_DATA` (34) / `SIGN_FINISH` (35)
|
||||
- No signature field extraction from signed contact messages (`txt_type=2`)
|
||||
| Feature | CMD | meshcore_py | meshcore_c |
|
||||
|---|---|---|---|
|
||||
| GET_CONTACTS (lastmod delta sync) | 4 | ✅ | ❌ |
|
||||
| ADD_UPDATE_CONTACT | 9 | ✅ | ❌ |
|
||||
| REMOVE_CONTACT | 15 | ✅ | ❌ |
|
||||
| SHARE_CONTACT | 16 | ✅ | ❌ |
|
||||
| EXPORT_CONTACT / IMPORT_CONTACT | 17 / 18 | ✅ | ❌ |
|
||||
| RESET_PATH | 13 | ✅ | ❌ |
|
||||
| GET_CONTACT_BY_KEY | 30 | ✅ | ❌ |
|
||||
| SET_ADVERT_NAME / SET_ADVERT_LATLON | 8 / 14 | ✅ | ❌ |
|
||||
| GET_ADVERT_PATH | 42 | ✅ | ❌ |
|
||||
| SET/GET_AUTOADD_CONFIG | 58 / 59 | ✅ | ❌ |
|
||||
| GET_ALLOWED_REPEAT_FREQ | 60 | ✅ | ❌ |
|
||||
|
||||
### 11. Network Layer Commands
|
||||
- `SEND_RAW_DATA` (25) — send arbitrary payload through mesh
|
||||
- `SEND_TRACE_PATH` (36) — trace route with SNR per hop
|
||||
- `SEND_CONTROL_DATA` (55) — node discovery, etc.
|
||||
- `SET_FLOOD_SCOPE` (54) / `SET_DEFAULT_FLOOD_SCOPE` (63) / `GET_DEFAULT_FLOOD_SCOPE` (64)
|
||||
Response parsing needed: `CONTACT_START` (2), `CONTACT` (3), `CONTACT_END` (4),
|
||||
`CONTACT_URI` (11), `ADVERT_PATH` (22), `AUTOADD_CONFIG` (25),
|
||||
`ALLOWED_REPEAT_FREQ` (26), `CONTACT_DELETED` (0x8F), `CONTACTS_FULL` (0x90),
|
||||
`NEW_ADVERT` push (0x8A).
|
||||
|
||||
### 12. Response Parsing Missing
|
||||
- `MC_RESP_CONTACT_MSG_RECV_V3` (16) — SNR-included format
|
||||
- `MC_RESP_CHANNEL_MSG_RECV_V3` (17) — SNR-included format
|
||||
- `MC_RESP_SIGN_START` / `MC_RESP_SIGNATURE` (19, 20)
|
||||
- `MC_RESP_CUSTOM_VARS` (21)
|
||||
- `MC_RESP_ADVERT_PATH` (22)
|
||||
- `MC_RESP_TUNING_PARAMS` (23)
|
||||
- `MC_RESP_AUTOADD_CONFIG` (25)
|
||||
- `MC_RESP_ALLOWED_REPEAT_FREQ` (26)
|
||||
- `MC_RESP_DEFAULT_FLOOD_SCOPE` (28)
|
||||
- `MC_PUSH_LOG_RX_DATA` (0x88) — RF packet monitor
|
||||
- `MC_PUSH_TRACE_DATA` (0x89) — trace response
|
||||
- `MC_PUSH_NEW_ADVERT` (0x8A) — new contact push
|
||||
- `MC_PUSH_TELEMETRY` (0x8B) — telemetry push
|
||||
- `MC_PUSH_BINARY_RESP` (0x8C) — binary response
|
||||
- `MC_RESP_PATH_DISCOVERY` (0x8D)
|
||||
- `MC_PUSH_CONTROL_DATA` (0x8E)
|
||||
- `MC_RESP_CONTACT_DELETED` (0x8F)
|
||||
- `MC_RESP_CONTACTS_FULL` (0x90)
|
||||
#### 2. Binary + anonymous requests — entirely missing
|
||||
Remote interrogation of other nodes.
|
||||
- `SEND_BINARY_REQ` (50) subtypes: STATUS, TELEMETRY, MMA, ACL, NEIGHBOURS
|
||||
- `SEND_ANON_REQ` (57) subtypes: REGIONS, OWNER, BASIC (remote clock)
|
||||
- `BINARY_RESPONSE` push (0x8C) parsing + tag tracking (`expected_ack` →
|
||||
response correlation, reusing the `MsgSent` ack-tag + timeout already parsed)
|
||||
- `TELEMETRY` push (0x8B) parsing (Cayenne LPP — likely decoded by the caller)
|
||||
|
||||
### 13. Battery Response — STORAGE INFO MISSING
|
||||
`meshcore_py` parses the full `BATT_AND_STORAGE` response (11 bytes): battery
|
||||
level + storage `used_kb` + `total_kb`. `meshcore_c` only parses the 2-byte
|
||||
battery voltage (`battery_mv`).
|
||||
### MINOR
|
||||
|
||||
### 14. CUSTOM\_VARS — COMMAND & RESPONSE MISSING
|
||||
`GET_CUSTOM_VARS` (40) / `SET_CUSTOM_VAR` (41) — key:value pairs stored on the
|
||||
device. Both command builders and `MC_RESP_CUSTOM_VARS` parsing are absent.
|
||||
#### 3. Direct-message reliability
|
||||
- `SEND_LOGIN` (26) / `SEND_LOGOUT` (29) / `SEND_STATUS_REQ` (27)
|
||||
- Retry helper: resend up to N times, auto-switch to flood after K direct
|
||||
failures (uses the already-parsed `MsgSent` suggested-timeout).
|
||||
|
||||
### 15. GET\_SELF\_TELEMETRY — MISSING
|
||||
`meshcore_py` can request the local node's own telemetry via `get_self_telemetry()`.
|
||||
The command builder and `MC_PUSH_TELEMETRY` response parsing are missing.
|
||||
#### 4. Device-management commands
|
||||
`REBOOT` (19) · `FACTORY_RESET` (51, two-step) · `SET_DEVICE_PIN` (37) ·
|
||||
`SET_OTHER_PARAMS` (38: telemetry modes, multi_acks, advert policy) ·
|
||||
`SET_TUNING_PARAMS` (21) / `GET_TUNING_PARAMS` (43) · `HAS_CONNECTION` (28) ·
|
||||
`SET_TX_POWER` (12).
|
||||
|
||||
#### 5. Crypto & security
|
||||
`EXPORT_PRIVATE_KEY` (23) / `IMPORT_PRIVATE_KEY` (24) ·
|
||||
`SIGN_START` (33) / `SIGN_DATA` (34) / `SIGN_FINISH` (35).
|
||||
(Signature *extraction* from received signed messages is already done.)
|
||||
|
||||
#### 6. Network-layer commands
|
||||
`SEND_RAW_DATA` (25) · `SEND_TRACE_PATH` (36, per-hop SNR) ·
|
||||
`SEND_CONTROL_DATA` (55) · `SET_FLOOD_SCOPE` (54) /
|
||||
`SET/GET_DEFAULT_FLOOD_SCOPE` (63 / 64).
|
||||
|
||||
#### 7. Remaining response/push parsing
|
||||
`SIGN_START`/`SIGNATURE` (19/20) · `CUSTOM_VARS` (21) · `TUNING_PARAMS` (23) ·
|
||||
`DEFAULT_FLOOD_SCOPE` (28) · `LOG_RX_DATA` push (0x88) · `TRACE_DATA` push
|
||||
(0x89) · `PATH_DISCOVERY` (0x8D) · `CONTROL_DATA` push (0x8E).
|
||||
|
||||
#### 8. Smaller data gaps
|
||||
- **Battery**: only the 2-byte voltage is parsed; the full `BATT_AND_STORAGE`
|
||||
response adds `used_kb` + `total_kb`.
|
||||
- **CUSTOM_VARS**: `GET_CUSTOM_VARS` (40) / `SET_CUSTOM_VAR` (41) + parse.
|
||||
- **GET_SELF_TELEMETRY**: local-node telemetry request + `TELEMETRY` parse.
|
||||
|
||||
### NICE TO HAVE
|
||||
- Auto-reconnect / connection-manager logic in the Arduino wrapper.
|
||||
- Event filtering / `wait_for_event(timeout)` helpers (C core stays struct-based).
|
||||
- Optional contact / self-info / time state tracking in the wrapper.
|
||||
- Synchronous request→response helpers (lock, await matching event, apply
|
||||
suggested timeout) — pairs with #2 binary requests and #3 retry.
|
||||
- Channel-log AES decryption (heavy; needs crypto — probably out of scope for
|
||||
the bare-metal core).
|
||||
- Cayenne LPP telemetry decode (application layer / caller).
|
||||
- More examples (chat, contacts sync, telemetry display) and more tests
|
||||
(contact parsing, error paths, edge cases).
|
||||
|
||||
---
|
||||
|
||||
## NICE TO HAVE (convenience, robustness, UX)
|
||||
|
||||
### 16. Auto-Reconnect / ConnectionManager
|
||||
`meshcore_py` has a `ConnectionManager` class with:
|
||||
- Automatic reconnect with configurable max attempts
|
||||
- `send_appstart()` callback after every reconnection
|
||||
- CONNECTED / DISCONNECTED event emission
|
||||
- Robust disconnect handling for all three transports (BLE/Serial/TCP)
|
||||
|
||||
The Arduino wrapper in `meshcore_c` has `begin()`, but no reconnect logic.
|
||||
|
||||
### 17. Event Subscription with Attribute Filtering
|
||||
`meshcore_py`'s `EventDispatcher` supports:
|
||||
- Subscribe to specific event types (or all via `None`)
|
||||
- Attribute-based filtering (e.g., "only ACKs with code=X")
|
||||
- `wait_for_event()` with timeout
|
||||
- `clone()` for safe async dispatch
|
||||
|
||||
The C core has no event system — the caller gets a raw `mc_event_t` struct.
|
||||
The Arduino wrapper has simple lambda callbacks with no filtering. This is
|
||||
fine for a C library but limits complex dispatch scenarios.
|
||||
|
||||
### 18. Contact State Tracking
|
||||
`meshcore_py` maintains internal dictionaries for contacts, self_info, and
|
||||
device time, auto-updating them on relevant events. The C core leaves all
|
||||
state management to the caller. Useful for real apps but adds memory/CPU cost.
|
||||
|
||||
### 19. Automatic Message Fetching
|
||||
`meshcore_py` has `start_auto_message_fetching()` and
|
||||
`stop_auto_message_fetching()` that use asyncio to automatically drain the
|
||||
radio's message queue when `MESSAGES_WAITING` events arrive.
|
||||
|
||||
The Arduino wrapper already auto-drains in `dispatch()` (enabled by
|
||||
`setAutoSync(true)`). The C core's examples also drain manually. **This is the
|
||||
one "nice to have" that is already well-addressed in `meshcore_c`.**
|
||||
|
||||
### 20. Synchronous Request/Response Helpers
|
||||
`meshcore_py` wraps fire-and-forget commands with `_sync` variants that:
|
||||
- Lock via `_mesh_request_lock`
|
||||
- Wait for the specific response event
|
||||
- Apply suggested timeout from `MSG_SENT`
|
||||
- Return parsed payload or `None`
|
||||
|
||||
Examples: `req_status_sync`, `req_telemetry_sync`, `send_path_discovery_sync`,
|
||||
`send_login_sync`, `req_neighbours_sync`, etc.
|
||||
|
||||
`sent_msg_with_retry` goes further: retry up to N times, auto-switch to flood
|
||||
after K failed direct attempts.
|
||||
|
||||
### 21. Channel Log Decryption
|
||||
`meshcore_py` can decrypt AES-encrypted channel messages found in the
|
||||
RF packet log (`meshcore_parser.py`). It tracks channels, computes HMACs and
|
||||
AES decryption, and matches duplicate packets. This requires cryptographic
|
||||
libraries (pycryptodome) and is likely too heavy for a bare-metal C core.
|
||||
|
||||
### 22. Cayenne LPP Telemetry Parsing
|
||||
`meshcore_py` parses Cayenne LPP binary telemetry frames into JSON with typed
|
||||
values (temperature, humidity, GPS, voltage, etc.). Requires an LPP library
|
||||
and is application-layer — probably belongs in the caller, not the core.
|
||||
|
||||
### 23. More Examples
|
||||
`meshcore_py` ships 33 example scripts covering chat, battery monitor, channel
|
||||
manager, contacts, pingbot, RSS bot, trace, OLLAMA integration, and many BLE
|
||||
workflows. `meshcore_c` has 4 good examples but many usage patterns are not
|
||||
demonstrated.
|
||||
|
||||
### 24. Test Coverage
|
||||
`meshcore_py` has 13+ unit test files. `meshcore_c` has one good unit test
|
||||
(`test_codec.c`) covering frame assembly, parsing, and resync. More tests
|
||||
for SELF_INFO, CONTACT parsing, error paths, and edge cases would be valuable.
|
||||
## What meshcore_c does better than meshcore_py
|
||||
- **Zero dependencies / no malloc / no I/O in the core** — drops into any C99
|
||||
target; transport is 100% external (Linux, ESP-IDF, STM32, Arduino, no `#ifdef`).
|
||||
- **Build simplicity** — one `CMakeLists.txt`, a single `cc`, or `pio test`.
|
||||
- **Struct-based, caller-owns-memory API**, with an Arduino wrapper that
|
||||
auto-drains the radio queue on `MsgWaiting`.
|
||||
|
||||
---
|
||||
|
||||
## What meshcore\_c Does BETTER Than meshcore\_py
|
||||
## Roadmap
|
||||
|
||||
1. **Zero dependencies** — The C core needs only `stdint.h`, `stddef.h`, and
|
||||
`string.h`. No malloc, no crypto, no third-party libraries. Truly portable
|
||||
to any C99 environment.
|
||||
### Phase 2 — Contacts & binary requests (next, major)
|
||||
1. Contact command builders: GET_CONTACTS, ADD_UPDATE_CONTACT, REMOVE_CONTACT,
|
||||
RESET_PATH, SHARE_CONTACT, EXPORT/IMPORT_CONTACT, GET_CONTACT_BY_KEY.
|
||||
2. Contact response parsing: CONTACT_START / CONTACT / CONTACT_END (+ CONTACT_URI,
|
||||
CONTACT_DELETED, CONTACTS_FULL, NEW_ADVERT).
|
||||
3. Binary-request infrastructure: `mc_cmd_send_binary_req` / `mc_cmd_send_anon_req`,
|
||||
`BINARY_RESPONSE` parsing, ack-tag correlation (reuse `MsgSent` tag+timeout).
|
||||
4. Tests + a `contacts` Linux example.
|
||||
|
||||
2. **No I/O in the core** — Transport is 100% external. Same code works on
|
||||
Linux, ESP-IDF, STM32 HAL, and Arduino without `#ifdef`.
|
||||
### Phase 3 — Device management & network commands (minor)
|
||||
REBOOT / FACTORY_RESET / SET_DEVICE_PIN / SET_OTHER_PARAMS · TUNING_PARAMS
|
||||
get/set · EXPORT/IMPORT_PRIVATE_KEY · CUSTOM_VARS · SET_ADVERT_NAME/LATLON ·
|
||||
flood scope get/set · HAS_CONNECTION · BATT_AND_STORAGE storage fields ·
|
||||
SEND_TRACE_PATH / SEND_RAW_DATA / SEND_CONTROL_DATA.
|
||||
|
||||
3. **Platform breadth** — Already ships examples for Linux, ESP-IDF, STM32,
|
||||
and Arduino. `meshcore_py` is Python-only (Linux/macOS/Windows, not
|
||||
embedded).
|
||||
|
||||
4. **Build simplicity** — One CMakeLists.txt, or compile with a single `cc`
|
||||
command. No package manager needed.
|
||||
|
||||
5. **Auto-drain in Arduino wrapper** — The `dispatch()` method automatically
|
||||
syncs messages when `MC_PUSH_MSG_WAITING` arrives, simplifying the user
|
||||
loop.
|
||||
|
||||
6. **Clean C API** — Pure struct-based API with no callbacks, no heap, no
|
||||
hidden state. The caller owns all memory.
|
||||
### Phase 4 — Polish (nice to have)
|
||||
Connection-manager reconnect · sync request/response + retry-with-flood-fallback
|
||||
helpers · event filtering · more examples and tests.
|
||||
|
||||
---
|
||||
|
||||
## Prioritized Implementation Roadmap
|
||||
|
||||
### Phase 1 — Core Protocol Completion (Major)
|
||||
1. `MSG_SENT` response parsing (ack tag + timeout tracking)
|
||||
2. `MC_RESP_STATS` parsing (core/radio/packet subtypes)
|
||||
3. Fix `SELF_INFO` byte offsets and add missing struct fields
|
||||
4. Add `CHANNEL_MSG_RECV_V3` and `CONTACT_MSG_RECV_V3` parsing
|
||||
5. Parse `repeat` and `path_hash_mode` in `DEVICE_INFO`
|
||||
6. Add `mc_cmd_send_txt_msg` and `mc_cmd_send_cmd` builders
|
||||
|
||||
### Phase 2 — Contact & Binary Requests (Major)
|
||||
7. Contact command builders: GET_CONTACTS, ADD_UPDATE_CONTACT, REMOVE_CONTACT,
|
||||
RESET_PATH, SHARE_CONTACT, EXPORT/IMPORT_CONTACT, GET_CONTACT_BY_KEY
|
||||
8. Contact response parsing: CONTACT_START, CONTACT, CONTACT_END
|
||||
9. Binary request infrastructure: `mc_cmd_send_binary_req`, tag tracking
|
||||
10. BINARY_RESPONSE parsing with sub-type dispatch (STATUS, TELEMETRY, etc.)
|
||||
|
||||
### Phase 3 — Device Management (Minor)
|
||||
11. REBOOT, FACTORY_RESET, SET_DEVICE_PIN
|
||||
12. SET_OTHER_PARAMS, SET_TUNING_PARAMS/GET_TUNING_PARAMS
|
||||
13. EXPORT_PRIVATE_KEY/IMPORT_PRIVATE_KEY
|
||||
14. CUSTOM_VARS get/set
|
||||
15. SET_ADVERT_NAME, SET_ADVERT_LATLON
|
||||
16. SET_FLOOD_SCOPE, SET_DEFAULT_FLOOD_SCOPE
|
||||
17. HasConnection, GetBattAndStorage (storage fields)
|
||||
18. SEND_TRACE_PATH, SEND_RAW_DATA, SEND_CONTROL_DATA
|
||||
|
||||
### Phase 4 — Polish (Nice to Have)
|
||||
19. ConnectionManager-style reconnect in the Arduino wrapper
|
||||
20. More unit tests
|
||||
21. Additional examples (chat, contacts sync, telemetry display)
|
||||
22. Message retry with path-reset-to-flood fallback
|
||||
|
||||
---
|
||||
|
||||
*Comparison performed against [meshcore_py](https://github.com/meshcore-dev/meshcore_py) at commit HEAD, 2026-06-08.*
|
||||
*Compared against [meshcore_py](https://github.com/meshcore-dev/meshcore_py),
|
||||
HEAD as of 2026-06-08. Protocol codes cross-checked against meshcore.js
|
||||
`constants.js` and meshcore_py `packets.py`.*
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
/*
|
||||
* meshcore_info.c
|
||||
*
|
||||
* One-shot: open a tty, interrogate the attached MeshCore Companion Radio for
|
||||
* everything it will tell us -- model/firmware/version, radio parameters,
|
||||
* device time, every configured channel (name + PSK), and runtime stats -- then
|
||||
* print it and exit. Read-only; sends only query commands.
|
||||
*
|
||||
* build: via CMakeLists.txt at the repo root, or:
|
||||
* cc -std=c99 -Wall -Wextra -I../../src \
|
||||
* meshcore_info.c ../../src/meshcore_companion.c -o meshcore_info
|
||||
*
|
||||
* run: ./meshcore_info /dev/ttyACM0
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
* Author: Scott Penrose / Digital Dimensions.
|
||||
*/
|
||||
#define _DEFAULT_SOURCE /* cfmakeraw, B115200, clock_gettime on glibc/c99 */
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/select.h>
|
||||
#include <termios.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "meshcore_companion.h"
|
||||
|
||||
#define MAX_CHANNELS 64
|
||||
|
||||
/* ------------------------------------------------------------- collected state */
|
||||
typedef struct {
|
||||
int have_device, have_self, have_time;
|
||||
mc_device_info_t dev;
|
||||
mc_self_info_t self;
|
||||
uint32_t epoch;
|
||||
|
||||
int nchan; /* channels we queried */
|
||||
int chan_got[MAX_CHANNELS];
|
||||
mc_channel_info_t chan[MAX_CHANNELS];
|
||||
|
||||
int have_core, have_radio, have_packets;
|
||||
mc_stats_t core, radio, packets;
|
||||
} info_t;
|
||||
|
||||
/* ------------------------------------------------------------------ tty + I/O */
|
||||
static int tty_open(const char *path) {
|
||||
int fd = open(path, O_RDWR | O_NOCTTY | O_NONBLOCK);
|
||||
if (fd < 0) { fprintf(stderr, "open %s: %s\n", path, strerror(errno)); return -1; }
|
||||
struct termios tio;
|
||||
if (tcgetattr(fd, &tio) != 0) { fprintf(stderr, "tcgetattr: %s\n", strerror(errno)); close(fd); return -1; }
|
||||
cfmakeraw(&tio);
|
||||
cfsetispeed(&tio, B115200);
|
||||
cfsetospeed(&tio, B115200);
|
||||
tio.c_cflag |= (CLOCAL | CREAD);
|
||||
tio.c_cc[VMIN] = 0; tio.c_cc[VTIME] = 0;
|
||||
if (tcsetattr(fd, TCSANOW, &tio) != 0) { fprintf(stderr, "tcsetattr: %s\n", strerror(errno)); close(fd); return -1; }
|
||||
tcflush(fd, TCIOFLUSH);
|
||||
return fd;
|
||||
}
|
||||
|
||||
static void send_payload(int fd, const uint8_t *payload, size_t len) {
|
||||
uint8_t frame[MC_RX_BUFSZ];
|
||||
size_t flen = mc_frame_encode(payload, len, frame, sizeof frame);
|
||||
size_t off = 0;
|
||||
while (off < flen) {
|
||||
ssize_t w = write(fd, frame + off, flen - off);
|
||||
if (w < 0) { if (errno == EAGAIN || errno == EINTR) continue; break; }
|
||||
off += (size_t)w;
|
||||
}
|
||||
}
|
||||
|
||||
static long now_ms(void) {
|
||||
struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return ts.tv_sec * 1000L + ts.tv_nsec / 1000000L;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ event collection */
|
||||
static void handle(info_t *c, const mc_event_t *ev) {
|
||||
switch (ev->code) {
|
||||
case MC_RESP_DEVICE_INFO: c->dev = ev->u.device_info; c->have_device = 1; break;
|
||||
case MC_RESP_SELF_INFO: c->self = ev->u.self_info; c->have_self = 1; break;
|
||||
case MC_RESP_CURR_TIME: c->epoch = ev->u.curr_time; c->have_time = 1; break;
|
||||
case MC_RESP_CHANNEL_INFO: {
|
||||
uint8_t i = ev->u.channel_info.channel_idx;
|
||||
if (i < MAX_CHANNELS) { c->chan[i] = ev->u.channel_info; c->chan_got[i] = 1; }
|
||||
break;
|
||||
}
|
||||
case MC_RESP_STATS:
|
||||
if (ev->u.stats.subtype == MC_STATS_CORE) { c->core = ev->u.stats; c->have_core = 1; }
|
||||
else if (ev->u.stats.subtype == MC_STATS_RADIO) { c->radio = ev->u.stats; c->have_radio = 1; }
|
||||
else if (ev->u.stats.subtype == MC_STATS_PACKETS) { c->packets = ev->u.stats; c->have_packets = 1; }
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Pump the tty until `done(c)` is satisfied or `cap_ms` elapses. */
|
||||
static void pump(int fd, mc_rx_t *rx, info_t *c, int (*done)(const info_t *), long cap_ms) {
|
||||
long deadline = now_ms() + cap_ms;
|
||||
for (;;) {
|
||||
if (done && done(c)) return;
|
||||
long rem = deadline - now_ms();
|
||||
if (rem <= 0) return;
|
||||
|
||||
fd_set rf; FD_ZERO(&rf); FD_SET(fd, &rf);
|
||||
struct timeval tv = { rem / 1000, (rem % 1000) * 1000 };
|
||||
int r = select(fd + 1, &rf, NULL, NULL, &tv);
|
||||
if (r < 0) { if (errno == EINTR) continue; return; }
|
||||
if (r == 0) return; /* timed out */
|
||||
|
||||
uint8_t in[256];
|
||||
ssize_t got = read(fd, in, sizeof in);
|
||||
if (got <= 0) { if (got < 0 && (errno == EAGAIN || errno == EINTR)) continue; return; }
|
||||
mc_rx_feed(rx, in, (size_t)got);
|
||||
|
||||
uint8_t pl[MC_MAX_PAYLOAD]; size_t pn;
|
||||
while (mc_rx_poll(rx, pl, sizeof pl, &pn)) {
|
||||
mc_event_t ev;
|
||||
if (mc_parse(pl, pn, &ev)) handle(c, &ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int identity_ready(const info_t *c) { return c->have_device && c->have_self && c->have_time; }
|
||||
static int details_ready(const info_t *c) {
|
||||
for (int i = 0; i < c->nchan; i++) if (!c->chan_got[i]) return 0;
|
||||
return c->have_core && c->have_radio && c->have_packets;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------- output */
|
||||
static void print_report(const info_t *c) {
|
||||
if (!c->have_device && !c->have_self) {
|
||||
printf("No response from the radio. Is it the serial companion firmware,\n"
|
||||
"bound to this port at 115200 8N1?\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if (c->have_device) {
|
||||
const mc_device_info_t *d = &c->dev;
|
||||
printf("Device\n");
|
||||
printf(" model : %s\n", d->model);
|
||||
printf(" firmware ver : %d\n", d->fw_ver);
|
||||
if (d->ver[0]) printf(" version : %s\n", d->ver);
|
||||
if (d->build_date[0]) printf(" build date : %s\n", d->build_date);
|
||||
printf(" max channels : %u\n", d->max_channels);
|
||||
printf(" max contacts : %u\n", d->max_contacts);
|
||||
printf(" BLE pin : %u\n", d->ble_pin);
|
||||
if (d->have_repeat) printf(" repeater mode : %s\n", d->repeat ? "yes" : "no");
|
||||
if (d->have_path_hash) printf(" path hash mode: %u\n", d->path_hash_mode);
|
||||
}
|
||||
|
||||
if (c->have_self) {
|
||||
const mc_self_info_t *s = &c->self;
|
||||
printf("\nRadio / identity\n");
|
||||
printf(" name : %s\n", s->name);
|
||||
printf(" public key : ");
|
||||
for (int i = 0; i < 8; i++) printf("%02x", s->public_key[i]);
|
||||
printf("...\n");
|
||||
printf(" frequency : %.3f MHz\n", s->radio_freq / 1000.0);
|
||||
printf(" bandwidth : %.1f kHz\n", s->radio_bw / 1000.0);
|
||||
printf(" spreading : SF%u\n", s->radio_sf);
|
||||
printf(" coding rate : 4/%u\n", s->radio_cr);
|
||||
printf(" tx power : %u dBm (max %u)\n", s->tx_power, s->max_tx_power);
|
||||
if (s->adv_lat || s->adv_lon)
|
||||
printf(" advert loc : %.6f, %.6f\n", s->adv_lat / 1e6, s->adv_lon / 1e6);
|
||||
printf(" telemetry mode: base=%u loc=%u env=%u\n", s->tm_base, s->tm_loc, s->tm_env);
|
||||
printf(" multi acks : %u\n", s->multi_acks);
|
||||
}
|
||||
|
||||
if (c->have_time) {
|
||||
time_t t = (time_t)c->epoch;
|
||||
struct tm tmv; char buf[32] = "";
|
||||
if (gmtime_r(&t, &tmv)) strftime(buf, sizeof buf, "%Y-%m-%d %H:%M:%S UTC", &tmv);
|
||||
printf("\nClock\n device time : %u (%s)\n", c->epoch, buf);
|
||||
}
|
||||
|
||||
if (c->nchan > 0) {
|
||||
printf("\nChannels (%d)\n", c->nchan);
|
||||
for (int i = 0; i < c->nchan; i++) {
|
||||
if (!c->chan_got[i]) { printf(" [%2d] (no response)\n", i); continue; }
|
||||
const mc_channel_info_t *ch = &c->chan[i];
|
||||
int configured = ch->name[0] != 0;
|
||||
if (configured) { // can print unconfigured
|
||||
printf(" [%2d] %-16s psk=", i, configured ? ch->name : "(unconfigured)");
|
||||
if (ch->have_secret) { for (int k = 0; k < MC_SECRET_LEN; k++) printf("%02x", ch->secret[k]); }
|
||||
else printf("(none)");
|
||||
printf("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (c->have_core || c->have_radio || c->have_packets) {
|
||||
printf("\nStats\n");
|
||||
if (c->have_core)
|
||||
printf(" battery : %u mV uptime %u s errors %u queue %u\n",
|
||||
c->core.u.core.battery_mv, c->core.u.core.uptime_secs,
|
||||
c->core.u.core.errors, c->core.u.core.queue_len);
|
||||
if (c->have_radio)
|
||||
printf(" radio : noise %d last RSSI %d last SNR %.1f dB air tx %u s rx %u s\n",
|
||||
c->radio.u.radio.noise_floor, c->radio.u.radio.last_rssi,
|
||||
(double)MC_SNR_DB(c->radio.u.radio.last_snr_q4),
|
||||
c->radio.u.radio.tx_air_secs, c->radio.u.radio.rx_air_secs);
|
||||
if (c->have_packets) {
|
||||
const mc_stats_t *p = &c->packets;
|
||||
printf(" packets : recv %u sent %u flood tx/rx %u/%u direct tx/rx %u/%u",
|
||||
p->u.packets.recv, p->u.packets.sent, p->u.packets.flood_tx,
|
||||
p->u.packets.flood_rx, p->u.packets.direct_tx, p->u.packets.direct_rx);
|
||||
if (p->has_recv_errors) printf(" recv errors %u", p->u.packets.recv_errors);
|
||||
printf("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------- main */
|
||||
int main(int argc, char **argv) {
|
||||
if (argc < 2) {
|
||||
fprintf(stderr, "usage: %s <tty> e.g. /dev/ttyACM0, /dev/ttyUSB0\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
int fd = tty_open(argv[1]);
|
||||
if (fd < 0) return 1;
|
||||
|
||||
info_t c;
|
||||
memset(&c, 0, sizeof c);
|
||||
mc_rx_t rx; mc_rx_init(&rx);
|
||||
|
||||
uint8_t cmd[MC_MAX_PAYLOAD]; size_t n;
|
||||
|
||||
/* Identity: AppStart -> SelfInfo, DeviceQuery -> DeviceInfo, plus time. */
|
||||
n = mc_cmd_app_start(cmd, sizeof cmd, "meshcore_info"); if (n) send_payload(fd, cmd, n);
|
||||
n = mc_cmd_device_query(cmd, sizeof cmd, 1); if (n) send_payload(fd, cmd, n);
|
||||
n = mc_cmd_get_device_time(cmd, sizeof cmd); if (n) send_payload(fd, cmd, n);
|
||||
pump(fd, &rx, &c, identity_ready, 2000);
|
||||
|
||||
/* Now that we know the channel count, enumerate channels + ask for stats. */
|
||||
if (c.have_device) {
|
||||
c.nchan = c.dev.max_channels;
|
||||
if (c.nchan > MAX_CHANNELS) c.nchan = MAX_CHANNELS;
|
||||
for (int i = 0; i < c.nchan; i++) {
|
||||
n = mc_cmd_get_channel(cmd, sizeof cmd, (uint8_t)i);
|
||||
if (n) send_payload(fd, cmd, n);
|
||||
}
|
||||
}
|
||||
n = mc_cmd_get_stats(cmd, sizeof cmd, MC_STATS_CORE); if (n) send_payload(fd, cmd, n);
|
||||
n = mc_cmd_get_stats(cmd, sizeof cmd, MC_STATS_RADIO); if (n) send_payload(fd, cmd, n);
|
||||
n = mc_cmd_get_stats(cmd, sizeof cmd, MC_STATS_PACKETS); if (n) send_payload(fd, cmd, n);
|
||||
pump(fd, &rx, &c, details_ready, 3000);
|
||||
|
||||
print_report(&c);
|
||||
close(fd);
|
||||
return c.have_device || c.have_self ? 0 : 1;
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "MeshCoreCompanion",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"description": "Client library for the MeshCore Companion Radio serial protocol. Portable C99 core (no I/O, no malloc, host-testable) with a thin Arduino C++ wrapper that connects a host MCU to a separate MeshCore companion radio over UART/USB serial. Receive and send on channels, set channel PSKs, read SNR/path metadata.",
|
||||
"keywords": ["meshcore", "lora", "companion", "serial", "mesh", "sx1262", "esp32", "nrf52"],
|
||||
"authors": [
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
name=MeshCoreCompanion
|
||||
version=0.1.0
|
||||
version=0.2.0
|
||||
author=Scott Penrose
|
||||
maintainer=Scott Penrose
|
||||
sentence=Client for the MeshCore Companion Radio serial protocol.
|
||||
|
||||
@@ -102,6 +102,20 @@ void MeshCoreCompanion::sendChannelText(uint8_t idx, const char *text, uint32_t
|
||||
sendPayload(p, n);
|
||||
}
|
||||
|
||||
void MeshCoreCompanion::sendTextMessage(const uint8_t *dst, size_t dstLen, const char *text, uint32_t senderTs) {
|
||||
if (senderTs == 0) senderTs = deviceEpochNow();
|
||||
uint8_t p[1 + 1 + 1 + 4 + 32 + MC_MAX_TEXT];
|
||||
size_t n = mc_cmd_send_txt_msg(p, sizeof(p), MC_TXT_PLAIN, 0, senderTs, dst, dstLen, text);
|
||||
sendPayload(p, n);
|
||||
}
|
||||
|
||||
void MeshCoreCompanion::sendCommand(const uint8_t *dst, size_t dstLen, const char *cmd, uint32_t senderTs) {
|
||||
if (senderTs == 0) senderTs = deviceEpochNow();
|
||||
uint8_t p[1 + 1 + 1 + 4 + 32 + MC_MAX_TEXT];
|
||||
size_t n = mc_cmd_send_cmd(p, sizeof(p), senderTs, dst, dstLen, cmd);
|
||||
sendPayload(p, n);
|
||||
}
|
||||
|
||||
void MeshCoreCompanion::syncNextMessage() {
|
||||
uint8_t p[1]; sendPayload(p, mc_cmd_sync_next_message(p, sizeof(p)));
|
||||
}
|
||||
@@ -131,10 +145,17 @@ void MeshCoreCompanion::dispatch(const mc_event_t &ev) {
|
||||
case MC_RESP_CHANNEL_INFO:
|
||||
if (_onChanInfo) _onChanInfo(ev.u.channel_info);
|
||||
break;
|
||||
case MC_RESP_SENT:
|
||||
if (_onMsgSent) _onMsgSent(ev.u.msg_sent);
|
||||
break;
|
||||
case MC_RESP_STATS:
|
||||
if (_onStats) _onStats(ev.u.stats);
|
||||
break;
|
||||
case MC_PUSH_MSG_WAITING:
|
||||
if (_autoSync && !_draining) { _draining = true; syncNextMessage(); }
|
||||
break;
|
||||
case MC_RESP_CHANNEL_MSG_RECV:
|
||||
case MC_RESP_CHANNEL_MSG_RECV_V3:
|
||||
if (_onText) _onText(ev.u.channel_msg);
|
||||
if (_draining) syncNextMessage();
|
||||
break;
|
||||
@@ -143,6 +164,7 @@ void MeshCoreCompanion::dispatch(const mc_event_t &ev) {
|
||||
if (_draining) syncNextMessage();
|
||||
break;
|
||||
case MC_RESP_CONTACT_MSG_RECV:
|
||||
case MC_RESP_CONTACT_MSG_RECV_V3:
|
||||
if (_onContact) _onContact(ev.u.contact_msg);
|
||||
if (_draining) syncNextMessage();
|
||||
break;
|
||||
|
||||
@@ -27,6 +27,8 @@ public:
|
||||
using DeviceInfoCb = std::function<void(const mc_device_info_t&)>;
|
||||
using ChannelInfoCb = std::function<void(const mc_channel_info_t&)>;
|
||||
using SelfInfoCb = std::function<void(const mc_self_info_t&)>;
|
||||
using MsgSentCb = std::function<void(const mc_msg_sent_t&)>;
|
||||
using StatsCb = std::function<void(const mc_stats_t&)>;
|
||||
using EventCb = std::function<void(const mc_event_t&)>;
|
||||
|
||||
explicit MeshCoreCompanion(Stream &io) : _io(io) {}
|
||||
@@ -51,6 +53,10 @@ public:
|
||||
bool setChannelHexSecret(uint8_t idx, const char *name, const char *hex32);
|
||||
/* senderTs == 0 uses the tracked device time if known, else 0. */
|
||||
void sendChannelText(uint8_t idx, const char *text, uint32_t senderTs = 0);
|
||||
/* Direct message / CLI command to a contact (dst = pubkey or 6-byte prefix).
|
||||
* senderTs == 0 uses the tracked device time if known. */
|
||||
void sendTextMessage(const uint8_t *dst, size_t dstLen, const char *text, uint32_t senderTs = 0);
|
||||
void sendCommand(const uint8_t *dst, size_t dstLen, const char *cmd, uint32_t senderTs = 0);
|
||||
void syncNextMessage();
|
||||
void drainMessages(); /* start a manual sync-drain loop */
|
||||
void getStats(uint8_t statsType);
|
||||
@@ -69,6 +75,8 @@ public:
|
||||
void onDeviceInfo(DeviceInfoCb cb) { _onDevInfo = cb; }
|
||||
void onChannelInfo(ChannelInfoCb cb) { _onChanInfo = cb; }
|
||||
void onSelfInfo(SelfInfoCb cb) { _onSelfInfo = cb; }
|
||||
void onMsgSent(MsgSentCb cb) { _onMsgSent = cb; }
|
||||
void onStats(StatsCb cb) { _onStats = cb; }
|
||||
void onEvent(EventCb cb) { _onEvent = cb; } /* every parsed frame */
|
||||
|
||||
private:
|
||||
@@ -93,6 +101,8 @@ private:
|
||||
DeviceInfoCb _onDevInfo;
|
||||
ChannelInfoCb _onChanInfo;
|
||||
SelfInfoCb _onSelfInfo;
|
||||
MsgSentCb _onMsgSent;
|
||||
StatsCb _onStats;
|
||||
EventCb _onEvent;
|
||||
};
|
||||
|
||||
|
||||
+128
-23
@@ -34,6 +34,45 @@ static void copy_rest_string(char *dst, size_t dst_cap, const uint8_t *src, size
|
||||
dst[i] = 0;
|
||||
}
|
||||
|
||||
/* Channel text message body parser, shared by the base (code 8) and V3 (code
|
||||
* 17) forms. V3 prefixes the body with [snr:i8][2 reserved]; base has no SNR. */
|
||||
static int parse_channel_text(const uint8_t *b, size_t n, int v3, mc_channel_msg_t *m) {
|
||||
size_t lead = v3 ? 3u : 0u;
|
||||
if (n < lead + 7) return 0;
|
||||
const uint8_t *q = b + lead;
|
||||
size_t qn = n - lead;
|
||||
m->channel_idx = (int8_t)q[0];
|
||||
m->path_len = q[1];
|
||||
m->txt_type = q[2];
|
||||
m->sender_ts = get_u32(q + 3);
|
||||
m->snr_q4 = v3 ? (int8_t)b[0] : MC_SNR_NONE;
|
||||
copy_rest_string(m->text, sizeof(m->text), q, 7, qn);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Contact text message body parser, shared by the base (code 7) and V3 (code
|
||||
* 16) forms. A signature (4 bytes) precedes the text when txt_type is
|
||||
* MC_TXT_SIGNED_PLAIN; the base form previously mis-read this as text. */
|
||||
static int parse_contact_text(const uint8_t *b, size_t n, int v3, mc_contact_msg_t *m) {
|
||||
size_t lead = v3 ? 3u : 0u;
|
||||
if (n < lead + 12) return 0;
|
||||
const uint8_t *q = b + lead;
|
||||
size_t qn = n - lead;
|
||||
memcpy(m->pubkey_prefix, q, 6);
|
||||
m->path_len = q[6];
|
||||
m->txt_type = q[7];
|
||||
m->sender_ts = get_u32(q + 8);
|
||||
m->snr_q4 = v3 ? (int8_t)b[0] : MC_SNR_NONE;
|
||||
size_t toff = 12;
|
||||
if (m->txt_type == MC_TXT_SIGNED_PLAIN && qn >= toff + 4) {
|
||||
memcpy(m->signature, q + toff, 4);
|
||||
m->has_signature = 1;
|
||||
toff += 4;
|
||||
}
|
||||
copy_rest_string(m->text, sizeof(m->text), q, toff, qn);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ======================================================================== */
|
||||
void mc_rx_init(mc_rx_t *rx) { rx->len = 0; }
|
||||
|
||||
@@ -163,6 +202,27 @@ size_t mc_cmd_send_channel_text(uint8_t *out, size_t cap, uint8_t txt_type,
|
||||
return i;
|
||||
}
|
||||
|
||||
size_t mc_cmd_send_txt_msg(uint8_t *out, size_t cap, uint8_t txt_type,
|
||||
uint8_t attempt, uint32_t sender_ts,
|
||||
const uint8_t *dst, size_t dst_len, const char *text) {
|
||||
size_t tlen = text ? strlen(text) : 0;
|
||||
size_t total = 1 + 1 + 1 + 4 + dst_len + tlen;
|
||||
if (cap < total) return 0;
|
||||
size_t i = 0;
|
||||
out[i++] = MC_CMD_SEND_TXT_MSG;
|
||||
out[i++] = txt_type;
|
||||
out[i++] = attempt;
|
||||
put_u32(out + i, sender_ts); i += 4;
|
||||
if (dst_len) { memcpy(out + i, dst, dst_len); i += dst_len; }
|
||||
memcpy(out + i, text, tlen); i += tlen;
|
||||
return i;
|
||||
}
|
||||
|
||||
size_t mc_cmd_send_cmd(uint8_t *out, size_t cap, uint32_t sender_ts,
|
||||
const uint8_t *dst, size_t dst_len, const char *cmd) {
|
||||
return mc_cmd_send_txt_msg(out, cap, MC_TXT_CLI_DATA, 0, sender_ts, dst, dst_len, cmd);
|
||||
}
|
||||
|
||||
size_t mc_cmd_set_radio_params(uint8_t *out, size_t cap, uint32_t freq_hz_x1000,
|
||||
uint32_t bw, uint8_t sf, uint8_t cr) {
|
||||
if (cap < 11) return 0;
|
||||
@@ -208,9 +268,54 @@ int mc_parse(const uint8_t *p, size_t len, mc_event_t *ev) {
|
||||
ev->u.battery_mv = get_u16(b);
|
||||
return 1;
|
||||
|
||||
case MC_RESP_SENT:
|
||||
if (n < 9) return 0;
|
||||
ev->u.msg_sent.type = b[0];
|
||||
ev->u.msg_sent.expected_ack = get_u32(b + 1);
|
||||
ev->u.msg_sent.suggested_timeout = get_u32(b + 5);
|
||||
return 1;
|
||||
|
||||
case MC_RESP_STATS: {
|
||||
if (n < 1) return 0;
|
||||
mc_stats_t *st = &ev->u.stats;
|
||||
st->subtype = b[0];
|
||||
const uint8_t *q = b + 1; /* stat fields start at payload offset 2 */
|
||||
size_t qn = n - 1;
|
||||
switch (st->subtype) {
|
||||
case MC_STATS_CORE: /* <H I H B> */
|
||||
if (qn < 9) return 0;
|
||||
st->u.core.battery_mv = get_u16(q);
|
||||
st->u.core.uptime_secs = get_u32(q + 2);
|
||||
st->u.core.errors = get_u16(q + 6);
|
||||
st->u.core.queue_len = q[8];
|
||||
return 1;
|
||||
case MC_STATS_RADIO: /* <h b b I I> */
|
||||
if (qn < 12) return 0;
|
||||
st->u.radio.noise_floor = (int16_t)get_u16(q);
|
||||
st->u.radio.last_rssi = (int8_t)q[2];
|
||||
st->u.radio.last_snr_q4 = (int8_t)q[3];
|
||||
st->u.radio.tx_air_secs = get_u32(q + 4);
|
||||
st->u.radio.rx_air_secs = get_u32(q + 8);
|
||||
return 1;
|
||||
case MC_STATS_PACKETS: /* <I I I I I I> + opt I */
|
||||
if (qn < 24) return 0;
|
||||
st->u.packets.recv = get_u32(q + 0);
|
||||
st->u.packets.sent = get_u32(q + 4);
|
||||
st->u.packets.flood_tx = get_u32(q + 8);
|
||||
st->u.packets.direct_tx = get_u32(q + 12);
|
||||
st->u.packets.flood_rx = get_u32(q + 16);
|
||||
st->u.packets.direct_rx = get_u32(q + 20);
|
||||
if (qn >= 28) { st->u.packets.recv_errors = get_u32(q + 24); st->has_recv_errors = 1; }
|
||||
return 1;
|
||||
default:
|
||||
return 1; /* unknown subtype: subtype recorded, no fields */
|
||||
}
|
||||
}
|
||||
|
||||
case MC_RESP_DEVICE_INFO: {
|
||||
/* [fw_ver:i8][max_contacts/2:u8][max_channels:u8][ble_pin:u32]
|
||||
[build_date:cstr12][model:rest] */
|
||||
[build_date:cstr12][model:cstr40][ver:cstr20]
|
||||
[repeat:u8 (fw>=9)][path_hash_mode:u8 (fw>=10)] */
|
||||
if (n < 1) return 0;
|
||||
mc_device_info_t *d = &ev->u.device_info;
|
||||
d->fw_ver = (int8_t)b[0];
|
||||
@@ -221,31 +326,26 @@ int mc_parse(const uint8_t *p, size_t len, mc_event_t *ev) {
|
||||
d->ble_pin = get_u32(b + off); off += 4;
|
||||
}
|
||||
if (n >= off + 12) { copy_cstring(d->build_date, sizeof(d->build_date), b + off, 12); off += 12; }
|
||||
copy_rest_string(d->model, sizeof(d->model), b, off, n);
|
||||
/* model is a 40-byte field on fw>=3; tolerate short (older) frames. */
|
||||
{
|
||||
size_t mlen = (n >= off + 40) ? 40 : (n > off ? n - off : 0);
|
||||
copy_cstring(d->model, sizeof(d->model), b + off, mlen); off += mlen;
|
||||
}
|
||||
if (n >= off + 20) { copy_cstring(d->ver, sizeof(d->ver), b + off, 20); off += 20; }
|
||||
if (d->fw_ver >= 9 && n > off) { d->repeat = b[off++]; d->have_repeat = 1; }
|
||||
if (d->fw_ver >= 10 && n > off) { d->path_hash_mode = b[off++]; d->have_path_hash = 1; }
|
||||
return 1;
|
||||
}
|
||||
|
||||
case MC_RESP_CHANNEL_MSG_RECV: {
|
||||
if (n < 7) return 0;
|
||||
mc_channel_msg_t *m = &ev->u.channel_msg;
|
||||
m->channel_idx = (int8_t)b[0];
|
||||
m->path_len = b[1];
|
||||
m->txt_type = b[2];
|
||||
m->sender_ts = get_u32(b + 3);
|
||||
copy_rest_string(m->text, sizeof(m->text), b, 7, n);
|
||||
return 1;
|
||||
}
|
||||
case MC_RESP_CHANNEL_MSG_RECV:
|
||||
return parse_channel_text(b, n, 0, &ev->u.channel_msg);
|
||||
case MC_RESP_CHANNEL_MSG_RECV_V3:
|
||||
return parse_channel_text(b, n, 1, &ev->u.channel_msg);
|
||||
|
||||
case MC_RESP_CONTACT_MSG_RECV: {
|
||||
if (n < 12) return 0;
|
||||
mc_contact_msg_t *m = &ev->u.contact_msg;
|
||||
memcpy(m->pubkey_prefix, b, 6);
|
||||
m->path_len = b[6];
|
||||
m->txt_type = b[7];
|
||||
m->sender_ts = get_u32(b + 8);
|
||||
copy_rest_string(m->text, sizeof(m->text), b, 12, n);
|
||||
return 1;
|
||||
}
|
||||
case MC_RESP_CONTACT_MSG_RECV:
|
||||
return parse_contact_text(b, n, 0, &ev->u.contact_msg);
|
||||
case MC_RESP_CONTACT_MSG_RECV_V3:
|
||||
return parse_contact_text(b, n, 1, &ev->u.contact_msg);
|
||||
|
||||
case MC_RESP_CHANNEL_DATA_RECV: {
|
||||
if (n < 8) return 0;
|
||||
@@ -285,7 +385,12 @@ int mc_parse(const uint8_t *p, size_t len, mc_event_t *ev) {
|
||||
memcpy(s->public_key, b + 3, 32);
|
||||
s->adv_lat = (int32_t)get_u32(b + 35);
|
||||
s->adv_lon = (int32_t)get_u32(b + 39);
|
||||
/* b[43..45] reserved */
|
||||
s->multi_acks = b[43];
|
||||
s->adv_loc_policy = b[44];
|
||||
s->telemetry_mode = b[45];
|
||||
s->tm_base = (uint8_t)(b[45] & 0x3);
|
||||
s->tm_loc = (uint8_t)((b[45] >> 2) & 0x3);
|
||||
s->tm_env = (uint8_t)((b[45] >> 4) & 0x3);
|
||||
s->manual_add_contacts = b[46];
|
||||
s->radio_freq = get_u32(b + 47);
|
||||
s->radio_bw = get_u32(b + 51);
|
||||
|
||||
@@ -86,11 +86,16 @@ enum {
|
||||
MC_RESP_DEVICE_INFO = 13,
|
||||
MC_RESP_PRIVATE_KEY = 14,
|
||||
MC_RESP_DISABLED = 15,
|
||||
MC_RESP_CONTACT_MSG_RECV_V3 = 16, /* SNR-prefixed variant of code 7 */
|
||||
MC_RESP_CHANNEL_MSG_RECV_V3 = 17, /* SNR-prefixed variant of code 8 */
|
||||
MC_RESP_CHANNEL_INFO = 18,
|
||||
MC_RESP_STATS = 24,
|
||||
MC_RESP_CHANNEL_DATA_RECV= 27
|
||||
};
|
||||
|
||||
/* ---- GET_STATS subtypes (stats_type arg / first STATS payload byte) ---- */
|
||||
enum { MC_STATS_CORE = 0, MC_STATS_RADIO = 1, MC_STATS_PACKETS = 2 };
|
||||
|
||||
/* ---- Push codes (unsolicited, radio -> app) ---- */
|
||||
enum {
|
||||
MC_PUSH_ADVERT = 0x80,
|
||||
@@ -116,6 +121,8 @@ enum { MC_ADVERT_ZERO_HOP = 0, MC_ADVERT_FLOOD = 1 };
|
||||
#define MC_PATH_DIRECT 0xFF /* path_len value meaning "received direct" */
|
||||
/* SNR is transmitted as a signed int8 scaled x4. Recover dB with this. */
|
||||
#define MC_SNR_DB(q4) ((float)(q4) / 4.0f)
|
||||
/* snr_q4 sentinel for non-V3 messages that carry no SNR. */
|
||||
#define MC_SNR_NONE ((int8_t)-128)
|
||||
|
||||
/* ======================================================================== *
|
||||
* Receive side: streaming frame assembler
|
||||
@@ -162,6 +169,16 @@ size_t mc_cmd_set_channel (uint8_t *out, size_t cap, uint8_t channel_idx,
|
||||
size_t mc_cmd_send_channel_text(uint8_t *out, size_t cap, uint8_t txt_type,
|
||||
uint8_t channel_idx, uint32_t sender_ts,
|
||||
const char *text);
|
||||
/* Direct (contact) text/command message (cmd 2). `dst` is the destination's
|
||||
* public key or 6-byte prefix (dst_len bytes, usually 6). */
|
||||
size_t mc_cmd_send_txt_msg (uint8_t *out, size_t cap, uint8_t txt_type,
|
||||
uint8_t attempt, uint32_t sender_ts,
|
||||
const uint8_t *dst, size_t dst_len,
|
||||
const char *text);
|
||||
/* CLI command to a repeater/companion: cmd 2, txt_type=MC_TXT_CLI_DATA, attempt 0. */
|
||||
size_t mc_cmd_send_cmd (uint8_t *out, size_t cap, uint32_t sender_ts,
|
||||
const uint8_t *dst, size_t dst_len,
|
||||
const char *cmd);
|
||||
size_t mc_cmd_set_radio_params (uint8_t *out, size_t cap, uint32_t freq_hz_x1000,
|
||||
uint32_t bw, uint8_t sf, uint8_t cr);
|
||||
size_t mc_cmd_get_stats (uint8_t *out, size_t cap, uint8_t stats_type);
|
||||
@@ -176,6 +193,11 @@ typedef struct {
|
||||
uint32_t ble_pin;
|
||||
char build_date[16];
|
||||
char model[MC_MAX_MODEL];
|
||||
char ver[24]; /* firmware version string (fw_ver>=3) */
|
||||
uint8_t repeat; /* repeater mode (fw_ver>=9); see have_repeat */
|
||||
uint8_t path_hash_mode; /* fw_ver>=10; see have_path_hash */
|
||||
int have_repeat;
|
||||
int have_path_hash;
|
||||
} mc_device_info_t;
|
||||
|
||||
typedef struct {
|
||||
@@ -183,6 +205,7 @@ typedef struct {
|
||||
uint8_t path_len; /* MC_PATH_DIRECT or flood hop count */
|
||||
uint8_t txt_type;
|
||||
uint32_t sender_ts;
|
||||
int8_t snr_q4; /* V3 only (code 17); MC_SNR_NONE otherwise */
|
||||
char text[MC_MAX_TEXT]; /* for channel msgs this is "Name: body" */
|
||||
} mc_channel_msg_t;
|
||||
|
||||
@@ -200,6 +223,9 @@ typedef struct {
|
||||
uint8_t path_len;
|
||||
uint8_t txt_type;
|
||||
uint32_t sender_ts;
|
||||
int8_t snr_q4; /* V3 only (code 16); MC_SNR_NONE otherwise */
|
||||
uint8_t signature[4]; /* present when txt_type==MC_TXT_SIGNED_PLAIN */
|
||||
int has_signature;
|
||||
char text[MC_MAX_TEXT];
|
||||
} mc_contact_msg_t;
|
||||
|
||||
@@ -213,13 +239,34 @@ typedef struct {
|
||||
typedef struct {
|
||||
uint8_t type, tx_power, max_tx_power;
|
||||
uint8_t public_key[32];
|
||||
int32_t adv_lat, adv_lon;
|
||||
int32_t adv_lat, adv_lon; /* degrees x 1e6 */
|
||||
uint8_t multi_acks;
|
||||
uint8_t adv_loc_policy;
|
||||
uint8_t telemetry_mode; /* raw byte; decoded below */
|
||||
uint8_t tm_base, tm_loc, tm_env; /* 2-bit fields: base=t&3, loc=(t>>2)&3, env=(t>>4)&3 */
|
||||
uint8_t manual_add_contacts;
|
||||
uint32_t radio_freq, radio_bw;
|
||||
uint8_t radio_sf, radio_cr;
|
||||
char name[MC_MAX_TEXT];
|
||||
} mc_self_info_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t type; /* result/type byte */
|
||||
uint32_t expected_ack; /* 4-byte ack tag (LE) to match an ACK push */
|
||||
uint32_t suggested_timeout; /* ms to wait for the ACK */
|
||||
} mc_msg_sent_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t subtype; /* MC_STATS_CORE / RADIO / PACKETS */
|
||||
int has_recv_errors; /* PACKETS only: recv_errors field present */
|
||||
union {
|
||||
struct { uint16_t battery_mv; uint32_t uptime_secs; uint16_t errors; uint8_t queue_len; } core;
|
||||
struct { int16_t noise_floor; int8_t last_rssi; int8_t last_snr_q4;
|
||||
uint32_t tx_air_secs, rx_air_secs; } radio;
|
||||
struct { uint32_t recv, sent, flood_tx, direct_tx, flood_rx, direct_rx, recv_errors; } packets;
|
||||
} u;
|
||||
} mc_stats_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t code; /* response or push code (first payload byte) */
|
||||
union {
|
||||
@@ -229,6 +276,8 @@ typedef struct {
|
||||
mc_contact_msg_t contact_msg;
|
||||
mc_channel_info_t channel_info;
|
||||
mc_self_info_t self_info;
|
||||
mc_msg_sent_t msg_sent; /* MC_RESP_SENT */
|
||||
mc_stats_t stats; /* MC_RESP_STATS */
|
||||
uint32_t curr_time; /* epoch secs */
|
||||
uint16_t battery_mv;
|
||||
int8_t err_code; /* MC_RESP_ERR (-1 if absent) */
|
||||
|
||||
@@ -21,6 +21,21 @@ static size_t make_inbound(uint8_t *out, const uint8_t *payload, size_t plen) {
|
||||
return plen + 3;
|
||||
}
|
||||
|
||||
/* little-endian writers for building test payloads */
|
||||
static void le16(uint8_t *p, uint16_t v) { p[0] = (uint8_t)v; p[1] = (uint8_t)(v >> 8); }
|
||||
static void le32(uint8_t *p, uint32_t v) {
|
||||
p[0] = (uint8_t)v; p[1] = (uint8_t)(v >> 8); p[2] = (uint8_t)(v >> 16); p[3] = (uint8_t)(v >> 24);
|
||||
}
|
||||
|
||||
/* Parse a freshly-built payload through the rx assembler; returns mc_parse result. */
|
||||
static int feed_parse(mc_rx_t *rx, uint8_t *frame, uint8_t *scratch, size_t scap,
|
||||
const uint8_t *payload, size_t plen, mc_event_t *ev) {
|
||||
size_t flen = make_inbound(frame, payload, plen), olen = 0;
|
||||
mc_rx_feed(rx, frame, flen);
|
||||
if (!mc_rx_poll(rx, scratch, scap, &olen)) return 0;
|
||||
return mc_parse(scratch, olen, ev);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
uint8_t scratch[512], frame[512], payload[300];
|
||||
size_t plen, flen, olen;
|
||||
@@ -110,6 +125,173 @@ int main(void) {
|
||||
ev.u.channel_data.data_len == 4 && ev.u.channel_data.data[0] == 0xDE &&
|
||||
ev.u.channel_data.data[3] == 0xEF, "channel_data payload");
|
||||
|
||||
printf("== parse: MSG_SENT ==\n");
|
||||
{
|
||||
size_t j = 0;
|
||||
payload[j++] = MC_RESP_SENT;
|
||||
payload[j++] = 1; /* type */
|
||||
le32(payload + j, 0x11223344); j += 4; /* expected_ack */
|
||||
le32(payload + j, 5000); j += 4; /* suggested_timeout */
|
||||
CHECK(feed_parse(&rx, frame, scratch, sizeof scratch, payload, j, &ev) == 1 &&
|
||||
ev.code == MC_RESP_SENT, "parse msg_sent");
|
||||
CHECK(ev.u.msg_sent.type == 1 && ev.u.msg_sent.expected_ack == 0x11223344u &&
|
||||
ev.u.msg_sent.suggested_timeout == 5000u, "msg_sent fields");
|
||||
}
|
||||
|
||||
printf("== parse: STATS (core/radio/packets) ==\n");
|
||||
{
|
||||
size_t j = 0;
|
||||
payload[j++] = MC_RESP_STATS; payload[j++] = MC_STATS_CORE;
|
||||
le16(payload + j, 4200); j += 2; /* battery_mv */
|
||||
le32(payload + j, 86400); j += 4; /* uptime_secs */
|
||||
le16(payload + j, 3); j += 2; /* errors */
|
||||
payload[j++] = 7; /* queue_len */
|
||||
CHECK(feed_parse(&rx, frame, scratch, sizeof scratch, payload, j, &ev) == 1 &&
|
||||
ev.u.stats.subtype == MC_STATS_CORE && ev.u.stats.u.core.battery_mv == 4200 &&
|
||||
ev.u.stats.u.core.uptime_secs == 86400 && ev.u.stats.u.core.errors == 3 &&
|
||||
ev.u.stats.u.core.queue_len == 7, "stats core");
|
||||
|
||||
j = 0;
|
||||
payload[j++] = MC_RESP_STATS; payload[j++] = MC_STATS_RADIO;
|
||||
le16(payload + j, (uint16_t)(int16_t)-120); j += 2; /* noise_floor i16 */
|
||||
payload[j++] = (uint8_t)(int8_t)-90; /* last_rssi i8 */
|
||||
payload[j++] = 40; /* last_snr_q4 */
|
||||
le32(payload + j, 1000); j += 4; /* tx_air_secs */
|
||||
le32(payload + j, 2000); j += 4; /* rx_air_secs */
|
||||
CHECK(feed_parse(&rx, frame, scratch, sizeof scratch, payload, j, &ev) == 1 &&
|
||||
ev.u.stats.subtype == MC_STATS_RADIO && ev.u.stats.u.radio.noise_floor == -120 &&
|
||||
ev.u.stats.u.radio.last_rssi == -90 && ev.u.stats.u.radio.last_snr_q4 == 40 &&
|
||||
ev.u.stats.u.radio.tx_air_secs == 1000 && ev.u.stats.u.radio.rx_air_secs == 2000,
|
||||
"stats radio (signed fields)");
|
||||
|
||||
j = 0;
|
||||
payload[j++] = MC_RESP_STATS; payload[j++] = MC_STATS_PACKETS;
|
||||
le32(payload + j, 10); j += 4; le32(payload + j, 20); j += 4;
|
||||
le32(payload + j, 1); j += 4; le32(payload + j, 2); j += 4;
|
||||
le32(payload + j, 3); j += 4; le32(payload + j, 4); j += 4;
|
||||
le32(payload + j, 5); j += 4; /* recv_errors */
|
||||
CHECK(feed_parse(&rx, frame, scratch, sizeof scratch, payload, j, &ev) == 1 &&
|
||||
ev.u.stats.u.packets.recv == 10 && ev.u.stats.u.packets.direct_rx == 4 &&
|
||||
ev.u.stats.has_recv_errors && ev.u.stats.u.packets.recv_errors == 5,
|
||||
"stats packets (+recv_errors)");
|
||||
}
|
||||
|
||||
printf("== parse: SELF_INFO extended fields ==\n");
|
||||
{
|
||||
size_t j = 0;
|
||||
payload[j++] = MC_RESP_SELF_INFO;
|
||||
payload[j++] = 1; payload[j++] = 22; payload[j++] = 30; /* type, txp, maxtxp */
|
||||
for (int i = 0; i < 32; i++) payload[j++] = (uint8_t)i; /* public_key */
|
||||
le32(payload + j, (uint32_t)(int32_t)-37000000); j += 4; /* adv_lat */
|
||||
le32(payload + j, (uint32_t)(int32_t)145000000); j += 4; /* adv_lon */
|
||||
payload[j++] = 1; /* multi_acks */
|
||||
payload[j++] = 2; /* adv_loc_policy */
|
||||
payload[j++] = 0x36; /* telemetry_mode: base=2 loc=1 env=3 */
|
||||
payload[j++] = 1; /* manual_add_contacts */
|
||||
le32(payload + j, 915000); j += 4; /* radio_freq */
|
||||
le32(payload + j, 250000); j += 4; /* radio_bw */
|
||||
payload[j++] = 11; /* radio_sf */
|
||||
payload[j++] = 5; /* radio_cr */
|
||||
memcpy(payload + j, "node", 4); j += 4;
|
||||
CHECK(feed_parse(&rx, frame, scratch, sizeof scratch, payload, j, &ev) == 1 &&
|
||||
ev.code == MC_RESP_SELF_INFO, "parse self_info");
|
||||
CHECK(ev.u.self_info.multi_acks == 1 && ev.u.self_info.adv_loc_policy == 2 &&
|
||||
ev.u.self_info.telemetry_mode == 0x36 && ev.u.self_info.tm_base == 2 &&
|
||||
ev.u.self_info.tm_loc == 1 && ev.u.self_info.tm_env == 3,
|
||||
"self_info multi_acks/loc_policy/telemetry split");
|
||||
CHECK(ev.u.self_info.adv_lat == -37000000 && ev.u.self_info.adv_lon == 145000000 &&
|
||||
ev.u.self_info.manual_add_contacts == 1 && ev.u.self_info.radio_freq == 915000 &&
|
||||
ev.u.self_info.radio_bw == 250000 && ev.u.self_info.radio_sf == 11 &&
|
||||
ev.u.self_info.radio_cr == 5 && strcmp(ev.u.self_info.name, "node") == 0,
|
||||
"self_info numeric + name");
|
||||
}
|
||||
|
||||
printf("== parse: CHANNEL_MSG_RECV_V3 (SNR) + base sentinel ==\n");
|
||||
{
|
||||
size_t j = 0;
|
||||
payload[j++] = MC_RESP_CHANNEL_MSG_RECV_V3;
|
||||
payload[j++] = 40; /* SNR q4 */
|
||||
payload[j++] = 0; payload[j++] = 0; /* reserved */
|
||||
payload[j++] = 2; /* channel_idx */
|
||||
payload[j++] = MC_PATH_DIRECT; /* path_len */
|
||||
payload[j++] = MC_TXT_PLAIN; /* txt_type */
|
||||
le32(payload + j, 1700000000u); j += 4; /* sender_ts */
|
||||
memcpy(payload + j, "Alice: hi", 9); j += 9;
|
||||
CHECK(feed_parse(&rx, frame, scratch, sizeof scratch, payload, j, &ev) == 1 &&
|
||||
ev.code == MC_RESP_CHANNEL_MSG_RECV_V3 && ev.u.channel_msg.snr_q4 == 40 &&
|
||||
ev.u.channel_msg.channel_idx == 2 && ev.u.channel_msg.path_len == MC_PATH_DIRECT &&
|
||||
ev.u.channel_msg.sender_ts == 1700000000u &&
|
||||
strcmp(ev.u.channel_msg.text, "Alice: hi") == 0, "channel_msg_v3 fields + SNR");
|
||||
|
||||
j = 0;
|
||||
payload[j++] = MC_RESP_CHANNEL_MSG_RECV;
|
||||
payload[j++] = 0; /* channel_idx */
|
||||
payload[j++] = 0; /* path_len */
|
||||
payload[j++] = MC_TXT_PLAIN; /* txt_type */
|
||||
le32(payload + j, 1); j += 4; memcpy(payload + j, "B: yo", 5); j += 5;
|
||||
CHECK(feed_parse(&rx, frame, scratch, sizeof scratch, payload, j, &ev) == 1 &&
|
||||
ev.u.channel_msg.snr_q4 == MC_SNR_NONE && strcmp(ev.u.channel_msg.text, "B: yo") == 0,
|
||||
"channel_msg base has MC_SNR_NONE");
|
||||
}
|
||||
|
||||
printf("== parse: CONTACT_MSG_RECV_V3 + signature skip ==\n");
|
||||
{
|
||||
size_t j = 0;
|
||||
payload[j++] = MC_RESP_CONTACT_MSG_RECV_V3;
|
||||
payload[j++] = (uint8_t)(int8_t)-8; /* SNR q4 */
|
||||
payload[j++] = 0; payload[j++] = 0; /* reserved */
|
||||
for (int i = 0; i < 6; i++) payload[j++] = (uint8_t)(0xC0 + i); /* pubkey_prefix */
|
||||
payload[j++] = 1; /* path_len */
|
||||
payload[j++] = MC_TXT_SIGNED_PLAIN; /* txt_type=2 */
|
||||
le32(payload + j, 1700000001u); j += 4; /* sender_ts */
|
||||
payload[j++]=0xAA; payload[j++]=0xBB; payload[j++]=0xCC; payload[j++]=0xDD; /* signature */
|
||||
memcpy(payload + j, "signed!", 7); j += 7;
|
||||
CHECK(feed_parse(&rx, frame, scratch, sizeof scratch, payload, j, &ev) == 1 &&
|
||||
ev.code == MC_RESP_CONTACT_MSG_RECV_V3 && ev.u.contact_msg.snr_q4 == -8 &&
|
||||
ev.u.contact_msg.txt_type == MC_TXT_SIGNED_PLAIN && ev.u.contact_msg.has_signature &&
|
||||
ev.u.contact_msg.signature[0] == 0xAA && ev.u.contact_msg.signature[3] == 0xDD &&
|
||||
ev.u.contact_msg.pubkey_prefix[0] == 0xC0 &&
|
||||
strcmp(ev.u.contact_msg.text, "signed!") == 0,
|
||||
"contact_msg_v3 signed: SNR/sig/text");
|
||||
}
|
||||
|
||||
printf("== parse: DEVICE_INFO with ver/repeat/path_hash (fw=10) ==\n");
|
||||
{
|
||||
size_t j = 0;
|
||||
payload[j++] = MC_RESP_DEVICE_INFO;
|
||||
payload[j++] = 10; /* fw_ver */
|
||||
payload[j++] = 50; /* max_contacts/2 */
|
||||
payload[j++] = 8; /* max_channels */
|
||||
le32(payload + j, 123456); j += 4; /* ble_pin */
|
||||
memset(payload + j, 0, 12); memcpy(payload + j, "1 Jan 2026", 10); j += 12;
|
||||
memset(payload + j, 0, 40); memcpy(payload + j, "Heltec V3", 9); j += 40;
|
||||
memset(payload + j, 0, 20); memcpy(payload + j, "v1.7.0", 6); j += 20;
|
||||
payload[j++] = 1; /* repeat (fw>=9) */
|
||||
payload[j++] = 2; /* path_hash_mode (fw>=10) */
|
||||
CHECK(feed_parse(&rx, frame, scratch, sizeof scratch, payload, j, &ev) == 1 &&
|
||||
ev.u.device_info.fw_ver == 10 && ev.u.device_info.max_contacts == 100 &&
|
||||
strcmp(ev.u.device_info.model, "Heltec V3") == 0 &&
|
||||
strcmp(ev.u.device_info.ver, "v1.7.0") == 0 &&
|
||||
ev.u.device_info.have_repeat && ev.u.device_info.repeat == 1 &&
|
||||
ev.u.device_info.have_path_hash && ev.u.device_info.path_hash_mode == 2,
|
||||
"device_info ver/repeat/path_hash");
|
||||
}
|
||||
|
||||
printf("== build: send_txt_msg / send_cmd ==\n");
|
||||
{
|
||||
uint8_t dst[6] = { 1, 2, 3, 4, 5, 6 };
|
||||
plen = mc_cmd_send_txt_msg(scratch, sizeof scratch, MC_TXT_PLAIN, 0, 1700000000u, dst, 6, "hi");
|
||||
CHECK(plen == 1 + 1 + 1 + 4 + 6 + 2 && scratch[0] == MC_CMD_SEND_TXT_MSG &&
|
||||
scratch[1] == MC_TXT_PLAIN && scratch[2] == 0, "send_txt_msg header");
|
||||
CHECK(scratch[3] == 0x00 && scratch[4] == 0xF1 && scratch[5] == 0x53 && scratch[6] == 0x65 &&
|
||||
scratch[7] == 1 && scratch[12] == 6 && scratch[13] == 'h' && scratch[14] == 'i',
|
||||
"send_txt_msg ts LE + dst + body");
|
||||
plen = mc_cmd_send_cmd(scratch, sizeof scratch, 1700000000u, dst, 6, "reboot");
|
||||
CHECK(plen == 1 + 1 + 1 + 4 + 6 + 6 && scratch[0] == MC_CMD_SEND_TXT_MSG &&
|
||||
scratch[1] == MC_TXT_CLI_DATA && scratch[2] == 0 && scratch[13] == 'r',
|
||||
"send_cmd uses CLI txt_type");
|
||||
}
|
||||
|
||||
printf("== resync: garbage before a valid frame ==\n");
|
||||
uint8_t junk[3] = { 0x00, 0x99, 0x01 };
|
||||
mc_rx_feed(&rx, junk, 3);
|
||||
|
||||
Executable
+108
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# testall.sh -- run every test suite available on this machine.
|
||||
#
|
||||
# * CMake + ctest (host build of the C core + the codec unit test)
|
||||
# * pio test -e native (the same unit test via PlatformIO's native env)
|
||||
# * pio ci (compile an Arduino example for an ESP32-S3 board)
|
||||
#
|
||||
# Each suite is optional: if a tool is missing the suite is skipped with a
|
||||
# warning, so this works on a cmake-only box, a PlatformIO-only box, or both.
|
||||
# Exit status is non-zero if any suite that DID run failed (or if nothing could
|
||||
# run at all).
|
||||
#
|
||||
# ./testall.sh
|
||||
# ARDUINO_BOARD=esp32dev ./testall.sh # override the board
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
set -u
|
||||
cd "$(dirname "$0")" || exit 2
|
||||
|
||||
ARDUINO_BOARD="${ARDUINO_BOARD:-esp32-s3-devkitc-1}"
|
||||
ARDUINO_EXAMPLE="examples/SensorChannelBridge/SensorChannelBridge.ino"
|
||||
|
||||
ran=0 failed=0 skipped=0
|
||||
results=()
|
||||
|
||||
have() { command -v "$1" >/dev/null 2>&1; }
|
||||
section() { printf '\n\033[1m==== %s ====\033[0m\n' "$1"; }
|
||||
warn() { printf '\033[33mWARN:\033[0m %s\n' "$*" >&2; }
|
||||
|
||||
# run_suite <label> <function-name>
|
||||
run_suite() {
|
||||
local label="$1" fn="$2"
|
||||
section "$label"
|
||||
if "$fn"; then
|
||||
results+=("PASS $label"); ran=$((ran + 1))
|
||||
else
|
||||
results+=("FAIL $label"); ran=$((ran + 1)); failed=$((failed + 1))
|
||||
fi
|
||||
}
|
||||
skip_suite() {
|
||||
results+=("SKIP $1 ($2)"); skipped=$((skipped + 1))
|
||||
warn "$1 skipped: $2"
|
||||
}
|
||||
|
||||
# Pick whichever PlatformIO CLI name exists.
|
||||
PIO=""
|
||||
if have pio; then PIO=pio
|
||||
elif have platformio; then PIO=platformio
|
||||
fi
|
||||
|
||||
# ---- Suite definitions -----------------------------------------------------
|
||||
|
||||
suite_cmake() {
|
||||
cmake -B build -S . || return 1
|
||||
cmake --build build || return 1
|
||||
if have ctest; then
|
||||
ctest --test-dir build --output-on-failure || return 1
|
||||
else
|
||||
warn "ctest not found -- built the core but did not run the unit test"
|
||||
fi
|
||||
}
|
||||
|
||||
suite_pio_native() {
|
||||
"$PIO" test -e native
|
||||
}
|
||||
|
||||
suite_pio_arduino() {
|
||||
"$PIO" ci --board="$ARDUINO_BOARD" \
|
||||
--project-option="framework=arduino" \
|
||||
--lib="." "$ARDUINO_EXAMPLE"
|
||||
}
|
||||
|
||||
# ---- Run what's available --------------------------------------------------
|
||||
|
||||
if have cmake; then
|
||||
run_suite "CMake build + ctest" suite_cmake
|
||||
else
|
||||
skip_suite "CMake build + ctest" "cmake not installed"
|
||||
fi
|
||||
|
||||
if [ -n "$PIO" ]; then
|
||||
run_suite "PlatformIO native test" suite_pio_native
|
||||
run_suite "Arduino compile ($ARDUINO_BOARD)" suite_pio_arduino
|
||||
else
|
||||
skip_suite "PlatformIO native test" "pio/platformio not installed"
|
||||
skip_suite "Arduino compile" "pio/platformio not installed"
|
||||
fi
|
||||
|
||||
# ---- Summary ---------------------------------------------------------------
|
||||
|
||||
section "Summary"
|
||||
for r in "${results[@]}"; do
|
||||
case "$r" in
|
||||
PASS*) printf '\033[32m%s\033[0m\n' "$r" ;;
|
||||
FAIL*) printf '\033[31m%s\033[0m\n' "$r" ;;
|
||||
*) printf '\033[33m%s\033[0m\n' "$r" ;;
|
||||
esac
|
||||
done
|
||||
printf '%d ran, %d failed, %d skipped\n' "$ran" "$failed" "$skipped"
|
||||
|
||||
if [ "$ran" -eq 0 ]; then
|
||||
warn "no test tools found (install cmake and/or platformio)"
|
||||
exit 2
|
||||
fi
|
||||
[ "$failed" -eq 0 ] || exit 1
|
||||
exit 0
|
||||
Reference in New Issue
Block a user