From 44ba5b4f18c853cf9d80c985ac29b7cba81f4e35 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 19 Oct 2026 21:14:41 -0700 Subject: [PATCH] Default radio profile crossover to off --- docs/_javascript/firmware_picker.js | 6 ++-- docs/_javascript/preset_test.js | 4 +-- docs/firmware_picker.md | 2 +- docs/preset_test.md | 6 ++-- docs/radio_profiles.md | 9 +++-- src/RadioProfiles.h | 2 +- src/helpers/RadioProfileCLI.cpp | 4 ++- src/helpers/RadioProfileCLI.h | 2 +- src/helpers/ui/RadioProfileSystemStatus.h | 2 +- test/fixtures/radio_profiles/chirp_math.cpp | 2 ++ test/fixtures/radio_profiles/cli_test.cpp | 37 ++++++++++++++++++++- test/test_firmware_picker.js | 7 ++++ test/test_preset_test.js | 4 +++ 13 files changed, 70 insertions(+), 17 deletions(-) diff --git a/docs/_javascript/firmware_picker.js b/docs/_javascript/firmware_picker.js index 1e2a72bf2..d55ccb0c7 100644 --- a/docs/_javascript/firmware_picker.js +++ b/docs/_javascript/firmware_picker.js @@ -2145,11 +2145,11 @@ section("Companion tempradio2: transmit on both", [ { label: "Transmit on both", commands: ["set radio2.cross on"], text: "After setting tempradio2 to rxtx, run this to send new messages on both radio and tempradio2. rxtx enables transmission on the second profile; cross on enables copying between the two profiles." }, - { label: "Default isolation", commands: ["set radio2.cross auto"], - text: "Keep ordinary local messages on radio and local LoRa OTA traffic on tempradio2. With auto, a permanent and a temporary profile remain isolated." }, + { label: "Default isolation", commands: ["set radio2.cross off"], + text: "Keep ordinary local messages on radio and local LoRa OTA traffic on tempradio2. Off disables copying between profiles, including two permanent profiles." }, { label: "Check", commands: ["get tempradio2", "get radio2.cross", "get radio2.status"], text: "Confirm tempradio2 is active in rxtx mode. TX=a,b reports primary and secondary transmit counts." }, - ], "radio2.cross is saved and stays set after tempradio2 expires or the node reboots. Turning it on also allows OTA traffic to cross profiles. RX-only mode still prevents transmission on the second profile. Use Default isolation to restore auto when needed."); + ], "radio2.cross is saved and stays set after tempradio2 expires or the node reboots. Turning it on also allows OTA traffic to cross profiles. RX-only mode still prevents transmission on the second profile. Use Default isolation to restore off when needed."); } if (full || infrastructure) { toggle("Device power saving", "set powersaving", "get powersaving", diff --git a/docs/_javascript/preset_test.js b/docs/_javascript/preset_test.js index a084dec32..4bfd086ca 100644 --- a/docs/_javascript/preset_test.js +++ b/docs/_javascript/preset_test.js @@ -431,8 +431,8 @@ primaryCancel: "get tempradioat\ndel tempradioat all", companionCancelBefore: - "get tempradioat2\ndel tempradioat2 all\nset radio2.cross auto", - companionCancelDuring: "set tempradio2 off\nset radio2.cross auto", + "get tempradioat2\ndel tempradioat2 all\nset radio2.cross off", + companionCancelDuring: "set tempradio2 off\nset radio2.cross off", companionLeaveIn30: "set radio2.cross on\ndel tempradioat2 all\nset tempradio2 " + tuple + ",rxtx,30", diff --git a/docs/firmware_picker.md b/docs/firmware_picker.md index 926d965f3..d4d6251d9 100644 --- a/docs/firmware_picker.md +++ b/docs/firmware_picker.md @@ -438,7 +438,7 @@ set radio2.cross on This copies new messages to both `radio` and `tempradio2`. The setting is saved, outlasts the temporary session and reboot, and also allows OTA traffic to cross -profiles. **Default isolation** copies `set radio2.cross auto`; **Check** shows +profiles. **Default isolation** copies `set radio2.cross off`; **Check** shows the temporary profile, crossing policy and per-profile transmit counters. See the [two-profile setup guide](radio_profiles.md#companion-messages-on-both-profiles). diff --git a/docs/preset_test.md b/docs/preset_test.md index 546a65c71..6296f3d8d 100644 --- a/docs/preset_test.md +++ b/docs/preset_test.md @@ -293,7 +293,7 @@ clock

Crossing is saved independently and remains on after the temporary second profile ends. Restore its previous value after the - test; auto is the normal default. + test; off is the normal default.

@@ -342,7 +342,7 @@ clock

- If get radio2.cross was not auto before the + If get radio2.cross was not off before the test, restore that recorded value instead.

Leave in 30 minutes

@@ -352,7 +352,7 @@ clock These commands clear any absolute second-profile schedule and give the test profile a fresh 30-minute lease. The saved second profile returns at expiry, but crossing remains on; afterward, - restore the value you recorded before the test (auto is + restore the value you recorded before the test (off is the normal default).

diff --git a/docs/radio_profiles.md b/docs/radio_profiles.md index d68ffd0fe..4bc272c93 100644 --- a/docs/radio_profiles.md +++ b/docs/radio_profiles.md @@ -189,8 +189,8 @@ 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 +ends or the node reboots. Run `set radio2.cross off` to restore the default +isolation between profiles. The firmware picker's Companion tempradio2 instructions include both choices and the status commands. ## Companion TX routing by contact or channel @@ -806,7 +806,7 @@ set radio2.cross on set radio2.cross off ``` -| Active primary | Active secondary | `auto` (default) | `on` | `off` | +| Active primary | Active secondary | `auto` | `on` | `off` (default) | | --- | --- | --- | --- | --- | | `radio` | `radio2` | Cross | Cross | Isolated | | `tempradio` | `tempradio2` | Cross | Cross | Isolated | @@ -816,6 +816,9 @@ set radio2.cross off This table describes MeshCore profiles. A Meshtext secondary profile is always isolated, including with `radio2.cross on`. +New configurations default to `off`. An explicitly saved `auto`, `on`, or `off` +setting is preserved across firmware updates and reboots. + 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` diff --git a/src/RadioProfiles.h b/src/RadioProfiles.h index 68f045aa9..7842d140c 100644 --- a/src/RadioProfiles.h +++ b/src/RadioProfiles.h @@ -71,7 +71,7 @@ class RadioProfiles { RadioProfileConfig secondary; bool primary_temporary = false; bool secondary_temporary = false; - RadioCrossMode cross = RadioCrossMode::Auto; + RadioCrossMode cross = RadioCrossMode::Off; // Infrastructure opts into BOTH at startup; Companions keep their own // contact/channel policy. Force applies only to locally generated replies. uint8_t reply_tx = RADIO_TX_AUTO; diff --git a/src/helpers/RadioProfileCLI.cpp b/src/helpers/RadioProfileCLI.cpp index edc554606..452b37ea8 100644 --- a/src/helpers/RadioProfileCLI.cpp +++ b/src/helpers/RadioProfileCLI.cpp @@ -213,6 +213,7 @@ bool RadioProfileCLI::commitSavedImage(const char* path) { void RadioProfileCLI::begin(FILESYSTEM* fs, Radio* radio, RTCClock* rtc, bool infrastructure_replies) { fs_ = fs; radio_ = radio; rtc_ = rtc; last_ms_ = millis(); hold_ = false; + cross_ = RadioCrossMode::Off; recoverable_corrupt_store_ = false; manual_secondary_ = false; secondary_intent_generation_ = 0; @@ -222,6 +223,7 @@ void RadioProfileCLI::begin(FILESYSTEM* fs, Radio* radio, RTCClock* rtc, bool in #endif infrastructure_replies_ = infrastructure_replies; if (radio_ && radio_->profiles()) { + radio_->profiles()->cross = cross_; radio_->profiles()->reply_tx = infrastructure_replies ? RADIO_TX_BOTH : RADIO_TX_AUTO; radio_->profiles()->reply_force = false; } @@ -270,7 +272,7 @@ void RadioProfileCLI::begin(FILESYSTEM* fs, Radio* radio, RTCClock* rtc, bool in // structurally valid backup exists; that backup is the last known-good // radio-profile image and must remain available for manual recovery. const ImageReadResult backup_image = readImage(BackupPath, bytes, sizeof(bytes)); - hold_ = true; saved_ = {}; primary_preamble_ = 0; cross_ = RadioCrossMode::Auto; + hold_ = true; saved_ = {}; primary_preamble_ = 0; cross_ = RadioCrossMode::Off; recoverable_corrupt_store_ = backup_image != ImageReadResult::Valid && backup_image != ImageReadResult::Unreadable; reply_setting_ = 0; // reject the entire image, including its reply override diff --git a/src/helpers/RadioProfileCLI.h b/src/helpers/RadioProfileCLI.h index 9c69f757b..8da7ec9ef 100644 --- a/src/helpers/RadioProfileCLI.h +++ b/src/helpers/RadioProfileCLI.h @@ -26,7 +26,7 @@ class RadioProfileCLI { RadioProfileConfig temporary_; RadioProfileConfig pending_temporary_; uint16_t primary_preamble_ = 0; - RadioCrossMode cross_ = RadioCrossMode::Auto; + RadioCrossMode cross_ = RadioCrossMode::Off; uint8_t reply_setting_ = 0; // reserved image byte: 0 = role default bool infrastructure_replies_ = false; // Saved Off can be an explicit veto, rather than an unused secondary slot. diff --git a/src/helpers/ui/RadioProfileSystemStatus.h b/src/helpers/ui/RadioProfileSystemStatus.h index 425ac1ad1..6b3568022 100644 --- a/src/helpers/ui/RadioProfileSystemStatus.h +++ b/src/helpers/ui/RadioProfileSystemStatus.h @@ -24,7 +24,7 @@ struct RadioProfileSystemStatus { bool dual_radio_enabled = false; bool secondary_temporary = false; RadioProfileMode secondary_mode = RadioProfileMode::Rx; - RadioCrossMode cross = RadioCrossMode::Auto; + RadioCrossMode cross = RadioCrossMode::Off; float noise_floor_1 = 0.0f; float noise_floor_2 = 0.0f; float noise_floor_1_seconds = 0.0f; diff --git a/test/fixtures/radio_profiles/chirp_math.cpp b/test/fixtures/radio_profiles/chirp_math.cpp index 8a6fdf01d..bd94f2c34 100644 --- a/test/fixtures/radio_profiles/chirp_math.cpp +++ b/test/fixtures/radio_profiles/chirp_math.cpp @@ -22,6 +22,8 @@ static void checkSafePreamble(unsigned sf, float bw, uint16_t symbols) { assert(Profiles::safePreamble(p, symbols) == originalSafePreamble(p, symbols)); } int main() { + Profiles fresh; + assert(fresh.cross==mesh::RadioCrossMode::Off && !fresh.canCross()); // Sharing the read-only validation table must preserve every supported // value and the strict float tolerance on both sides of its boundary. for (float bw : {7.8f, 10.4f, 15.6f, 20.8f, 31.25f, 41.7f, diff --git a/test/fixtures/radio_profiles/cli_test.cpp b/test/fixtures/radio_profiles/cli_test.cpp index a7f568b32..7fc4a1c6a 100644 --- a/test/fixtures/radio_profiles/cli_test.cpp +++ b/test/fixtures/radio_profiles/cli_test.cpp @@ -503,6 +503,41 @@ static void spectrumOverlap() { } int main(int argc, char** argv) { + static_assert(uint8_t(mesh::RadioCrossMode::Auto)==0 && uint8_t(mesh::RadioCrossMode::On)==1 + && uint8_t(mesh::RadioCrossMode::Off)==2, "saved crossover encoding must remain compatible"); + { + Fixture fresh; + assert(fresh.radio.p.cross==mesh::RadioCrossMode::Off && !fresh.radio.p.canCross()); + assert(!strcmp(fresh.cmd("get radio2.cross"),"> off") && fresh.fs.files.empty()); + for(const char* mode:{"auto","on","off"}) { + char command[40];snprintf(command,sizeof(command),"set radio2.cross %s",mode);fresh.cmd(command); + const auto expected=!strcmp(mode,"auto")?mesh::RadioCrossMode::Auto + : !strcmp(mode,"on")?mesh::RadioCrossMode::On:mesh::RadioCrossMode::Off; + assert(fresh.fs.files["/radio_profiles"][18]==uint8_t(expected)); + Radio reboot;mesh::RadioProfileCLI restored;restored.begin(&fresh.fs,&reboot,&fresh.clock); + assert(reboot.p.cross==expected); + restored.handle("get radio2.cross",fresh.reply); + char reply[16];snprintf(reply,sizeof(reply),"> %s",mode);assert(!strcmp(fresh.reply,reply)); + } + fresh.cmd("set radio2.cross on");fresh.fs.files.clear(); + fresh.cli.begin(&fresh.fs,&fresh.radio,&fresh.clock); + assert(fresh.radio.p.cross==mesh::RadioCrossMode::Off); + assert(!strcmp(fresh.cmd("get radio2.cross"),"> off")); + } + for(bool semantic:{false,true}) { + Fixture invalid;invalid.cmd("set radio2.cross auto"); + auto& image=invalid.fs.files["/radio_profiles"]; + if(semantic) { + image[18]=255; + uint32_t crc=0xffffffffU; + for(size_t i=0;i<20;++i) {crc^=image[i];for(unsigned bit=0;bit<8;++bit)crc=(crc>>1)^((crc&1)?0xedb88320U:0);} + memcpy(image.data()+20,&crc,4); + } else image[0]^=1; + Radio radio;radio.p.cross=mesh::RadioCrossMode::On; + mesh::RadioProfileCLI restored;restored.begin(&invalid.fs,&radio,&invalid.clock); + assert(radio.p.cross==mesh::RadioCrossMode::Off); + restored.handle("get radio2.cross",invalid.reply);assert(!strcmp(invalid.reply,"> off")); + } if (argc == 2) { if (!strcmp(argv[1], "permanent_order")) permanentScheduleOrder(); else if (!strcmp(argv[1], "temporary_boundary")) adjacentTemporarySchedules(); @@ -647,7 +682,7 @@ int main(int argc, char** argv) { { Fixture f; assert(!strcmp(f.cmd("get radio2"),"> off")); - assert(!strcmp(f.cmd("get radio2.cross"),"> auto")); + assert(!strcmp(f.cmd("get radio2.cross"),"> off")); assert(strstr(f.cmd("set radio2 910.5,500,7,5,rxtx"),"timing self-test pending")); assert(!f.radio.p.enabled()); f.advance(2000); assert(f.radio.p.enabled()); assert(strstr(f.cmd("get radio2.timing"),"self-test pending")); diff --git a/test/test_firmware_picker.js b/test/test_firmware_picker.js index e90616b34..0ccf6ff9b 100644 --- a/test/test_firmware_picker.js +++ b/test/test_firmware_picker.js @@ -1118,6 +1118,13 @@ assert.strictEqual(rakCommands.find(s => s.title.startsWith('RS232')).actions[0] assert(commands(rakCommands).includes('set gps on')); assert(!rakCommands.some(s => s.title === 'MQTT broker connections')); const indicator = picker.runtimeDirections(findControlled('SenseCapIndicator-LoRa_companion_radio_full'), {}); +const isolation = picker.runtimeDirections(Object.assign({}, + findControlled('SenseCapIndicator-LoRa_companion_radio_full'), + {releaseFamily: 'v1.17.1.6-halo-keymind-cascade-dev-test'}), {}) + .find(section => section.title === 'Companion tempradio2: transmit on both'); +assert(isolation); +assert.deepStrictEqual(isolation.actions.find(action => action.label === 'Default isolation').commands, + ['set radio2.cross off']); assert(commands(indicator).includes('set companion.transport ble')); assert(!indicator.some(s => s.title.startsWith('MQTT'))); // Never apply old hardware capabilities to another release, or to an unknown diff --git a/test/test_preset_test.js b/test/test_preset_test.js index 8365a27d0..e4dcb3924 100644 --- a/test/test_preset_test.js +++ b/test/test_preset_test.js @@ -265,6 +265,10 @@ assert.strictEqual( "set tempradioat 910.1,500,8,7,+1,+2880\nget tempradioat" ); assert.strictEqual(commands.primaryCancel, "get tempradioat\ndel tempradioat all"); +assert.strictEqual(commands.companionCancelBefore, + "get tempradioat2\ndel tempradioat2 all\nset radio2.cross off"); +assert.strictEqual(commands.companionCancelDuring, + "set tempradio2 off\nset radio2.cross off"); assert.strictEqual( commands.companionNow, "set radio2.cross on\nset tempradio2 910.1,500,8,7,rxtx,2880"