Restructure into dual-purpose meshcore_c library
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>
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* MeshCoreCompanion.cpp
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
#include "MeshCoreCompanion.h"
|
||||
|
||||
void MeshCoreCompanion::begin(bool sendHandshake) {
|
||||
mc_rx_init(&_rx);
|
||||
_draining = false;
|
||||
if (sendHandshake) {
|
||||
appStart(); /* triggers SelfInfo */
|
||||
deviceQuery(); /* triggers DeviceInfo */
|
||||
}
|
||||
}
|
||||
|
||||
void MeshCoreCompanion::sendPayload(const uint8_t *payload, size_t len) {
|
||||
if (len == 0) return;
|
||||
size_t flen = mc_frame_encode(payload, len, _frame, sizeof(_frame));
|
||||
if (flen) _io.write(_frame, flen);
|
||||
}
|
||||
|
||||
void MeshCoreCompanion::loop() {
|
||||
uint8_t tmp[64];
|
||||
int avail;
|
||||
while ((avail = _io.available()) > 0) {
|
||||
size_t want = (avail < (int)sizeof(tmp)) ? (size_t)avail : sizeof(tmp);
|
||||
size_t n = _io.readBytes(tmp, want);
|
||||
if (n == 0) break;
|
||||
mc_rx_feed(&_rx, tmp, n);
|
||||
}
|
||||
size_t olen;
|
||||
while (mc_rx_poll(&_rx, _scratch, sizeof(_scratch), &olen)) {
|
||||
mc_event_t ev;
|
||||
if (mc_parse(_scratch, olen, &ev)) dispatch(ev);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t MeshCoreCompanion::deviceEpochNow() const {
|
||||
if (!_haveTime) return 0;
|
||||
return _epochBase + (uint32_t)((millis() - _millisBase) / 1000UL);
|
||||
}
|
||||
|
||||
/* ---- commands ---- */
|
||||
void MeshCoreCompanion::appStart(const char *name) {
|
||||
uint8_t p[1 + 1 + 6 + 32];
|
||||
size_t n = mc_cmd_app_start(p, sizeof(p), name);
|
||||
sendPayload(p, n);
|
||||
}
|
||||
|
||||
void MeshCoreCompanion::deviceQuery(uint8_t appTargetVer) {
|
||||
uint8_t p[2]; sendPayload(p, mc_cmd_device_query(p, sizeof(p), appTargetVer));
|
||||
}
|
||||
|
||||
void MeshCoreCompanion::getDeviceTime() {
|
||||
uint8_t p[1]; sendPayload(p, mc_cmd_get_device_time(p, sizeof(p)));
|
||||
}
|
||||
|
||||
void MeshCoreCompanion::setDeviceTime(uint32_t epochSecs) {
|
||||
uint8_t p[5]; sendPayload(p, mc_cmd_set_device_time(p, sizeof(p), epochSecs));
|
||||
/* optimistically track it locally too */
|
||||
_epochBase = epochSecs; _millisBase = millis(); _haveTime = true;
|
||||
}
|
||||
|
||||
void MeshCoreCompanion::sendSelfAdvert(bool flood) {
|
||||
uint8_t p[2];
|
||||
sendPayload(p, mc_cmd_send_self_advert(p, sizeof(p), flood ? MC_ADVERT_FLOOD : MC_ADVERT_ZERO_HOP));
|
||||
}
|
||||
|
||||
void MeshCoreCompanion::getChannel(uint8_t idx) {
|
||||
uint8_t p[2]; sendPayload(p, mc_cmd_get_channel(p, sizeof(p), idx));
|
||||
}
|
||||
|
||||
void MeshCoreCompanion::setChannel(uint8_t idx, const char *name, const uint8_t secret[MC_SECRET_LEN]) {
|
||||
uint8_t p[2 + MC_NAME_LEN + MC_SECRET_LEN];
|
||||
sendPayload(p, mc_cmd_set_channel(p, sizeof(p), idx, name, secret));
|
||||
}
|
||||
|
||||
bool MeshCoreCompanion::setChannelHexSecret(uint8_t idx, const char *name, const char *hex32) {
|
||||
if (!hex32) return false;
|
||||
uint8_t secret[MC_SECRET_LEN];
|
||||
for (int i = 0; i < MC_SECRET_LEN; i++) {
|
||||
char hi = hex32[i * 2], lo = hex32[i * 2 + 1];
|
||||
if (!hi || !lo) return false;
|
||||
auto nib = [](char c) -> int {
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||
return -1;
|
||||
};
|
||||
int h = nib(hi), l = nib(lo);
|
||||
if (h < 0 || l < 0) return false;
|
||||
secret[i] = (uint8_t)((h << 4) | l);
|
||||
}
|
||||
setChannel(idx, name, secret);
|
||||
return true;
|
||||
}
|
||||
|
||||
void MeshCoreCompanion::sendChannelText(uint8_t idx, const char *text, uint32_t senderTs) {
|
||||
if (senderTs == 0) senderTs = deviceEpochNow(); /* 0 if unknown */
|
||||
uint8_t p[1 + 1 + 1 + 4 + MC_MAX_TEXT];
|
||||
size_t n = mc_cmd_send_channel_text(p, sizeof(p), MC_TXT_PLAIN, idx, senderTs, text);
|
||||
sendPayload(p, n);
|
||||
}
|
||||
|
||||
void MeshCoreCompanion::syncNextMessage() {
|
||||
uint8_t p[1]; sendPayload(p, mc_cmd_sync_next_message(p, sizeof(p)));
|
||||
}
|
||||
|
||||
void MeshCoreCompanion::drainMessages() {
|
||||
if (!_draining) { _draining = true; syncNextMessage(); }
|
||||
}
|
||||
|
||||
void MeshCoreCompanion::getStats(uint8_t statsType) {
|
||||
uint8_t p[2]; sendPayload(p, mc_cmd_get_stats(p, sizeof(p), statsType));
|
||||
}
|
||||
|
||||
/* ---- dispatch ---- */
|
||||
void MeshCoreCompanion::dispatch(const mc_event_t &ev) {
|
||||
if (_onEvent) _onEvent(ev);
|
||||
|
||||
switch (ev.code) {
|
||||
case MC_RESP_CURR_TIME:
|
||||
_epochBase = ev.u.curr_time; _millisBase = millis(); _haveTime = true;
|
||||
break;
|
||||
case MC_RESP_DEVICE_INFO:
|
||||
if (_onDevInfo) _onDevInfo(ev.u.device_info);
|
||||
break;
|
||||
case MC_RESP_SELF_INFO:
|
||||
if (_onSelfInfo) _onSelfInfo(ev.u.self_info);
|
||||
break;
|
||||
case MC_RESP_CHANNEL_INFO:
|
||||
if (_onChanInfo) _onChanInfo(ev.u.channel_info);
|
||||
break;
|
||||
case MC_PUSH_MSG_WAITING:
|
||||
if (_autoSync && !_draining) { _draining = true; syncNextMessage(); }
|
||||
break;
|
||||
case MC_RESP_CHANNEL_MSG_RECV:
|
||||
if (_onText) _onText(ev.u.channel_msg);
|
||||
if (_draining) syncNextMessage();
|
||||
break;
|
||||
case MC_RESP_CHANNEL_DATA_RECV:
|
||||
if (_onData) _onData(ev.u.channel_data);
|
||||
if (_draining) syncNextMessage();
|
||||
break;
|
||||
case MC_RESP_CONTACT_MSG_RECV:
|
||||
if (_onContact) _onContact(ev.u.contact_msg);
|
||||
if (_draining) syncNextMessage();
|
||||
break;
|
||||
case MC_RESP_NO_MORE_MESSAGES:
|
||||
_draining = false;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* MeshCoreCompanion.h
|
||||
*
|
||||
* Arduino/C++ convenience wrapper over the portable meshcore_companion C core.
|
||||
* Inject any Stream (Serial1 on a Grove UART, USB CDC, SoftwareSerial, ...),
|
||||
* call loop() often, and register lambda callbacks.
|
||||
*
|
||||
* The wrapper auto-drains the radio's message queue: when the radio sends the
|
||||
* MsgWaiting push it transparently issues SyncNextMessage repeatedly until the
|
||||
* queue is empty, delivering each message through onChannelMessage / onChannelData.
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
* Author: Scott Penrose / Digital Dimensions.
|
||||
*/
|
||||
#ifndef MESHCORE_COMPANION_HPP
|
||||
#define MESHCORE_COMPANION_HPP
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <functional>
|
||||
#include "meshcore_companion.h"
|
||||
|
||||
class MeshCoreCompanion {
|
||||
public:
|
||||
using TextMsgCb = std::function<void(const mc_channel_msg_t&)>;
|
||||
using DataMsgCb = std::function<void(const mc_channel_data_t&)>;
|
||||
using ContactMsgCb = std::function<void(const mc_contact_msg_t&)>;
|
||||
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 EventCb = std::function<void(const mc_event_t&)>;
|
||||
|
||||
explicit MeshCoreCompanion(Stream &io) : _io(io) {}
|
||||
|
||||
/* Reset the receiver and (by default) send AppStart + DeviceQuery so the
|
||||
* radio reports SelfInfo and DeviceInfo. */
|
||||
void begin(bool sendHandshake = true);
|
||||
|
||||
/* Pump: read available serial bytes, decode and dispatch frames.
|
||||
* Call this every loop iteration. Non-blocking. */
|
||||
void loop();
|
||||
|
||||
/* ---- commands (fire-and-forget; replies arrive via callbacks) ---- */
|
||||
void appStart(const char *name = "esp32");
|
||||
void deviceQuery(uint8_t appTargetVer = 1);
|
||||
void getDeviceTime();
|
||||
void setDeviceTime(uint32_t epochSecs);
|
||||
void sendSelfAdvert(bool flood = true);
|
||||
void getChannel(uint8_t idx);
|
||||
void setChannel(uint8_t idx, const char *name, const uint8_t secret[MC_SECRET_LEN]);
|
||||
/* Set a channel using a 32-hex-char PSK string (-> 16 bytes). false if malformed. */
|
||||
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);
|
||||
void syncNextMessage();
|
||||
void drainMessages(); /* start a manual sync-drain loop */
|
||||
void getStats(uint8_t statsType);
|
||||
|
||||
/* ---- behaviour ---- */
|
||||
void setAutoSync(bool on) { _autoSync = on; }
|
||||
|
||||
/* ---- device time (valid after a CurrTime response) ---- */
|
||||
bool haveDeviceTime() const { return _haveTime; }
|
||||
uint32_t deviceEpochNow() const;
|
||||
|
||||
/* ---- callbacks ---- */
|
||||
void onChannelMessage(TextMsgCb cb) { _onText = cb; }
|
||||
void onChannelData(DataMsgCb cb) { _onData = cb; }
|
||||
void onContactMessage(ContactMsgCb cb){ _onContact = cb; }
|
||||
void onDeviceInfo(DeviceInfoCb cb) { _onDevInfo = cb; }
|
||||
void onChannelInfo(ChannelInfoCb cb) { _onChanInfo = cb; }
|
||||
void onSelfInfo(SelfInfoCb cb) { _onSelfInfo = cb; }
|
||||
void onEvent(EventCb cb) { _onEvent = cb; } /* every parsed frame */
|
||||
|
||||
private:
|
||||
void sendPayload(const uint8_t *payload, size_t len);
|
||||
void dispatch(const mc_event_t &ev);
|
||||
|
||||
Stream &_io;
|
||||
mc_rx_t _rx;
|
||||
uint8_t _scratch[MC_RX_BUFSZ];
|
||||
uint8_t _frame[MC_RX_BUFSZ + 3];
|
||||
|
||||
bool _autoSync = true;
|
||||
bool _draining = false;
|
||||
|
||||
bool _haveTime = false;
|
||||
uint32_t _epochBase = 0;
|
||||
uint32_t _millisBase = 0;
|
||||
|
||||
TextMsgCb _onText;
|
||||
DataMsgCb _onData;
|
||||
ContactMsgCb _onContact;
|
||||
DeviceInfoCb _onDevInfo;
|
||||
ChannelInfoCb _onChanInfo;
|
||||
SelfInfoCb _onSelfInfo;
|
||||
EventCb _onEvent;
|
||||
};
|
||||
|
||||
#endif /* MESHCORE_COMPANION_HPP */
|
||||
@@ -0,0 +1,313 @@
|
||||
/*
|
||||
* meshcore_companion.c -- portable C99 core, no I/O, no malloc.
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
#include "meshcore_companion.h"
|
||||
#include <string.h>
|
||||
|
||||
/* ---- little-endian helpers ---- */
|
||||
static void put_u16(uint8_t *p, uint16_t v) { p[0] = (uint8_t)v; p[1] = (uint8_t)(v >> 8); }
|
||||
static void put_u32(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);
|
||||
}
|
||||
static uint16_t get_u16(const uint8_t *p) { return (uint16_t)(p[0] | (p[1] << 8)); }
|
||||
static uint32_t get_u32(const uint8_t *p) {
|
||||
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
|
||||
}
|
||||
|
||||
/* Copy a fixed-width NUL-padded cstring field of `field_len` into dst (cap incl
|
||||
* terminator). Advances nothing; returns field_len consumed by caller. */
|
||||
static void copy_cstring(char *dst, size_t dst_cap, const uint8_t *src, size_t field_len) {
|
||||
size_t i = 0;
|
||||
for (; i < field_len && i + 1 < dst_cap; i++) {
|
||||
if (src[i] == 0) break;
|
||||
dst[i] = (char)src[i];
|
||||
}
|
||||
dst[i] = 0;
|
||||
}
|
||||
|
||||
/* Copy remaining bytes [off..len) as a NUL-terminated string. */
|
||||
static void copy_rest_string(char *dst, size_t dst_cap, const uint8_t *src, size_t off, size_t len) {
|
||||
size_t n = (off < len) ? (len - off) : 0, i = 0;
|
||||
for (; i < n && i + 1 < dst_cap; i++) dst[i] = (char)src[off + i];
|
||||
dst[i] = 0;
|
||||
}
|
||||
|
||||
/* ======================================================================== */
|
||||
void mc_rx_init(mc_rx_t *rx) { rx->len = 0; }
|
||||
|
||||
size_t mc_rx_feed(mc_rx_t *rx, const uint8_t *data, size_t n) {
|
||||
size_t space = sizeof(rx->buf) - rx->len, take = (n < space) ? n : space;
|
||||
memcpy(rx->buf + rx->len, data, take);
|
||||
rx->len += take;
|
||||
return take;
|
||||
}
|
||||
|
||||
static void rx_drop_front(mc_rx_t *rx, size_t k) {
|
||||
if (k >= rx->len) { rx->len = 0; return; }
|
||||
memmove(rx->buf, rx->buf + k, rx->len - k);
|
||||
rx->len -= k;
|
||||
}
|
||||
|
||||
int mc_rx_poll(mc_rx_t *rx, uint8_t *out, size_t out_cap, size_t *out_len) {
|
||||
while (rx->len >= 3) {
|
||||
uint8_t type = rx->buf[0];
|
||||
if (type != MC_FRAME_RADIO_TO_APP && type != MC_FRAME_APP_TO_RADIO) {
|
||||
rx_drop_front(rx, 1); /* not a frame lead, resync */
|
||||
continue;
|
||||
}
|
||||
uint16_t flen = get_u16(rx->buf + 1);
|
||||
if (flen == 0) { rx_drop_front(rx, 1); continue; }
|
||||
if (flen > MC_MAX_PAYLOAD) { /* cannot hold it; skip lead byte */
|
||||
rx_drop_front(rx, 1);
|
||||
continue;
|
||||
}
|
||||
size_t need = (size_t)3 + flen;
|
||||
if (rx->len < need) return 0; /* wait for the rest */
|
||||
size_t copy = (flen < out_cap) ? flen : out_cap;
|
||||
memcpy(out, rx->buf + 3, copy);
|
||||
*out_len = copy;
|
||||
rx_drop_front(rx, need);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ======================================================================== */
|
||||
size_t mc_frame_encode(const uint8_t *payload, size_t payload_len,
|
||||
uint8_t *out, size_t out_cap) {
|
||||
if (payload_len > 0xFFFF || out_cap < payload_len + 3) return 0;
|
||||
out[0] = MC_FRAME_APP_TO_RADIO;
|
||||
put_u16(out + 1, (uint16_t)payload_len);
|
||||
memcpy(out + 3, payload, payload_len);
|
||||
return payload_len + 3;
|
||||
}
|
||||
|
||||
/* ---- command builders ---- */
|
||||
size_t mc_cmd_app_start(uint8_t *out, size_t cap, const char *app_name) {
|
||||
size_t nlen = app_name ? strlen(app_name) : 0;
|
||||
size_t total = 1 + 1 + 6 + nlen;
|
||||
if (cap < total) return 0;
|
||||
size_t i = 0;
|
||||
out[i++] = MC_CMD_APP_START;
|
||||
out[i++] = 1; /* app version */
|
||||
memset(out + i, 0, 6); i += 6; /* reserved */
|
||||
memcpy(out + i, app_name, nlen); i += nlen;
|
||||
return i;
|
||||
}
|
||||
|
||||
size_t mc_cmd_device_query(uint8_t *out, size_t cap, uint8_t app_target_ver) {
|
||||
if (cap < 2) return 0;
|
||||
out[0] = MC_CMD_DEVICE_QUERY; out[1] = app_target_ver; return 2;
|
||||
}
|
||||
|
||||
size_t mc_cmd_get_device_time(uint8_t *out, size_t cap) {
|
||||
if (cap < 1) return 0;
|
||||
out[0] = MC_CMD_GET_DEVICE_TIME;
|
||||
return 1;
|
||||
}
|
||||
|
||||
size_t mc_cmd_set_device_time(uint8_t *out, size_t cap, uint32_t epoch_secs) {
|
||||
if (cap < 5) return 0;
|
||||
out[0] = MC_CMD_SET_DEVICE_TIME; put_u32(out + 1, epoch_secs); return 5;
|
||||
}
|
||||
|
||||
size_t mc_cmd_sync_next_message(uint8_t *out, size_t cap) {
|
||||
if (cap < 1) return 0;
|
||||
out[0] = MC_CMD_SYNC_NEXT_MESSAGE;
|
||||
return 1;
|
||||
}
|
||||
|
||||
size_t mc_cmd_send_self_advert(uint8_t *out, size_t cap, uint8_t advert_type) {
|
||||
if (cap < 2) return 0;
|
||||
out[0] = MC_CMD_SEND_SELF_ADVERT; out[1] = advert_type; return 2;
|
||||
}
|
||||
|
||||
size_t mc_cmd_get_channel(uint8_t *out, size_t cap, uint8_t channel_idx) {
|
||||
if (cap < 2) return 0;
|
||||
out[0] = MC_CMD_GET_CHANNEL; out[1] = channel_idx; return 2;
|
||||
}
|
||||
|
||||
size_t mc_cmd_set_channel(uint8_t *out, size_t cap, uint8_t channel_idx,
|
||||
const char *name, const uint8_t secret[MC_SECRET_LEN]) {
|
||||
size_t total = 1 + 1 + MC_NAME_LEN + MC_SECRET_LEN;
|
||||
if (cap < total) return 0;
|
||||
size_t i = 0;
|
||||
out[i++] = MC_CMD_SET_CHANNEL;
|
||||
out[i++] = channel_idx;
|
||||
/* 32-byte NUL-padded name, last byte forced NUL (matches meshcore.js) */
|
||||
memset(out + i, 0, MC_NAME_LEN);
|
||||
if (name) {
|
||||
size_t nlen = strlen(name);
|
||||
if (nlen > MC_NAME_LEN - 1) nlen = MC_NAME_LEN - 1;
|
||||
memcpy(out + i, name, nlen);
|
||||
}
|
||||
i += MC_NAME_LEN;
|
||||
memcpy(out + i, secret, MC_SECRET_LEN); i += MC_SECRET_LEN;
|
||||
return i;
|
||||
}
|
||||
|
||||
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) {
|
||||
size_t tlen = text ? strlen(text) : 0;
|
||||
size_t total = 1 + 1 + 1 + 4 + tlen;
|
||||
if (cap < total) return 0;
|
||||
size_t i = 0;
|
||||
out[i++] = MC_CMD_SEND_CHANNEL_TXT_MSG;
|
||||
out[i++] = txt_type;
|
||||
out[i++] = channel_idx;
|
||||
put_u32(out + i, sender_ts); i += 4;
|
||||
memcpy(out + i, text, tlen); i += tlen;
|
||||
return i;
|
||||
}
|
||||
|
||||
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;
|
||||
size_t i = 0;
|
||||
out[i++] = MC_CMD_SET_RADIO_PARAMS;
|
||||
put_u32(out + i, freq_hz_x1000); i += 4;
|
||||
put_u32(out + i, bw); i += 4;
|
||||
out[i++] = sf; out[i++] = cr;
|
||||
return i;
|
||||
}
|
||||
|
||||
size_t mc_cmd_get_stats(uint8_t *out, size_t cap, uint8_t stats_type) {
|
||||
if (cap < 2) return 0;
|
||||
out[0] = MC_CMD_GET_STATS; out[1] = stats_type; return 2;
|
||||
}
|
||||
|
||||
/* ======================================================================== */
|
||||
int mc_parse(const uint8_t *p, size_t len, mc_event_t *ev) {
|
||||
if (len < 1) return 0;
|
||||
memset(ev, 0, sizeof(*ev));
|
||||
ev->code = p[0];
|
||||
const uint8_t *b = p + 1; /* body after code byte */
|
||||
size_t n = len - 1;
|
||||
|
||||
switch (ev->code) {
|
||||
case MC_RESP_OK:
|
||||
case MC_RESP_DISABLED:
|
||||
case MC_RESP_NO_MORE_MESSAGES:
|
||||
case MC_PUSH_MSG_WAITING:
|
||||
return 1;
|
||||
|
||||
case MC_RESP_ERR:
|
||||
ev->u.err_code = (n >= 1) ? (int8_t)b[0] : (int8_t)-1;
|
||||
return 1;
|
||||
|
||||
case MC_RESP_CURR_TIME:
|
||||
if (n < 4) return 0;
|
||||
ev->u.curr_time = get_u32(b);
|
||||
return 1;
|
||||
|
||||
case MC_RESP_BATTERY_VOLTAGE:
|
||||
if (n < 2) return 0;
|
||||
ev->u.battery_mv = get_u16(b);
|
||||
return 1;
|
||||
|
||||
case MC_RESP_DEVICE_INFO: {
|
||||
/* [fw_ver:i8][max_contacts/2:u8][max_channels:u8][ble_pin:u32]
|
||||
[build_date:cstr12][model:rest] */
|
||||
if (n < 1) return 0;
|
||||
mc_device_info_t *d = &ev->u.device_info;
|
||||
d->fw_ver = (int8_t)b[0];
|
||||
size_t off = 1;
|
||||
if (n >= off + 6) {
|
||||
d->max_contacts = (uint16_t)(b[off] * 2); off += 1;
|
||||
d->max_channels = b[off]; off += 1;
|
||||
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);
|
||||
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_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_CHANNEL_DATA_RECV: {
|
||||
if (n < 8) return 0;
|
||||
mc_channel_data_t *d = &ev->u.channel_data;
|
||||
d->snr_q4 = (int8_t)b[0];
|
||||
/* b[1], b[2] reserved */
|
||||
d->channel_idx = (int8_t)b[3];
|
||||
d->path_len = b[4];
|
||||
d->data_type = get_u16(b + 5);
|
||||
d->data_len = b[7];
|
||||
size_t avail = n - 8;
|
||||
size_t dl = d->data_len;
|
||||
if (dl > avail) dl = avail;
|
||||
if (dl > sizeof(d->data)) dl = sizeof(d->data);
|
||||
memcpy(d->data, b + 8, dl);
|
||||
d->data_len = (uint8_t)dl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
case MC_RESP_CHANNEL_INFO: {
|
||||
if (n < 1 + MC_NAME_LEN) return 0;
|
||||
mc_channel_info_t *c = &ev->u.channel_info;
|
||||
c->channel_idx = b[0];
|
||||
copy_cstring(c->name, sizeof(c->name), b + 1, MC_NAME_LEN);
|
||||
size_t off = 1 + MC_NAME_LEN;
|
||||
if (n - off >= MC_SECRET_LEN) {
|
||||
memcpy(c->secret, b + off, MC_SECRET_LEN);
|
||||
c->have_secret = 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
case MC_RESP_SELF_INFO: {
|
||||
if (n < 55) return 0;
|
||||
mc_self_info_t *s = &ev->u.self_info;
|
||||
s->type = b[0]; s->tx_power = b[1]; s->max_tx_power = b[2];
|
||||
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->manual_add_contacts = b[46];
|
||||
s->radio_freq = get_u32(b + 47);
|
||||
s->radio_bw = get_u32(b + 51);
|
||||
s->radio_sf = (n > 55) ? b[55] : 0;
|
||||
s->radio_cr = (n > 56) ? b[56] : 0;
|
||||
copy_rest_string(s->name, sizeof(s->name), b, 57, n);
|
||||
return 1;
|
||||
}
|
||||
|
||||
case MC_PUSH_ADVERT:
|
||||
case MC_PUSH_PATH_UPDATED:
|
||||
if (n < 32) return 0;
|
||||
memcpy(ev->u.pubkey32, b, 32);
|
||||
return 1;
|
||||
|
||||
case MC_PUSH_SEND_CONFIRMED:
|
||||
if (n < 8) return 0;
|
||||
ev->u.send_confirmed.ack_code = get_u32(b);
|
||||
ev->u.send_confirmed.round_trip = get_u32(b + 4);
|
||||
return 1;
|
||||
|
||||
default:
|
||||
return 0; /* recognised code byte set in ev->code, body not parsed */
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
* meshcore_companion.h
|
||||
*
|
||||
* Portable C99 client for the MeshCore Companion Radio serial protocol.
|
||||
*
|
||||
* This core has NO I/O, NO dynamic allocation and NO Arduino dependency.
|
||||
* You feed it bytes received from the radio and it hands you decoded frames;
|
||||
* you ask it to build command frames and it writes them into a buffer you own.
|
||||
* The transport (UART, USB-CDC, TCP, a unit-test harness) is entirely yours.
|
||||
*
|
||||
* Wire format (verified against meshcore.js, MIT, (c) Liam Cottle):
|
||||
* frame = [type:u8][len:u16 LE][payload:len bytes]
|
||||
* type 0x3C ('<') app -> radio (commands we send)
|
||||
* type 0x3E ('>') radio -> app (responses / push notifications we receive)
|
||||
* payload[0] is the command code (outbound) or response/push code (inbound).
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
* Author: Scott Penrose / Digital Dimensions.
|
||||
* Protocol reference: https://github.com/meshcore-dev/meshcore.js
|
||||
*/
|
||||
#ifndef MESHCORE_COMPANION_H
|
||||
#define MESHCORE_COMPANION_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ---- Compile-time sizing (override before including if you need more) ---- */
|
||||
#ifndef MC_MAX_PAYLOAD
|
||||
#define MC_MAX_PAYLOAD 255 /* largest companion payload we will buffer */
|
||||
#endif
|
||||
#ifndef MC_MAX_TEXT
|
||||
#define MC_MAX_TEXT 184 /* max message text we keep (incl. NUL) */
|
||||
#endif
|
||||
#ifndef MC_MAX_DATA
|
||||
#define MC_MAX_DATA 184 /* max channel-data payload we keep */
|
||||
#endif
|
||||
#ifndef MC_MAX_MODEL
|
||||
#define MC_MAX_MODEL 48 /* DeviceInfo model/manufacturer string */
|
||||
#endif
|
||||
#define MC_NAME_LEN 32 /* channel / advert name field width (cstring) */
|
||||
#define MC_SECRET_LEN 16 /* 128-bit channel secret (PSK) */
|
||||
#define MC_RX_BUFSZ (MC_MAX_PAYLOAD + 8)
|
||||
|
||||
/* ---- Frame lead bytes ---- */
|
||||
#define MC_FRAME_APP_TO_RADIO 0x3C
|
||||
#define MC_FRAME_RADIO_TO_APP 0x3E
|
||||
|
||||
/* ---- Command codes (app -> radio) ---- */
|
||||
enum {
|
||||
MC_CMD_APP_START = 1,
|
||||
MC_CMD_SEND_TXT_MSG = 2,
|
||||
MC_CMD_SEND_CHANNEL_TXT_MSG = 3,
|
||||
MC_CMD_GET_CONTACTS = 4,
|
||||
MC_CMD_GET_DEVICE_TIME = 5,
|
||||
MC_CMD_SET_DEVICE_TIME = 6,
|
||||
MC_CMD_SEND_SELF_ADVERT = 7,
|
||||
MC_CMD_SET_ADVERT_NAME = 8,
|
||||
MC_CMD_SYNC_NEXT_MESSAGE = 10,
|
||||
MC_CMD_SET_RADIO_PARAMS = 11,
|
||||
MC_CMD_SET_TX_POWER = 12,
|
||||
MC_CMD_DEVICE_QUERY = 22,
|
||||
MC_CMD_GET_CHANNEL = 31,
|
||||
MC_CMD_SET_CHANNEL = 32,
|
||||
MC_CMD_GET_STATS = 56
|
||||
};
|
||||
|
||||
/* ---- Response codes (radio -> app) ---- */
|
||||
enum {
|
||||
MC_RESP_OK = 0,
|
||||
MC_RESP_ERR = 1,
|
||||
MC_RESP_CONTACTS_START = 2,
|
||||
MC_RESP_CONTACT = 3,
|
||||
MC_RESP_END_OF_CONTACTS = 4,
|
||||
MC_RESP_SELF_INFO = 5,
|
||||
MC_RESP_SENT = 6,
|
||||
MC_RESP_CONTACT_MSG_RECV = 7,
|
||||
MC_RESP_CHANNEL_MSG_RECV = 8,
|
||||
MC_RESP_CURR_TIME = 9,
|
||||
MC_RESP_NO_MORE_MESSAGES = 10,
|
||||
MC_RESP_EXPORT_CONTACT = 11,
|
||||
MC_RESP_BATTERY_VOLTAGE = 12,
|
||||
MC_RESP_DEVICE_INFO = 13,
|
||||
MC_RESP_PRIVATE_KEY = 14,
|
||||
MC_RESP_DISABLED = 15,
|
||||
MC_RESP_CHANNEL_INFO = 18,
|
||||
MC_RESP_STATS = 24,
|
||||
MC_RESP_CHANNEL_DATA_RECV= 27
|
||||
};
|
||||
|
||||
/* ---- Push codes (unsolicited, radio -> app) ---- */
|
||||
enum {
|
||||
MC_PUSH_ADVERT = 0x80,
|
||||
MC_PUSH_PATH_UPDATED = 0x81,
|
||||
MC_PUSH_SEND_CONFIRMED= 0x82,
|
||||
MC_PUSH_MSG_WAITING = 0x83, /* "drain me": loop SYNC_NEXT_MESSAGE */
|
||||
MC_PUSH_RAW_DATA = 0x84,
|
||||
MC_PUSH_LOGIN_SUCCESS = 0x85,
|
||||
MC_PUSH_LOGIN_FAIL = 0x86,
|
||||
MC_PUSH_STATUS_RESP = 0x87,
|
||||
MC_PUSH_LOG_RX_DATA = 0x88,
|
||||
MC_PUSH_TRACE_DATA = 0x89,
|
||||
MC_PUSH_NEW_ADVERT = 0x8A,
|
||||
MC_PUSH_TELEMETRY = 0x8B,
|
||||
MC_PUSH_BINARY_RESP = 0x8C
|
||||
};
|
||||
|
||||
/* ---- Text message subtypes ---- */
|
||||
enum { MC_TXT_PLAIN = 0, MC_TXT_CLI_DATA = 1, MC_TXT_SIGNED_PLAIN = 2 };
|
||||
/* ---- Self-advert flood mode ---- */
|
||||
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)
|
||||
|
||||
/* ======================================================================== *
|
||||
* Receive side: streaming frame assembler
|
||||
* ======================================================================== */
|
||||
typedef struct {
|
||||
uint8_t buf[MC_RX_BUFSZ];
|
||||
size_t len;
|
||||
} mc_rx_t;
|
||||
|
||||
void mc_rx_init(mc_rx_t *rx);
|
||||
|
||||
/* Append received bytes. Returns the number of bytes accepted (bytes beyond
|
||||
* the buffer capacity are dropped; this only happens if a peer floods us with
|
||||
* a frame larger than MC_MAX_PAYLOAD, which the poller will resync past). */
|
||||
size_t mc_rx_feed(mc_rx_t *rx, const uint8_t *data, size_t n);
|
||||
|
||||
/* Pull the next complete payload (the bytes after the 3-byte header) into
|
||||
* out[]. Returns 1 and sets *out_len if a frame is ready, 0 if more bytes are
|
||||
* needed. Call repeatedly until it returns 0. Garbage / oversized frames are
|
||||
* resynced automatically by skipping one byte at a time. */
|
||||
int mc_rx_poll(mc_rx_t *rx, uint8_t *out, size_t out_cap, size_t *out_len);
|
||||
|
||||
/* ======================================================================== *
|
||||
* Transmit side: wrap a payload into an on-wire app->radio frame
|
||||
* ======================================================================== */
|
||||
/* Writes [0x3C][len LE][payload] into out[]. Returns total bytes, or 0 on
|
||||
* overflow / oversize. */
|
||||
size_t mc_frame_encode(const uint8_t *payload, size_t payload_len,
|
||||
uint8_t *out, size_t out_cap);
|
||||
|
||||
/* ======================================================================== *
|
||||
* Command payload builders (write payload only; wrap with mc_frame_encode)
|
||||
* Each returns the payload length, or 0 if it would overflow `cap`.
|
||||
* ======================================================================== */
|
||||
size_t mc_cmd_app_start (uint8_t *out, size_t cap, const char *app_name);
|
||||
size_t mc_cmd_device_query (uint8_t *out, size_t cap, uint8_t app_target_ver);
|
||||
size_t mc_cmd_get_device_time (uint8_t *out, size_t cap);
|
||||
size_t mc_cmd_set_device_time (uint8_t *out, size_t cap, uint32_t epoch_secs);
|
||||
size_t mc_cmd_sync_next_message(uint8_t *out, size_t cap);
|
||||
size_t mc_cmd_send_self_advert (uint8_t *out, size_t cap, uint8_t advert_type);
|
||||
size_t mc_cmd_get_channel (uint8_t *out, size_t cap, uint8_t channel_idx);
|
||||
size_t mc_cmd_set_channel (uint8_t *out, size_t cap, uint8_t channel_idx,
|
||||
const char *name, const uint8_t secret[MC_SECRET_LEN]);
|
||||
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);
|
||||
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);
|
||||
|
||||
/* ======================================================================== *
|
||||
* Parsed events
|
||||
* ======================================================================== */
|
||||
typedef struct {
|
||||
int8_t fw_ver;
|
||||
uint16_t max_contacts; /* already x2 from the wire field */
|
||||
uint8_t max_channels;
|
||||
uint32_t ble_pin;
|
||||
char build_date[16];
|
||||
char model[MC_MAX_MODEL];
|
||||
} mc_device_info_t;
|
||||
|
||||
typedef struct {
|
||||
int8_t channel_idx;
|
||||
uint8_t path_len; /* MC_PATH_DIRECT or flood hop count */
|
||||
uint8_t txt_type;
|
||||
uint32_t sender_ts;
|
||||
char text[MC_MAX_TEXT]; /* for channel msgs this is "Name: body" */
|
||||
} mc_channel_msg_t;
|
||||
|
||||
typedef struct {
|
||||
int8_t snr_q4; /* divide by 4 for dB; see MC_SNR_DB() */
|
||||
int8_t channel_idx;
|
||||
uint8_t path_len;
|
||||
uint16_t data_type;
|
||||
uint8_t data_len;
|
||||
uint8_t data[MC_MAX_DATA];
|
||||
} mc_channel_data_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t pubkey_prefix[6];
|
||||
uint8_t path_len;
|
||||
uint8_t txt_type;
|
||||
uint32_t sender_ts;
|
||||
char text[MC_MAX_TEXT];
|
||||
} mc_contact_msg_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t channel_idx;
|
||||
char name[MC_NAME_LEN + 1];
|
||||
uint8_t secret[MC_SECRET_LEN];
|
||||
int have_secret;
|
||||
} mc_channel_info_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t type, tx_power, max_tx_power;
|
||||
uint8_t public_key[32];
|
||||
int32_t adv_lat, adv_lon;
|
||||
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 code; /* response or push code (first payload byte) */
|
||||
union {
|
||||
mc_device_info_t device_info;
|
||||
mc_channel_msg_t channel_msg;
|
||||
mc_channel_data_t channel_data;
|
||||
mc_contact_msg_t contact_msg;
|
||||
mc_channel_info_t channel_info;
|
||||
mc_self_info_t self_info;
|
||||
uint32_t curr_time; /* epoch secs */
|
||||
uint16_t battery_mv;
|
||||
int8_t err_code; /* MC_RESP_ERR (-1 if absent) */
|
||||
uint8_t pubkey32[32]; /* advert / path-updated pushes */
|
||||
struct { uint32_t ack_code, round_trip; } send_confirmed;
|
||||
} u;
|
||||
} mc_event_t;
|
||||
|
||||
/* Decode one received payload. Returns 1 if recognised (ev->code set and the
|
||||
* matching union member filled), 0 if the code is unknown to this build. */
|
||||
int mc_parse(const uint8_t *payload, size_t len, mc_event_t *ev);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
#endif
|
||||
#endif /* MESHCORE_COMPANION_H */
|
||||
Reference in New Issue
Block a user