Only test/fixtures/cli_reply_text.json conflicted; messages and profiles
identical, source token hashes regenerated for the merged sources.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Share contact scans and use the display driver shutdown hook for every panel. Preserve contact ordering, empty-key rejection, replay checks and all firmware features.
A TX line had no RAW of its own, so meshcoretomqtt published each
transmission with the last frame it had received, and a serial observer's
own radio2 transmissions never showed. The repeater and room server now
print the frame as sent (RAW), and RADIO: radio2 when it goes out on radio2,
just before the dispatcher's TX line, as they already do for RX.
The dispatcher calls mesh::usbLogTxFrame, a weak function only those two
roles define; Companion builds keep it undefined and pay a null test. RX/TX
lines and compact logging are unchanged.
Test: test_usb_log_radio2.py compiles both roles' real hook and runs the
lines through meshcoretomqtt's parsing: the TX now carries the sent frame,
tagged radio2 only when sent on radio2. cli_reply_text.json re-pinned.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011i5tJJtj95MB4oUgncCUe5
Only test/fixtures/cli_reply_text.json conflicted; messages and profiles
were identical, source token hashes regenerated for the merged sources.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MESH_RADIO_PRESETS_RESIDENT (default 1) selects where the preset table
lives. With 0, every preset command, loadSlot(), activeSlot() and table()
read /presets into a stack-local table, work on it, save and drop it.
Only the pending-revert mirror (slot, previous tuple), the countdown and
the timebase stay resident: RadioPresetCLI is 32 B on 32-bit targets
(static_assert <= 40 B) instead of ~220 B. Unrelated CLI lines never
touch /presets and plain reads never write flash.
Hooks are now a static function table plus a context pointer passed to
begin(), so the object keeps one pointer instead of seven. Replies,
revert/commit/retry and boot-revert behave the same in both modes; the
host tests run every harness in both modes and add read-only, unrelated
command and failed-seed-save cases. cli_reply_text hashes refreshed for
the begin() call sites.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
isTempRadioActive() on CommonCLICallbacks is only needed by OTA and by
radio presets, so declare it under ENABLE_OTA || MESH_ENABLE_RADIO_PRESETS
again (no vtable slot or override thunk otherwise). The repeater's
login_full_admin flag is also preset-only. wio-e5-mini_repeater now links
byte-for-byte the same size as upstream.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/presets is shared with infrastructure roles, so a node reflashed to
Companion can carry a pending remote load. begin() reverts it through
applyAndSaveRadioParams, which saves _prefs; running it before loadPrefs
would overwrite the saved Companion prefs with constructor defaults. Start
it after loadPrefs, the HIL startup frequency and sanitising, just before
configureRadioFromPrefs(). Contract test asserts the ordering.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Companions (T114, Cardputer) handle get/add/load/del preset from local
operators only: USB/TCP terminal, framed command 0x42, browser CLI and the
UI. An authorized LoRa CLI command (nonzero sender timestamp) is not
consumed, since companions have no revert path. Loads go through
applyAndSaveRadioParams with the same replies as "set radio". Exposes
loadRadioPreset/activeRadioPreset/radioPresetTable for the Cardputer UI.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- CommonCLI.cpp included RadioPresetFile.h only on nRF52/STM32; ESP32 and
RP2040 builds could not see the file helpers. Include it unconditionally.
- Room server and sensor now commit a pending remote preset load on a
successful strict-admin login, like the repeater; drop the doc caveat.
- Rename RadioPresetTable::NAME_MAX to MAX_NAME_LEN: newlib/ESP-IDF
<limits.h> defines NAME_MAX as a macro. Host harnesses now predefine
NAME_MAX so such collisions fail on host too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CommonCLI owns a RadioPresetCLI wired through static hook trampolines
(current/accepts/apply via the existing primary-radio save+apply path,
/presets storage via RadioPresetFile.h). begin() runs at the end of
loadPrefs, loop() next to the radio profiles, and dispatch precedes
radio profiles with remote = sender_timestamp != 0. A fresh strict-admin
repeater login (LoRa or ESP-NOW bridge) commits a pending remote load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The marker in Dispatcher::logPacketLine cost the RAK4631 Full Companion 72
bytes it doesn't have (712776 of 712704). Companions aren't serial observers,
so print it where only repeaters and room servers pay for it: their logRxRaw,
right after the RAW line and before the RX line, using the radio's current
receive profile.
12:34:56 - 29/10/2026 U RAW: 15A3…
12:34:56 - 29/10/2026 U RADIO: radio2
12:34:56 - 29/10/2026 U: RX, len=64 …
The shared dispatcher is back to upstream, so Companion images are unchanged.
Only receptions are marked in the USB log now; the MQTT bridge still tags
both directions.
test_usb_log_radio2.py compiles both roles' real logRxRaw and checks the
marker against meshcoretomqtt's patterns, and that the dispatcher and
Companion sources don't carry it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011i5tJJtj95MB4oUgncCUe5
Full ESP32 repeater builds keep up to 254 neighbours, and a 16-byte radio2
field on each pushed them past the DRAM check (checkprogsize). Instead:
- Radio2DiscoverTrailer is 8 bytes (bandwidth in 100 Hz steps, which is all
the status format prints; sf/cr/state/mode/temporary in bitfields).
- The repeater keeps a 24-entry cache keyed by key prefix (288 bytes): a
reply without a trailer changes nothing, a node's newer report replaces
its older one, and a full cache keeps On (MediaMesh) reports over Off ones.
- The discovery snapshot reads from it; JSON output is unchanged.
The room server keeps its per-neighbour field (now 8 bytes).
Tests: test_radio2_neighbour_cache.py compiles the real cache functions;
the trailer test pins the 8-byte size; the lifecycle harness looks radio2 up
the same way the repeater does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011i5tJJtj95MB4oUgncCUe5
Observers already publish radio2 packets, but nothing says they came over
radio2, so analyzers can't tell MediaMesh traffic or MediaMesh repeaters
apart. Add optional keys, all absent on the main radio, so existing
consumers see byte-identical messages:
- packets and raw: "rx_radio_id"/"tx_radio_id": "radio2" and
"mediamesh": true, from the packet's own radio_profile (captured on
receive, kept by outbound copies), not the radio's profile at publish
time. rx_radio_id is CoreScope's multi-radio key (#1869).
- status: "radios": [{id: "radio2", radio, mode, mediamesh, temporary}]
while radio2 is on with the MeshCore protocol; "radio" stays the main
radio.
- neighbors (repeater and room server): the refresh's DISCOVER_REQ sets
the radio2 flag (0x02, docs/radio_profiles.md); each neighbour's trailer
is kept and published as "radio2" {mode, radio, temporary}, with
"self.radio2" for this node. No trailer leaves the key out (unknown,
never off).
Radio2DiscoverTrailer parses the trailer (host-tested). publishRaw now
takes the direction. MQTT_IMPLEMENTATION.md documents the keys.
Tests: new payload-builder cases (golden prefix, rx/tx/raw tags, status
radios, neighbors radio2, measurements), trailer parsing, and the
neighbor lifecycle fixture now checks radio2 end to end for both roles.
Harnesses updated for the new field and the publishRaw signature.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011i5tJJtj95MB4oUgncCUe5
Keep selected chats bound to full contact keys and route keyboard channel sends through the active transport. Deliver accepted plain, room and Meshtext messages to local history and notifications once; disarm rejected sends and retries.
Use shared live radio settings, GPS acquisition and power-saving policy, screen timers and red night palette. Report save failures, preserve verified custom preferences, and guard display controls and shutdown.
Add executable sanitizer regressions for routing, retries, selection, storage faults, UI lifecycle and GPS. Both Companion builds and the shared T-Deck regression pass; no physical Cardputer testing performed.
Integrate mjgodat's keyboard/display Companion, USB Companion, Repeater and
Room Server support for Cardputer-Adv with Cap LoRa-1262.
Fix UI retry dispatch and ACK ownership, packet/storage bounds, numeric
input validation, sparse channel selection, optional-display shutdown,
keyboard layout limits, reserved GPIOs and Windows cross-drive includes.
Retain all 350 contacts and queue capacity using the existing contact cache;
account for the custom UI's runtime allocations in the RAM admission gate.
Carry the MultiMote and DejaVu notices with firmware packages and add CI
coverage for all four board roles plus the shared T-Deck Companion.
Validation: all four Cardputer roles and the T-Deck BLE Companion build;
Cardputer sanitizer, UI, layout, ownership, licensing and storage tests;
65 RAM-policy tests and 14 contact-cache tests. PlatformIO ran serially.
PR: https://github.com/mikecarper/MeshCore/pull/26