diff --git a/boards/station-g3-esp32.json b/boards/station-g3-esp32.json index 9b74f2037..5551ab7f0 100644 --- a/boards/station-g3-esp32.json +++ b/boards/station-g3-esp32.json @@ -14,7 +14,7 @@ ], "f_cpu": "240000000L", "f_flash": "80000000L", - "flash_mode": "qio", + "flash_mode": "dio", "hwids": [["0x303A", "0x1001"]], "mcu": "esp32s3", "variant": "esp32s3" diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 1cb4d9b95..5f755bb2d 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -1861,6 +1861,10 @@ required in this recovery console; it is not the normal running CLI. KISS firmware uses binary slot commands during normal operation, documented in the [KISS protocol](kiss_modem_protocol.md). +If G2 firmware works on a G3 but the G3 image behaves differently, see the +[G2/G3 compatibility checks](test-results/station_g3_g2_compatibility.md) for +the verified pin assignments, startup fixes, and saved amplifier settings. + --- ### System diff --git a/docs/test-results/station_g3_g2_compatibility.md b/docs/test-results/station_g3_g2_compatibility.md new file mode 100644 index 000000000..84db93c09 --- /dev/null +++ b/docs/test-results/station_g3_g2_compatibility.md @@ -0,0 +1,174 @@ + + + + +# Station G3 and G2 compatibility audit + +Source audit on 2026-10-18, starting from +`1147c28b279f01443c9ec44c3b71cc767f11482e`. A user reported that G2 firmware +works on a physical G3 while G3 firmware has problems. The comparison below +identifies differences in configuration and startup; it does not establish the +cause of that device's symptoms or claim a successful hardware retest. + +## Radio and peripheral pins + +The G2 radio map matches G3 physical slot 1 exactly. G3's `_r2` suffix refers +to physical socket 2, not motherboard revision 2. Current G3 firmware detects +the populated socket at boot and retains both legacy target names. + +| Signal or build macro | G2 | G3 slot 1 | G3 slot 2 | +| --- | --- | --- | --- | +| SX1262 DIO1 / interrupt | 48 | 48 | 2 | +| SPI NSS / chip select | 11 | 11 | 43 | +| SX1262 reset | 21 | 21 | 44 | +| SX1262 BUSY | 47 | 47 | 1 | +| SPI SCK | 12 | 12 | 39 | +| SPI MISO | 14 | 14 | 41 | +| SPI MOSI | 13 | 13 | 40 | +| External LNA control | Not controlled by G2 firmware | 10, active low | 42, active low | +| PA PL1 control | Not controlled by G2 firmware | 9, shared; high selects higher level | 9, shared | +| I2C SDA / SCL | 5 / 6 | 5 / 6 | 5 / 6 | +| User button | 38 | 38 | 38 | +| `PIN_GPS_RX` / `PIN_GPS_TX` | 15 / 7 | 15 / 7 | 15 / 7 | + +The radio and FEM maps agree with the manufacturer's +[ESP32-S3 pinout viewer](https://tools.bqvoy.com/stationg3/pinout/?board=BQESP32V1M-40pin) +and its [pin data](https://tools.bqvoy.com/stationg3/pinout/boards.json), and the tracked +[G2 recipe](https://github.com/mikecarper/MeshCore/blob/1147c28b279f01443c9ec44c3b71cc767f11482e/variants/station_g2/platformio.ini), +[G3 recipes](https://github.com/mikecarper/MeshCore/blob/1147c28b279f01443c9ec44c3b71cc767f11482e/variants/station_g3_esp32/platformio.ini), and +[runtime socket table](https://github.com/mikecarper/MeshCore/blob/1147c28b279f01443c9ec44c3b71cc767f11482e/variants/station_g3_esp32/StationG3RadioSlot.h). +The GPS macros name the peripheral's signals: the ESP32 startup calls +`Serial1.setPins(PIN_GPS_TX, PIN_GPS_RX)`, giving MCU RX GPIO7 and TX GPIO15. +No incorrect radio map was established. Both base recipes select a 1.8 V +TCXO and disable the SX1262's internal boosted RX gain by default. The base TX +power is 7 dBm in both; their configured maximum chip-drive limits differ +(G2: 19 dBm; G3: 22 dBm). This audit does not change those power limits. + +## Ordinary-profile flash compatibility + +Before this change, both board manifests declared QIO flash, but the G2 recipe +overrode it to DIO. Ordinary G3 profiles inherited QIO. The G3 board manifest +now selects DIO as well, following the known-working G2 startup policy. +All G3 role recipes, including the legacy `_r2` targets, inherit that setting. + +ESP32-S3 Full builds already force DIO through +[the Full partition policy](https://github.com/mikecarper/MeshCore/blob/1147c28b279f01443c9ec44c3b71cc767f11482e/scripts/esp32_full_partition.py). This change +therefore corrects an ordinary-profile difference; it does not explain a +failure in an already verified DIO Full image. The PSRAM memory type remains +`qio_opi`. Flash capacity, partition layouts, USB configuration, and radio pin +assignments are unchanged. + +Ordinary G2 firmware uses the legacy `default.csv` partition table, while G3 +uses `default_16MB.csv`. Electrical compatibility does not make those layouts +interchangeable. When moving a node that currently runs a G2 image to a G3 +image, check its installed layout and use the documented +[partition migration](../esp32_wifi_partition_migration.md) if needed to retain +its identity and settings. This fix does not change either layout. + +The two board manifests otherwise have matching build/upload settings; their +board names and manufacturer-page URLs distinguish them. DIO is a compatibility +choice here, not a claim that QIO caused the user's reported problem. + +## Bounded radio startup probe + +The G3-only socket probe added in `f765604cb8` imposed a 100 ms BUSY wait and +only three RC-standby attempts before rejecting a socket. G2 goes directly +through RadioLib's normal initialization, which permits a longer reset and +RC-standby settling interval. This is a separate, reproducible startup +compatibility gap, independent of the flash-mode difference. + +The corrected probe shares one rollover-safe 1,000 ms deadline across reset, +BUSY waits, and standby/version retries. It services cooperative USB while +waiting. It retains identification-only operation, safe chip-select/reset +cleanup, explicit socket overrides, and the existing fallback rules. + +The native production-code fixture models both sockets and timer rollover. +Elapsed times below are simulated fixture clocks, not physical measurements: + +| Modeled condition | Previous probe | Corrected probe | +| --- | --- | --- | +| BUSY remains high for 200 ms after reset | Rejected at 111 ms, no version-register reads | Detected at 202 ms, 16 bytes read | +| First five standby commands ignored | Rejected at 44 ms, 48 bytes read | Detected at 67 ms, 96 bytes read | +| BUSY never clears | Rejected early | Rejected at exactly 1,000 ms, no register reads | +| Socket absent | Rejected after three attempts | Rejected at exactly 1,000 ms | +| 800 ms BUSY followed by persistent command refusal | Rejected early | Rejected at exactly 1,000 ms total | + +An immediately responsive chip still completes its probe in under 20 ms in +the fixture. Negative controls restoring the old short BUSY budget or the +three-attempt limit fail the delayed-startup regressions. These results prove +the software tolerance gap; they do not prove it caused the user's hardware +failure. No transmit or TCXO-setting override was introduced. + +## Saved FEM and socket settings + +G3 firmware actively drives its LNA and PA-level GPIOs. G2 firmware does not, +so a saved FEM setting can have a visible effect only after switching to G3 +firmware. Once the normal CLI has started, read these values before changing +settings: + +```text +get radio.slot +get radio.fem.rxgain +get radio.fem.txgain +``` + +If startup reports `Station G3 radio unavailable`, the normal CLI has not +started. Press Enter and use the limited USB recovery commands instead: + +```text +!g3 help +!g3 get radio.slot +``` + +That recovery console accepts only socket selection, help, and reboot; the +normal FEM getters are unavailable there. + +`radio.slot` reports the saved selector, active socket, and detected sockets. +An explicit socket override does not silently fall back to another socket. +`radio.fem.rxgain` controls the external LNA and is distinct from +`radio.rxgain`, which controls the SX1262's internal boosted gain. + +Preserve the saved values and jumper positions while diagnosing the device. +The manufacturer documents GPIO control as effective with the corresponding +LNA P/LNA S or PA PL1 jumper removed; PA PL2 selects the hardware power-level +pair. Changing gain or jumpers is an operator choice, not an automatic part of +this compatibility fix. + +## Git history and attribution + +| Change | Introducing commit and author | +| --- | --- | +| Initial G2 support and board manifest | [acac20dc89](https://github.com/meshcore-dev/MeshCore/commit/acac20dc89b6bc183125bcb9341eb33c56e1ece8), Rastislav Vysoky, 2025-03-07 | +| G2 radio pin flags moved into the variant recipe | [7cb2e0863a](https://github.com/meshcore-dev/MeshCore/commit/7cb2e0863a8171a300c062547d95697e1c69f009), recrof, 2025-10-12 | +| Original G3 slot-1 map, PA9 and LNA10 | [75fb07fc2c](https://github.com/meshcore-dev/MeshCore/commit/75fb07fc2c9d87238c1a2c88142d7e1e64415979), Chris / Cisien, 2026-05-09; merged in [upstream PR #2515](https://github.com/meshcore-dev/MeshCore/pull/2515) | +| G2 DIO recipe override | [1a43123d2c](https://github.com/mikecarper/MeshCore/commit/1a43123d2c5ca409e7f310d74e4495ea1c7f6d52), mikecarper, 2026-08-24 | +| G3 second-slot recipes and GPIO map | [d8d72b7833](https://github.com/mikecarper/MeshCore/commit/d8d72b783346b2d775a87dbda87588e48eacc51a), Adam Gessaman / agessaman, 2026-09-05 | +| Runtime socket discovery and shared pin binding | [f765604cb8](https://github.com/mikecarper/MeshCore/commit/f765604cb83eb2556228efd5a09e829186aee093), mikecarper, 2026-10-15 | + +`git blame` and the introducing diffs were checked, rather than attributing +copied pin values to the latest refactor. Original G3 support is present in +upstream `dev`; the second-slot recipes and +runtime discovery were added in this fork. The original G3 pull request reports +hardware testing by its author. That historical report does not qualify the +current firmware or the user's device. + +## Validation boundary + +The host recipe regression resolves every tracked G3 environment, checks that +ordinary roles cannot override DIO, and verifies that `qio_opi` PSRAM remains +unchanged. It also executes the existing Full policy for canonical Repeater, +legacy `_r2` Repeater, and Full Companion configurations. A deliberate nested +QIO override must fail that check. + +| Check | Status | +| --- | --- | +| G3 recipe suite, including all 16 current role environments and the deliberate QIO override | PASS, 10 test methods | +| Existing ESP32 Full partition policy suite | PASS, 11 test methods | +| Native socket-detection suite, including 144 selection cases and 20 delayed/deadline cases | PASS, 6 test methods, ASan/UBSan | +| Combined socket detection/settings, boot recovery, radio pins, KISS slot and GPIO-hold suites | PASS, 36 test methods, including the 6 detection methods above | +| Ordinary Repeater, legacy `_r2` Repeater and KISS firmware builds | PASS; all bootloader and application image headers verified as DIO | +| Canonical Full Companion package | PASS; 24 linked capability checks, internal RAM and application-slot checks passed; bootloader/application and merged-image headers verified as DIO | +| Physical G3 boot and RF receive/transmit | Not performed for this change | + +No successful physical G3 boot, RF receive/transmit trial, or reproduction of +the user's original problem is claimed by these host checks. diff --git a/test/test_station_g3_sensor_recipes.py b/test/test_station_g3_sensor_recipes.py index e7680c4d1..32ac94b69 100644 --- a/test/test_station_g3_sensor_recipes.py +++ b/test/test_station_g3_sensor_recipes.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Host contracts for the Station G3 Sensor role on each RF daughterboard slot. +"""Host contracts for Station G3 boot recipes and each Sensor RF slot. Resolve tracked source recipes without starting PlatformIO, building firmware, flashing hardware, or changing a release catalog. These fixtures check routing @@ -15,6 +15,7 @@ import tempfile import unittest from test_upstream_observer_recipes import Recipes +from test_esp32_full_partition import apply_policy ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "scripts")) @@ -63,6 +64,66 @@ class StationG3SensorRecipesTests(unittest.TestCase): def setUpClass(cls): cls.recipes = Recipes() + def assert_dio_boot_recipes(self, recipes): + board = json.loads((ROOT / "boards/station-g3-esp32.json").read_text()) + self.assertEqual(board["build"]["flash_mode"], "dio") + self.assertEqual(board["build"]["arduino"]["memory_type"], "qio_opi") + targets = [section for section in recipes.config.sections() + if section.startswith("env:Station_G3_ESP32_")] + self.assertGreaterEqual(len(targets), 16) + for slot in SLOTS: + for role in ("repeater", "room_server", "sensor", + "repeater_observer_mqtt", "room_server_observer_mqtt"): + self.assertIn("env:" + slot + "_" + role, targets) + for role in ("companion_radio_usb", "companion_radio_ble", + "companion_radio_wifi", "companion_radio_full", "kiss_modem"): + self.assertIn("env:Station_G3_ESP32_" + role, targets) + for section in targets: + with self.subTest(target=section): + self.assertEqual(recipes.value(section, "board"), "station-g3-esp32") + flash_mode = recipes.value(section, "board_build.flash_mode") + self.assertEqual(flash_mode or board["build"]["flash_mode"], "dio") + memory_type = recipes.value(section, "board_build.arduino.memory_type") + self.assertEqual(memory_type or board["build"]["arduino"]["memory_type"], "qio_opi") + psram_type = recipes.value(section, "board_build.psram_type") + self.assertIn(psram_type, ("", "opi")) + + for target in ("Station_G3_ESP32_repeater", "Station_G3_ESP32_r2_repeater", + "Station_G3_ESP32_companion_radio_full"): + section = "env:" + target + values = { + "build.mcu": board["build"]["mcu"], + "build.flash_mode": recipes.value(section, "board_build.flash_mode") + or board["build"]["flash_mode"], + "build.arduino.memory_type": board["build"]["arduino"]["memory_type"], + "build.partitions": recipes.value(section, "board_build.partitions"), + "upload.flash_size": board["upload"]["flash_size"], + } + with self.subTest(full_target=target): + # build.sh injects this existing policy for Full promotion; + # ordinary tracked recipes do not list it in extra_scripts. + full, _ = apply_policy(values, companion="companion_radio" in target) + self.assertEqual(full.values["build.flash_mode"], "dio") + self.assertEqual(full.values["build.arduino.memory_type"], "qio_opi") + + def test_all_g3_roles_inherit_dio_without_changing_psram_or_full_policy(self): + self.assert_dio_boot_recipes(self.recipes) + + def test_nested_role_override_cannot_silently_restore_qio(self): + recipes = Recipes() + recipes.config.set("env:Station_G3_ESP32_r2_sensor", "board_build.flash_mode", "qio") + # Exercise resolved inheritance, not a textual manifest-only assertion. + # subTest records failures instead of raising, so collect this control + # with an independent TestResult whose failure must be observable. + control = StationG3SensorRecipesTests( + "test_all_g3_roles_inherit_dio_without_changing_psram_or_full_policy") + control.recipes = recipes + result = unittest.TestResult() + control.run(result) + self.assertEqual(len(result.failures), 1) + self.assertFalse(result.errors) + self.assertIn("'qio' != 'dio'", result.failures[0][1]) + def test_each_sensor_inherits_its_own_slot_and_only_one_pin_set(self): text = (ROOT / "variants/station_g3_esp32/platformio.ini").read_text() for slot, pins in SLOTS.items(): diff --git a/test/test_station_g3_slot_detection.py b/test/test_station_g3_slot_detection.py index 6cc7eb73a..4a2dbabaf 100644 --- a/test/test_station_g3_slot_detection.py +++ b/test/test_station_g3_slot_detection.py @@ -13,7 +13,9 @@ VARIANT = ROOT / "variants/station_g3_esp32" def harness(mutation=None): source = (VARIANT / "target.cpp").read_text() - methods = "\n".join(body(source, signature) for signature in ( + start = source.index("constexpr uint32_t kProbeTimeoutMs =") + methods = source[start:source.index(";", start) + 1] + "\n" + methods += "\n".join(body(source, signature) for signature in ( "bool waitForProbeReady(", "bool probeRadioSlot(", "bool initializeSlot(", "bool radio_init()")) if mutation == "accept-empty": @@ -24,6 +26,10 @@ def harness(mutation=None): guard = "delayMicroseconds(1);" assert guard in methods, "production post-command BUSY guard missing" methods = methods.replace(guard, "", 1) + if mutation == "old-busy-timeout": + methods = methods.replace("kProbeTimeoutMs = 1000", "kProbeTimeoutMs = 100") + if mutation == "old-attempt-limit": + methods = methods.replace("while (!found)", "for (unsigned attempt=0; attempt<3 && !found; ++attempt)", 1) return r''' #include #include @@ -55,6 +61,8 @@ static unsigned now_ms=0,present=0,working=0,stuck=0,reads=0,recoveries=0,rx=0,r static uint64_t now_us=0,busy_asserts_at[3]={},busy_clears_at[3]={}; static bool busy_scheduled[3]={}; static unsigned settle_guards=0,busy_high_samples=0; +static uint64_t reset_released_at[3]={},startup_busy_us=0; +static unsigned standby_count[3]={},refused_standby_count=0,usb_services=0; static bool returned=false; static int levels[64]; static std::vector initializations; @@ -64,13 +72,19 @@ void delayMicroseconds(unsigned us) { assert(us==1);++settle_guards;now_us+=us;n void pinMode(int,int) {} void digitalWrite(int pin,int level) { assert(pin>=0 && pin<64);levels[pin]=level; - for(unsigned slot=1;slot<=2;++slot) - if(pin==station_g3::pinsForSlot(slot).reset && level==LOW) busy_scheduled[slot]=false; + for(unsigned slot=1;slot<=2;++slot) { + if(pin!=station_g3::pinsForSlot(slot).reset) continue; + if(level==LOW) { busy_scheduled[slot]=false;standby_count[slot]=0; } + else reset_released_at[slot]=now_us; + } } int digitalRead(int pin) { for(unsigned slot=1;slot<=2;++slot) { if(pin!=station_g3::pinsForSlot(slot).busy) continue; if(stuck&(1<<(slot-1))) return HIGH; + // Independent reset/command-readiness model. Some supported modules are + // busy longer at startup, or ignore several initial standby commands. + if((present&(1<<(slot-1))) && now_us=busy_asserts_at[slot] && now_us=4) { ++reads; if(!(present&(1<<(slot-1)))) return 0xff; + if(standby_count[slot]<=refused_standby_count) return 0xff; return offset-4<6?"SX1261"[offset-4]:0; } return 0x22; @@ -121,6 +136,7 @@ struct SPI { assert(transaction && levels[station_g3::pinsForSlot(slot).nss]==HIGH); assert(index==(command==0x80?2:20));transaction=false; if(command==0x80 && (present&(1<<(slot-1)))) { + ++standby_count[slot]; busy_scheduled[slot]=true; busy_asserts_at[slot]=now_us+1; busy_clears_at[slot]=now_us+51; @@ -148,7 +164,7 @@ namespace station_g3 { void rememberActiveSlot(uint8_t slot) { remembered=slot;slotState().last=slot; } void serviceBootRecovery(StationG3Board&,bool first) { assert(first);++recoveries;throw Recovery(); } } -namespace mesh { void serviceUsbLoggingPort() {} void serviceUsbTerminalPort() {} } +namespace mesh { void serviceUsbLoggingPort() {++usb_services;} void serviceUsbTerminalPort() {++usb_services;} } @METHODS@ void reset(unsigned mask,unsigned successful,unsigned mode,unsigned last,unsigned busy=0) { station_g3::slotState()=station_g3::SlotState(); @@ -157,8 +173,10 @@ void reset(unsigned mask,unsigned successful,unsigned mode,unsigned last,unsigne spi.end();initializations.clear(); now_ms=reads=recoveries=rx=remembered=0;present=mask;working=successful;stuck=busy; now_us=settle_guards=busy_high_samples=0; + startup_busy_us=refused_standby_count=usb_services=0; for(unsigned slot=1;slot<=2;++slot) { busy_scheduled[slot]=false;busy_asserts_at[slot]=busy_clears_at[slot]=0; + reset_released_at[slot]=standby_count[slot]=0; } for(int& value:levels) value=HIGH; returned=false; @@ -185,16 +203,47 @@ int main() { assert(reads==oldreads && initializations.back()==expected); assert(levels[station_g3::pinsForSlot(expected==1?2:1).reset]==LOW); } - assert(now_ms<500); + assert(now_ms<=2000); // One shared one-second probe budget per socket. } reset(3,3,0,0,1);assert(radio_init()); assert(station_g3::slotState().detected==2 && station_g3::slotState().active==2); - assert(now_ms<500); + assert(now_ms<=2000); // A present device exercises both the 1-us post-NSS guard and the ensuing // real BUSY wait. Neither a no-op delay shim nor an early LOW is enough. reset(1,1,0,0);assert(probeRadioSlot(1)); assert(settle_guards==1 && busy_high_samples==1 && reads==16 && now_ms>=12); assert(!spi.transaction && levels[station_g3::pinsForSlot(1).reset]==LOW); + assert(now_ms<20); // No new delay for a fast, immediately responsive chip. + + for(unsigned slot=1;slot<=2;++slot) for(bool wrap:{false,true}) { + for(unsigned scenario=0;scenario<5;++scenario) { + const unsigned bit=1U<<(slot-1); + reset(scenario==3?0:bit,bit,0,0,scenario==2?bit:0); + if(wrap) { now_us=uint64_t(UINT32_MAX-100)*1000;now_ms=now_us/1000; } + if(scenario==0) startup_busy_us=200000; + if(scenario==1) refused_standby_count=5; + // Mixed startup BUSY and command refusal must not each get a fresh + // one-second budget: this chip stays unresponsive after BUSY clears. + if(scenario==4) { startup_busy_us=800000;refused_standby_count=1000; } + const uint32_t started=millis(); + const bool found=probeRadioSlot(slot); + const uint32_t elapsed=uint32_t(millis()-started); + if(scenario==0) { + assert(found && "200 ms startup BUSY must be tolerated"); + assert(elapsed>=200 && elapsed<220 && usb_services>0); + } else if(scenario==1) { + assert(found && "five ignored startup commands must be retried"); + assert(reads==96 && elapsed>=50 && elapsed<100 && usb_services>0); + } else { + assert(!found && elapsed==1000 && usb_services>0); + if(scenario==2) assert(reads==0); // Stuck BUSY never permits SPI reads. + } + assert(!spi.transaction && spi.slot==0); + assert(levels[station_g3::pinsForSlot(slot).reset]==LOW); + assert(levels[11]==HIGH && levels[43]==HIGH); + assert(initializations.empty() && rx==0 && remembered==0); + } + } // Corrupt policy cannot shift outside its mask or silently become auto. station_g3::slotState().bootMode=255; assert(station_g3::slotState().choose(3)==0); @@ -219,6 +268,10 @@ class StationG3DetectionTests(unittest.TestCase): self.assertNotEqual(run.returncode,0,"negative control unexpectedly passed") if mutation=="no-busy-settle": self.assertIn("ReadRegister before delayed BUSY completed",run.stderr) + elif mutation=="old-busy-timeout": + self.assertIn("200 ms startup BUSY must be tolerated",run.stderr) + elif mutation=="old-attempt-limit": + self.assertIn("five ignored startup commands must be retried",run.stderr) else:self.assertEqual(run.returncode,0,run.stdout+run.stderr) def test_real_probe_selection_matrix_recovery_and_busy_deadlines(self): @@ -233,5 +286,11 @@ class StationG3DetectionTests(unittest.TestCase): def test_delayed_busy_requires_post_command_guard_negative_control(self): self.run_case("no-busy-settle") + def test_old_short_busy_budget_rejects_supported_delayed_startup(self): + self.run_case("old-busy-timeout") + + def test_old_three_attempt_limit_rejects_initial_command_refusals(self): + self.run_case("old-attempt-limit") + if __name__=="__main__":unittest.main() diff --git a/variants/station_g3_esp32/platformio.ini b/variants/station_g3_esp32/platformio.ini index 750d3765d..518c2faa1 100644 --- a/variants/station_g3_esp32/platformio.ini +++ b/variants/station_g3_esp32/platformio.ini @@ -8,6 +8,8 @@ [Station_G3_ESP32_common] extends = esp32_base board = station-g3-esp32 +; The board manifest uses DIO for the known-working G2/ESP32-S3 Full startup +; policy, including ordinary and legacy _r2 roles. qio_opi PSRAM is unchanged. board_build.partitions = default_16MB.csv build_flags = ${esp32_base.build_flags} diff --git a/variants/station_g3_esp32/target.cpp b/variants/station_g3_esp32/target.cpp index 58b31f537..a971cff57 100644 --- a/variants/station_g3_esp32/target.cpp +++ b/variants/station_g3_esp32/target.cpp @@ -49,19 +49,25 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); #endif namespace { -bool waitForProbeReady(const station_g3::RadioPins& pins) { - const uint32_t started = millis(); +constexpr uint32_t kProbeTimeoutMs = 1000; + +bool waitForProbeReady(const station_g3::RadioPins& pins, uint32_t started) { while (digitalRead(pins.busy)) { - if (static_cast(millis() - started) >= 100) return false; + if (static_cast(millis() - started) >= kProbeTimeoutMs) return false; +#if MESH_USB_CONSOLE_COOPERATIVE + mesh::serviceUsbLoggingPort(); + mesh::serviceUsbTerminalPort(); +#endif delay(1); } - return true; + return static_cast(millis() - started) < kProbeTimeoutMs; } // Identification only: no SetTx, CAD, packet send, or saved profile changes. // Use a small bounded transaction rather than RadioLib's 10 reset attempts // (each may wait a second) on every empty socket. bool probeRadioSlot(uint8_t slot) { + const uint32_t started = millis(); const auto& pins = station_g3::pinsForSlot(slot); spi.end(); station_g3::slotState().candidate = slot; @@ -79,8 +85,11 @@ bool probeRadioSlot(uint8_t slot) { delay(10); spi.begin(pins.sck, pins.miso, pins.mosi); bool found = false; - for (unsigned attempt = 0; attempt < 3 && !found; ++attempt) { - if (!waitForProbeReady(pins)) break; + // Match RadioLib's reset/RC-standby tolerance: some radios ignore their + // first commands after reset. Share one budget across BUSY and all retries, + // so an empty socket remains bounded without resetting a slow chip again. + while (!found) { + if (!waitForProbeReady(pins, started)) break; spi.beginTransaction(SPISettings(2000000, MSBFIRST, SPI_MODE0)); digitalWrite(pins.nss, LOW); spi.transfer(RADIOLIB_SX126X_CMD_SET_STANDBY); @@ -89,7 +98,7 @@ bool probeRadioSlot(uint8_t slot) { spi.endTransaction(); // SX126x BUSY can assert just after NSS rises (RadioLib uses the same guard). delayMicroseconds(1); - if (!waitForProbeReady(pins)) break; + if (!waitForProbeReady(pins, started)) break; char version[16] = {}; spi.beginTransaction(SPISettings(2000000, MSBFIRST, SPI_MODE0)); digitalWrite(pins.nss, LOW); @@ -102,7 +111,16 @@ bool probeRadioSlot(uint8_t slot) { spi.endTransaction(); // SX1262 intentionally uses RadioLib's SX1261 silicon version signature. found = memcmp(version, RADIOLIB_SX1262_CHIP_TYPE, 6) == 0; - if (!found) delay(10); + if (!found) { +#if MESH_USB_CONSOLE_COOPERATIVE + mesh::serviceUsbLoggingPort(); + mesh::serviceUsbTerminalPort(); +#endif + const uint32_t elapsed = static_cast(millis() - started); + if (elapsed >= kProbeTimeoutMs) break; + const uint32_t remaining = kProbeTimeoutMs - elapsed; + delay(remaining < 10 ? remaining : 10); + } } // Leave the unselected socket in reset, not receiving or driving an IRQ. // The chosen socket's std_init will perform its own verified reset.