4 Commits

Author SHA1 Message Date
Scott Penrose b9e727ade8 bump version for RSSI/SNR fix 2026-06-10 15:21:10 +10:00
Scott Penrose 1444edba0c Bump to version 3 to get rssi et al 2026-06-10 11:29:28 +10:00
Scott Penrose 493826bef5 fully drain sync messages 2026-06-10 11:26:13 +10:00
Scott Penrose cc8f61158f Fix links for arduino libs 2026-06-08 17:04:54 +10:00
7 changed files with 38 additions and 8 deletions
+4
View File
@@ -9,6 +9,10 @@ channel PSKs, and read link metadata.
This is the inverse of the phone/web app: your host MCU plays the *client*, the This is the inverse of the phone/web app: your host MCU plays the *client*, the
companion radio plays the *server*. companion radio plays the *server*.
> The canonical repository is on Gitea at
> <https://gitea.sh3d.com.au/Sh3d/meshcore_c>. A mirror is kept on GitHub at
> <https://github.com/SH3D/meshcore_c> for **issues** and **pull requests**.
## Design: portable C core + Arduino wrapper ## Design: portable C core + Arduino wrapper
Two layers, so the protocol is reusable and testable far beyond Arduino: Two layers, so the protocol is reusable and testable far beyond Arduino:
+6 -1
View File
@@ -1,12 +1,17 @@
{ {
"name": "MeshCoreCompanion", "name": "MeshCoreCompanion",
"version": "0.2.0", "version": "0.2.2",
"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.", "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"], "keywords": ["meshcore", "lora", "companion", "serial", "mesh", "sx1262", "esp32", "nrf52"],
"authors": [ "authors": [
{ "name": "Scott Penrose", "maintainer": true } { "name": "Scott Penrose", "maintainer": true }
], ],
"license": "MIT", "license": "MIT",
"homepage": "https://gitea.sh3d.com.au/Sh3d/meshcore_c",
"repository": {
"type": "git",
"url": "https://gitea.sh3d.com.au/Sh3d/meshcore_c.git"
},
"frameworks": ["arduino"], "frameworks": ["arduino"],
"platforms": ["espressif32", "nordicnrf52", "raspberrypi", "ststm32"], "platforms": ["espressif32", "nordicnrf52", "raspberrypi", "ststm32"],
"headers": ["MeshCoreCompanion.h", "meshcore_companion.h"], "headers": ["MeshCoreCompanion.h", "meshcore_companion.h"],
+2 -2
View File
@@ -1,9 +1,9 @@
name=MeshCoreCompanion name=MeshCoreCompanion
version=0.2.0 version=0.2.2
author=Scott Penrose author=Scott Penrose
maintainer=Scott Penrose maintainer=Scott Penrose
sentence=Client for the MeshCore Companion Radio serial protocol. sentence=Client for the MeshCore Companion Radio serial protocol.
paragraph=Connect a host MCU (e.g. an ESP32 display board) to a separate MeshCore companion radio over UART/USB serial. Portable C99 protocol core plus a thin Arduino wrapper. Send/receive on channels, set channel PSKs, read SNR/path metadata. Auto-drains the radio message queue. paragraph=Connect a host MCU (e.g. an ESP32 display board) to a separate MeshCore companion radio over UART/USB serial. Portable C99 protocol core plus a thin Arduino wrapper. Send/receive on channels, set channel PSKs, read SNR/path metadata. Auto-drains the radio message queue.
category=Communication category=Communication
url=https://github.com/digitaldimensions/MeshCoreCompanion url=https://github.com/SH3D/meshcore_c
architectures=esp32,nrf52,stm32,rp2040 architectures=esp32,nrf52,stm32,rp2040
+12 -2
View File
@@ -32,6 +32,7 @@ void MeshCoreCompanion::loop() {
while (mc_rx_poll(&_rx, _scratch, sizeof(_scratch), &olen)) { while (mc_rx_poll(&_rx, _scratch, sizeof(_scratch), &olen)) {
mc_event_t ev; mc_event_t ev;
if (mc_parse(_scratch, olen, &ev)) dispatch(ev); if (mc_parse(_scratch, olen, &ev)) dispatch(ev);
else if (_onUnparsed) _onUnparsed(_scratch, olen); /* frame we couldn't decode */
} }
} }
@@ -121,7 +122,11 @@ void MeshCoreCompanion::syncNextMessage() {
} }
void MeshCoreCompanion::drainMessages() { void MeshCoreCompanion::drainMessages() {
if (!_draining) { _draining = true; syncNextMessage(); } // Always (re)issue a sync — recovers a wedged drain (a lost reply leaving
// _draining stuck true), not only when idle. Safe to call periodically:
// the radio answers each sync with the next queued message or NoMoreMessages.
_draining = true;
syncNextMessage();
} }
void MeshCoreCompanion::getStats(uint8_t statsType) { void MeshCoreCompanion::getStats(uint8_t statsType) {
@@ -217,7 +222,12 @@ void MeshCoreCompanion::dispatch(const mc_event_t &ev) {
if (_onBinaryResp) _onBinaryResp(ev.u.binary_resp); if (_onBinaryResp) _onBinaryResp(ev.u.binary_resp);
break; break;
case MC_PUSH_MSG_WAITING: case MC_PUSH_MSG_WAITING:
if (_autoSync && !_draining) { _draining = true; syncNextMessage(); } // Always (re)start the drain on a MsgWaiting push. If a prior sync
// reply was lost (e.g. the host stalled and its UART RX overflowed),
// _draining can be left stuck true; gating on !_draining would then
// ignore every later push and silently stop pulling messages.
// Re-kicking unconditionally self-heals that.
if (_autoSync) { _draining = true; syncNextMessage(); }
break; break;
case MC_RESP_CHANNEL_MSG_RECV: case MC_RESP_CHANNEL_MSG_RECV:
case MC_RESP_CHANNEL_MSG_RECV_V3: case MC_RESP_CHANNEL_MSG_RECV_V3:
+6 -1
View File
@@ -46,7 +46,7 @@ public:
/* ---- commands (fire-and-forget; replies arrive via callbacks) ---- */ /* ---- commands (fire-and-forget; replies arrive via callbacks) ---- */
void appStart(const char *name = "esp32"); void appStart(const char *name = "esp32");
void deviceQuery(uint8_t appTargetVer = 1); void deviceQuery(uint8_t appTargetVer = 3); /* 3 = request V3 frames (SNR+RSSI) */
void getDeviceTime(); void getDeviceTime();
void setDeviceTime(uint32_t epochSecs); void setDeviceTime(uint32_t epochSecs);
void sendSelfAdvert(bool flood = true); void sendSelfAdvert(bool flood = true);
@@ -105,6 +105,10 @@ public:
void onContactsDone(ContactsDoneCb cb){ _onContactsDone = cb; } void onContactsDone(ContactsDoneCb cb){ _onContactsDone = cb; }
void onBinaryResponse(BinaryRespCb cb){ _onBinaryResp = cb; } void onBinaryResponse(BinaryRespCb cb){ _onBinaryResp = cb; }
void onEvent(EventCb cb) { _onEvent = cb; } /* every parsed frame */ void onEvent(EventCb cb) { _onEvent = cb; } /* every parsed frame */
/* Raw frames the parser did NOT recognise (payload incl. code byte). For
* diagnosing firmware/protocol mismatches — normally unused. */
using RawCb = std::function<void(const uint8_t* payload, size_t len)>;
void onUnparsedFrame(RawCb cb) { _onUnparsed = cb; }
private: private:
void sendPayload(const uint8_t *payload, size_t len); void sendPayload(const uint8_t *payload, size_t len);
@@ -134,6 +138,7 @@ private:
ContactsDoneCb _onContactsDone; ContactsDoneCb _onContactsDone;
BinaryRespCb _onBinaryResp; BinaryRespCb _onBinaryResp;
EventCb _onEvent; EventCb _onEvent;
RawCb _onUnparsed;
}; };
#endif /* MESHCORE_COMPANION_HPP */ #endif /* MESHCORE_COMPANION_HPP */
+3 -1
View File
@@ -46,6 +46,7 @@ static int parse_channel_text(const uint8_t *b, size_t n, int v3, mc_channel_msg
m->txt_type = q[2]; m->txt_type = q[2];
m->sender_ts = get_u32(q + 3); m->sender_ts = get_u32(q + 3);
m->snr_q4 = v3 ? (int8_t)b[0] : MC_SNR_NONE; m->snr_q4 = v3 ? (int8_t)b[0] : MC_SNR_NONE;
m->rssi = v3 ? (int8_t)b[1] : MC_RSSI_NONE; /* V3 lead: [snr][rssi][?] */
copy_rest_string(m->text, sizeof(m->text), q, 7, qn); copy_rest_string(m->text, sizeof(m->text), q, 7, qn);
return 1; return 1;
} }
@@ -63,6 +64,7 @@ static int parse_contact_text(const uint8_t *b, size_t n, int v3, mc_contact_msg
m->txt_type = q[7]; m->txt_type = q[7];
m->sender_ts = get_u32(q + 8); m->sender_ts = get_u32(q + 8);
m->snr_q4 = v3 ? (int8_t)b[0] : MC_SNR_NONE; m->snr_q4 = v3 ? (int8_t)b[0] : MC_SNR_NONE;
m->rssi = v3 ? (int8_t)b[1] : MC_RSSI_NONE; /* V3 lead: [snr][rssi][?] */
size_t toff = 12; size_t toff = 12;
if (m->txt_type == MC_TXT_SIGNED_PLAIN && qn >= toff + 4) { if (m->txt_type == MC_TXT_SIGNED_PLAIN && qn >= toff + 4) {
memcpy(m->signature, q + toff, 4); memcpy(m->signature, q + toff, 4);
@@ -148,7 +150,7 @@ size_t mc_cmd_app_start(uint8_t *out, size_t cap, const char *app_name) {
if (cap < total) return 0; if (cap < total) return 0;
size_t i = 0; size_t i = 0;
out[i++] = MC_CMD_APP_START; out[i++] = MC_CMD_APP_START;
out[i++] = 1; /* app version */ out[i++] = 3; /* app version (3 = accept V3 frames with SNR+RSSI) */
memset(out + i, 0, 6); i += 6; /* reserved */ memset(out + i, 0, 6); i += 6; /* reserved */
memcpy(out + i, app_name, nlen); i += nlen; memcpy(out + i, app_name, nlen); i += nlen;
return i; return i;
+5 -1
View File
@@ -30,7 +30,7 @@ extern "C" {
/* Library version. Keep in sync with library.json and library.properties; /* Library version. Keep in sync with library.json and library.properties;
* check_version.sh verifies all three match and that a git tag exists. */ * check_version.sh verifies all three match and that a git tag exists. */
#define MESHCORE_COMPANION_VERSION "0.2.0" #define MESHCORE_COMPANION_VERSION "0.2.2"
/* ---- Compile-time sizing (override before including if you need more) ---- */ /* ---- Compile-time sizing (override before including if you need more) ---- */
#ifndef MC_MAX_PAYLOAD #ifndef MC_MAX_PAYLOAD
@@ -154,6 +154,8 @@ enum { MC_ADVERT_ZERO_HOP = 0, MC_ADVERT_FLOOD = 1 };
#define MC_SNR_DB(q4) ((float)(q4) / 4.0f) #define MC_SNR_DB(q4) ((float)(q4) / 4.0f)
/* snr_q4 sentinel for non-V3 messages that carry no SNR. */ /* snr_q4 sentinel for non-V3 messages that carry no SNR. */
#define MC_SNR_NONE ((int8_t)-128) #define MC_SNR_NONE ((int8_t)-128)
/* rssi sentinel for non-V3 messages that carry no RSSI (0 dBm never occurs). */
#define MC_RSSI_NONE ((int8_t)0)
/* ======================================================================== * /* ======================================================================== *
* Receive side: streaming frame assembler * Receive side: streaming frame assembler
@@ -262,6 +264,7 @@ typedef struct {
uint8_t txt_type; uint8_t txt_type;
uint32_t sender_ts; uint32_t sender_ts;
int8_t snr_q4; /* V3 only (code 17); MC_SNR_NONE otherwise */ int8_t snr_q4; /* V3 only (code 17); MC_SNR_NONE otherwise */
int8_t rssi; /* dBm; V3 only (code 17); MC_RSSI_NONE otherwise */
char text[MC_MAX_TEXT]; /* for channel msgs this is "Name: body" */ char text[MC_MAX_TEXT]; /* for channel msgs this is "Name: body" */
} mc_channel_msg_t; } mc_channel_msg_t;
@@ -280,6 +283,7 @@ typedef struct {
uint8_t txt_type; uint8_t txt_type;
uint32_t sender_ts; uint32_t sender_ts;
int8_t snr_q4; /* V3 only (code 16); MC_SNR_NONE otherwise */ int8_t snr_q4; /* V3 only (code 16); MC_SNR_NONE otherwise */
int8_t rssi; /* dBm; V3 only (code 16); MC_RSSI_NONE otherwise */
uint8_t signature[4]; /* present when txt_type==MC_TXT_SIGNED_PLAIN */ uint8_t signature[4]; /* present when txt_type==MC_TXT_SIGNED_PLAIN */
int has_signature; int has_signature;
char text[MC_MAX_TEXT]; char text[MC_MAX_TEXT];