Remove stale byte-identical root duplicates and promote the canonical
library to the repo root: one source of truth (src/meshcore_companion.{c,h})
serving both a portable C library and a publishable C++ Arduino/PlatformIO
library.
- Portable C99 core + C++ Arduino wrapper in src/
- Arduino sketch in examples/, new Linux tty example in examples-linux/
- CMakeLists.txt for the Linux/native host build (core + example + test)
- Host codec unit test in test/
- README rewritten around the two purposes
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6.5 KiB
meshcore_c
A client for the MeshCore Companion Radio serial protocol, for when the radio lives on one device and your application (display, sensor hub, gateway, desktop tool) 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 channel PSKs, and read link metadata. Your host plays the client, the companion radio plays the server — the inverse of the phone/web app.
Sibling of meshcore.js (JS) and
meshcore_py (Python): this is the C / C++ one.
Two ways to use this repo
The protocol logic is a single portable C99 core with no I/O, no malloc, and
no Arduino dependency. Everything else is a thin layer over it, so one repo serves
two audiences from one source of truth (src/meshcore_companion.{c,h}):
-
Portable C library — drop
src/meshcore_companion.{c,h}into any project. It assembles inbound frames from a byte stream, builds outbound command frames into buffers you own, and parses payloads into plain structs. You supply the transport. A complete Linux example (POSIXtermios) lives inexamples-linux/, and the host unit test intest/runs with no hardware. The same core drops into ESP-IDF, bare nRF52/STM32, or any host bridge unchanged (those examples are planned). -
C++ Arduino library —
src/MeshCoreCompanion.{h,cpp}wrap the core in an Arduino-friendly class: inject anyStream(Serial1on a Grove UART, USB CDC,SoftwareSerial), callloop()often, register lambda callbacks. It auto-drains the radio's queue: on theMsgWaitingpush it loopsSyncNextMessageuntil empty, so you just receiveonChannelMessage(...). 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 example: any Linux tty (USB or raw UART)
└── test/
└── test_codec.c # host unit test (no hardware)
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
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
the MeshCore companion protocol and the meshcore.js reference client.
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
to — not BLE, and not the WiFi/TCP variant. Companions compile in one interface at
a time.
Build & run the C library (Linux / host)
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:
cc -std=c99 -Wall -Wextra -Isrc \
examples-linux/tty_bridge/meshcore_tty.c src/meshcore_companion.c -o meshcore_tty
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:
#include "MeshCoreCompanion.h"
MeshCoreCompanion mc(Serial1);
void setup() {
Serial1.begin(115200, SERIAL_8N1, /*RX*/16, /*TX*/17);
mc.onDeviceInfo([](const mc_device_info_t& d){
mc.setChannelHexSecret(2, "sensors", "000102...0f"); // 32 hex chars
mc.getDeviceTime();
});
mc.onChannelMessage([](const mc_channel_msg_t& m){
// m.text is "SenderName: body"
});
mc.onChannelData([](const mc_channel_data_t& d){
// d.data / d.data_len, plus MC_SNR_DB(d.snr_q4), d.path_len
});
mc.begin(); // AppStart + DeviceQuery
}
void loop() {
mc.loop(); // pump + auto-drain
// mc.sendChannelText(2, "hello");
}
See examples/SensorChannelBridge/ for a complete sketch.
What's covered
| Need | C core API | Arduino wrapper |
|---|---|---|
| Handshake / identity | mc_cmd_app_start, mc_cmd_device_query |
begin(), appStart(), deviceQuery() → onDeviceInfo/onSelfInfo |
| Receive channel text | mc_parse → MC_RESP_CHANNEL_MSG_RECV |
onChannelMessage (auto-drained) |
| Receive channel data + metadata | mc_parse → MC_RESP_CHANNEL_DATA_RECV |
onChannelData (SNR, path, type) |
| Send on a channel | mc_cmd_send_channel_text |
sendChannelText(idx, text) |
| Set / read channel PSK | mc_cmd_set_channel, mc_cmd_get_channel |
setChannel, setChannelHexSecret, getChannel → onChannelInfo |
| Device time | mc_cmd_get/set_device_time |
getDeviceTime, setDeviceTime, deviceEpochNow() |
| Adverts | mc_cmd_send_self_advert |
sendSelfAdvert(flood) |
| Radio params / stats | mc_cmd_set_radio_params, mc_cmd_get_stats |
getStats |
Adding a command the wrapper doesn't surface yet is one builder plus one case.
Notes
- Timestamps: channel messages carry an epoch-seconds sender timestamp. Call
getDeviceTime()once so outgoing text can be stamped; otherwise it sends0. - PSK derivation:
set_channeltakes a raw 16-byte secret. Derive one from a passphrase host-side (e.g. SHA-256 prefix, asmeshcore.js's transport-key util shows) — kept out of the core to stay dependency-free. - Buffer sizes: override
MC_MAX_PAYLOAD,MC_MAX_TEXT,MC_MAX_DATAbefore including the header if your traffic needs more.
License
MIT. Protocol derived from MeshCore and meshcore.js (MIT, © Liam Cottle).