14 KiB
meshcore_c vs meshcore_py — Feature Gap Analysis
Date: 2026-06-08
Summary
meshcore_c is a clean, well-structured portable C99 core + Arduino C++ wrapper for
the MeshCore Companion Radio serial protocol. It correctly implements the
wire-level framing (0x3C/0x3E, LE length), handshake, channel text/data
send/receive, PSK programming, and basic radio parameters — the minimum needed
for a display board or sensor hub to talk to a companion radio. The code is
concise, has zero external dependencies in the C core, and ships with four
platform examples (Linux POSIX, ESP-IDF, STM32 HAL, Arduino).
Compared to the reference Python library meshcore_py, this C implementation
covers roughly 30% of the protocol surface. The gaps fall naturally into
three categories below.
MAJOR GAPS (core protocol missing — things that prevent important use cases)
1. Contact Management — ENTIRELY MISSING
meshcore_py has an extensive contacts subsystem: fetch with delta sync
(lastmod), add/update, remove, share, import/export (meshcore:// URI), get by
public key, change path, change flags, autoadd config. None of this exists in
meshcore_c.
| Feature | meshcore_py | meshcore_c |
|---|---|---|
| GET_CONTACTS (with lastmod sync) | ✅ | ❌ |
| ADD_UPDATE_CONTACT | ✅ | ❌ |
| REMOVE_CONTACT | ✅ | ❌ |
| SHARE_CONTACT | ✅ | ❌ |
| EXPORT_CONTACT / IMPORT_CONTACT | ✅ | ❌ |
| RESET_PATH | ✅ | ❌ |
| GET_CONTACT_BY_KEY | ✅ | ❌ |
| SET_ADVERT_NAME | ✅ | ❌ |
| SET_ADVERT_LATLON | ✅ | ❌ |
| GET_ADVERT_PATH | ✅ | ❌ |
| SET_AUTOADD_CONFIG / GET_AUTOADD_CONFIG | ✅ | ❌ |
| GET_ALLOWED_REPEAT_FREQ | ✅ | ❌ |
| SET_PATH_HASH_MODE / GET_PATH_HASH_MODE | ✅ | ❌ |
Missing commands: 9, 8, 14, 15, 16, 17, 18, 30, 42, 58, 59, 60, 61 Missing response parsing: CONTACT_START, CONTACT, CONTACT_END, CONTACT_URI, CONTACT_DELETED (0x8F), CONTACTS_FULL (0x90), ADVERT_PATH, AUTOADD_CONFIG, ALLOWED_REPEAT_FREQ, DEFAULT_FLOOD_SCOPE
Impact: No way to manage the mesh node list from C. Critical for any application that needs to discover and maintain contacts (repeaters, gateways).
2. MSG_SENT Response — NOT PARSED
meshcore_py parses MSG_SENT (response code 6) which carries:
expected_ack(4 bytes) — tag used to match the ACK confirmationsuggested_timeout(4 bytes) — how long to wait for the ACK
meshcore_c has MC_RESP_SENT = 6 in the enum but no parsing case in
mc_parse() and no send_msg command builder (only send_channel_text).
The Arduino wrapper's sendChannelText sends fire-and-forget.
Impact: No way to confirm message delivery. No timeout-driven retry.
3. Binary Requests — ENTIRELY MISSING
meshcore_py supports BINARY_REQ (CMD 50) with subtypes:
- STATUS (node health: battery, uptime, RSSI, SNR, airtime, packet counters)
- TELEMETRY (Cayenne LPP sensor data from remote nodes)
- MMA (min/max/avg telemetry over a time range)
- ACL (access control list — node permissions)
- NEIGHBOURS (list of nodes heard recently, with SNR/age)
And anonymous requests via SEND_ANON_REQ (CMD 57):
- REGIONS, OWNER, BASIC (remote clock)
Missing:
mc_cmd_send_binary_reqbuildermc_cmd_send_anon_reqbuilderMC_RESP_BINARY_RESPONSE(0x8C) parsing- Tracking infrastructure (
expected_ack→BINARY_RESPONSEtag matching)
Impact: No telemetry interrogation, no remote health checks, no neighbour discovery. A major gap for monitoring/gateway applications.
4. STATS Response — ENUM EXISTS, PARSING MISSING
mc_cmd_get_stats builds the command, and MC_RESP_STATS = 24 is in the enum.
But mc_parse() has no case for MC_RESP_STATS, so requesting stats
returns a raw event code with no parsed data.
meshcore_py parses three stat subtypes:
- Core stats: battery_mv, uptime_secs, errors, queue_len
- Radio stats: noise_floor, last_rssi, last_snr, tx_air_secs, rx_air_secs
- Packet stats: recv, sent, flood_tx, direct_tx, flood_rx, direct_rx, recv_errors
Impact: Stats command is wired but returns unparseable data. The C struct
definitions for stats need to be added to mc_event_t.
5. SELF_INFO Parsing — INCOMPLETE
meshcore_c's mc_parse for MC_RESP_SELF_INFO reads 55 bytes minimum but
the byte offsets are from an older firmware version. Compared to meshcore_py:
| Field | meshcore_py (current firmware) | meshcore_c |
|---|---|---|
multi_acks |
offset 43 (1 byte) | ❌ treats as reserved |
adv_loc_policy |
offset 44 (1 byte) | ❌ treats as reserved |
telemetry_mode |
offset 45 (1 byte, decoded to 3×2-bit fields) | ❌ treats as reserved |
manual_add_contacts |
offset 46 (1 byte) | ✅ |
radio_freq / radio_bw |
offsets 47-54 | ✅ |
radio_sf / radio_cr |
offsets 55-56 | ✅ (conditional guard) |
The C struct mc_self_info_t has a manual_add_contacts field but not
multi_acks, adv_loc_policy, or the three telemetry_mode_* sub-fields.
Impact: Self-info data is incomplete. Cannot read/write advanced device settings through the C API.
6. DEVICE_INFO — MISSING FIELDS
meshcore_py parses additional fields for firmware v9+ and v10+:
repeat(repeater mode, fw ≥ 9)path_hash_mode(fw ≥ 10)verstring (20-byte version field)max_contactscorrectly applied as×2on the wire value
meshcore_c applies ×2 to max_contacts but does not parse:
repeatflagpath_hash_modeverstring
Impact: Cannot detect repeater mode or path hash configuration from the device query response.
7. CHANNEL_MSG_RECV — V3 FORMAT MISSING
Newer MeshCore firmware (v3+) sends CHANNEL_MSG_RECV_V3 (response 17) which
includes an SNR byte at the start. meshcore_c only parses MC_RESP_CHANNEL_MSG_RECV
(response 8). The same gap exists for CONTACT_MSG_RECV_V3 (response 16).
Impact: On newer firmware, V3 messages pass through as unknown events with no SNR or RSSI data available.
MINOR GAPS (feature-completeness items)
8. Direct/Contact Message Sending
- Missing command builders:
mc_cmd_send_txt_msg(CMD 2),mc_cmd_send_cmd(CMD 2 with txt_type=1) - Missing command:
SEND_LOGIN(26),SEND_LOGOUT(29),SEND_STATUS_REQ(27) meshcore_pyhas the full message retry flow withsend_msg_with_retryincluding automatic path-reset-to-flood fallback.
9. Device Configuration Commands
SET_DEVICE_PIN(37) — for BLE PIN pairingSET_OTHER_PARAMS(38) — telemetry modes, multi_acks, advert policyREBOOT(19) — simple but essentialFACTORY_RESET(51) — with two-step safety patternSET_TUNING_PARAMS(21) /GET_TUNING_PARAMS(43) — rx_delay, airtime_factorHAS_CONNECTION(28) — check connectivity
10. Crypto & Security
EXPORT_PRIVATE_KEY(23) /IMPORT_PRIVATE_KEY(24)SIGN_START(33) /SIGN_DATA(34) /SIGN_FINISH(35)- No signature field extraction from signed contact messages (
txt_type=2)
11. Network Layer Commands
SEND_RAW_DATA(25) — send arbitrary payload through meshSEND_TRACE_PATH(36) — trace route with SNR per hopSEND_CONTROL_DATA(55) — node discovery, etc.SET_FLOOD_SCOPE(54) /SET_DEFAULT_FLOOD_SCOPE(63) /GET_DEFAULT_FLOOD_SCOPE(64)
12. Response Parsing Missing
MC_RESP_CONTACT_MSG_RECV_V3(16) — SNR-included formatMC_RESP_CHANNEL_MSG_RECV_V3(17) — SNR-included formatMC_RESP_SIGN_START/MC_RESP_SIGNATURE(19, 20)MC_RESP_CUSTOM_VARS(21)MC_RESP_ADVERT_PATH(22)MC_RESP_TUNING_PARAMS(23)MC_RESP_AUTOADD_CONFIG(25)MC_RESP_ALLOWED_REPEAT_FREQ(26)MC_RESP_DEFAULT_FLOOD_SCOPE(28)MC_PUSH_LOG_RX_DATA(0x88) — RF packet monitorMC_PUSH_TRACE_DATA(0x89) — trace responseMC_PUSH_NEW_ADVERT(0x8A) — new contact pushMC_PUSH_TELEMETRY(0x8B) — telemetry pushMC_PUSH_BINARY_RESP(0x8C) — binary responseMC_RESP_PATH_DISCOVERY(0x8D)MC_PUSH_CONTROL_DATA(0x8E)MC_RESP_CONTACT_DELETED(0x8F)MC_RESP_CONTACTS_FULL(0x90)
13. Battery Response — STORAGE INFO MISSING
meshcore_py parses the full BATT_AND_STORAGE response (11 bytes): battery
level + storage used_kb + total_kb. meshcore_c only parses the 2-byte
battery voltage (battery_mv).
14. CUSTOM_VARS — COMMAND & RESPONSE MISSING
GET_CUSTOM_VARS (40) / SET_CUSTOM_VAR (41) — key:value pairs stored on the
device. Both command builders and MC_RESP_CUSTOM_VARS parsing are absent.
15. GET_SELF_TELEMETRY — MISSING
meshcore_py can request the local node's own telemetry via get_self_telemetry().
The command builder and MC_PUSH_TELEMETRY response parsing are missing.
NICE TO HAVE (convenience, robustness, UX)
16. Auto-Reconnect / ConnectionManager
meshcore_py has a ConnectionManager class with:
- Automatic reconnect with configurable max attempts
send_appstart()callback after every reconnection- CONNECTED / DISCONNECTED event emission
- Robust disconnect handling for all three transports (BLE/Serial/TCP)
The Arduino wrapper in meshcore_c has begin(), but no reconnect logic.
17. Event Subscription with Attribute Filtering
meshcore_py's EventDispatcher supports:
- Subscribe to specific event types (or all via
None) - Attribute-based filtering (e.g., "only ACKs with code=X")
wait_for_event()with timeoutclone()for safe async dispatch
The C core has no event system — the caller gets a raw mc_event_t struct.
The Arduino wrapper has simple lambda callbacks with no filtering. This is
fine for a C library but limits complex dispatch scenarios.
18. Contact State Tracking
meshcore_py maintains internal dictionaries for contacts, self_info, and
device time, auto-updating them on relevant events. The C core leaves all
state management to the caller. Useful for real apps but adds memory/CPU cost.
19. Automatic Message Fetching
meshcore_py has start_auto_message_fetching() and
stop_auto_message_fetching() that use asyncio to automatically drain the
radio's message queue when MESSAGES_WAITING events arrive.
The Arduino wrapper already auto-drains in dispatch() (enabled by
setAutoSync(true)). The C core's examples also drain manually. This is the
one "nice to have" that is already well-addressed in meshcore_c.
20. Synchronous Request/Response Helpers
meshcore_py wraps fire-and-forget commands with _sync variants that:
- Lock via
_mesh_request_lock - Wait for the specific response event
- Apply suggested timeout from
MSG_SENT - Return parsed payload or
None
Examples: req_status_sync, req_telemetry_sync, send_path_discovery_sync,
send_login_sync, req_neighbours_sync, etc.
sent_msg_with_retry goes further: retry up to N times, auto-switch to flood
after K failed direct attempts.
21. Channel Log Decryption
meshcore_py can decrypt AES-encrypted channel messages found in the
RF packet log (meshcore_parser.py). It tracks channels, computes HMACs and
AES decryption, and matches duplicate packets. This requires cryptographic
libraries (pycryptodome) and is likely too heavy for a bare-metal C core.
22. Cayenne LPP Telemetry Parsing
meshcore_py parses Cayenne LPP binary telemetry frames into JSON with typed
values (temperature, humidity, GPS, voltage, etc.). Requires an LPP library
and is application-layer — probably belongs in the caller, not the core.
23. More Examples
meshcore_py ships 33 example scripts covering chat, battery monitor, channel
manager, contacts, pingbot, RSS bot, trace, OLLAMA integration, and many BLE
workflows. meshcore_c has 4 good examples but many usage patterns are not
demonstrated.
24. Test Coverage
meshcore_py has 13+ unit test files. meshcore_c has one good unit test
(test_codec.c) covering frame assembly, parsing, and resync. More tests
for SELF_INFO, CONTACT parsing, error paths, and edge cases would be valuable.
What meshcore_c Does BETTER Than meshcore_py
-
Zero dependencies — The C core needs only
stdint.h,stddef.h, andstring.h. No malloc, no crypto, no third-party libraries. Truly portable to any C99 environment. -
No I/O in the core — Transport is 100% external. Same code works on Linux, ESP-IDF, STM32 HAL, and Arduino without
#ifdef. -
Platform breadth — Already ships examples for Linux, ESP-IDF, STM32, and Arduino.
meshcore_pyis Python-only (Linux/macOS/Windows, not embedded). -
Build simplicity — One CMakeLists.txt, or compile with a single
cccommand. No package manager needed. -
Auto-drain in Arduino wrapper — The
dispatch()method automatically syncs messages whenMC_PUSH_MSG_WAITINGarrives, simplifying the user loop. -
Clean C API — Pure struct-based API with no callbacks, no heap, no hidden state. The caller owns all memory.
Prioritized Implementation Roadmap
Phase 1 — Core Protocol Completion (Major)
MSG_SENTresponse parsing (ack tag + timeout tracking)MC_RESP_STATSparsing (core/radio/packet subtypes)- Fix
SELF_INFObyte offsets and add missing struct fields - Add
CHANNEL_MSG_RECV_V3andCONTACT_MSG_RECV_V3parsing - Parse
repeatandpath_hash_modeinDEVICE_INFO - Add
mc_cmd_send_txt_msgandmc_cmd_send_cmdbuilders
Phase 2 — Contact & Binary Requests (Major)
- Contact command builders: GET_CONTACTS, ADD_UPDATE_CONTACT, REMOVE_CONTACT, RESET_PATH, SHARE_CONTACT, EXPORT/IMPORT_CONTACT, GET_CONTACT_BY_KEY
- Contact response parsing: CONTACT_START, CONTACT, CONTACT_END
- Binary request infrastructure:
mc_cmd_send_binary_req, tag tracking - BINARY_RESPONSE parsing with sub-type dispatch (STATUS, TELEMETRY, etc.)
Phase 3 — Device Management (Minor)
- REBOOT, FACTORY_RESET, SET_DEVICE_PIN
- SET_OTHER_PARAMS, SET_TUNING_PARAMS/GET_TUNING_PARAMS
- EXPORT_PRIVATE_KEY/IMPORT_PRIVATE_KEY
- CUSTOM_VARS get/set
- SET_ADVERT_NAME, SET_ADVERT_LATLON
- SET_FLOOD_SCOPE, SET_DEFAULT_FLOOD_SCOPE
- HasConnection, GetBattAndStorage (storage fields)
- SEND_TRACE_PATH, SEND_RAW_DATA, SEND_CONTROL_DATA
Phase 4 — Polish (Nice to Have)
- ConnectionManager-style reconnect in the Arduino wrapper
- More unit tests
- Additional examples (chat, contacts sync, telemetry display)
- Message retry with path-reset-to-flood fallback
Comparison performed against meshcore_py at commit HEAD, 2026-06-08.