Port Meshtext settings to verified atomic nRF52 storage and use the existing radio RNG and hardware entropy. Keep the ExtraFS boundary, GPS, contacts, channel count and 8 KiB stack unchanged; omit unused library data to fit the image. Require linked codec and Companion transport evidence and build the real RAK Full profile in CI. Add configuration and storage-failure regression coverage.
847 lines
43 KiB
Markdown
847 lines
43 KiB
Markdown
<!-- meshcore-hosted-doc-link:start -->
|
||
<p class="meshcore-hosted-doc-link"><a href="https://mikecarper.github.io/MeshCore/radio_profiles/">View this page on MeshCore Docs</a>.</p>
|
||
<!-- meshcore-hosted-doc-link:end -->
|
||
|
||
# Two LoRa profiles on one radio
|
||
|
||
`radio2` lets one LoRa transceiver alternate between two frequencies or modulation
|
||
settings. It is useful for listening to two networks, or keeping your usual
|
||
channel available during a LoRa firmware update.
|
||
|
||
There are **at most two active profiles**. `tempradio` temporarily replaces
|
||
`radio`; `tempradio2` temporarily replaces `radio2`. These are time-shared receive
|
||
windows, not simultaneous receivers. Receiving or transmitting a packet on one
|
||
profile makes the other unavailable until that packet finishes.
|
||
|
||
The second-profile commands are shared by repeater, Companion, room server,
|
||
sensor and standalone terminal-chat firmware with a LoRa radio. ESP-NOW-only
|
||
devices reject LoRa profile commands. KISS is a raw host-controlled modem: it
|
||
exposes equivalent RAM-only binary `SetRadio2`/`SetTempRadio2` commands and
|
||
logical KISS Data ports, documented in [the KISS modem protocol](kiss_modem_protocol.md).
|
||
It does not use the Mesh CLI's persistent profile file or Mesh retry queues.
|
||
|
||
## Quick setup
|
||
|
||
Keep the primary channel configured with `radio`, then add the second:
|
||
|
||
```text
|
||
set radio2 910.5,500,8,5,rx
|
||
get radio2
|
||
get radio2.status
|
||
```
|
||
|
||
The tuple is **frequency in MHz, bandwidth in kHz, spreading factor, coding-rate
|
||
denominator, mode, optional preamble in symbols**. `5` means coding rate 4/5.
|
||
|
||
| Mode | Receive on profile 2 | Transmit on profile 2 |
|
||
| --- | --- | --- |
|
||
| `rx` | Yes | Only infrastructure replies explicitly configured with `tx.reply ... force` |
|
||
| `rxtx` (also `rx&tx`) | Yes | Yes |
|
||
| `off` | No | No |
|
||
|
||
For transmission on both permanent profiles:
|
||
|
||
```text
|
||
set radio2 910.5,500,8,5,rxtx
|
||
```
|
||
|
||
The same encoded message is queued separately for each allowed transmit profile.
|
||
Each has its own direct/flood retry slots and channel-busy backoff. A forwarding
|
||
echo on one profile cannot cancel the other profile's retry. Packet storage and
|
||
the physical transceiver's airtime budget are shared; exhausted queues can still
|
||
drop packets.
|
||
|
||
## Keep the main channel during an update
|
||
|
||
```text
|
||
set tempradio2 910.5,500,8,5,rxtx,120
|
||
get tempradio2
|
||
```
|
||
|
||
This enables the second profile for 120 minutes (2 hours). Locally generated
|
||
LoRa OTA requests use the temporary second profile. Infrastructure OTA responses
|
||
follow `tx.reply` below (both TX-capable profiles by default). Ordinary local traffic stays
|
||
on the primary profile by default. Use an OTA-capable build and the usual OTA
|
||
setup on every participating node; a temporary profile does not add OTA support
|
||
to firmware built without it.
|
||
|
||
Local settings take effect after a short reply allowance. On repeaters, immediate
|
||
remote `radio2`/`tempradio2` changes (including `off`) wait until their exact reply
|
||
copies finish, with at least one successfully transmitted. A reply that cannot
|
||
be queued or whose copies all fail cancels the change. Queued replies have a
|
||
five-minute deadline; an already-transmitting copy may finish under the normal
|
||
radio watchdog. This confirms transmission, not reception by the remote client.
|
||
|
||
Permanent remote changes are prepared in an uncommitted file before replying;
|
||
the saved configuration is replaced only after reply transmission. If that
|
||
replacement fails, the old channel remains active and the commit retries.
|
||
Local `get radio2.status` reports this pending state. Roles without the tracked remote
|
||
reply path require local USB for these immediate secondary-profile changes.
|
||
|
||
Temporary periods and schedules live in RAM and disappear on reboot. Waiting
|
||
for a reply does not extend a temporary lease. Timer expiry also uses monotonic
|
||
time, so setting the clock backwards cannot extend a temporary session.
|
||
|
||
## Repeater, room-server, and sensor replies
|
||
|
||
These roles default to **`tx.reply both`**: locally generated replies are queued
|
||
on `radio` and on an active `radio2` that permits TX. This includes replies to
|
||
remote CLI/login/telemetry requests, ACKs, returned paths, delayed GPIO command
|
||
completion, room subscription posts, and locally served LoRa OTA catalog,
|
||
manifest, data, proof, and leaves responses. Each copy has its own retry state.
|
||
Forwarded packets, periodic adverts, sensor pushes, and OTA requests continue
|
||
to use the normal crossing rules.
|
||
|
||
```text
|
||
get tx.reply
|
||
set tx.reply both
|
||
set tx.reply both force
|
||
set tx.reply radio
|
||
set tx.reply auto
|
||
```
|
||
|
||
| Choice | Replies transmit on |
|
||
| --- | --- |
|
||
| `both` | Primary plus an active `rxtx` second profile. Default. |
|
||
| `radio` | Primary only. |
|
||
| `radio2` | Second only; no primary fallback. |
|
||
| `auto` | Normal `radio2.cross` behavior, including permanent/temporary isolation. |
|
||
| `off` | Neither profile. This also suppresses remote CLI replies and ACKs. |
|
||
|
||
Append **`force`** to `both` or `radio2` to allow these replies on an active
|
||
second profile configured as `rx`. Without `force`, `rx` remains receive-only.
|
||
Sending the command again without `force` removes the override. It never turns
|
||
on `radio2 off`, changes the saved `rx` mode, or enables ordinary forwarding/TX
|
||
there. Existing crossover packet filters still apply to the extra copy.
|
||
|
||
The explicit choices override `radio2.cross`, including `off` and mixed
|
||
permanent/temporary profiles. `radio` means the active primary profile,
|
||
including `tempradio`; `radio2` also includes an active `tempradio2`.
|
||
For example, an OTA source can retain its normal channel while allowing OTA
|
||
and management responses on a temporary receive profile:
|
||
|
||
```text
|
||
set tempradio2 910.5,500,8,5,rx,120
|
||
set tx.reply both force
|
||
```
|
||
|
||
The source must have OTA support and an active temporary-radio session as
|
||
usual. A node fetching an update still needs `rxtx` for its OTA requests.
|
||
Forcing replies does not change OTA's receive gates, scan timing, or preambles.
|
||
|
||
This setting survives reboot; older saved profiles adopt `both` without force.
|
||
The getter reports when the chosen second profile cannot transmit. Already
|
||
queued replies retain their choice; disabling/changing a profile invalidates
|
||
stale queued packets. A parameterized repeater `tempradio` command waits for
|
||
both admitted reply copies to drain before changing the primary settings;
|
||
at least one must transmit successfully. Those tracked replies omit alternate
|
||
paths and transport retries so no late success copy can outlive the handoff.
|
||
Queue capacity and the shared transceiver's airtime budget still limit delivery.
|
||
OTA data/proof admission counts both copies against its queue credit and
|
||
reserves receive capacity before allowing the response.
|
||
|
||
Companions use the per-contact/channel choices below; `tx.reply` is an
|
||
infrastructure setting.
|
||
|
||
## Companion messages on both profiles
|
||
|
||
To send ordinary Companion messages on both `radio` and an active `tempradio2`,
|
||
configure the temporary profile in `rxtx` mode and enable crossing:
|
||
|
||
```text
|
||
set radio2.cross on
|
||
get tempradio2
|
||
get radio2.cross
|
||
get radio2.status
|
||
```
|
||
|
||
`rxtx` permits transmission on the second profile; `radio2.cross on` copies new
|
||
messages between the two profiles. `TX=a,b` reports primary and secondary
|
||
transmit counts. Crossing also applies to OTA traffic.
|
||
|
||
The crossing setting is saved and remains enabled after the temporary session
|
||
ends or the node reboots. Run `set radio2.cross auto` to restore the default
|
||
isolation between permanent and temporary profiles. The firmware picker's
|
||
Companion tempradio2 instructions include both choices and the status commands.
|
||
|
||
## Companion TX routing by contact or channel
|
||
|
||
Companions can save a transmit choice for each contact and each configured
|
||
channel. The commands apply to messages sent from the phone app, USB/BLE/TCP
|
||
Companion clients, and the text terminal:
|
||
|
||
```text
|
||
set tx.user "Alice Jones" radio2
|
||
get tx.user "Alice Jones"
|
||
set tx.channel 0 both
|
||
get tx.channel 0
|
||
set tx.channel #wardriving radio
|
||
get tx.channel #wardriving
|
||
```
|
||
|
||
Channel indices match the app and the terminal's `channels` list; Public is
|
||
normally slot `0`. Names must match exactly, including capitalization and any
|
||
leading `#`. Quotes always select a literal name, so `"1"` is a channel named
|
||
`1`, while unquoted `1` selects slot 1. Quote contact names beginning with
|
||
`key:` to distinguish them from public-key selectors. Duplicate names are
|
||
rejected: use the channel index or a contact's public key instead.
|
||
|
||
| Choice | Locally addressed traffic transmits on |
|
||
| --- | --- |
|
||
| `auto` | Existing behavior, including `radio2.cross` and temporary-profile isolation. This is the default. |
|
||
| `radio` | The primary profile only. |
|
||
| `radio2` | The second profile only; sending fails if it is off or RX-only. |
|
||
| `both` | The primary profile and the second profile whenever the second is enabled for TX. |
|
||
| `off` | Neither profile for this contact/channel. Reception stays enabled. |
|
||
|
||
Explicit choices override `radio2.cross`, including `off` and the isolation
|
||
between a permanent and temporary profile. They do not turn on radio2 or
|
||
change its `rx`/`rxtx` mode. For example, leave `radio2.cross auto` and use
|
||
`set tx.channel 0 both` to send Public messages on both `radio` and an active
|
||
`tempradio2`, while other channels keep their default routing.
|
||
|
||
`radio` means the currently active primary profile, whether permanent or
|
||
`tempradio`; `radio2` similarly includes `tempradio2`. There are still at most
|
||
two active profiles. Getters show the saved choice and `active=` for newly
|
||
initiated messages with the current profile configuration. A temporarily
|
||
unavailable second profile is reported explicitly.
|
||
|
||
Contact selection uses the full stored public key after resolving a unique
|
||
name or key prefix. It never routes by the short on-air destination hash.
|
||
For key selection, prefix 12–64 hexadecimal digits (whole bytes) with `key:`:
|
||
|
||
```text
|
||
set tx.user key:0123456789AB radio2
|
||
get tx.user key:0123456789AB
|
||
```
|
||
|
||
Replace the example prefix with your contact's key; use more digits if it is
|
||
ambiguous. `get tx.user` and `get tx.channel` report override counts and query
|
||
syntax. To return a target to the global behavior:
|
||
|
||
```text
|
||
set tx.user "Alice Jones" auto
|
||
set tx.channel 0 auto
|
||
```
|
||
|
||
A user's setting covers addressed texts, login/CLI/telemetry requests, replies,
|
||
ACKs, and returned paths to that contact. Thus `off` also prevents ACKs to
|
||
that contact. Channel settings cover group text and group data, including
|
||
an explicitly routed group datagram. Contact choices do not filter other
|
||
people's messages inside a group channel. Adverts, raw traces, raw packets,
|
||
and OTA packets without a contact/channel destination retain global routing.
|
||
Packets and their retries already queued retain the choice used when created;
|
||
changing or disabling the radio profile still invalidates stale queued work.
|
||
Message timeout estimates use the selected profiles' airtime, independently
|
||
of the scanner's current receive profile.
|
||
|
||
Choices survive reboot and normal phone-app updates to the same contact or
|
||
channel key. Replacing a channel's key or deleting/replacing a contact clears
|
||
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 for ESP32-S3 **Full Companion** builds using
|
||
SX1262 LoRa and for the RAK4631 **Full Companion** starting with 1.17.1.10
|
||
preview3. An ESP-NOW primary and other builds default off. RAK4631 1.17.1.10
|
||
preview2 and earlier builds did not include this backend. 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 <label>` saves a Meshtext-only NodeInfo name of 1–4 Unicode
|
||
scalar characters, with valid UTF-8 and at most 16 encoded bytes. Combining
|
||
characters count separately. `get mt.shortname` shows the saved/effective
|
||
name. Stock NodeInfo accepts only four UTF-8 **bytes**, so the effective wire
|
||
name is the longest complete UTF-8 prefix that fits. The saved value is not
|
||
changed. `set mt.shortname default` clears the override and uses the last four
|
||
hexadecimal digits of the effective MT node ID. This does not change the
|
||
MeshCore node name, add a label to chat text or transmit anything.
|
||
|
||
`send mt.nodeinfo <channel slot>` explicitly queues **one** standard
|
||
`NODEINFO_APP` broadcast on the selected configured MT channel. The slot is
|
||
the Companion channel index, not `mt.slot` (the frequency slot). The packet
|
||
contains the actual sender ID, the global MeshCore node name as its long name
|
||
(UTF-8-safe truncated to 24 bytes), the effective short name, `CLIENT_MUTE`
|
||
(no relaying), and the no-DM flag. It omits MAC, public keys and unsupported
|
||
metadata. The usual radio2 TX-mode, pending-TX, airtime and profile guards
|
||
apply. The reply says **queued**, not delivered or acknowledged.
|
||
|
||
There are no automatic NodeInfo advertisements: not at boot, on a timer, when
|
||
names change, when chat is sent, or in response to received packets. Received
|
||
NodeInfo is still ignored; this backend does not maintain a node database.
|
||
|
||
Region, frequency slot, hop limit, shortname and node-ID override are saved
|
||
atomically together; changing one preserves the others. Current 40-byte `MT03`
|
||
settings include both overrides. New firmware accepts valid legacy
|
||
`MT01`/`MT02` settings and defaults the shortname to the sender-ID suffix and
|
||
the node ID to the MeshCore identity; `MT01` also defaults the hop limit to 7.
|
||
Older MT firmware cannot read `MT03`, so downgrading does not preserve access
|
||
to these settings. Invalid or unsupported stored settings block MT operation
|
||
rather than silently selecting another network.
|
||
|
||
Private channels can be provisioned directly through the Companion CLI:
|
||
|
||
```text
|
||
set mt.preset <longfast|shortturbo|...>
|
||
set mt.channel <slot> <mt-name> <32/64 hex digits|AQ==>
|
||
get mt.channel <slot>
|
||
delete mt.channel <slot>
|
||
```
|
||
|
||
`set mt.preset` accepts the recognized presets above and explicitly saves a
|
||
manual `rxtx` radio2 RF tuple using the current region and frequency slot. It
|
||
is not an automatic owner: deleting the channel does not remove that saved RF
|
||
configuration. Set region/slot first; changing them later does not overwrite
|
||
the manual frequency, so rerun `set mt.preset` if you intend to retune it.
|
||
|
||
`set mt.channel` accepts a 32-digit hexadecimal AES-128 PSK, a 64-digit
|
||
hexadecimal AES-256 PSK, or the literal `AQ==` for the public key. It does not
|
||
accept arbitrary Base64 private keys. The stored format interprets an all-zero
|
||
upper 16-byte half as AES-128, including in a 64-digit input; use a nonzero
|
||
upper half for AES-256. Slot numbers are zero-based and build-specific, as in
|
||
the app. `get mt.channel` reports name, wire name and AES size, never the PSK.
|
||
The firmware console masks key input echo; keep your client-side logs and
|
||
stored keys private as well. SET and DELETE save transactionally and restore
|
||
the previous channel if storage fails. DELETE applies only to an MT channel,
|
||
not a MeshCore slot.
|
||
|
||
For a private channel named `Team` on a protocol-compatible peer:
|
||
|
||
```text
|
||
set mt.region us
|
||
set mt.preset longfast
|
||
set mt.channel 1 mt-Team <your-32-or-64-hex-digit-key>
|
||
get mt.channel 1
|
||
get mt.node
|
||
set mt.shortname HALO
|
||
get mt.status
|
||
send mt.nodeinfo 1
|
||
channel 1 Hello
|
||
```
|
||
|
||
Replace the key placeholder with your actual hexadecimal PSK and send after
|
||
`get mt.status` reports active. The peer must use the same preset, frequency,
|
||
PSK and exact case-sensitive wire name `Team`; `mt-` is only the local marker.
|
||
Alternatively, `set mt.channel 1 mt-longfast <your-key>` preserves a private
|
||
PSK but uses wire name `LongFast` and can acquire automatic RF without
|
||
`set mt.preset`, provided radio2 automatic ownership is permitted. This route
|
||
is available for both 128-bit and 256-bit private keys even when the stock app
|
||
cannot provision or export the latter.
|
||
|
||
When radio2 was never manually configured, a recognized preset can acquire an
|
||
automatic `rxtx` profile. Its RF tuple is RAM-only and stays available for RX
|
||
while the required channels exist. Removing the last MT channel, losing its
|
||
last recognized-preset dependency, or detecting a conflict releases only the
|
||
automatic profile. Explicit manual RF settings are retained: named aliases
|
||
must match their BW/SF/CR and fit the selected region, but do not overwrite
|
||
their frequency. The runtime MT overlay uses its required 16-symbol preamble;
|
||
the saved manual preamble is retained.
|
||
|
||
`set radio2 off` is an explicit manual veto, including after reboot. Older
|
||
saved off profiles are conservatively treated as manual. Local
|
||
`set radio2 auto` clears the saved manual RF/off choice and permits automatic
|
||
ownership after two seconds; it rejects pending changes, leases and schedules
|
||
rather than cancelling them. `get radio2` distinguishes `auto-owned`, `off (manual)`
|
||
and `off (automatic allowed)`. Temporary profiles, scheduled changes and reply
|
||
commit barriers take precedence and can suspend MT operation. Automatic
|
||
cleanup does not undo later manual edits.
|
||
|
||
All MT channels share this one secondary modem profile. Different recognized
|
||
presets, or mixing any MT and `r2-` channels, are rejected instead of selecting
|
||
whichever channel was edited last. Loaded conflicts also block MT traffic.
|
||
Neither `radio2.cross on` nor MeshCore channel routes cross MT frames between
|
||
protocols.
|
||
|
||
The app uses its existing channel index/text commands and receive events.
|
||
Outgoing MT chat is plain text exactly as entered, with the sender node ID in
|
||
the standard radio header. It contains no custom name envelope or NodeInfo.
|
||
Incoming text uses the exact sender-ID prefix `!12345678: ` before the received
|
||
payload when passed to the MeshCore app. Changing the shortname only changes
|
||
future manually requested NodeInfo, not chat or the packet's sender ID;
|
||
`set mt.node` changes the actual Meshtext sender ID. Neither setting change
|
||
emits advertisements. A successful
|
||
send response means **queued**, not acknowledged by a remote node. This backend
|
||
does not relay MT packets or provide ACKs, administrative commands, direct
|
||
messages, automatic node-info adverts or a node database. MeshCore emergency forwarding
|
||
does not apply to MT traffic. Interoperability is with stock peers' PSK channel
|
||
text using AES-CTR, not AES-CCM. AES-CTR channel traffic is not authenticated;
|
||
successful decoding is not proof of sender identity.
|
||
|
||
The codec supports AES-128 and AES-256 and up to 228 UTF-8 bytes of wire text,
|
||
without an added label/node-ID prefix. The standard app channel editor
|
||
still provisions 16-byte keys. An MT-only 32-byte binary SET/opt-in GET
|
||
extension is available for capable clients, with protection against legacy
|
||
16-byte edits truncating an existing 256-bit key; see
|
||
[Companion protocol](companion_protocol.md#4-set-channel). This does not give
|
||
stock apps seamless 256-bit provisioning or sharing. Current V3 Companion
|
||
receive events carry at most 165 text bytes **including the node-ID prefix**;
|
||
longer incoming MT text is UTF-8-safe truncated for the app.
|
||
|
||
Radio1 and radio2 still time-share **one physical radio**. The device cannot
|
||
receive either network while tuned to the other or transmitting; long MT
|
||
airtime, particularly LongFast, can leave substantial MeshCore receive gaps.
|
||
The compatible protocol's fixed short preamble also does not provide the scan-aware long
|
||
preamble described below. Do not treat this as continuous coverage of both
|
||
networks or guaranteed message delivery.
|
||
|
||
## Choose whether traffic crosses between profiles
|
||
|
||
```text
|
||
get radio2.cross
|
||
set radio2.cross auto
|
||
set radio2.cross on
|
||
set radio2.cross off
|
||
```
|
||
|
||
| Active primary | Active secondary | `auto` (default) | `on` | `off` |
|
||
| --- | --- | --- | --- | --- |
|
||
| `radio` | `radio2` | Cross | Cross | Isolated |
|
||
| `tempradio` | `tempradio2` | Cross | Cross | Isolated |
|
||
| `radio` | `tempradio2` | Isolated | Cross | Isolated |
|
||
| `tempradio` | `radio2` | Isolated | Cross | Isolated |
|
||
|
||
This table describes MeshCore profiles. A Meshtext secondary profile is
|
||
always isolated, including with `radio2.cross on`.
|
||
|
||
These are the global rules used by Companion targets set to `auto`; an explicit
|
||
contact/channel choice above overrides crossing for its own outbound packets.
|
||
Ordinary crossing respects `rx`. The infrastructure-only `tx.reply ... force`
|
||
override above permits locally generated replies on that profile.
|
||
With crossing allowed, a packet received there may still be forwarded on the
|
||
primary profile. Existing routing, forwarding and packet-filter settings apply.
|
||
An RX-only `tempradio2` also blocks locally generated OTA requests under
|
||
`auto` or `off`; they do not fall back to the normal primary channel. Use `rxtx`
|
||
for a fetching node that needs to exchange requests and data. Infrastructure
|
||
OTA responses follow `tx.reply` independently of this crossing table.
|
||
|
||
## Preamble
|
||
|
||
Append the preamble to the end of a command:
|
||
|
||
```text
|
||
set radio2 910.5,500,8,5,rxtx,88
|
||
set tempradio2 910.5,500,7,5,rxtx,120,128
|
||
set radio 909.5,62.5,7,5,32
|
||
set tempradio 909.5,62.5,7,5,120,32
|
||
```
|
||
|
||
Existing primary `radio`, `tempradio`, `radioat` and `tempradioat` commands accept
|
||
the optional trailing preamble where those primary commands are supported by
|
||
the role. Getters include the preamble. Omit it, or use `auto`/`0`, to calculate
|
||
it automatically. Explicit values are used as entered, subject to the radio's
|
||
limits and safe airtime arithmetic.
|
||
|
||
Automatic dual-profile preambles round **up to a multiple of eight**, with a
|
||
32-symbol minimum and a 128-symbol maximum. When a second profile activates,
|
||
the radio first self-tests four successful RX-to-RX switches in each direction
|
||
using the actual tuples. It defers the test while receiving, transmitting or
|
||
busy, and never counts a failed switch. The 128-symbol automatic preamble is
|
||
provisional until the test completes. The largest successful hop, rounded up
|
||
with a **10% guard**, then supplies the per-switch allowance for both the
|
||
automatic preambles and the scan dwell. There is no fixed switch-time floor.
|
||
The measurement is reset when either profile changes, including when a
|
||
`tempradio2` session starts or ends; it is not saved to flash.
|
||
The rapid startup hops briefly interrupt reception on each channel; a new
|
||
packet beginning during this test may be missed. An already detected packet
|
||
defers the test instead of being interrupted.
|
||
|
||
Each idle scan starts with **4.6 symbols on the slower
|
||
profile**, then spends the remaining budgeted time on the faster profile,
|
||
also with a **4.6-symbol floor**. Slower
|
||
means a longer LoRa symbol (`2^SF / bandwidth`), regardless of profile number.
|
||
The slower preamble sets the return deadline. The calculation allows two slow
|
||
visits within that preamble, reserving the self-tested, 10%-guarded switch time
|
||
per hop. There is no extra fixed main-loop allowance. Fast-channel transmit
|
||
preambles include sixteen acquisition symbols.
|
||
For a primary SF7 / 62.5 kHz profile with a 763 us measured maximum switch
|
||
(840 us after the 10% guard, from the BLE-on T1000-E trial):
|
||
|
||
| Secondary profile | Automatic secondary preamble | Previously tested explicit preamble | Primary preamble |
|
||
| --- | ---: | ---: | ---: |
|
||
| SF9 / 500 kHz | 32 | 32 | 32 |
|
||
| SF8 / 500 kHz | 40 | 64 | 32 |
|
||
| SF7 / 500 kHz | 64 | 80 | 32 |
|
||
|
||
With preamble 32 on that slower profile, its visit is 9.421 ms and the faster
|
||
visit is 21.667 ms, plus the actual switching time. Increasing the slow preamble
|
||
also increases the time available on the fast channel. If the symbols are equal
|
||
in length, the primary profile goes first. Detected packets hold the current
|
||
channel until reception finishes.
|
||
|
||
The SF8 / 500 kHz value is calculated from the same formula as every other
|
||
profile; the older pair-specific 88-symbol floor has been removed. The explicit
|
||
values come from [historical production validation](test-results/radio_profiles_validation.md):
|
||
the final fast-channel attempts each received 50/50 packets at 4.8 slow symbols.
|
||
Automatic values use the measured switch allowance below.
|
||
Those explicit-preamble tests used the older 4.8-symbol policy; they do not
|
||
validate the newer 4.6-symbol slow dwell. The
|
||
[4.6/0.3 ms two-profile bench](test-results/radio_dwell_policy_validation.md#hardware-context) received 197/200
|
||
at SF7/62.5 + SF8/500 with explicit 32-symbol preambles. The newer four-channel
|
||
5.1/6.1/7.7 tests also had misses, so this is a timing policy, not a claim of zero packet loss.
|
||
The automatic fast preamble covers the blind interval during the slow visit,
|
||
both switches and the acquisition margin. These settings do not guarantee
|
||
reception during overlapping packets or long pauses in the firmware loop.
|
||
**Transmitters on each channel also need a long enough preamble.** Changing the
|
||
receiver's setting does not lengthen packets sent by other nodes.
|
||
|
||
### Chirp calculator and warnings
|
||
|
||
`get radio2.timing` (alias `get radio.timing`) reports the active pair in
|
||
`radio,radio2` order: dwell in chirps, recommended preambles in symbols, and
|
||
the switching/loop allowances. Before calibration it reports `timing self-test
|
||
pending` and the provisional preambles. On nRF52 boards, enabling `radio2`
|
||
uses RTC2 for both retune timing and scan visits (about 31 us resolution);
|
||
turning `radio2` off stops RTC2 and returns to the usual `micros()` clock.
|
||
Previously, the nRF52 `micros()` fallback counted 1,024 Hz RTOS ticks: a real
|
||
sub-millisecond retune could be reported as exactly 977 us, and two ticks as
|
||
1,953 us. Those old numbers are timer artifacts, not measured RF costs.
|
||
For SF7/62.5 on `radio` and SF8/500 on `radio2`, one live BLE-on read returned:
|
||
|
||
```text
|
||
> chirps=4.60,42.84; need=32,40; switch=706us; loop=0us; test=4/4,4/4 ready; WARN recommended preamble: radio2=40
|
||
```
|
||
|
||
Later good hops in another BLE-on trial raised the measured maximum to 763 us
|
||
(840 us with the 10% guard); the recommendation stayed at 40 symbols.
|
||
|
||
The measured switch duration is wall time, so an interrupt during the hop is
|
||
included. Bluetooth's SoftDevice may still preempt application code; this
|
||
clock correction does not stop it. Ordinary Companion/USB work in the main
|
||
loop does not run inside the synchronous retune, but it can delay when the
|
||
next scan visit begins. The zero fixed loop allowance is not a guarantee of
|
||
lossless reception under arbitrary application load.
|
||
|
||
`WARN recommended preamble` identifies **each** profile whose recommendation
|
||
exceeds the standard 32 symbols or its explicitly selected value. For example,
|
||
with SF7/62.5 on the primary profile, `set radio2 910.5,500,7,5,rx,32`
|
||
initially replies with `timing self-test pending`. Once active and measured,
|
||
`get radio2` warns:
|
||
|
||
```text
|
||
> 910.500,500.000,7,5,rx,32; WARN recommended preamble: radio2=64; short override: radio2
|
||
```
|
||
|
||
Successful `set radio ...` replies also include the recommended preamble for
|
||
each affected profile, calculated with the requested settings. A `short override`
|
||
note identifies an explicit value below the estimate; it is not silently
|
||
rewritten. Warnings are also included in profile settings/getters, scan info,
|
||
temporary settings and schedule acknowledgments. These preamble-bearing replies
|
||
also include `switch=547us` (or the current measured value) after the self-test
|
||
for that exact profile pair. The first ACK for a new pair says `timing self-test
|
||
pending` instead: it is sent before activation, so it cannot contain the future
|
||
measurement. Query `get radio2` after activation to see the measured value beside
|
||
the resulting preamble. `off` disables the dual-profile
|
||
warning. `get radio2.status` shows self-test progress and the resulting guarded
|
||
switch budget; its separate `max` field remains a lifetime diagnostic that can
|
||
include failed/rolled-back retunes and is not used for calibration. If the
|
||
recommendation exceeds 128, it additionally reports
|
||
`WARN need >128`; an automatic preamble stays at 128, which may not be enough
|
||
for reliable reception on that pair. Explicit preambles are not rewritten and
|
||
still use the radio's ordinary validation limits. A timing readout does not
|
||
change settings or transmit a packet.
|
||
|
||
The shared `RadioProfiles::calculateChirpTiming()` function accepts both tuples,
|
||
optional visit times in microseconds, a per-switch allowance and loop margin.
|
||
Using microseconds throughout:
|
||
|
||
```text
|
||
T[i] = 2^SF[i] * 1000 / BW_kHz[i]
|
||
slow = profile with the longer T (radio wins ties)
|
||
L[slow] = max(requested_visit_us[slow], ceil(4.6 * T[slow]))
|
||
L[fast] = max(requested_visit_us[fast], ceil(4.6 * T[fast]))
|
||
O = 2 * switch_us
|
||
C = L[radio] + L[radio2] + O
|
||
|
||
P[slow] = round_up_to_8(max(32, 2 * C / T[slow]))
|
||
P[fast] = round_up_to_8(max(32, (L[slow] + O) / T[fast] + 16))
|
||
```
|
||
|
||
Automatic selection uses the minimum visits to choose a preamble, then allocates
|
||
the fast visit from half the slow preamble's duration. Diagnostics feed the
|
||
actual allocated visits back into the same function, including longer visits
|
||
caused by an explicit slow preamble. Too-short slow preambles that cannot fit
|
||
both minimum visits remain rejected by the existing configuration validation.
|
||
|
||
The automatic scheduler uses these same role-specific minima and computes:
|
||
|
||
```text
|
||
slow_dwell = ceil(4.6 * T[slow])
|
||
fast_dwell = max(ceil(4.6 * T[fast]),
|
||
floor(P_configured[slow] * T[slow] / 2 - slow_dwell - O))
|
||
```
|
||
|
||
For the tested SF7/62.5 + SF8/500 pair with slow preamble 32, this yields
|
||
9,421 us slow and 21,197 us fast (41.40 fast chirps) with the 1,075 us guarded
|
||
switch allowance above. Swapping `radio` and `radio2` swaps the allocations automatically.
|
||
|
||
Production scheduling and automatic preambles use the **self-tested maximum
|
||
successful switch plus 10%**, without an additional fixed loop allowance. Subsequent
|
||
successful hops can only raise that allowance until the next profile change.
|
||
The measured recommendation is rounded up to eight symbols and capped at 128
|
||
for automatic settings. The calculator and warnings retain the *uncapped*
|
||
recommendation so a slow board is not shown as safe merely because it hit the
|
||
limit. Older V4 tests peaked at 836 us (827 us in the 4.6/0.3 ms run), and
|
||
Indicator reached 8428 us; these are historical measurements, not default
|
||
budgets for other boards.
|
||
The model preserves two slow-profile return opportunities and the existing
|
||
16-symbol fast-profile acquisition allowance. These are conservative policies,
|
||
not a fitted success curve, and neither packet overlap nor arbitrary application
|
||
stalls can be made safe by a finite preamble recommendation.
|
||
|
||
See the [implementation and validation notes](test-results/radio_dwell_policy_validation.md).
|
||
|
||
## Schedule the second profile
|
||
|
||
Use UTC Unix timestamps (seconds), or `+N` for N whole minutes from now:
|
||
|
||
```text
|
||
set radioat2 910.5,500,8,5,rxtx,START[,PREAMBLE]
|
||
set tempradioat2 910.5,500,8,5,rxtx,START,END[,PREAMBLE]
|
||
get radioat2
|
||
get radioat2 1
|
||
get tempradioat2
|
||
get tempradioat2 1
|
||
del radioat2 1
|
||
del tempradioat2 all
|
||
```
|
||
|
||
Replace `START` and `END` with timestamps or `+N`, and the optional `PREAMBLE`
|
||
with a number; omit the square
|
||
brackets. Each schedule family has four slots. Times must be in the future and
|
||
within 24 days. Overlapping temporary second-profile sessions are rejected.
|
||
`radioat2` changes the saved second profile when it starts; `tempradioat2`
|
||
restores the saved profile at its end. Pending schedules do not survive reboot.
|
||
|
||
For example, `set tempradioat2 910.5,500,8,5,rxtx,+5,+15` starts in 5 minutes
|
||
and restores in 15 minutes (10 minutes active). Both offsets are measured from
|
||
the same node-clock snapshot when accepting the command, not from each other.
|
||
`set radioat2 910.5,500,8,5,rxtx,+5` schedules a saved change in 5 minutes.
|
||
Offsets must be positive whole minutes. Absolute and relative endpoints can be
|
||
mixed; bare numbers still mean UTC seconds. Existing clock requirements apply.
|
||
|
||
## Return to one profile
|
||
|
||
```text
|
||
set radio2 off
|
||
```
|
||
|
||
This saves the disabled state and cancels all second-profile temporary activity
|
||
and schedules.
|
||
|
||
```text
|
||
set tempradio2 off
|
||
```
|
||
|
||
This ends temporary second-profile activity, cancels its temporary schedules and
|
||
restores saved `radio2`. If the saved second profile is off, this returns to one
|
||
profile. Packets/retries bound to an expired or changed session are discarded.
|
||
|
||
## Power saving and diagnostics
|
||
|
||
Dual mode suspends RX power saving and MCU idle sleep so receive visits can run
|
||
promptly. The saved RX power-saving setting returns when profile 2 is off.
|
||
The scan uses **normal receive**, following the
|
||
[CAD/RX measurements](test-results/cad_scan_validation.md), with the updated 4.6-symbol dwell on the slower
|
||
channel, followed by the remaining fast-channel visit. It does not use CAD for scanning.
|
||
The separate CAD check before an initial transmission still follows `cad`.
|
||
|
||
SX1262 dual-profile mode keeps the oscillator running in XOSC standby between
|
||
visits, so each retune does not need to restart the TCXO. It restores the prior
|
||
standby policy when returning to one profile, together with the saved RXPS
|
||
setting. Radio reinitialization temporarily uses RC standby until the TCXO has
|
||
been configured again. Other radio families retain their existing behavior.
|
||
This does not disable the TCXO supply or its initial startup delay, and it is
|
||
not conditional on 500 kHz bandwidth. Keeping the oscillator on uses additional
|
||
power during the short standby intervals.
|
||
|
||
The switch-time self-test is not a forced settling delay or a proof of RF
|
||
acquisition. The earlier standalone
|
||
[keep-warm scan experiment](test-results/mixed_scan_validation.md) averaged about 1.19 ms
|
||
of overhead per hop (scan-cycle time minus both receive windows, divided by
|
||
two). That experiment used a leaner receive path; it is not a measurement or
|
||
guarantee of the production implementation. Measure the target board before
|
||
shortening its timing margins or transmit preambles.
|
||
|
||
The [production-path XIAO timing test](test-results/radio_profile_switch_validation.md)
|
||
measured **1.434 ms average / 1.467 ms maximum** with warm standby, compared
|
||
with **3.166 ms average** using RC standby: about **1.732 ms saved per hop**.
|
||
It covered 12,000 retunes across SF7/8/9 at 500 kHz paired with SF7/62.5 kHz.
|
||
These measurements include observing BUSY low, but exclude application/UI
|
||
scheduling and are not a packet-delivery qualification or a guarantee for
|
||
other boards, especially those using I/O expanders.
|
||
|
||
A same-image follow-up batching SF/BW/CR into one modulation command reduced
|
||
warm switching from **1.444 ms to 1.158 ms average** (batched maximum **1.198 ms**),
|
||
saving another **0.286 ms / 19.8%**. RadioLib's modulation caches and LDRO
|
||
calculation remain synchronized; frequency, preamble, packet guards, and RX
|
||
restart are unchanged. A later [HIL-only screening](test-results/radio_profile_switch_validation.md#expanded-screening-redundant-rx-setup-and-spi-v5)
|
||
tested redundant standby/IRQ/buffer/packet setup and SPI transfer changes.
|
||
Its fastest candidate measured 0.535 ms mean on XIAO but 8.203 ms on the
|
||
Indicator, whose radio GPIOs go through an I2C expander. The validated production
|
||
implementation now opts XIAO S3 WIO and Indicator LoRa into buffered 8 MHz SPI
|
||
and a guarded fast-RX state machine. The `heltec_v4_kiss_modem` and
|
||
`Station_G3_ESP32_kiss_modem` targets use the same guarded path only for KISS
|
||
dual-profile scanning; their normal board roles keep their established
|
||
HAL/timing policy. V4 uses its existing 160 MHz role override and Station G3
|
||
uses its base 240 MHz setting, but both still need an exact-image hardware
|
||
timing check before a nominal 600 µs target is claimed. It skips redundant
|
||
standby, IRQ mapping,
|
||
buffer-base writes and modem queries only during an owned continuous RX-to-RX
|
||
retune. Preamble/packet parameters (including the IQ workaround), stale-IRQ
|
||
clearing, RX commands and BUSY waits remain. TX, CAD, sleep, reset, RXPS and
|
||
failed commands revoke reuse; failed RX startup rolls back the profile.
|
||
Sleep entry uses standby and wake restores the existing TCXO voltage/delay.
|
||
Other variants do not opt in to faster SPI or the fast-RX state machine.
|
||
Within a valid owned fast-RX retune, identical acknowledged SF/BW/CR/LDRO
|
||
settings now reuse the previous modulation command: frequency-only hops omit
|
||
`SetModulationParams` (0x8B). A changed effective LDRO value also forces a write,
|
||
including when automatic LDRO policy changes. Ordinary setters and lost RX
|
||
context invalidate that acknowledgement; initial setup, reset, sleep, failed
|
||
commands and full RX setup conservatively reapply the tuple. Frequency,
|
||
preamble/packet setup, IRQ clearing and BUSY checks are unchanged. This follow-up
|
||
has [same-image hardware measurements](test-results/separated_radio_modulation_cache_validation.md):
|
||
frequency-only hops averaged 0.452 ms on XIAO and 7.601 ms on Indicator.
|
||
The mixed-bandwidth timing figures above predate it. Four-channel SF6 scans
|
||
still missed packets; faster switching is not proof of loss-free acquisition.
|
||
See [production and USB validation](test-results/radio_profile_switch_validation.md#production-integration-and-usb-recovery-v8)
|
||
for current measurements, lifecycle tests and limitations.
|
||
The Indicator result shows why each board now measures its own switch time.
|
||
That measurement adjusts automatic preambles and dwell, but it cannot verify
|
||
RF acquisition or lengthen packets transmitted by other nodes.
|
||
|
||
Single-channel noise-floor calibration and RSSI interference comparison are
|
||
suspended while scanning. Retries use the radio's preamble/header activity
|
||
indicators. LR2021 extra-SF side detectors are suspended and restored as well.
|
||
|
||
`get radio2.status` reports the active second-profile mode, MeshCore RX/TX counts
|
||
for each profile, switch count, switch failures, longest measured switch and
|
||
both effective preambles. A long switch time or rising error count indicates
|
||
that the chosen settings need investigation on that board.
|
||
|
||
`get radio2.scan` shows which profile goes first, the two idle receive-window
|
||
lengths in microseconds, and both effective preambles. It reports `off` in
|
||
single-profile mode.
|