Restore G3 startup tolerance and G2-compatible flash mode
This commit is contained in:
1 parent
5d741318f5
commit
015eff4c22
7 files changed
+334
-16
No files matched your search
@@ -14,7 +14,7 @@
|
||||
],
|
||||
"f_cpu": "240000000L",
|
||||
"f_flash": "80000000L",
|
||||
"flash_mode": "qio",
|
||||
"flash_mode": "dio",
|
||||
"hwids": [["0x303A", "0x1001"]],
|
||||
"mcu": "esp32s3",
|
||||
"variant": "esp32s3"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
<!-- meshcore-hosted-doc-link:start -->
|
||||
<p class="meshcore-hosted-doc-link"><a href="https://mikecarper.github.io/MeshCore/test-results/station_g3_g2_compatibility/">View this page on MeshCore Docs</a>.</p>
|
||||
<!-- meshcore-hosted-doc-link:end -->
|
||||
|
||||
# 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.
|
||||
@@ -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():
|
||||
|
||||
@@ -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 <cassert>
|
||||
#include <cstdio>
|
||||
@@ -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<unsigned> 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<reset_released_at[slot]+startup_busy_us) return HIGH;
|
||||
// Immediately after NSS rises BUSY still reads LOW. It asserts only
|
||||
// after the guard interval, then remains HIGH during command handling.
|
||||
if(busy_scheduled[slot] && now_us>=busy_asserts_at[slot] && now_us<busy_clears_at[slot]) {
|
||||
@@ -113,6 +127,7 @@ struct SPI {
|
||||
if(offset>=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()
|
||||
@@ -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}
|
||||
|
||||
@@ -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<uint32_t>(millis() - started) >= 100) return false;
|
||||
if (static_cast<uint32_t>(millis() - started) >= kProbeTimeoutMs) return false;
|
||||
#if MESH_USB_CONSOLE_COOPERATIVE
|
||||
mesh::serviceUsbLoggingPort();
|
||||
mesh::serviceUsbTerminalPort();
|
||||
#endif
|
||||
delay(1);
|
||||
}
|
||||
return true;
|
||||
return static_cast<uint32_t>(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<uint32_t>(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.
|
||||
|
||||
Reference in new issue
Block a user