From f304b8ff771429a99961d722b99544fd37c99a9f Mon Sep 17 00:00:00 2001 From: mikecarper Date: Sun, 11 Oct 2026 02:08:02 -0700 Subject: [PATCH] Add Meshtext Companion channel chat and automatic radio2 setup --- .github/workflows/run-unit-tests.yml | 13 +- build_legacy.sh | 3 + docs/cli_commands.md | 4 + docs/companion_protocol.md | 31 +- docs/radio_profiles.md | 267 ++++++ docs/terminal_chat_cli.md | 15 + .../companion_radio/MeshtextCompanion.cpp | 604 +++++++++++++ examples/companion_radio/MyMesh.cpp | 441 ++++++++-- examples/companion_radio/MyMesh.h | 58 +- licenses/ESPP-Meshtext-MIT.txt | 37 + scripts/check_firmware_capabilities.py | 18 + scripts/firmware_build_recipe.py | 3 +- scripts/firmware_memory_manifest.py | 4 + scripts/package_cascade_release.py | 9 +- scripts/package_esp32_partition_migration.py | 4 + scripts/package_meshtext_license.py | 151 ++++ src/Dispatcher.cpp | 204 ++++- src/Dispatcher.h | 34 +- src/Mesh.cpp | 6 +- src/Mesh.h | 8 + src/RadioProfiles.h | 16 +- src/helpers/CLICommandUtils.h | 11 + src/helpers/CompanionChannelAlias.h | 71 ++ src/helpers/CompanionMeshtextChannel.h | 97 ++ src/helpers/MeshtextChat.cpp | 368 ++++++++ src/helpers/MeshtextChat.h | 82 ++ src/helpers/MeshtextConfig.h | 22 + src/helpers/RadioProfileCLI.cpp | 250 +++++- src/helpers/RadioProfileCLI.h | 38 +- src/helpers/radiolib/CustomSX1262.h | 31 + src/helpers/radiolib/CustomSX1262Wrapper.h | 12 + src/helpers/radiolib/RadioLibWrappers.cpp | 35 + src/helpers/radiolib/RadioLibWrappers.h | 8 + test/fixtures/companion_lost_reply.cpp | 5 +- test/fixtures/companion_meshtext.cpp | 831 ++++++++++++++++++ .../test_cli_command_utils.cpp | 5 + test/test_companion_channel_alias.py | 656 ++++++++++++++ test/test_companion_lost_reply.py | 3 +- test/test_companion_meshtext.py | 215 +++++ test/test_companion_terminal_profile.py | 16 +- test/test_companion_tx_routing.py | 39 +- .../test_contact_import.cpp | 202 ++++- test/test_meshtext_auto_radio2_hil.py | 234 +++++ test/test_meshtext_auto_radio2_hil_sdk.py | 175 ++++ test/test_meshtext_chat.py | 491 +++++++++++ test/test_meshtext_license.py | 203 +++++ test/test_meshtext_transport.py | 354 ++++++++ test/test_radio_profile_auto.py | 375 ++++++++ test/test_radio_profile_spiffs_missing.py | 2 +- test/test_sx1262_protocol_batching.py | 173 ++++ tools/hil/mercer_mt_flash.sh | 241 +++++ tools/hil/meshtext_auto_radio2.py | 653 ++++++++++++++ 52 files changed, 7700 insertions(+), 128 deletions(-) create mode 100644 examples/companion_radio/MeshtextCompanion.cpp create mode 100644 licenses/ESPP-Meshtext-MIT.txt create mode 100644 scripts/package_meshtext_license.py create mode 100644 src/helpers/CompanionChannelAlias.h create mode 100644 src/helpers/CompanionMeshtextChannel.h create mode 100644 src/helpers/MeshtextChat.cpp create mode 100644 src/helpers/MeshtextChat.h create mode 100644 src/helpers/MeshtextConfig.h create mode 100644 test/fixtures/companion_meshtext.cpp create mode 100644 test/test_companion_channel_alias.py create mode 100644 test/test_companion_meshtext.py create mode 100644 test/test_meshtext_auto_radio2_hil.py create mode 100644 test/test_meshtext_auto_radio2_hil_sdk.py create mode 100644 test/test_meshtext_chat.py create mode 100644 test/test_meshtext_license.py create mode 100644 test/test_meshtext_transport.py create mode 100644 test/test_radio_profile_auto.py create mode 100644 test/test_sx1262_protocol_batching.py create mode 100644 tools/hil/mercer_mt_flash.sh create mode 100644 tools/hil/meshtext_auto_radio2.py diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index 060b0962a..cbb75f236 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -234,6 +234,7 @@ jobs: - name: Verify Companion contact and channel TX routing run: | + python3 -B test/test_companion_channel_alias.py -v python3 -B test/test_companion_tx_routing.py -v python3 -B test/test_companion_discovery.py -v python3 -B test/test_repeater_discovery.py -v @@ -269,6 +270,16 @@ jobs: python3 -B test/test_identity_and_settings_recovery.py -v python3 -B test/test_contact_cache.py -v + - name: Verify Meshtext chat codec, transport and Companion integration + run: | + python3 -B test/test_meshtext_chat.py -v + python3 -B test/test_meshtext_transport.py -v + python3 -B test/test_meshtext_license.py -v + python3 -B test/test_radio_profile_auto.py -v + python3 -B test/test_companion_meshtext.py -v + python3 -B test/test_meshtext_auto_radio2_hil.py -v + python3 -B test/test_meshtext_auto_radio2_hil_sdk.py -v + - name: Verify receive recovery and pinned radio status transport run: | python3 -B test/test_dispatcher_startup_watchdog.py -v @@ -311,7 +322,7 @@ jobs: working-directory: test run: >- python3 -B -m unittest -v - test_sx1262_profile_switch test_sx1262_batched_modulation + test_sx1262_profile_switch test_sx1262_batched_modulation test_sx1262_protocol_batching test_radio_chirp_math - name: Verify radio lab collectors and compiled experiment contracts diff --git a/build_legacy.sh b/build_legacy.sh index af4317088..13704b39a 100755 --- a/build_legacy.sh +++ b/build_legacy.sh @@ -4743,6 +4743,9 @@ collect_build_artifacts() { ;; esac + python3 scripts/package_meshtext_license.py collect "$build_output_dir" \ + --stem "${OUTPUT_DIR}/${firmware_filename}" || return $? + python3 scripts/firmware_memory_manifest.py package "$build_output_dir" \ --stem "${OUTPUT_DIR}/${firmware_filename}" || return $? } diff --git a/docs/cli_commands.md b/docs/cli_commands.md index ffaf2146e..b99edbaf8 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -7,6 +7,10 @@ See [two LoRa profiles](radio_profiles.md) for `radio2`, `tempradio2`, scheduling, RX-only mode, cross-transmission policy, and optional radio preambles. +For supported Full Companions, see [Meshtext channel chat](radio_profiles.md#meshtext-channel-chat) +for `mt-` channels and the explicit `send mt.nodeinfo ` command. +Chat remains plain text; NodeInfo is sent only when that command is requested. + See [carrier-wave RF checks](carrier_wave.md) for `cw on [seconds]`, `cw2 on [seconds]`, and stopping a carrier with `off`. diff --git a/docs/companion_protocol.md b/docs/companion_protocol.md index 85ecc08ca..6547a57a7 100644 --- a/docs/companion_protocol.md +++ b/docs/companion_protocol.md @@ -545,6 +545,11 @@ Byte 1: Channel index (0 through max_channels - 1) **Response**: `PACKET_CHANNEL_INFO` (0x12) with channel details +On builds with `MESH_ENABLE_MESHTEXT_CHAT=1`, a capable client can append a +third request byte of `32` (`1F 20`) to retrieve the complete 32-byte +secret for an `mt-` channel. Ordinary two-byte requests, and requests for +MeshCore channels, retain the original 16-byte secret response. + --- ### 4. Set Channel @@ -583,7 +588,27 @@ Bytes 34-49: Secret (16 bytes) [16 bytes of secret] ``` -**Note**: The 32-byte secret variant is unsupported and returns `PACKET_ERROR`. +**Meshtext-only extension**: With `MESH_ENABLE_MESHTEXT_CHAT=1`, a 66-byte +`SET_CHANNEL` frame can supply 32 secret bytes at offsets 34–65 for an `mt-` +channel. Non-MT channels and feature-disabled builds reject this variant with +`PACKET_ERROR`; the standard 50-byte MeshCore command is unchanged. The MT +backend uses AES-256 when the upper 16 bytes are nonzero, otherwise AES-128. + +A legacy 50-byte SET of an existing MT channel preserves its upper 16 key +bytes when the supplied lower half is unchanged. This permits an old client's +GET/edit/SET round trip without silently truncating a provisioned AES-256 key. +To explicitly replace or downgrade that key, send the full 32-byte variant +(zero the upper half for AES-128). A client using only the legacy GET cannot +export a complete AES-256 key. Stock app editors do not gain 256-bit +provisioning or sharing from this firmware extension. + +For local CLI provisioning without implementing the binary extension, use +`set mt.channel` with a 32- or 64-digit hexadecimal PSK; see the +[private-channel commands](radio_profiles.md#meshtext-channel-chat). + +MT channel changes can also fail with a bad-state error for conflicting +presets, an unsuitable manual radio2 profile, ambiguous region or a busy +automatic owner. See [Meshtext channel chat](radio_profiles.md#meshtext-channel-chat). **Response**: `PACKET_OK` (0x00) on success, `PACKET_ERROR` (0x01) on failure @@ -1035,7 +1060,9 @@ Bytes 2-33: Channel Name (32 bytes, null-terminated) Bytes 34-49: Secret (16 bytes) ``` -**Note**: The device returns the 16-byte channel secret in this response. +**Note**: The normal response contains the 16-byte channel secret. An opt-in +32-byte MT GET described above extends the secret through byte 65; response +code, index and name fields remain unchanged. **PACKET_DEVICE_INFO** (0x0D): ``` diff --git a/docs/radio_profiles.md b/docs/radio_profiles.md index 5fafff99a..08791bd6e 100644 --- a/docs/radio_profiles.md +++ b/docs/radio_profiles.md @@ -241,6 +241,270 @@ its choice. They use reserved bytes in existing saved records, so record sizes and the phone protocol are unchanged. Older firmware ignores these bytes and may reset the choices when it saves the records. +## Companion channel-name routing aliases + +Companions recognize the case-sensitive prefixes `r2-` and `mt-`, immediately +after the optional leading `#`. Keep the prefix in the app's displayed name. +For MeshCore aliases, the firmware strips exactly one prefix when deriving an +automatic hashtag key: + +```text +#emergency -> hashtag key for #emergency +#r2-emergency -> the same key for #emergency +#mt-emergency -> the same key for #emergency only in MeshCore fallback mode +``` + +Only the app's automatic, name-derived hashtag key is normalized; a supplied +private key is not replaced. Non-hashtag names such as `r2-Private` also retain +their supplied key. Empty or nested prefixes (`r2-`, `#r2-mt-emergency`) are +rejected. MeshCore hashtag names remain case-sensitive. + +When radio2 is configured (`rx` or `rxtx`), `r2-` binds the channel to radio2 for +both send and receive, including emergency retransmissions. It overrides global +crossing and cannot be changed with `set tx.channel`; rename the channel to +remove the binding. RX-only radio2 cannot send. If radio2 is off/unconfigured, +ignore the routing prefix and use ordinary MeshCore routing instead. The key +stays the same as radio2 is enabled, disabled, or a temporary window expires. + +If both the ordinary and `r2-` slots have the same key, a packet received on +radio2 selects the `r2-` slot; a packet received on radio1 selects the ordinary +slot. Global packet deduplication still delivers each packet once, not once in +each slot. Receiving a radio2-only channel on the wrong profile does not mark +that authenticated packet seen, so it can still arrive later on radio2. + +The saved channel record retains the original displayed name and app-supplied +key for editing and sharing. Runtime copies resolve the actual wire key. +Repeater/room/sensor hashtag behavior is unchanged; those nodes can join the +canonical MeshCore channel (`#emergency`). Older Companion firmware does not +recognize these aliases. + +### Meshtext channel chat + +`MESH_ENABLE_MESHTEXT_CHAT` enables the text-only Meshtext backend in the +Companion. It defaults on only for ESP32-S3 **Full Companion** builds using +SX1262 LoRa, not an ESP-NOW primary. Other builds default off. This is not +a replacement for stock peer firmware or a cross-protocol bridge. + +In an enabled build, an `mt-` name always requests Meshtext, even if radio2 is +off or setup is blocked. It never falls back to sending MeshCore frames. In a +feature-disabled build, `mt-` remains an ordinary MeshCore naming alias while +radio2 is off; with radio2 configured, MT channel changes/sends are unsupported. +The `r2-` MeshCore behavior above is unchanged in either build. + +With the region configured, join `#mt-longfast`, for example, through the app's +normal hashtag-channel editor. A recognized preset name selects the stock +peer channel name (`LongFast`) and, **only for an automatically +name-derived hashtag key**, the expanded public `AQ==` key. This public key is +not a secret. An explicitly +supplied private AES-128 or AES-256 key stays private and unchanged. A custom +name such as `mt-Private` uses the exact tail (`Private`) as its wire name and +the supplied key; custom wire names are limited to 11 bytes and need a manual +radio2 profile or another MT channel with a recognized preset. Preset names +are ASCII case-insensitive; the `mt-` marker is still case-sensitive. + +Recognized aliases and modulation are: + +| Alias tail | Stock wire name | BW (kHz) | SF | CR | +| --- | --- | ---: | ---: | ---: | +| `shortturbo` | `ShortTurbo` | 500 | 7 | 5 | +| `shortfast` | `ShortFast` | 250 | 7 | 5 | +| `shortslow` | `ShortSlow` | 250 | 8 | 5 | +| `mediumturbo` | `MediumTurbo` | 500 | 9 | 5 | +| `mediumfast` | `MediumFast` | 250 | 9 | 5 | +| `mediumslow` | `MediumSlow` | 250 | 10 | 5 | +| `longturbo` | `LongTurbo` | 500 | 11 | 8 | +| `longfast` | `LongFast` | 250 | 11 | 5 | +| `longmod` or `longmoderate` | `LongMod` | 125 | 11 | 8 | +| `longslow` | `LongSlow` | 125 | 12 | 8 | + +The selected region must have room for the bandwidth; for example, 500 kHz +presets are unavailable in the supported EU868 plan. Use these CLI commands: + +```text +get mt.status +get mt.region +get mt.slot +get mt.hops +get mt.node +get mt.shortname +set mt.node !12345678 +set mt.node default +set mt.region auto +set mt.region us +set mt.slot 0 +set mt.hops 7 +set mt.shortname HALO +set mt.shortname default +send mt.nodeinfo 0 +get radio2 +set radio2 auto +``` + +`mt.region` accepts `auto`, `us`, `eu868`, `eu433`, or `anz`. `auto` infers US +only from a primary frequency at least 902 MHz and below 915 MHz, EU868 from +869.4–869.65 MHz, and EU433 from 433–434 MHz. The US/ANZ overlap at 915–928 MHz +is ambiguous: explicitly select `us` or `anz`. `mt.slot` is a 1-based frequency +slot, or `0` for the stock preset-name-derived slot; unavailable slots are +blocked. Check `get mt.status` after configuration; a successful settings save +does not mean the MT backend is active. + +Outgoing MT messages default to a hop limit of **7**. `set mt.hops <0..7>` +changes this budget, and `get mt.hops` or `get mt.status` shows it. `0` sends +only the initial over-air hop with no forwarding budget; larger values permit +protocol-compatible peers to forward within that budget. The Companion itself still +does not relay MT packets. + +`get mt.node` shows the effective Meshtext sender node ID. By default it is +SHA-derived from the MeshCore public identity and remains stable for that +identity. `set mt.node !12345678` sets a saved override; exactly eight hexadecimal +digits are required, with or without the leading `!`. Input is hexadecimal, +not decimal, and is never silently truncated. IDs `00000000` through `00000003` +and `ffffffff` are reserved and rejected. `set mt.node default` clears the +override and restores the identity-derived ID. + +The example ID is illustrative: choose an ID unique among your protocol-compatible +peers. Collisions can merge different senders into one identity and interfere +with packet deduplication. Changing the MT ID does not alter the MeshCore +private key, reset the outgoing packet-ID sequence or send automatic traffic. +Settings changes are refused while an MT transmission is +pending. Neither node IDs nor text labels are authenticated. + +`set mt.shortname