Better examples
This commit is contained in:
@@ -1,136 +1,52 @@
|
|||||||
# meshcore_c
|
# MeshCoreCompanion
|
||||||
|
|
||||||
A client for the **MeshCore Companion Radio** serial protocol, for when the radio
|
A client library for the **MeshCore Companion Radio** serial protocol, for when
|
||||||
lives on one device and your application (display, sensor hub, gateway, desktop
|
the radio lives on one MCU and your application (display, sensor hub, gateway)
|
||||||
tool) lives on another. Talk to a separate companion radio — e.g. a Seeed XIAO +
|
lives on another. Talk to a separate companion radio — e.g. a Seeed XIAO +
|
||||||
Wio-SX1262 — over a UART / USB-serial link and exchange channel messages, set
|
Wio-SX1262 — over a UART/USB serial link and exchange channel messages, set
|
||||||
channel PSKs, and read link metadata. Your host plays the *client*, the companion
|
channel PSKs, and read link metadata.
|
||||||
radio plays the *server* — the inverse of the phone/web app.
|
|
||||||
|
|
||||||
This is the **C / C++** one. Sibling implementations in other languages:
|
This is the inverse of the phone/web app: your host MCU plays the *client*, the
|
||||||
|
companion radio plays the *server*.
|
||||||
|
|
||||||
- **JavaScript** — [`meshcore.js`](https://github.com/meshcore-dev/meshcore.js)
|
## Design: portable C core + Arduino wrapper
|
||||||
- **Python** — [`meshcore_py`](https://github.com/meshcore-dev/meshcore_py)
|
|
||||||
- **Rust** — [`meshcore-rs`](https://github.com/andrewdavidmackenzie/meshcore-rs)
|
|
||||||
|
|
||||||
## Two ways to use this repo
|
Two layers, so the protocol is reusable and testable far beyond Arduino:
|
||||||
|
|
||||||
The protocol logic is a single portable **C99 core** with no I/O, no `malloc`, and
|
- **`meshcore_companion.h/.c`** — a portable **C99** core. No I/O, no `malloc`,
|
||||||
no Arduino dependency. Everything else is a thin layer over it, so one repo serves
|
no Arduino. It assembles inbound frames from a byte stream, builds outbound
|
||||||
two audiences from **one source of truth** (`src/meshcore_companion.{c,h}`):
|
command frames into buffers you own, and parses payloads into plain structs.
|
||||||
|
It compiles and runs on a desktop for unit testing (see `test/`), and drops
|
||||||
|
into ESP-IDF, bare nRF52/STM32, or a host bridge unchanged.
|
||||||
|
- **`MeshCoreCompanion.h/.cpp`** — a thin C++ wrapper. Inject any `Stream`
|
||||||
|
(`Serial1` on a Grove UART, USB CDC, `SoftwareSerial`), call `loop()` often,
|
||||||
|
register lambda callbacks. It also **auto-drains** the radio's queue: on the
|
||||||
|
`MsgWaiting` push it loops `SyncNextMessage` until empty, so you just receive
|
||||||
|
`onChannelMessage(...)`.
|
||||||
|
|
||||||
1. **Portable C library** — drop `src/meshcore_companion.{c,h}` into any project.
|
Transport is injected; the core never touches a port. "Physical serial only" is
|
||||||
It assembles inbound frames from a byte stream, builds outbound command frames
|
simply which `Stream` you hand the wrapper.
|
||||||
into buffers you own, and parses payloads into plain structs. You supply the
|
|
||||||
transport. The same core drops into Linux, ESP-IDF, bare STM32/nRF52, or any
|
|
||||||
host bridge unchanged — see the example for each platform under `examples-*/`,
|
|
||||||
plus the host unit test in `test/` that runs with no hardware.
|
|
||||||
|
|
||||||
2. **C++ Arduino library** — `src/MeshCoreCompanion.{h,cpp}` wrap the core in an
|
|
||||||
Arduino-friendly class: inject any `Stream` (`Serial1` on a Grove UART, USB
|
|
||||||
CDC, `SoftwareSerial`), call `loop()` often, register lambda callbacks. It
|
|
||||||
**auto-drains** the radio's queue: on the `MsgWaiting` push it loops
|
|
||||||
`SyncNextMessage` until empty, so you just receive `onChannelMessage(...)`.
|
|
||||||
The repo root is a publishable Arduino / PlatformIO library
|
|
||||||
(`library.properties` + `library.json`).
|
|
||||||
|
|
||||||
## Layout
|
|
||||||
|
|
||||||
```
|
|
||||||
meshcore_c/
|
|
||||||
├── library.properties # Arduino IDE / Library Manager metadata
|
|
||||||
├── library.json # PlatformIO metadata
|
|
||||||
├── CMakeLists.txt # Linux / native (host) build of the C core + examples + test
|
|
||||||
├── src/
|
|
||||||
│ ├── meshcore_companion.h/.c # portable C99 core — single source of truth
|
|
||||||
│ └── MeshCoreCompanion.h/.cpp # C++ Arduino wrapper (built by Arduino/PlatformIO)
|
|
||||||
├── examples/
|
|
||||||
│ └── SensorChannelBridge/ # Arduino sketch (.ino)
|
|
||||||
├── examples-linux/
|
|
||||||
│ └── tty_bridge/ # portable C: any Linux tty (USB or raw UART)
|
|
||||||
├── examples-esp-idf/
|
|
||||||
│ └── tty_bridge/ # portable C: ESP-IDF UART driver (esp32/s3/c3)
|
|
||||||
├── examples-stm32/
|
|
||||||
│ └── uart_bridge/ # portable C: STM32 HAL UART drop-in
|
|
||||||
└── test/
|
|
||||||
└── test_codec.c # host unit test (no hardware)
|
|
||||||
```
|
|
||||||
|
|
||||||
The portable-C examples all compile the *same* `src/meshcore_companion.c`; only
|
|
||||||
the byte transport differs per platform.
|
|
||||||
|
|
||||||
The Arduino/PlatformIO build only compiles `src/` (the manifests' `srcFilter` is
|
|
||||||
scoped there), so the Linux example and host test never enter a firmware build.
|
|
||||||
The CMake build only compiles the pure-C parts and ignores the Arduino wrapper
|
|
||||||
(which needs `<Arduino.h>`) and the manifests.
|
|
||||||
|
|
||||||
## Wire protocol
|
## Wire protocol
|
||||||
|
|
||||||
`frame = [type:u8][len:u16 LE][payload:len bytes]`, type `0x3C` for app→radio,
|
`frame = [type:u8][len:u16 LE][payload:len bytes]`, type `0x3C` for app→radio,
|
||||||
`0x3E` for radio→app. `payload[0]` is the command/response/push code. Derived from
|
`0x3E` for radio→app. `payload[0]` is the command/response/push code. Derived
|
||||||
the MeshCore companion protocol and the `meshcore.js` reference client.
|
from the MeshCore companion protocol and the `meshcore.js` reference client.
|
||||||
|
|
||||||
|
## Install (PlatformIO)
|
||||||
|
|
||||||
|
Drop this folder into your project's `lib/`, or add to `platformio.ini`:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
lib_deps = symlink:///path/to/MeshCoreCompanion
|
||||||
|
```
|
||||||
|
|
||||||
The companion radio must be flashed with the **serial** companion variant
|
The companion radio must be flashed with the **serial** companion variant
|
||||||
(`companion_radio_usb`) and have its serial interface bound to the port you wire
|
(`companion_radio_usb`) and have its serial interface bound to the UART you wire
|
||||||
to — not BLE, and not the WiFi/TCP variant. Companions compile in one interface at
|
to — not BLE, and not the WiFi/TCP variant. Companions compile in one interface
|
||||||
a time.
|
at a time. If you are using an OS (e.g. Linux) you can use USB variant as the drivers will present a /dev/ttyX interface that looks same as serial. If wanting to use bare metal evices such as AVR/ESP32/nRF52/STM serial, then see notes below about changes to radio.
|
||||||
|
|
||||||
## Build & run the C library (Linux / host)
|
## Quick start
|
||||||
|
|
||||||
```sh
|
|
||||||
cmake -B build && cmake --build build
|
|
||||||
ctest --test-dir build --output-on-failure # run the codec unit test
|
|
||||||
|
|
||||||
./build/meshcore_tty /dev/ttyACM0 # USB-CDC companion
|
|
||||||
./build/meshcore_tty /dev/ttyUSB0 2 000102030405060708090a0b0c0d0e0f sensors
|
|
||||||
# ^tty ^ch ^16-byte PSK (32 hex chars) ^name
|
|
||||||
```
|
|
||||||
|
|
||||||
Or compile the example directly, no CMake:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cc -std=c99 -Wall -Wextra -Isrc \
|
|
||||||
examples-linux/tty_bridge/meshcore_tty.c src/meshcore_companion.c -o meshcore_tty
|
|
||||||
```
|
|
||||||
|
|
||||||
### Host test via PlatformIO
|
|
||||||
|
|
||||||
The repo also ships a `platformio.ini` with a host `native` environment, so you
|
|
||||||
can run the same codec test through PlatformIO with no hardware:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
pio test -e native
|
|
||||||
```
|
|
||||||
|
|
||||||
This compiles only the portable C core (`src/*.c`) plus `test/test_codec.c`; the
|
|
||||||
C++ Arduino wrapper is excluded (it needs `<Arduino.h>`). Pass/fail is driven by
|
|
||||||
the test program's exit code via a tiny custom runner (`test/test_custom_runner.py`).
|
|
||||||
|
|
||||||
## Other platform examples (same C core)
|
|
||||||
|
|
||||||
Each example compiles `src/meshcore_companion.c` directly and supplies only a
|
|
||||||
platform-specific UART transport — no copy of the core is kept anywhere.
|
|
||||||
|
|
||||||
- **ESP-IDF** — `examples-esp-idf/tty_bridge/`. A full IDF project using the UART
|
|
||||||
driver:
|
|
||||||
```sh
|
|
||||||
cd examples-esp-idf/tty_bridge
|
|
||||||
idf.py set-target esp32s3 && idf.py build flash monitor
|
|
||||||
```
|
|
||||||
- **STM32** — `examples-stm32/uart_bridge/`. A HAL drop-in (STM32 builds are
|
|
||||||
board/toolchain specific): add `meshcore_stm32.c` + `src/` to a CubeMX/CubeIDE
|
|
||||||
project and call `meshcore_setup()` / `meshcore_poll()` from `main()`. See its
|
|
||||||
README.
|
|
||||||
|
|
||||||
The transport is just two operations — *write bytes* and *read available bytes* —
|
|
||||||
so porting to bare-metal STM32, nRF52 (nRF5 SDK or Zephyr), or any other MCU only
|
|
||||||
changes those calls; the protocol code is identical.
|
|
||||||
|
|
||||||
## Use the Arduino library
|
|
||||||
|
|
||||||
Install via PlatformIO (`lib_deps = symlink:///path/to/meshcore_c`), the Arduino
|
|
||||||
Library Manager (once published), or by copying the repo into your `libraries/`
|
|
||||||
folder. Then:
|
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
#include "MeshCoreCompanion.h"
|
#include "MeshCoreCompanion.h"
|
||||||
@@ -159,32 +75,44 @@ void loop() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
See `examples/SensorChannelBridge/` for a complete sketch.
|
|
||||||
|
|
||||||
## What's covered
|
## What's covered
|
||||||
|
|
||||||
| Need | C core API | Arduino wrapper |
|
| Need | API |
|
||||||
|------|------------|-----------------|
|
|------|-----|
|
||||||
| Handshake / identity | `mc_cmd_app_start`, `mc_cmd_device_query` | `begin()`, `appStart()`, `deviceQuery()` → `onDeviceInfo`/`onSelfInfo` |
|
| Handshake / identity | `begin()`, `appStart()`, `deviceQuery()` → `onDeviceInfo`, `onSelfInfo` |
|
||||||
| Receive channel text | `mc_parse` → `MC_RESP_CHANNEL_MSG_RECV` | `onChannelMessage` (auto-drained) |
|
| Receive channel text | `onChannelMessage` (auto-drained) |
|
||||||
| Receive channel data + metadata | `mc_parse` → `MC_RESP_CHANNEL_DATA_RECV` | `onChannelData` (SNR, path, type) |
|
| Receive channel data + metadata | `onChannelData` (SNR, path length, data type) |
|
||||||
| Send on a channel | `mc_cmd_send_channel_text` | `sendChannelText(idx, text)` |
|
| Send on a channel | `sendChannelText(idx, text)` |
|
||||||
| Set / read channel PSK | `mc_cmd_set_channel`, `mc_cmd_get_channel` | `setChannel`, `setChannelHexSecret`, `getChannel` → `onChannelInfo` |
|
| Set / read channel PSK | `setChannel`, `setChannelHexSecret`, `getChannel` → `onChannelInfo` |
|
||||||
| Device time | `mc_cmd_get/set_device_time` | `getDeviceTime`, `setDeviceTime`, `deviceEpochNow()` |
|
| Device time | `getDeviceTime`, `setDeviceTime`, `deviceEpochNow()` |
|
||||||
| Adverts | `mc_cmd_send_self_advert` | `sendSelfAdvert(flood)` |
|
| Adverts | `sendSelfAdvert(flood)` |
|
||||||
| Radio params / stats | `mc_cmd_set_radio_params`, `mc_cmd_get_stats` | `getStats` |
|
| Radio params / stats | `getStats`, plus `mc_cmd_set_radio_params` in the core |
|
||||||
|
|
||||||
Adding a command the wrapper doesn't surface yet is one builder plus one `case`.
|
The C core also exposes raw command builders and `mc_parse` for codes the
|
||||||
|
wrapper doesn't surface yet (contacts, battery, send-confirmed, etc.) — adding a
|
||||||
|
new command is one builder plus one `case`.
|
||||||
|
|
||||||
|
## Testing the core (no hardware)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd test
|
||||||
|
cc -std=c99 -Wall -Wextra -I../src test_codec.c ../src/meshcore_companion.c -o t && ./t
|
||||||
|
```
|
||||||
|
|
||||||
|
Exercises command encoding, frame reassembly across split reads, every parser,
|
||||||
|
and resync past line garbage.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- **Timestamps:** channel messages carry an epoch-seconds sender timestamp. Call
|
- **Timestamps:** channel messages carry an epoch-seconds sender timestamp.
|
||||||
`getDeviceTime()` once so outgoing text can be stamped; otherwise it sends `0`.
|
Call `getDeviceTime()` (or `setDeviceTime()`) once so `sendChannelText` can
|
||||||
- **PSK derivation:** `set_channel` takes a raw 16-byte secret. Derive one from a
|
stamp outgoing messages; otherwise it sends `0`.
|
||||||
passphrase host-side (e.g. SHA-256 prefix, as `meshcore.js`'s transport-key util
|
- **PSK derivation:** `setChannel` takes a raw 16-byte secret. If you need to
|
||||||
shows) — kept out of the core to stay dependency-free.
|
derive one from a passphrase, do it host-side (e.g. SHA-256 prefix, as
|
||||||
- **Buffer sizes:** override `MC_MAX_PAYLOAD`, `MC_MAX_TEXT`, `MC_MAX_DATA` before
|
`meshcore.js`'s transport-key util shows) — kept out of the core to stay
|
||||||
including the header if your traffic needs more.
|
dependency-free.
|
||||||
|
- **Buffer sizes:** override `MC_MAX_PAYLOAD`, `MC_MAX_TEXT`, `MC_MAX_DATA`
|
||||||
|
before including the header if your traffic needs more.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user