From c2c2646e3d90d600bf7a097447171d5028bef141 Mon Sep 17 00:00:00 2001 From: Scott Penrose Date: Mon, 8 Jun 2026 13:37:27 +1000 Subject: [PATCH] more complete radio support --- CMakeLists.txt | 4 + README.md | 10 + REVIEW.md | 98 ++++------ TODO.md | 3 + examples-linux/contacts/meshcore_contacts.c | 127 +++++++++++++ src/MeshCoreCompanion.cpp | 49 +++++ src/MeshCoreCompanion.h | 26 +++ src/meshcore_companion.c | 192 ++++++++++++++++++++ src/meshcore_companion.h | 107 ++++++++++- test/test_codec.c | 123 +++++++++++++ 10 files changed, 676 insertions(+), 63 deletions(-) create mode 100644 TODO.md create mode 100644 examples-linux/contacts/meshcore_contacts.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 91985ad..21b8114 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,6 +31,10 @@ target_link_libraries(meshcore_tty PRIVATE meshcore_companion) add_executable(meshcore_info examples-linux/info/meshcore_info.c) target_link_libraries(meshcore_info PRIVATE meshcore_companion) +# Linux example: list the radio's contacts, then exit. +add_executable(meshcore_contacts examples-linux/contacts/meshcore_contacts.c) +target_link_libraries(meshcore_contacts PRIVATE meshcore_companion) + # Host unit test for the codec (no hardware required). enable_testing() add_executable(test_codec test/test_codec.c) diff --git a/README.md b/README.md index a088b63..ef1697b 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ Two layers, so the protocol is reusable and testable far beyond Arduino: Transport is injected; the core never touches a port. "Physical serial only" is simply which `Stream` you hand the wrapper. +XXX Add a why - because I want control from other embedded systems, talk about how I do compile meshcore directly in and juse use a SX??? lora modem, that however uses a lot of resources and memroy and some hardware doesn't have access to enough pins to do SPI. Also I write code that runs on low level embedded linux SBC or SOC that are dependent on C. + ## Wire protocol `frame = [type:u8][len:u16 LE][payload:len bytes]`, type `0x3C` for app→radio, @@ -151,6 +153,14 @@ cd test && cc -std=c99 -Wall -Wextra -I../src test_codec.c ../src/meshcore_compa The unit test exercises command encoding, frame reassembly across split reads, every parser, and resync past line garbage. +## Raw Serial Port + +For embedded linux, windows etc, there is USB drivvers that expose a serial port we can access from the raw c. But from a bare metal (or FreeRTOS etc) embedded CPU we need TTL serial port access. To do this is relatively simple on most of the MeshCore hardware supported for Companion mode, however can't be done from online flasher. + +XXX How to build example, lets start with a XIAO ESP32S3 with SX1262. Minimum steps here to show somone how to do it and work out what pins can be used on that board for RX/TX + +And add a Arduino example (basic ESP32S3 dev) that also programs it, e.g. Name, Network to Australia Narrow band and to add a channel. Basically a zero config just plug it in and the host device sets it up how it needs. For completeness this examle code then loops watching the channel programmed and responding to any hello sent with a response. + ## Notes - **Timestamps:** channel messages carry an epoch-seconds sender timestamp. diff --git a/REVIEW.md b/REVIEW.md index d9381f2..13b6d67 100644 --- a/REVIEW.md +++ b/REVIEW.md @@ -1,6 +1,6 @@ # meshcore_c vs meshcore_py — Feature Gap Analysis -Date: 2026-06-08 (Phase 1 complete) +Date: 2026-06-08 (Phase 1 + 2 complete) ## Summary @@ -9,12 +9,12 @@ Companion Radio **serial** protocol. It implements the wire framing (0x3C/0x3E, LE length), handshake, channel text/data send/receive (incl. the V3 SNR variants), direct/contact + CLI message sending, channel PSK programming, full device/self/stats parsing, and basic radio parameters. Zero external deps in the -core; ships examples for Linux (`tty_bridge`, `info`), ESP-IDF, STM32 HAL, and -Arduino. +core; ships examples for Linux (`tty_bridge`, `info`, `contacts`), ESP-IDF, +STM32 HAL, and Arduino. -Against the reference `meshcore_py`, coverage is now roughly **45%**. The two -remaining **major** areas are contact management and binary (telemetry/status) -requests. Everything else is feature-completeness or convenience. +Against the reference `meshcore_py`, coverage is now roughly **65%**. With +Phase 2 done (contacts + binary requests), no **major** subsystems remain — the +rest is device-management/network commands and convenience features. --- @@ -31,73 +31,59 @@ contact recv (SNR) · `send_txt_msg` / `send_cmd` (direct + CLI, CMD 2) · `get_stats` → **Stats** (core/radio/packets) · self-advert · auto-drain on `MsgWaiting` (Arduino wrapper). +**Phase 2:** contacts — `get_contacts` (delta sync), `add_update_contact`, +`remove_contact`, `reset_path`, `share_contact`, `export_contact` / +`import_contact`, `get_contact_by_key`; parsing of **ContactsStart / Contact / +EndOfContacts**, **NewAdvert** (0x8A as a contact), **ContactUri**, +**ContactDeleted**, **ContactsFull**, **AdvertPath**, **AutoaddConfig**, +**AllowedRepeatFreq**. Binary — `send_binary_req` / `send_anon_req`, +**BinaryResponse** (0x8C) parse with ack-tag correlation, and `mc_parse_status` +to decode a STATUS response. Arduino wrapper gains `onContact` / +`onContactsDone` / `onBinaryResponse` + matching command methods. + Host unit tests (`test/test_codec.c`) cover all of the above parsers/builders. --- ## Remaining gaps -### MAJOR - -#### 1. Contact management — entirely missing -No way to enumerate or manage the mesh node list from C. - -| Feature | CMD | meshcore_py | meshcore_c | -|---|---|---|---| -| GET_CONTACTS (lastmod delta sync) | 4 | ✅ | ❌ | -| ADD_UPDATE_CONTACT | 9 | ✅ | ❌ | -| REMOVE_CONTACT | 15 | ✅ | ❌ | -| SHARE_CONTACT | 16 | ✅ | ❌ | -| EXPORT_CONTACT / IMPORT_CONTACT | 17 / 18 | ✅ | ❌ | -| RESET_PATH | 13 | ✅ | ❌ | -| GET_CONTACT_BY_KEY | 30 | ✅ | ❌ | -| SET_ADVERT_NAME / SET_ADVERT_LATLON | 8 / 14 | ✅ | ❌ | -| GET_ADVERT_PATH | 42 | ✅ | ❌ | -| SET/GET_AUTOADD_CONFIG | 58 / 59 | ✅ | ❌ | -| GET_ALLOWED_REPEAT_FREQ | 60 | ✅ | ❌ | - -Response parsing needed: `CONTACT_START` (2), `CONTACT` (3), `CONTACT_END` (4), -`CONTACT_URI` (11), `ADVERT_PATH` (22), `AUTOADD_CONFIG` (25), -`ALLOWED_REPEAT_FREQ` (26), `CONTACT_DELETED` (0x8F), `CONTACTS_FULL` (0x90), -`NEW_ADVERT` push (0x8A). - -#### 2. Binary + anonymous requests — entirely missing -Remote interrogation of other nodes. -- `SEND_BINARY_REQ` (50) subtypes: STATUS, TELEMETRY, MMA, ACL, NEIGHBOURS -- `SEND_ANON_REQ` (57) subtypes: REGIONS, OWNER, BASIC (remote clock) -- `BINARY_RESPONSE` push (0x8C) parsing + tag tracking (`expected_ack` → - response correlation, reusing the `MsgSent` ack-tag + timeout already parsed) -- `TELEMETRY` push (0x8B) parsing (Cayenne LPP — likely decoded by the caller) +*Phase 2 cleared the two former major gaps — contact management and +binary/anonymous requests. No major subsystems remain; everything below is +feature-completeness or convenience.* ### MINOR -#### 3. Direct-message reliability +#### 1. Direct-message reliability - `SEND_LOGIN` (26) / `SEND_LOGOUT` (29) / `SEND_STATUS_REQ` (27) - Retry helper: resend up to N times, auto-switch to flood after K direct failures (uses the already-parsed `MsgSent` suggested-timeout). -#### 4. Device-management commands +#### 2. Device-management commands `REBOOT` (19) · `FACTORY_RESET` (51, two-step) · `SET_DEVICE_PIN` (37) · `SET_OTHER_PARAMS` (38: telemetry modes, multi_acks, advert policy) · `SET_TUNING_PARAMS` (21) / `GET_TUNING_PARAMS` (43) · `HAS_CONNECTION` (28) · -`SET_TX_POWER` (12). +`SET_TX_POWER` (12). Also the advert/autoadd command *builders* whose responses +are already parsed: `SET_ADVERT_NAME` (8), `SET_ADVERT_LATLON` (14), +`GET_ADVERT_PATH` (42), `SET/GET_AUTOADD_CONFIG` (58/59), +`GET_ALLOWED_REPEAT_FREQ` (60). -#### 5. Crypto & security +#### 3. Crypto & security `EXPORT_PRIVATE_KEY` (23) / `IMPORT_PRIVATE_KEY` (24) · `SIGN_START` (33) / `SIGN_DATA` (34) / `SIGN_FINISH` (35). (Signature *extraction* from received signed messages is already done.) -#### 6. Network-layer commands +#### 4. Network-layer commands `SEND_RAW_DATA` (25) · `SEND_TRACE_PATH` (36, per-hop SNR) · `SEND_CONTROL_DATA` (55) · `SET_FLOOD_SCOPE` (54) / `SET/GET_DEFAULT_FLOOD_SCOPE` (63 / 64). -#### 7. Remaining response/push parsing +#### 5. Remaining response/push parsing `SIGN_START`/`SIGNATURE` (19/20) · `CUSTOM_VARS` (21) · `TUNING_PARAMS` (23) · `DEFAULT_FLOOD_SCOPE` (28) · `LOG_RX_DATA` push (0x88) · `TRACE_DATA` push -(0x89) · `PATH_DISCOVERY` (0x8D) · `CONTROL_DATA` push (0x8E). +(0x89) · `TELEMETRY` push (0x8B, Cayenne LPP) · `PATH_DISCOVERY` (0x8D) · +`CONTROL_DATA` push (0x8E). -#### 8. Smaller data gaps +#### 6. Smaller data gaps - **Battery**: only the 2-byte voltage is parsed; the full `BATT_AND_STORAGE` response adds `used_kb` + `total_kb`. - **CUSTOM_VARS**: `GET_CUSTOM_VARS` (40) / `SET_CUSTOM_VAR` (41) + parse. @@ -108,7 +94,7 @@ Remote interrogation of other nodes. - Event filtering / `wait_for_event(timeout)` helpers (C core stays struct-based). - Optional contact / self-info / time state tracking in the wrapper. - Synchronous request→response helpers (lock, await matching event, apply - suggested timeout) — pairs with #2 binary requests and #3 retry. + suggested timeout) — builds on the binary-request ack-tag + the retry helper (#1). - Channel-log AES decryption (heavy; needs crypto — probably out of scope for the bare-metal core). - Cayenne LPP telemetry decode (application layer / caller). @@ -128,24 +114,18 @@ Remote interrogation of other nodes. ## Roadmap -### Phase 2 — Contacts & binary requests (next, major) -1. Contact command builders: GET_CONTACTS, ADD_UPDATE_CONTACT, REMOVE_CONTACT, - RESET_PATH, SHARE_CONTACT, EXPORT/IMPORT_CONTACT, GET_CONTACT_BY_KEY. -2. Contact response parsing: CONTACT_START / CONTACT / CONTACT_END (+ CONTACT_URI, - CONTACT_DELETED, CONTACTS_FULL, NEW_ADVERT). -3. Binary-request infrastructure: `mc_cmd_send_binary_req` / `mc_cmd_send_anon_req`, - `BINARY_RESPONSE` parsing, ack-tag correlation (reuse `MsgSent` tag+timeout). -4. Tests + a `contacts` Linux example. - -### Phase 3 — Device management & network commands (minor) +### Phase 3 — Device management & network commands (next, minor) REBOOT / FACTORY_RESET / SET_DEVICE_PIN / SET_OTHER_PARAMS · TUNING_PARAMS get/set · EXPORT/IMPORT_PRIVATE_KEY · CUSTOM_VARS · SET_ADVERT_NAME/LATLON · -flood scope get/set · HAS_CONNECTION · BATT_AND_STORAGE storage fields · -SEND_TRACE_PATH / SEND_RAW_DATA / SEND_CONTROL_DATA. +autoadd-config + advert-path + allowed-repeat-freq command builders · flood scope +get/set · HAS_CONNECTION · BATT_AND_STORAGE storage fields · SEND_TRACE_PATH / +SEND_RAW_DATA / SEND_CONTROL_DATA · direct-message retry helper (SEND_LOGIN/LOGOUT). ### Phase 4 — Polish (nice to have) Connection-manager reconnect · sync request/response + retry-with-flood-fallback -helpers · event filtering · more examples and tests. +helpers · event filtering · TELEMETRY/Cayenne-LPP decode · more examples and tests. + +*(Phases 1 & 2 complete — see "Already implemented".)* --- diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..68dc678 --- /dev/null +++ b/TODO.md @@ -0,0 +1,3 @@ +# Arduion BLE Version (low priority) + +Look at possibility of adding BLE or TCP wrappers for ESP32/nRF52 ble stacks to connect to companion nodes. diff --git a/examples-linux/contacts/meshcore_contacts.c b/examples-linux/contacts/meshcore_contacts.c new file mode 100644 index 0000000..b08dc60 --- /dev/null +++ b/examples-linux/contacts/meshcore_contacts.c @@ -0,0 +1,127 @@ +/* + * meshcore_contacts.c + * + * One-shot: open a tty, ask the companion radio for its contact list, print + * each contact (name, key prefix, type, path, location, last-seen), then exit. + * Demonstrates the Phase 2 contact API: GET_CONTACTS -> CONTACTS_START, a stream + * of CONTACT records, then END_OF_CONTACTS. + * + * build: via CMakeLists.txt at the repo root, or: + * cc -std=c99 -Wall -Wextra -I../../src \ + * meshcore_contacts.c ../../src/meshcore_companion.c -o meshcore_contacts + * + * run: ./meshcore_contacts /dev/ttyACM0 + * + * SPDX-License-Identifier: MIT + * Author: Scott Penrose / Digital Dimensions. + */ +#define _DEFAULT_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "meshcore_companion.h" + +static int tty_open(const char *path) { + int fd = open(path, O_RDWR | O_NOCTTY | O_NONBLOCK); + if (fd < 0) { fprintf(stderr, "open %s: %s\n", path, strerror(errno)); return -1; } + struct termios tio; + if (tcgetattr(fd, &tio) != 0) { close(fd); return -1; } + cfmakeraw(&tio); + cfsetispeed(&tio, B115200); cfsetospeed(&tio, B115200); + tio.c_cflag |= (CLOCAL | CREAD); + tio.c_cc[VMIN] = 0; tio.c_cc[VTIME] = 0; + if (tcsetattr(fd, TCSANOW, &tio) != 0) { close(fd); return -1; } + tcflush(fd, TCIOFLUSH); + return fd; +} + +static void send_payload(int fd, const uint8_t *payload, size_t len) { + uint8_t frame[MC_RX_BUFSZ]; + size_t flen = mc_frame_encode(payload, len, frame, sizeof frame), off = 0; + while (off < flen) { + ssize_t w = write(fd, frame + off, flen - off); + if (w < 0) { if (errno == EAGAIN || errno == EINTR) continue; break; } + off += (size_t)w; + } +} + +static long now_ms(void) { + struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); + return ts.tv_sec * 1000L + ts.tv_nsec / 1000000L; +} + +static int g_expected = -1; /* CONTACTS_START count, or -1 if not seen */ +static int g_got = 0; +static int g_done = 0; + +static void print_contact(const mc_contact_t *c) { + printf(" %-20s ", c->adv_name[0] ? c->adv_name : "(unnamed)"); + for (int i = 0; i < 6; i++) printf("%02x", c->public_key[i]); + printf(" type=%u", c->type); + if (c->out_path_len == 0xFF) printf(" path=direct"); + else printf(" path=%u hop(s)", c->out_path_len); + if (c->adv_lat || c->adv_lon) + printf(" loc=%.5f,%.5f", c->adv_lat / 1e6, c->adv_lon / 1e6); + printf("\n"); +} + +static void handle(const mc_event_t *ev) { + switch (ev->code) { + case MC_RESP_CONTACTS_START: + g_expected = (int)ev->u.contacts_count; + printf("Contacts (%d)\n", g_expected); + break; + case MC_RESP_CONTACT: + g_got++; + print_contact(&ev->u.contact); + break; + case MC_RESP_END_OF_CONTACTS: + g_done = 1; + break; + default: break; + } +} + +int main(int argc, char **argv) { + if (argc < 2) { fprintf(stderr, "usage: %s \n", argv[0]); return 2; } + int fd = tty_open(argv[1]); + if (fd < 0) return 1; + + mc_rx_t rx; mc_rx_init(&rx); + uint8_t cmd[MC_MAX_PAYLOAD]; size_t n; + n = mc_cmd_app_start(cmd, sizeof cmd, "meshcore_contacts"); if (n) send_payload(fd, cmd, n); + n = mc_cmd_device_query(cmd, sizeof cmd, 1); if (n) send_payload(fd, cmd, n); + n = mc_cmd_get_contacts(cmd, sizeof cmd, 0); if (n) send_payload(fd, cmd, n); + + long deadline = now_ms() + 4000; + while (!g_done && now_ms() < deadline) { + fd_set rf; FD_ZERO(&rf); FD_SET(fd, &rf); + long rem = deadline - now_ms(); + struct timeval tv = { rem / 1000, (rem % 1000) * 1000 }; + int r = select(fd + 1, &rf, NULL, NULL, &tv); + if (r < 0) { if (errno == EINTR) continue; break; } + if (r == 0) break; + uint8_t in[256]; + ssize_t got = read(fd, in, sizeof in); + if (got <= 0) { if (got < 0 && (errno == EAGAIN || errno == EINTR)) continue; break; } + mc_rx_feed(&rx, in, (size_t)got); + uint8_t pl[MC_MAX_PAYLOAD]; size_t pn; + while (mc_rx_poll(&rx, pl, sizeof pl, &pn)) { + mc_event_t ev; + if (mc_parse(pl, pn, &ev)) handle(&ev); + } + } + + if (g_expected < 0) printf("No contact list received (is the radio connected?).\n"); + else if (!g_done) printf("(timed out after %d of %d contacts)\n", g_got, g_expected); + close(fd); + return g_expected >= 0 ? 0 : 1; +} diff --git a/src/MeshCoreCompanion.cpp b/src/MeshCoreCompanion.cpp index a7985c3..54707a2 100644 --- a/src/MeshCoreCompanion.cpp +++ b/src/MeshCoreCompanion.cpp @@ -128,6 +128,45 @@ void MeshCoreCompanion::getStats(uint8_t statsType) { uint8_t p[2]; sendPayload(p, mc_cmd_get_stats(p, sizeof(p), statsType)); } +/* ---- contacts ---- */ +void MeshCoreCompanion::getContacts(uint32_t sinceLastmod) { + uint8_t p[5]; sendPayload(p, mc_cmd_get_contacts(p, sizeof(p), sinceLastmod)); +} +void MeshCoreCompanion::addUpdateContact(const mc_contact_t &c) { + uint8_t p[1 + 32 + 1 + 1 + 1 + 64 + 32 + 4 + 4 + 4]; + sendPayload(p, mc_cmd_add_update_contact(p, sizeof(p), &c)); +} +void MeshCoreCompanion::removeContact(const uint8_t pubkey[32]) { + uint8_t p[33]; sendPayload(p, mc_cmd_remove_contact(p, sizeof(p), pubkey)); +} +void MeshCoreCompanion::resetPath(const uint8_t pubkey[32]) { + uint8_t p[33]; sendPayload(p, mc_cmd_reset_path(p, sizeof(p), pubkey)); +} +void MeshCoreCompanion::shareContact(const uint8_t pubkey[32]) { + uint8_t p[33]; sendPayload(p, mc_cmd_share_contact(p, sizeof(p), pubkey)); +} +void MeshCoreCompanion::getContactByKey(const uint8_t pubkey[32]) { + uint8_t p[33]; sendPayload(p, mc_cmd_get_contact_by_key(p, sizeof(p), pubkey)); +} +void MeshCoreCompanion::exportContact(const uint8_t pubkey[32]) { + uint8_t p[33]; sendPayload(p, mc_cmd_export_contact(p, sizeof(p), pubkey)); +} +void MeshCoreCompanion::importContact(const uint8_t *card, size_t len) { + uint8_t p[MC_MAX_PAYLOAD]; sendPayload(p, mc_cmd_import_contact(p, sizeof(p), card, len)); +} + +/* ---- binary / anonymous requests ---- */ +void MeshCoreCompanion::sendBinaryReq(const uint8_t dst[32], uint8_t reqType, + const uint8_t *data, size_t len) { + uint8_t p[MC_MAX_PAYLOAD]; + sendPayload(p, mc_cmd_send_binary_req(p, sizeof(p), dst, reqType, data, len)); +} +void MeshCoreCompanion::sendAnonReq(const uint8_t dst[32], uint8_t reqType, + const uint8_t *data, size_t len) { + uint8_t p[MC_MAX_PAYLOAD]; + sendPayload(p, mc_cmd_send_anon_req(p, sizeof(p), dst, reqType, data, len)); +} + /* ---- dispatch ---- */ void MeshCoreCompanion::dispatch(const mc_event_t &ev) { if (_onEvent) _onEvent(ev); @@ -151,6 +190,16 @@ void MeshCoreCompanion::dispatch(const mc_event_t &ev) { case MC_RESP_STATS: if (_onStats) _onStats(ev.u.stats); break; + case MC_RESP_CONTACT: + case MC_PUSH_NEW_ADVERT: + if (_onContact2) _onContact2(ev.u.contact); + break; + case MC_RESP_END_OF_CONTACTS: + if (_onContactsDone) _onContactsDone(ev.u.contacts_lastmod); + break; + case MC_PUSH_BINARY_RESP: + if (_onBinaryResp) _onBinaryResp(ev.u.binary_resp); + break; case MC_PUSH_MSG_WAITING: if (_autoSync && !_draining) { _draining = true; syncNextMessage(); } break; diff --git a/src/MeshCoreCompanion.h b/src/MeshCoreCompanion.h index f832da8..1134394 100644 --- a/src/MeshCoreCompanion.h +++ b/src/MeshCoreCompanion.h @@ -29,6 +29,9 @@ public: using SelfInfoCb = std::function; using MsgSentCb = std::function; using StatsCb = std::function; + using ContactCb = std::function; + using ContactsDoneCb = std::function; + using BinaryRespCb = std::function; using EventCb = std::function; explicit MeshCoreCompanion(Stream &io) : _io(io) {} @@ -61,6 +64,23 @@ public: void drainMessages(); /* start a manual sync-drain loop */ void getStats(uint8_t statsType); + /* ---- contacts ---- */ + void getContacts(uint32_t sinceLastmod = 0); /* → onContact... then onContactsDone */ + void addUpdateContact(const mc_contact_t &c); + void removeContact(const uint8_t pubkey[32]); + void resetPath(const uint8_t pubkey[32]); + void shareContact(const uint8_t pubkey[32]); + void getContactByKey(const uint8_t pubkey[32]); + void exportContact(const uint8_t pubkey[32] = nullptr); /* nullptr = own card */ + void importContact(const uint8_t *card, size_t len); + + /* ---- binary / anonymous requests ---- */ + void sendBinaryReq(const uint8_t dst[32], uint8_t reqType, + const uint8_t *data = nullptr, size_t len = 0); + void sendAnonReq(const uint8_t dst[32], uint8_t reqType, + const uint8_t *data = nullptr, size_t len = 0); + void requestStatus(const uint8_t dst[32]) { sendBinaryReq(dst, MC_BINARY_REQ_STATUS); } + /* ---- behaviour ---- */ void setAutoSync(bool on) { _autoSync = on; } @@ -77,6 +97,9 @@ public: void onSelfInfo(SelfInfoCb cb) { _onSelfInfo = cb; } void onMsgSent(MsgSentCb cb) { _onMsgSent = cb; } void onStats(StatsCb cb) { _onStats = cb; } + void onContact(ContactCb cb) { _onContact2 = cb; } /* CONTACT + NEW_ADVERT */ + void onContactsDone(ContactsDoneCb cb){ _onContactsDone = cb; } + void onBinaryResponse(BinaryRespCb cb){ _onBinaryResp = cb; } void onEvent(EventCb cb) { _onEvent = cb; } /* every parsed frame */ private: @@ -103,6 +126,9 @@ private: SelfInfoCb _onSelfInfo; MsgSentCb _onMsgSent; StatsCb _onStats; + ContactCb _onContact2; + ContactsDoneCb _onContactsDone; + BinaryRespCb _onBinaryResp; EventCb _onEvent; }; diff --git a/src/meshcore_companion.c b/src/meshcore_companion.c index 8299f60..e393adb 100644 --- a/src/meshcore_companion.c +++ b/src/meshcore_companion.c @@ -73,6 +73,24 @@ static int parse_contact_text(const uint8_t *b, size_t n, int v3, mc_contact_msg return 1; } +/* Contact record, shared by CONTACT (3) and NEW_ADVERT (0x8A). */ +static int parse_contact(const uint8_t *b, size_t n, mc_contact_t *c) { + /* pubkey32 + type + flags + path_len + path64 + name32 + 3*u32 + u32 */ + if (n < 32 + 1 + 1 + 1 + 64 + 32 + 4 + 4 + 4) return 0; + size_t o = 0; + memcpy(c->public_key, b + o, 32); o += 32; + c->type = b[o++]; + c->flags = b[o++]; + c->out_path_len = b[o++]; + memcpy(c->out_path, b + o, 64); o += 64; + copy_cstring(c->adv_name, sizeof(c->adv_name), b + o, 32); o += 32; + c->last_advert = get_u32(b + o); o += 4; + c->adv_lat = (int32_t)get_u32(b + o); o += 4; + c->adv_lon = (int32_t)get_u32(b + o); o += 4; + c->lastmod = get_u32(b + o); o += 4; + return 1; +} + /* ======================================================================== */ void mc_rx_init(mc_rx_t *rx) { rx->len = 0; } @@ -239,6 +257,84 @@ size_t mc_cmd_get_stats(uint8_t *out, size_t cap, uint8_t stats_type) { out[0] = MC_CMD_GET_STATS; out[1] = stats_type; return 2; } +/* ---- contacts ---- */ +size_t mc_cmd_get_contacts(uint8_t *out, size_t cap, uint32_t since_lastmod) { + if (since_lastmod > 0) { + if (cap < 5) return 0; + out[0] = MC_CMD_GET_CONTACTS; put_u32(out + 1, since_lastmod); return 5; + } + if (cap < 1) return 0; + out[0] = MC_CMD_GET_CONTACTS; return 1; +} + +/* [code][pubkey:32] */ +static size_t cmd_key_only(uint8_t *out, size_t cap, uint8_t code, const uint8_t key[32]) { + if (cap < 33) return 0; + out[0] = code; memcpy(out + 1, key, 32); return 33; +} +size_t mc_cmd_remove_contact(uint8_t *out, size_t cap, const uint8_t pubkey[32]) { + return cmd_key_only(out, cap, MC_CMD_REMOVE_CONTACT, pubkey); +} +size_t mc_cmd_reset_path(uint8_t *out, size_t cap, const uint8_t pubkey[32]) { + return cmd_key_only(out, cap, MC_CMD_RESET_PATH, pubkey); +} +size_t mc_cmd_share_contact(uint8_t *out, size_t cap, const uint8_t pubkey[32]) { + return cmd_key_only(out, cap, MC_CMD_SHARE_CONTACT, pubkey); +} +size_t mc_cmd_get_contact_by_key(uint8_t *out, size_t cap, const uint8_t pubkey[32]) { + return cmd_key_only(out, cap, MC_CMD_GET_CONTACT_BY_KEY, pubkey); +} +size_t mc_cmd_export_contact(uint8_t *out, size_t cap, const uint8_t pubkey[32]) { + if (pubkey == NULL) { if (cap < 1) return 0; out[0] = MC_CMD_EXPORT_CONTACT; return 1; } + return cmd_key_only(out, cap, MC_CMD_EXPORT_CONTACT, pubkey); +} +size_t mc_cmd_import_contact(uint8_t *out, size_t cap, const uint8_t *card, size_t card_len) { + size_t total = 1 + card_len; + if (cap < total) return 0; + out[0] = MC_CMD_IMPORT_CONTACT; + if (card_len) memcpy(out + 1, card, card_len); + return total; +} +size_t mc_cmd_add_update_contact(uint8_t *out, size_t cap, const mc_contact_t *c) { + size_t total = 1 + 32 + 1 + 1 + 1 + 64 + 32 + 4 + 4 + 4; /* 144 */ + if (cap < total) return 0; + size_t o = 0; + out[o++] = MC_CMD_ADD_UPDATE_CONTACT; + memcpy(out + o, c->public_key, 32); o += 32; + out[o++] = c->type; + out[o++] = c->flags; + out[o++] = c->out_path_len; + memcpy(out + o, c->out_path, 64); o += 64; + memset(out + o, 0, 32); + { size_t nl = strlen(c->adv_name); if (nl > 32) nl = 32; memcpy(out + o, c->adv_name, nl); } + o += 32; + put_u32(out + o, c->last_advert); o += 4; + put_u32(out + o, (uint32_t)c->adv_lat); o += 4; + put_u32(out + o, (uint32_t)c->adv_lon); o += 4; + return o; +} + +/* ---- binary / anonymous requests ---- */ +static size_t cmd_req(uint8_t *out, size_t cap, uint8_t code, const uint8_t dst[32], + uint8_t req_type, const uint8_t *data, size_t data_len) { + size_t total = 1 + 32 + 1 + data_len; + if (cap < total) return 0; + size_t o = 0; + out[o++] = code; + memcpy(out + o, dst, 32); o += 32; + out[o++] = req_type; + if (data_len) { memcpy(out + o, data, data_len); o += data_len; } + return o; +} +size_t mc_cmd_send_binary_req(uint8_t *out, size_t cap, const uint8_t dst[32], + uint8_t req_type, const uint8_t *data, size_t data_len) { + return cmd_req(out, cap, MC_CMD_SEND_BINARY_REQ, dst, req_type, data, data_len); +} +size_t mc_cmd_send_anon_req(uint8_t *out, size_t cap, const uint8_t dst[32], + uint8_t req_type, const uint8_t *data, size_t data_len) { + return cmd_req(out, cap, MC_CMD_SEND_ANON_REQ, dst, req_type, data, data_len); +} + /* ======================================================================== */ int mc_parse(const uint8_t *p, size_t len, mc_event_t *ev) { if (len < 1) return 0; @@ -412,7 +508,103 @@ int mc_parse(const uint8_t *p, size_t len, mc_event_t *ev) { ev->u.send_confirmed.round_trip = get_u32(b + 4); return 1; + case MC_RESP_CONTACTS_START: + if (n < 4) return 0; + ev->u.contacts_count = get_u32(b); + return 1; + + case MC_RESP_CONTACT: + case MC_PUSH_NEW_ADVERT: + return parse_contact(b, n, &ev->u.contact); + + case MC_RESP_END_OF_CONTACTS: + if (n < 4) return 0; + ev->u.contacts_lastmod = get_u32(b); + return 1; + + case MC_RESP_CONTACT_URI: { + size_t dl = n; + if (dl > sizeof(ev->u.contact_uri.data)) dl = sizeof(ev->u.contact_uri.data); + memcpy(ev->u.contact_uri.data, b, dl); + ev->u.contact_uri.len = (uint8_t)dl; + return 1; + } + + case MC_RESP_ADVERT_PATH: { + if (n < 5) return 0; + ev->u.advert_path.timestamp = get_u32(b); + ev->u.advert_path.path_len = b[4]; + size_t pl = n - 5; + if (pl > sizeof(ev->u.advert_path.path)) pl = sizeof(ev->u.advert_path.path); + memcpy(ev->u.advert_path.path, b + 5, pl); + return 1; + } + + case MC_RESP_AUTOADD_CONFIG: + if (n < 1) return 0; + ev->u.autoadd_config = b[0]; + return 1; + + case MC_RESP_ALLOWED_REPEAT_FREQ: { + uint8_t cnt = 0; + size_t o = 0; + while (o + 8 <= n && cnt < 8) { + uint32_t lo = get_u32(b + o), hi = get_u32(b + o + 4); + if (lo == 0 && hi == 0) break; + ev->u.allowed_freq.pair[cnt].min = lo; + ev->u.allowed_freq.pair[cnt].max = hi; + cnt++; o += 8; + } + ev->u.allowed_freq.count = cnt; + return 1; + } + + case MC_PUSH_BINARY_RESP: { + if (n < 5) return 0; /* reserved(1) + tag(4) */ + mc_binary_resp_t *r = &ev->u.binary_resp; + /* b[0] reserved */ + r->tag = get_u32(b + 1); + size_t dl = n - 5; + if (dl > sizeof(r->data)) dl = sizeof(r->data); + memcpy(r->data, b + 5, dl); + r->data_len = (uint8_t)dl; + return 1; + } + + case MC_PUSH_CONTACT_DELETED: + if (n < 32) return 0; + memcpy(ev->u.deleted_key, b, 32); + return 1; + + case MC_PUSH_CONTACTS_FULL: + return 1; + default: return 0; /* recognised code byte set in ev->code, body not parsed */ } } + +/* Decode a STATUS binary-response payload (see mc_status_t). */ +int mc_parse_status(const uint8_t *d, size_t len, mc_status_t *s) { + if (len < 52) return 0; + memset(s, 0, sizeof(*s)); + s->bat_mv = get_u16(d + 0); + s->tx_queue_len = get_u16(d + 2); + s->noise_floor = (int16_t)get_u16(d + 4); + s->last_rssi = (int16_t)get_u16(d + 6); + s->nb_recv = get_u32(d + 8); + s->nb_sent = get_u32(d + 12); + s->airtime_secs = get_u32(d + 16); + s->uptime_secs = get_u32(d + 20); + s->sent_flood = get_u32(d + 24); + s->sent_direct = get_u32(d + 28); + s->recv_flood = get_u32(d + 32); + s->recv_direct = get_u32(d + 36); + s->full_evts = get_u16(d + 40); + s->last_snr_q4 = (int16_t)get_u16(d + 42); + s->direct_dups = get_u16(d + 44); + s->flood_dups = get_u16(d + 46); + s->rx_airtime_secs = get_u32(d + 48); + if (len >= 56) { s->recv_errors = get_u32(d + 52); s->has_recv_errors = 1; } + return 1; +} diff --git a/src/meshcore_companion.h b/src/meshcore_companion.h index c2243ef..4ecee46 100644 --- a/src/meshcore_companion.h +++ b/src/meshcore_companion.h @@ -59,15 +59,37 @@ enum { MC_CMD_SET_DEVICE_TIME = 6, MC_CMD_SEND_SELF_ADVERT = 7, MC_CMD_SET_ADVERT_NAME = 8, + MC_CMD_ADD_UPDATE_CONTACT = 9, MC_CMD_SYNC_NEXT_MESSAGE = 10, MC_CMD_SET_RADIO_PARAMS = 11, MC_CMD_SET_TX_POWER = 12, + MC_CMD_RESET_PATH = 13, + MC_CMD_SET_ADVERT_LATLON = 14, + MC_CMD_REMOVE_CONTACT = 15, + MC_CMD_SHARE_CONTACT = 16, + MC_CMD_EXPORT_CONTACT = 17, + MC_CMD_IMPORT_CONTACT = 18, MC_CMD_DEVICE_QUERY = 22, + MC_CMD_GET_CONTACT_BY_KEY = 30, MC_CMD_GET_CHANNEL = 31, MC_CMD_SET_CHANNEL = 32, - MC_CMD_GET_STATS = 56 + MC_CMD_SEND_BINARY_REQ = 50, + MC_CMD_GET_STATS = 56, + MC_CMD_SEND_ANON_REQ = 57 }; +/* ---- Binary-request subtypes (SEND_BINARY_REQ data[0]) ---- */ +enum { + MC_BINARY_REQ_STATUS = 0x01, + MC_BINARY_REQ_KEEP_ALIVE = 0x02, + MC_BINARY_REQ_TELEMETRY = 0x03, + MC_BINARY_REQ_MMA = 0x04, + MC_BINARY_REQ_ACL = 0x05, + MC_BINARY_REQ_NEIGHBOURS = 0x06 +}; +/* ---- Anonymous-request subtypes (SEND_ANON_REQ) ---- */ +enum { MC_ANON_REQ_REGIONS = 0x01, MC_ANON_REQ_OWNER = 0x02, MC_ANON_REQ_BASIC = 0x03 }; + /* ---- Response codes (radio -> app) ---- */ enum { MC_RESP_OK = 0, @@ -81,7 +103,7 @@ enum { MC_RESP_CHANNEL_MSG_RECV = 8, MC_RESP_CURR_TIME = 9, MC_RESP_NO_MORE_MESSAGES = 10, - MC_RESP_EXPORT_CONTACT = 11, + MC_RESP_CONTACT_URI = 11, /* reply to EXPORT_CONTACT */ MC_RESP_BATTERY_VOLTAGE = 12, MC_RESP_DEVICE_INFO = 13, MC_RESP_PRIVATE_KEY = 14, @@ -89,7 +111,10 @@ enum { MC_RESP_CONTACT_MSG_RECV_V3 = 16, /* SNR-prefixed variant of code 7 */ MC_RESP_CHANNEL_MSG_RECV_V3 = 17, /* SNR-prefixed variant of code 8 */ MC_RESP_CHANNEL_INFO = 18, + MC_RESP_ADVERT_PATH = 22, MC_RESP_STATS = 24, + MC_RESP_AUTOADD_CONFIG = 25, + MC_RESP_ALLOWED_REPEAT_FREQ = 26, MC_RESP_CHANNEL_DATA_RECV= 27 }; @@ -108,9 +133,11 @@ enum { MC_PUSH_STATUS_RESP = 0x87, MC_PUSH_LOG_RX_DATA = 0x88, MC_PUSH_TRACE_DATA = 0x89, - MC_PUSH_NEW_ADVERT = 0x8A, + MC_PUSH_NEW_ADVERT = 0x8A, /* carries a full contact record */ MC_PUSH_TELEMETRY = 0x8B, - MC_PUSH_BINARY_RESP = 0x8C + MC_PUSH_BINARY_RESP = 0x8C, + MC_PUSH_CONTACT_DELETED = 0x8F, + MC_PUSH_CONTACTS_FULL = 0x90 }; /* ---- Text message subtypes ---- */ @@ -183,6 +210,27 @@ 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); +/* ---- contacts (Phase 2) ---- */ +/* GET_CONTACTS; pass since_lastmod>0 for a delta sync, or 0 for all. */ +size_t mc_cmd_get_contacts (uint8_t *out, size_t cap, uint32_t since_lastmod); +size_t mc_cmd_remove_contact (uint8_t *out, size_t cap, const uint8_t pubkey[32]); +size_t mc_cmd_reset_path (uint8_t *out, size_t cap, const uint8_t pubkey[32]); +size_t mc_cmd_share_contact (uint8_t *out, size_t cap, const uint8_t pubkey[32]); +size_t mc_cmd_get_contact_by_key(uint8_t *out, size_t cap, const uint8_t pubkey[32]); +/* EXPORT_CONTACT; pubkey==NULL exports this node's own card. Reply: CONTACT_URI. */ +size_t mc_cmd_export_contact (uint8_t *out, size_t cap, const uint8_t pubkey[32]); +size_t mc_cmd_import_contact (uint8_t *out, size_t cap, const uint8_t *card, size_t card_len); + +/* ---- binary / anonymous requests (Phase 2) ---- */ +/* SEND_BINARY_REQ: dst is a full 32-byte key; req_type is MC_BINARY_REQ_*; + * data is the type-specific request blob (may be NULL). */ +size_t mc_cmd_send_binary_req (uint8_t *out, size_t cap, const uint8_t dst[32], + uint8_t req_type, const uint8_t *data, size_t data_len); +size_t mc_cmd_send_anon_req (uint8_t *out, size_t cap, const uint8_t dst[32], + uint8_t req_type, const uint8_t *data, size_t data_len); +/* mc_cmd_add_update_contact() and mc_parse_status() are declared below, after the + * mc_contact_t / mc_status_t struct definitions they reference. */ + /* ======================================================================== * * Parsed events * ======================================================================== */ @@ -267,6 +315,43 @@ typedef struct { } u; } mc_stats_t; +/* A mesh contact, as parsed from CONTACT (3) / NEW_ADVERT (0x8A) and as built + * for ADD_UPDATE_CONTACT. Lat/lon are degrees x 1e6. */ +typedef struct { + uint8_t public_key[32]; + uint8_t type; + uint8_t flags; + uint8_t out_path_len; + uint8_t out_path[64]; + char adv_name[MC_NAME_LEN + 1]; + uint32_t last_advert; + int32_t adv_lat, adv_lon; + uint32_t lastmod; +} mc_contact_t; + +/* Raw binary response (BINARY_RESPONSE push, 0x8C): a 4-byte tag matching the + * MsgSent.expected_ack of the request, plus opaque payload. Decode per the + * request type you sent (e.g. mc_parse_status for a STATUS request). */ +typedef struct { + uint32_t tag; + uint8_t data_len; + uint8_t data[MC_MAX_DATA]; +} mc_binary_resp_t; + +/* Decoded STATUS binary response (mc_parse_status). */ +typedef struct { + uint16_t bat_mv, tx_queue_len; + int16_t noise_floor, last_rssi; + uint32_t nb_recv, nb_sent, airtime_secs, uptime_secs; + uint32_t sent_flood, sent_direct, recv_flood, recv_direct; + uint16_t full_evts; + int16_t last_snr_q4; /* divide by 4 for dB */ + uint16_t direct_dups, flood_dups; + uint32_t rx_airtime_secs; + uint32_t recv_errors; /* fw v1.12+; see has_recv_errors */ + int has_recv_errors; +} mc_status_t; + typedef struct { uint8_t code; /* response or push code (first payload byte) */ union { @@ -278,6 +363,15 @@ typedef struct { mc_self_info_t self_info; mc_msg_sent_t msg_sent; /* MC_RESP_SENT */ mc_stats_t stats; /* MC_RESP_STATS */ + mc_contact_t contact; /* CONTACT (3) / NEW_ADVERT */ + mc_binary_resp_t binary_resp; /* MC_PUSH_BINARY_RESP */ + uint32_t contacts_count; /* CONTACTS_START: number to come */ + uint32_t contacts_lastmod; /* END_OF_CONTACTS: newest lastmod*/ + uint8_t deleted_key[32]; /* CONTACT_DELETED */ + uint8_t autoadd_config; /* AUTOADD_CONFIG */ + struct { uint8_t len; uint8_t data[MC_MAX_DATA]; } contact_uri; /* CONTACT_URI */ + struct { uint32_t timestamp; uint8_t path_len; uint8_t path[64]; } advert_path; + struct { uint8_t count; struct { uint32_t min, max; } pair[8]; } allowed_freq; uint32_t curr_time; /* epoch secs */ uint16_t battery_mv; int8_t err_code; /* MC_RESP_ERR (-1 if absent) */ @@ -290,6 +384,11 @@ typedef struct { * 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); +/* Builders/parsers that reference the structs above. */ +size_t mc_cmd_add_update_contact(uint8_t *out, size_t cap, const mc_contact_t *c); +/* Decode a STATUS binary-response payload (resp.data/.data_len). 1 on success. */ +int mc_parse_status (const uint8_t *data, size_t len, mc_status_t *out); + #ifdef __cplusplus } /* extern "C" */ #endif diff --git a/test/test_codec.c b/test/test_codec.c index 5d56b46..60cd949 100644 --- a/test/test_codec.c +++ b/test/test_codec.c @@ -292,6 +292,129 @@ int main(void) { "send_cmd uses CLI txt_type"); } + printf("== build: contact commands ==\n"); + { + uint8_t key[32]; for (int i = 0; i < 32; i++) key[i] = (uint8_t)(0x10 + i); + plen = mc_cmd_get_contacts(scratch, sizeof scratch, 0); + CHECK(plen == 1 && scratch[0] == MC_CMD_GET_CONTACTS, "get_contacts (all)"); + plen = mc_cmd_get_contacts(scratch, sizeof scratch, 0x01020304); + CHECK(plen == 5 && scratch[1] == 0x04 && scratch[4] == 0x01, "get_contacts (since lastmod LE)"); + plen = mc_cmd_remove_contact(scratch, sizeof scratch, key); + CHECK(plen == 33 && scratch[0] == MC_CMD_REMOVE_CONTACT && scratch[1] == 0x10 && + scratch[32] == 0x2F, "remove_contact (code + 32-byte key)"); + plen = mc_cmd_reset_path(scratch, sizeof scratch, key); + CHECK(plen == 33 && scratch[0] == MC_CMD_RESET_PATH, "reset_path"); + plen = mc_cmd_get_contact_by_key(scratch, sizeof scratch, key); + CHECK(plen == 33 && scratch[0] == MC_CMD_GET_CONTACT_BY_KEY, "get_contact_by_key"); + plen = mc_cmd_export_contact(scratch, sizeof scratch, NULL); + CHECK(plen == 1 && scratch[0] == MC_CMD_EXPORT_CONTACT, "export_contact (self)"); + plen = mc_cmd_export_contact(scratch, sizeof scratch, key); + CHECK(plen == 33 && scratch[0] == MC_CMD_EXPORT_CONTACT, "export_contact (key)"); + + mc_contact_t c; memset(&c, 0, sizeof c); + memcpy(c.public_key, key, 32); + c.type = 1; c.flags = 0x80; c.out_path_len = 2; + c.out_path[0] = 0xAB; c.out_path[1] = 0xCD; + strcpy(c.adv_name, "RepeaterX"); + c.last_advert = 1700000000u; c.adv_lat = -37000000; c.adv_lon = 145000000; + plen = mc_cmd_add_update_contact(scratch, sizeof scratch, &c); + CHECK(plen == 144 && scratch[0] == MC_CMD_ADD_UPDATE_CONTACT && scratch[1] == 0x10 && + scratch[33] == 1 && scratch[34] == 0x80 && scratch[35] == 2 && + scratch[36] == 0xAB, "add_update_contact header + path"); + CHECK(memcmp(scratch + 100, "RepeaterX", 9) == 0 && scratch[109] == 0 && + scratch[132] == 0x00 && scratch[133] == 0xF1, /* last_advert 0x6553F100 LE */ + "add_update_contact name(32) + last_advert"); + } + + printf("== build: binary / anon requests ==\n"); + { + uint8_t dst[32]; for (int i = 0; i < 32; i++) dst[i] = (uint8_t)(0x40 + i); + plen = mc_cmd_send_binary_req(scratch, sizeof scratch, dst, MC_BINARY_REQ_STATUS, NULL, 0); + CHECK(plen == 34 && scratch[0] == MC_CMD_SEND_BINARY_REQ && scratch[1] == 0x40 && + scratch[32] == 0x5F && scratch[33] == MC_BINARY_REQ_STATUS, + "send_binary_req STATUS (code+dst+type)"); + uint8_t blob[3] = { 0xAA, 0xBB, 0xCC }; + plen = mc_cmd_send_anon_req(scratch, sizeof scratch, dst, MC_ANON_REQ_BASIC, blob, 3); + CHECK(plen == 37 && scratch[0] == MC_CMD_SEND_ANON_REQ && scratch[33] == MC_ANON_REQ_BASIC && + scratch[34] == 0xAA && scratch[36] == 0xCC, "send_anon_req BASIC + data"); + } + + printf("== parse: contact stream (START / CONTACT / END) ==\n"); + { + size_t j = 0; + payload[j++] = MC_RESP_CONTACTS_START; + le32(payload + j, 2); j += 4; + CHECK(feed_parse(&rx, frame, scratch, sizeof scratch, payload, j, &ev) == 1 && + ev.code == MC_RESP_CONTACTS_START && ev.u.contacts_count == 2, "contacts_start count"); + + j = 0; + payload[j++] = MC_RESP_CONTACT; + for (int i = 0; i < 32; i++) payload[j++] = (uint8_t)(0x10 + i); /* pubkey */ + payload[j++] = 1; /* type */ + payload[j++] = 0x80; /* flags */ + payload[j++] = 2; /* out_path_len */ + memset(payload + j, 0, 64); payload[j] = 0xAB; payload[j + 1] = 0xCD; j += 64; + memset(payload + j, 0, 32); memcpy(payload + j, "RepeaterX", 9); j += 32; + le32(payload + j, 1700000000u); j += 4; /* last_advert */ + le32(payload + j, (uint32_t)-37000000); j += 4; /* adv_lat */ + le32(payload + j, (uint32_t)145000000); j += 4; /* adv_lon */ + le32(payload + j, 4242u); j += 4; /* lastmod */ + CHECK(feed_parse(&rx, frame, scratch, sizeof scratch, payload, j, &ev) == 1 && + ev.code == MC_RESP_CONTACT && ev.u.contact.public_key[0] == 0x10 && + ev.u.contact.type == 1 && ev.u.contact.flags == 0x80 && + ev.u.contact.out_path_len == 2 && ev.u.contact.out_path[1] == 0xCD && + strcmp(ev.u.contact.adv_name, "RepeaterX") == 0 && + ev.u.contact.last_advert == 1700000000u && ev.u.contact.adv_lat == -37000000 && + ev.u.contact.adv_lon == 145000000 && ev.u.contact.lastmod == 4242u, + "contact record fields"); + + j = 0; + payload[j++] = MC_RESP_END_OF_CONTACTS; + le32(payload + j, 4242u); j += 4; + CHECK(feed_parse(&rx, frame, scratch, sizeof scratch, payload, j, &ev) == 1 && + ev.code == MC_RESP_END_OF_CONTACTS && ev.u.contacts_lastmod == 4242u, + "end_of_contacts lastmod"); + + j = 0; + payload[j++] = MC_PUSH_CONTACT_DELETED; + for (int i = 0; i < 32; i++) payload[j++] = (uint8_t)(0x10 + i); + CHECK(feed_parse(&rx, frame, scratch, sizeof scratch, payload, j, &ev) == 1 && + ev.code == MC_PUSH_CONTACT_DELETED && ev.u.deleted_key[31] == 0x2F, + "contact_deleted key"); + } + + printf("== parse: BINARY_RESPONSE + STATUS decode ==\n"); + { + size_t j = 0; + payload[j++] = MC_PUSH_BINARY_RESP; + payload[j++] = 0; /* reserved */ + le32(payload + j, 0xDEADBEEFu); j += 4; /* tag */ + size_t blob = j; /* STATUS blob starts here */ + le16(payload + j, 4200); j += 2; /* bat */ + le16(payload + j, 1); j += 2; /* tx_queue_len */ + le16(payload + j, (uint16_t)(int16_t)-115); j += 2; /* noise_floor */ + le16(payload + j, (uint16_t)(int16_t)-90); j += 2; /* last_rssi */ + le32(payload + j, 1000); j += 4; le32(payload + j, 500); j += 4; /* nb_recv, nb_sent */ + le32(payload + j, 200); j += 4; le32(payload + j, 3600); j += 4; /* airtime, uptime */ + le32(payload + j, 10); j += 4; le32(payload + j, 20); j += 4; /* sent_flood/direct */ + le32(payload + j, 30); j += 4; le32(payload + j, 40); j += 4; /* recv_flood/direct */ + le16(payload + j, 2); j += 2; /* full_evts */ + le16(payload + j, 36); j += 2; /* last_snr (q4 -> 9 dB) */ + le16(payload + j, 1); j += 2; le16(payload + j, 2); j += 2; /* dups */ + le32(payload + j, 150); j += 4; /* rx_airtime */ + le32(payload + j, 5); j += 4; /* recv_errors */ + CHECK(feed_parse(&rx, frame, scratch, sizeof scratch, payload, j, &ev) == 1 && + ev.code == MC_PUSH_BINARY_RESP && ev.u.binary_resp.tag == 0xDEADBEEFu && + ev.u.binary_resp.data_len == (uint8_t)(j - blob), "binary_resp tag + data_len"); + + mc_status_t st; + CHECK(mc_parse_status(ev.u.binary_resp.data, ev.u.binary_resp.data_len, &st) == 1 && + st.bat_mv == 4200 && st.noise_floor == -115 && st.last_rssi == -90 && + st.nb_recv == 1000 && st.uptime_secs == 3600 && st.last_snr_q4 == 36 && + st.rx_airtime_secs == 150 && st.has_recv_errors && st.recv_errors == 5, + "parse_status fields"); + } + printf("== resync: garbage before a valid frame ==\n"); uint8_t junk[3] = { 0x00, 0x99, 0x01 }; mc_rx_feed(&rx, junk, 3);