# 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. When setting a saved, temporary or scheduled second profile, the response warns if its occupied bandwidth overlaps the primary channel or comes within **6 kHz of either edge**. This check uses both channel bandwidths, regardless of SF or coding rate. The change still applies. The warning also appears when reading the second profile, including while the switching timing self-test is pending. The warning suggests a nearby center frequency with the same bandwidth, SF and coding rate that clears the buffer. It chooses the nearest whole-kHz frequency on the requested side of the primary channel (above for identical centers), checks the radio's supported frequency range, and tries the other side if needed. The suggestion is not applied automatically. For example, with `radio` at **910.525 MHz / 62.5 kHz**, a second 62.5 kHz profile at 910.550 MHz produces: ```text WARN radio2 overlaps radio1 (6kHz guard); try 910.594MHz ``` The center spacing must exceed `(primary bandwidth + second bandwidth) / 2 + 6 kHz`; touching that boundary also warns. Frequency comparisons use the stored RF centers rounded to the nearest Hz. This is a channel-spacing aid; choose the suggested frequency only when it also suits the network and local band plan. | 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. An active temporary profile keeps its radio generation until those tracked reply copies finish or are retired, but its remaining lease continues counting down during that wait. 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 requested settings remain pending and the commit retries. An expired temporary profile returns to the last saved configuration during those post-reply storage retries; the uncommitted settings do not become active. 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 media and SSH TX routing Companions also save independent application-data routes: ```text get tx.media set tx.media radio2 get tx.ssh set tx.ssh radio ``` Both accept `auto|radio|radio2|both|off` and default to `auto`. An explicit application route overrides the channel's TX choice for that datagram only; `auto` preserves the channel/global behavior. `radio2` never falls back to `radio`, and must be an enabled `rxtx` **MeshCore** profile. `both` uses the profiles available for MeshCore transmission. `off` blocks transmission, not reception. Getters show the saved choice and current `active=` mask; `auto` shows `active=channel/global`, because the effective route depends on the destination channel (or global rules for raw data). Settings survive reboot; a failed save returns an error and keeps the previous choice. Already queued packets retain their original route. `tx.media` recognizes Open TAK photo/voice channel data type `0x5643`, raw clip packets `0x56`/`0x57`, and well-formed binary fetch requests `0x72`. Text announcements (`VE3:`, `VM1:`, `VI1:`), text fetch requests (`VR3:`), ordinary chat, contact ACKs, adverts, OTA, and unrelated raw data keep their normal routing. The format is defined in [Open TAK's voice/media wire implementation](https://github.com/jade-on-mesh/meshcore-open-tak/blob/main/lib/vox/voice_wire.dart). `tx.ssh` uses a **new opt-in application contract**, not automatic inspection of encrypted SSH or arbitrary IP tunnels. A tunnel client must use either: - `CMD_SEND_CHANNEL_DATA` with data type `0x5353` (wire bytes `53 53`), or - `CMD_SEND_RAW_DATA` with the four-byte prefix `53 53 48 01` (`SSH`, version 1), followed by at least one application byte, on **every** tunnel packet. The firmware does not add/remove the prefix, decrypt the tunnel, or run an SSH server. Both endpoints must understand this framing. Untagged existing tunnel apps remain unchanged and are **not** affected by `tx.ssh` until they adopt the contract. Other channel data types do not acquire an SSH route merely because their contents start with `SSH`. Raw packets still require a direct path, including zero-hop; this does not enable raw flooding. For a media-only side channel, configure `radio2`/`tempradio2` in `rxtx`, set `radio2.cross off`, then set `tx.media radio2`. Ordinary text can stay on the primary profile without routing every raw application onto radio2. ## ESP32 Companion Ethernet over LoRa (KISS) ESP32 **Full Companion** builds with a supported SX1262/SX1276 modem can carry raw Ethernet on radio2 while the primary profile stays MeshCore. This separate feature is named `ethernet`, not `tx.ssh`: it transports the **whole Ethernet link**, including ARP, TCP acknowledgments, IP fragments and SSH on any port. There is no Ethernet-to-MeshCore forwarding. The mode is off by default. It requires an explicit RF tuple; it never chooses a frequency or starts WiFi automatically. For a dedicated lab link: ```text set ethernet.radio 909.5,500,5,5,512 set ethernet on get ethernet get ethernet.radio get radio2.timing ``` Use an appropriate frequency and permitted power for your network. Both ends must match frequency, bandwidth, SF, coding rate and sync word. Ethernet uses sync word `0x16`, explicit headers, normal IQ and a LoRa CRC. The optional preamble defaults to 32 symbols; it can be set explicitly (8–65528, subject to driver/timing limits). SX1276 requires SF7 or higher. Do not assume 32 symbols is enough for a fast SF5/BW500 link. Run `get radio2.timing` after its switching self-test completes, and accommodate the slower end of the link. The lab example uses a 32.8 ms preamble and was tested on a Heltec V4/V3 pair, not an I/O-expander-controlled board. Both primary and secondary preambles matter if primary MeshCore traffic must also be heard while time-sharing. Longer preambles consume airtime; they do not prevent half-duplex collisions. On that V4/V3 pair, the actual MeshCore-open SSH backend verified the remote host key, authenticated with a public key, opened a PTY and received exactly 4096 test bytes with a matching SHA-256 check. Radio1 stayed on the USA receive profile (`910.525,62.5,7,5`); its TX counter remained zero on both boards. Raw Ethernet still had losses, so this demonstrates TCP/SSH recovery, not loss-free RF or a minimum preamble guaranteed for every board. The bench used an explicit test-image TX-frequency lock; it did not transmit test packets on the USA profile. The settings are saved in a checked atomic NVS blob. `set ethernet off` closes the host connection and releases only this feature's runtime radio2 lease; it does not rewrite the saved MeshCore profiles. A manually configured radio2 must already match the Ethernet RF tuple, including explicit preamble, and be `rxtx`; its protocol is borrowed only while Ethernet is enabled. An explicit saved `radio2 off`, a temporary/scheduled profile, or conflicting `r2-`/`mt-` channels prevents automatic takeover. Those channel aliases cannot be added while Ethernet is enabled. Clear conflicts or disable Ethernet first. Only one secondary profile can be active: Meshtext and Ethernet cannot share radio2 at the same time. ### Attach a Linux Ethernet interface Connect the Companion to station WiFi. Its **station IP** exposes a single KISS-over-TCP listener on port **5003**, separate from Companion binary TCP 5000, OTA TCP 5001 and the existing text terminal on TCP 5002. Use [tncattach](https://github.com/markqvist/tncattach) on the host: ```sh sudo tncattach -T -H 192.168.1.100 -P 5003 --ethernet --mtu 230 --noipv6 --ipv4 10.10.10.10/24 ``` Replace the station IP. Attach the other end with a different IP in the same subnet, then use normal host tools such as ping or SSH. A peer running [MeshTNC's Ethernet-over-LoRa mode](https://github.com/datapartyjs/MeshTNC#ethernet-over-lora) uses the same raw Ethernet framing; configure its PHY to match this link. No custom `SSH` prefix or MeshCore packet wrapper is added. **Enable only on a trusted LAN.** KISS has no authentication and permits raw RF transmission. The service binds to the station address, not the setup AP; do not expose port 5003 to the Internet. The ESP32 does not run an SSH server: SSH connects the attached host computers. The host MTU must fit one RF frame: MTU 230 plus the 14-byte Ethernet header is 244 bytes. Ethernet frames of 14–255 bytes are supported; larger frames are rejected, never truncated. There is no firmware fragmentation protocol, compression, retransmission or autonomous Ethernet forwarding. Queues are bounded and host TCP is backpressured. Host disconnects, WiFi/IP changes, disabling and profile changes discard stale queued/partial host traffic; an RF frame already transmitting cannot be unsent. One transceiver time-shares reception between the two profiles. Short preambles on an unmodified peer can be missed while listening to MeshCore; TCP handles losses but performance is not equivalent to two physical radios. Host/codec and firmware-build tests do not establish an RF interoperability or throughput guarantee. nRF52 and non-Full Companion builds do not acquire this Ethernet service. `tx.media` and tagged `tx.ssh` remain independent MeshCore application policies; neither controls this raw Ethernet link. ## 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. ### Local emergency-channel notice When a Companion app adds `#emergency` (or `#r2-emergency`), the firmware seeds its local chat with this notice, using `#EMERGENCY` as the sender: ```text #EMERGENCY: This is an emergency channel. Do not use for testing or small talk. ``` This is a local onboarding message, not an RF transmission or an emergency alert. It does not change the node's name. It is added only after the channel is successfully saved, not on every connection, reboot, key edit, or alias rename. Deleting and adding the channel again adds a fresh notice. Queued real messages are delivered first: the notice waits for an empty receive inbox and for held one-key DMs to be released, retrying as the app downloads messages. If later traffic fills the inbox, the local notice is the first channel message eligible for eviction. As with other pending received messages, it is RAM-backed until downloaded by the app; a reboot before download can discard it. ### 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.retry get mt.rebroadcast 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.rebroadcast off 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 does not relay MT packets unless explicitly enabled as described below. `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