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:
Scott Penrose
2026-06-08 02:06:32 +10:00
commit cdfceba34d
13 changed files with 1539 additions and 0 deletions
+126
View File
@@ -0,0 +1,126 @@
/*
* test_codec.c -- host-side unit test for the portable core.
* Build & run: cc -std=c99 -Wall -Wextra -I../src test_codec.c ../src/meshcore_companion.c -o t && ./t
* SPDX-License-Identifier: MIT
*/
#include "meshcore_companion.h"
#include <stdio.h>
#include <string.h>
#include <assert.h>
static int fails = 0;
#define CHECK(cond, msg) do { \
if (cond) { printf(" ok %s\n", msg); } \
else { printf(" FAIL %s\n", msg); fails++; } } while (0)
/* Build a radio->app frame (0x3E + len + payload) for feeding the assembler. */
static size_t make_inbound(uint8_t *out, const uint8_t *payload, size_t plen) {
out[0] = MC_FRAME_RADIO_TO_APP;
out[1] = (uint8_t)plen; out[2] = (uint8_t)(plen >> 8);
memcpy(out + 3, payload, plen);
return plen + 3;
}
int main(void) {
uint8_t scratch[512], frame[512], payload[300];
size_t plen, flen, olen;
printf("== command builders ==\n");
plen = mc_cmd_device_query(scratch, sizeof scratch, 1);
CHECK(plen == 2 && scratch[0] == MC_CMD_DEVICE_QUERY && scratch[1] == 1, "device_query payload");
flen = mc_frame_encode(scratch, plen, frame, sizeof frame);
CHECK(flen == 5 && frame[0] == MC_FRAME_APP_TO_RADIO && frame[1] == 2 && frame[2] == 0,
"frame_encode wraps with 0x3C + len16");
/* set_channel: 1 + 1 + 32 + 16 = 50 bytes */
uint8_t secret[16];
for (int i = 0; i < 16; i++) secret[i] = (uint8_t)(0xA0 + i);
plen = mc_cmd_set_channel(scratch, sizeof scratch, 2, "sensors", secret);
CHECK(plen == 50 && scratch[0] == MC_CMD_SET_CHANNEL && scratch[1] == 2, "set_channel length & header");
CHECK(memcmp(scratch + 2, "sensors", 7) == 0 && scratch[2 + 7] == 0, "set_channel name NUL-padded");
CHECK(memcmp(scratch + 2 + 32, secret, 16) == 0, "set_channel secret tail");
plen = mc_cmd_send_channel_text(scratch, sizeof scratch, MC_TXT_PLAIN, 2, 0x11223344, "tank=87%");
CHECK(scratch[0] == MC_CMD_SEND_CHANNEL_TXT_MSG && scratch[1] == MC_TXT_PLAIN && scratch[2] == 2,
"send_channel_text header");
CHECK(scratch[3] == 0x44 && scratch[6] == 0x11, "send_channel_text timestamp LE");
CHECK(memcmp(scratch + 7, "tank=87%", 8) == 0, "send_channel_text body");
printf("== rx assembler + parse: DeviceInfo ==\n");
mc_rx_t rx; mc_rx_init(&rx);
/* fw_ver=8, maxc/2=50, maxch=8, blepin=123456, build="7 Jun 2026", model="XIAO-S3" */
size_t k = 0;
payload[k++] = MC_RESP_DEVICE_INFO;
payload[k++] = 8;
payload[k++] = 50;
payload[k++] = 8;
payload[k++] = 0x40; payload[k++] = 0xE2; payload[k++] = 0x01; payload[k++] = 0x00; /* 123456 LE */
memcpy(payload + k, "7 Jun 2026\0", 12); k += 12; /* 12-byte cstring field */
memcpy(payload + k, "XIAO-S3", 7); k += 7;
flen = make_inbound(frame, payload, k);
/* feed in two awkward chunks to exercise reassembly */
mc_rx_feed(&rx, frame, 4);
mc_rx_feed(&rx, frame + 4, flen - 4);
mc_event_t ev;
int got = mc_rx_poll(&rx, scratch, sizeof scratch, &olen);
CHECK(got == 1, "poll produced a frame from split feed");
CHECK(mc_parse(scratch, olen, &ev) == 1 && ev.code == MC_RESP_DEVICE_INFO, "parse device_info");
CHECK(ev.u.device_info.fw_ver == 8 && ev.u.device_info.max_contacts == 100 &&
ev.u.device_info.max_channels == 8 && ev.u.device_info.ble_pin == 123456,
"device_info numeric fields");
CHECK(strcmp(ev.u.device_info.build_date, "7 Jun 2026") == 0, "device_info build date");
CHECK(strcmp(ev.u.device_info.model, "XIAO-S3") == 0, "device_info model");
printf("== parse: ChannelMsgRecv (text) ==\n");
k = 0;
payload[k++] = MC_RESP_CHANNEL_MSG_RECV;
payload[k++] = 2; /* channel idx */
payload[k++] = MC_PATH_DIRECT; /* path len */
payload[k++] = MC_TXT_PLAIN;
payload[k++]=0x44;payload[k++]=0x33;payload[k++]=0x22;payload[k++]=0x11; /* ts */
memcpy(payload + k, "node3: tank=87%", 15); k += 15;
flen = make_inbound(frame, payload, k);
mc_rx_feed(&rx, frame, flen);
got = mc_rx_poll(&rx, scratch, sizeof scratch, &olen);
CHECK(got == 1 && mc_parse(scratch, olen, &ev) == 1 && ev.code == MC_RESP_CHANNEL_MSG_RECV,
"parse channel_msg");
CHECK(ev.u.channel_msg.channel_idx == 2 && ev.u.channel_msg.path_len == MC_PATH_DIRECT &&
ev.u.channel_msg.sender_ts == 0x11223344, "channel_msg fields");
CHECK(strcmp(ev.u.channel_msg.text, "node3: tank=87%") == 0, "channel_msg text (Name: body)");
printf("== parse: ChannelDataRecv (metadata) ==\n");
k = 0;
payload[k++] = MC_RESP_CHANNEL_DATA_RECV;
payload[k++] = (uint8_t)40; /* snr q4 = 40 -> 10.0 dB */
payload[k++] = 0; payload[k++] = 0;
payload[k++] = 2; /* channel idx */
payload[k++] = 3; /* path len (flood, 3 hops) */
payload[k++] = 0xFF; payload[k++] = 0xFF; /* data type 0xFFFF (Dev) */
payload[k++] = 4; /* data len */
payload[k++]=0xDE;payload[k++]=0xAD;payload[k++]=0xBE;payload[k++]=0xEF;
flen = make_inbound(frame, payload, k);
mc_rx_feed(&rx, frame, flen);
got = mc_rx_poll(&rx, scratch, sizeof scratch, &olen);
CHECK(got == 1 && mc_parse(scratch, olen, &ev) == 1 && ev.code == MC_RESP_CHANNEL_DATA_RECV,
"parse channel_data");
CHECK(ev.u.channel_data.snr_q4 == 40 && MC_SNR_DB(ev.u.channel_data.snr_q4) == 10.0f,
"channel_data SNR x4 decode");
CHECK(ev.u.channel_data.path_len == 3 && ev.u.channel_data.data_type == 0xFFFF &&
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("== resync: garbage before a valid frame ==\n");
uint8_t junk[3] = { 0x00, 0x99, 0x01 };
mc_rx_feed(&rx, junk, 3);
uint8_t okp[1] = { MC_RESP_OK };
flen = make_inbound(frame, okp, 1);
mc_rx_feed(&rx, frame, flen);
got = mc_rx_poll(&rx, scratch, sizeof scratch, &olen);
CHECK(got == 1 && mc_parse(scratch, olen, &ev) == 1 && ev.code == MC_RESP_OK,
"resync past junk to find OK frame");
printf("\n%s (%d failure%s)\n", fails ? "TESTS FAILED" : "ALL TESTS PASSED",
fails, fails == 1 ? "" : "s");
return fails ? 1 : 0;
}