/* * 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 #include #include 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; } /* little-endian writers for building test payloads */ static void le16(uint8_t *p, uint16_t v) { p[0] = (uint8_t)v; p[1] = (uint8_t)(v >> 8); } static void le32(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); } /* Parse a freshly-built payload through the rx assembler; returns mc_parse result. */ static int feed_parse(mc_rx_t *rx, uint8_t *frame, uint8_t *scratch, size_t scap, const uint8_t *payload, size_t plen, mc_event_t *ev) { size_t flen = make_inbound(frame, payload, plen), olen = 0; mc_rx_feed(rx, frame, flen); if (!mc_rx_poll(rx, scratch, scap, &olen)) return 0; return mc_parse(scratch, olen, ev); } 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("== parse: MSG_SENT ==\n"); { size_t j = 0; payload[j++] = MC_RESP_SENT; payload[j++] = 1; /* type */ le32(payload + j, 0x11223344); j += 4; /* expected_ack */ le32(payload + j, 5000); j += 4; /* suggested_timeout */ CHECK(feed_parse(&rx, frame, scratch, sizeof scratch, payload, j, &ev) == 1 && ev.code == MC_RESP_SENT, "parse msg_sent"); CHECK(ev.u.msg_sent.type == 1 && ev.u.msg_sent.expected_ack == 0x11223344u && ev.u.msg_sent.suggested_timeout == 5000u, "msg_sent fields"); } printf("== parse: STATS (core/radio/packets) ==\n"); { size_t j = 0; payload[j++] = MC_RESP_STATS; payload[j++] = MC_STATS_CORE; le16(payload + j, 4200); j += 2; /* battery_mv */ le32(payload + j, 86400); j += 4; /* uptime_secs */ le16(payload + j, 3); j += 2; /* errors */ payload[j++] = 7; /* queue_len */ CHECK(feed_parse(&rx, frame, scratch, sizeof scratch, payload, j, &ev) == 1 && ev.u.stats.subtype == MC_STATS_CORE && ev.u.stats.u.core.battery_mv == 4200 && ev.u.stats.u.core.uptime_secs == 86400 && ev.u.stats.u.core.errors == 3 && ev.u.stats.u.core.queue_len == 7, "stats core"); j = 0; payload[j++] = MC_RESP_STATS; payload[j++] = MC_STATS_RADIO; le16(payload + j, (uint16_t)(int16_t)-120); j += 2; /* noise_floor i16 */ payload[j++] = (uint8_t)(int8_t)-90; /* last_rssi i8 */ payload[j++] = 40; /* last_snr_q4 */ le32(payload + j, 1000); j += 4; /* tx_air_secs */ le32(payload + j, 2000); j += 4; /* rx_air_secs */ CHECK(feed_parse(&rx, frame, scratch, sizeof scratch, payload, j, &ev) == 1 && ev.u.stats.subtype == MC_STATS_RADIO && ev.u.stats.u.radio.noise_floor == -120 && ev.u.stats.u.radio.last_rssi == -90 && ev.u.stats.u.radio.last_snr_q4 == 40 && ev.u.stats.u.radio.tx_air_secs == 1000 && ev.u.stats.u.radio.rx_air_secs == 2000, "stats radio (signed fields)"); j = 0; payload[j++] = MC_RESP_STATS; payload[j++] = MC_STATS_PACKETS; le32(payload + j, 10); j += 4; le32(payload + j, 20); j += 4; le32(payload + j, 1); j += 4; le32(payload + j, 2); j += 4; le32(payload + j, 3); j += 4; le32(payload + j, 4); j += 4; le32(payload + j, 5); j += 4; /* recv_errors */ CHECK(feed_parse(&rx, frame, scratch, sizeof scratch, payload, j, &ev) == 1 && ev.u.stats.u.packets.recv == 10 && ev.u.stats.u.packets.direct_rx == 4 && ev.u.stats.has_recv_errors && ev.u.stats.u.packets.recv_errors == 5, "stats packets (+recv_errors)"); } printf("== parse: SELF_INFO extended fields ==\n"); { size_t j = 0; payload[j++] = MC_RESP_SELF_INFO; payload[j++] = 1; payload[j++] = 22; payload[j++] = 30; /* type, txp, maxtxp */ for (int i = 0; i < 32; i++) payload[j++] = (uint8_t)i; /* public_key */ le32(payload + j, (uint32_t)(int32_t)-37000000); j += 4; /* adv_lat */ le32(payload + j, (uint32_t)(int32_t)145000000); j += 4; /* adv_lon */ payload[j++] = 1; /* multi_acks */ payload[j++] = 2; /* adv_loc_policy */ payload[j++] = 0x36; /* telemetry_mode: base=2 loc=1 env=3 */ payload[j++] = 1; /* manual_add_contacts */ le32(payload + j, 915000); j += 4; /* radio_freq */ le32(payload + j, 250000); j += 4; /* radio_bw */ payload[j++] = 11; /* radio_sf */ payload[j++] = 5; /* radio_cr */ memcpy(payload + j, "node", 4); j += 4; CHECK(feed_parse(&rx, frame, scratch, sizeof scratch, payload, j, &ev) == 1 && ev.code == MC_RESP_SELF_INFO, "parse self_info"); CHECK(ev.u.self_info.multi_acks == 1 && ev.u.self_info.adv_loc_policy == 2 && ev.u.self_info.telemetry_mode == 0x36 && ev.u.self_info.tm_base == 2 && ev.u.self_info.tm_loc == 1 && ev.u.self_info.tm_env == 3, "self_info multi_acks/loc_policy/telemetry split"); CHECK(ev.u.self_info.adv_lat == -37000000 && ev.u.self_info.adv_lon == 145000000 && ev.u.self_info.manual_add_contacts == 1 && ev.u.self_info.radio_freq == 915000 && ev.u.self_info.radio_bw == 250000 && ev.u.self_info.radio_sf == 11 && ev.u.self_info.radio_cr == 5 && strcmp(ev.u.self_info.name, "node") == 0, "self_info numeric + name"); } printf("== parse: CHANNEL_MSG_RECV_V3 (SNR) + base sentinel ==\n"); { size_t j = 0; payload[j++] = MC_RESP_CHANNEL_MSG_RECV_V3; payload[j++] = 40; /* SNR q4 */ payload[j++] = 0; payload[j++] = 0; /* reserved */ payload[j++] = 2; /* channel_idx */ payload[j++] = MC_PATH_DIRECT; /* path_len */ payload[j++] = MC_TXT_PLAIN; /* txt_type */ le32(payload + j, 1700000000u); j += 4; /* sender_ts */ memcpy(payload + j, "Alice: hi", 9); j += 9; CHECK(feed_parse(&rx, frame, scratch, sizeof scratch, payload, j, &ev) == 1 && ev.code == MC_RESP_CHANNEL_MSG_RECV_V3 && ev.u.channel_msg.snr_q4 == 40 && ev.u.channel_msg.channel_idx == 2 && ev.u.channel_msg.path_len == MC_PATH_DIRECT && ev.u.channel_msg.sender_ts == 1700000000u && strcmp(ev.u.channel_msg.text, "Alice: hi") == 0, "channel_msg_v3 fields + SNR"); j = 0; payload[j++] = MC_RESP_CHANNEL_MSG_RECV; payload[j++] = 0; /* channel_idx */ payload[j++] = 0; /* path_len */ payload[j++] = MC_TXT_PLAIN; /* txt_type */ le32(payload + j, 1); j += 4; memcpy(payload + j, "B: yo", 5); j += 5; CHECK(feed_parse(&rx, frame, scratch, sizeof scratch, payload, j, &ev) == 1 && ev.u.channel_msg.snr_q4 == MC_SNR_NONE && strcmp(ev.u.channel_msg.text, "B: yo") == 0, "channel_msg base has MC_SNR_NONE"); } printf("== parse: CONTACT_MSG_RECV_V3 + signature skip ==\n"); { size_t j = 0; payload[j++] = MC_RESP_CONTACT_MSG_RECV_V3; payload[j++] = (uint8_t)(int8_t)-8; /* SNR q4 */ payload[j++] = 0; payload[j++] = 0; /* reserved */ for (int i = 0; i < 6; i++) payload[j++] = (uint8_t)(0xC0 + i); /* pubkey_prefix */ payload[j++] = 1; /* path_len */ payload[j++] = MC_TXT_SIGNED_PLAIN; /* txt_type=2 */ le32(payload + j, 1700000001u); j += 4; /* sender_ts */ payload[j++]=0xAA; payload[j++]=0xBB; payload[j++]=0xCC; payload[j++]=0xDD; /* signature */ memcpy(payload + j, "signed!", 7); j += 7; CHECK(feed_parse(&rx, frame, scratch, sizeof scratch, payload, j, &ev) == 1 && ev.code == MC_RESP_CONTACT_MSG_RECV_V3 && ev.u.contact_msg.snr_q4 == -8 && ev.u.contact_msg.txt_type == MC_TXT_SIGNED_PLAIN && ev.u.contact_msg.has_signature && ev.u.contact_msg.signature[0] == 0xAA && ev.u.contact_msg.signature[3] == 0xDD && ev.u.contact_msg.pubkey_prefix[0] == 0xC0 && strcmp(ev.u.contact_msg.text, "signed!") == 0, "contact_msg_v3 signed: SNR/sig/text"); } printf("== parse: DEVICE_INFO with ver/repeat/path_hash (fw=10) ==\n"); { size_t j = 0; payload[j++] = MC_RESP_DEVICE_INFO; payload[j++] = 10; /* fw_ver */ payload[j++] = 50; /* max_contacts/2 */ payload[j++] = 8; /* max_channels */ le32(payload + j, 123456); j += 4; /* ble_pin */ memset(payload + j, 0, 12); memcpy(payload + j, "1 Jan 2026", 10); j += 12; memset(payload + j, 0, 40); memcpy(payload + j, "Heltec V3", 9); j += 40; memset(payload + j, 0, 20); memcpy(payload + j, "v1.7.0", 6); j += 20; payload[j++] = 1; /* repeat (fw>=9) */ payload[j++] = 2; /* path_hash_mode (fw>=10) */ CHECK(feed_parse(&rx, frame, scratch, sizeof scratch, payload, j, &ev) == 1 && ev.u.device_info.fw_ver == 10 && ev.u.device_info.max_contacts == 100 && strcmp(ev.u.device_info.model, "Heltec V3") == 0 && strcmp(ev.u.device_info.ver, "v1.7.0") == 0 && ev.u.device_info.have_repeat && ev.u.device_info.repeat == 1 && ev.u.device_info.have_path_hash && ev.u.device_info.path_hash_mode == 2, "device_info ver/repeat/path_hash"); } printf("== build: send_txt_msg / send_cmd ==\n"); { uint8_t dst[6] = { 1, 2, 3, 4, 5, 6 }; plen = mc_cmd_send_txt_msg(scratch, sizeof scratch, MC_TXT_PLAIN, 0, 1700000000u, dst, 6, "hi"); CHECK(plen == 1 + 1 + 1 + 4 + 6 + 2 && scratch[0] == MC_CMD_SEND_TXT_MSG && scratch[1] == MC_TXT_PLAIN && scratch[2] == 0, "send_txt_msg header"); CHECK(scratch[3] == 0x00 && scratch[4] == 0xF1 && scratch[5] == 0x53 && scratch[6] == 0x65 && scratch[7] == 1 && scratch[12] == 6 && scratch[13] == 'h' && scratch[14] == 'i', "send_txt_msg ts LE + dst + body"); plen = mc_cmd_send_cmd(scratch, sizeof scratch, 1700000000u, dst, 6, "reboot"); CHECK(plen == 1 + 1 + 1 + 4 + 6 + 6 && scratch[0] == MC_CMD_SEND_TXT_MSG && scratch[1] == MC_TXT_CLI_DATA && scratch[2] == 0 && scratch[13] == 'r', "send_cmd uses CLI txt_type"); } 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; }