Cover WiFi scan fixes across every ESP32 recipe

This commit is contained in:
mikecarper committed 2026-10-06 09:58:40 -07:00
1 parent df5e591319
commit 7581fa3abd
8 files changed
+130 -11

No files matched your search

+5 -1
View File
@@ -274,7 +274,11 @@ jobs:
python3 -B test/test_esp32_boot_file_lifecycle.py -v
- name: Verify WiFi OTA startup and WebConfig handoff
run: python3 -B test/test_wifi_ota_start.py -v
run: |
python3 -B test/test_wifi_ota_start.py -v
python3 -B test/test_webconfig_ap_start.py -v
python3 -B test/test_esp32_wifi_scan_fix.py -v
python3 -B test/test_esp32_wifi_scan_recipes.py -v
- name: Verify browser OTA completion and immediate HTTP listener restart
run: python3 -B test/test_async_web_lifecycle.py -v
+18
View File
@@ -3983,6 +3983,21 @@ append_platformio_extra_script() {
export PLATFORMIO_EXTRA_SCRIPTS
}
ensure_esp32_wifi_scan_fix() {
local env_platform=$1
local pio_env_name=$2
local hook="pre:scripts/esp32_wifi_scan_fix.py"
local item
local -a hooks=()
[ "$env_platform" = "ESP32_PLATFORM" ] || return 0
pio_env_option_contains "$pio_env_name" extra_scripts "scripts/esp32_wifi_scan_fix.py" && return 0
read -r -a hooks <<< "${PLATFORMIO_EXTRA_SCRIPTS//$'\n'/ }"
for item in "${hooks[@]}"; do
[ "$item" = "$hook" ] && return 0
done
append_platformio_extra_script "$hook"
}
apply_lora_ota_flag_order_fix() {
local env_name=$1
local pio_env_name=$2
@@ -5108,6 +5123,9 @@ build_firmware_one_profile() {
unset MESHCORE_COMPANION_RADIO_FULL
fi
# Keep the reviewed scan SDK fix when a target overrides extra_scripts.
ensure_esp32_wifi_scan_fix "$env_platform" "$pio_env_name"
if [ "$env_platform" = "ESP32_PLATFORM" ] \
&& ! pio_env_option_contains "$pio_env_name" extra_scripts \
"scripts/check_esp32_dram.py"; then
@@ -1,4 +1,4 @@
# Heltec V4 scan-first AP validation — 2026-10-06
# Heltec V4 scan-first AP validation - 2026-10-06
The expanded-partition Full repeater is the recommended image for these V4s.
Minimal remains a compatibility profile for the smaller portable OTA slot;
@@ -48,7 +48,7 @@ ordinary standard profiles retain their existing implementations.
| Minimal app | PASS, 1,281,576 bytes; 29,144 bytes spare in the 1,310,720-byte portable slot; 6 capability markers and memory gate passed. |
| Minimal SHA-256 | `4f0d55aa3c0c85e76c50a2ba60738294515552dafa5e9496d6a33d73f1072e27` |
| Minimal running body hash | `060106D2CA3CA669` |
| Focused host suite | PASS, 65 Python tests, including four compiled AP fixture variants: Arduino 2/3 × open/password AP. |
| Focused host suite | PASS, 65 Python tests, including four compiled AP fixture variants: Arduino 2/3 x open/password AP. |
| Build profile tests | PASS, `test/test_build_profiles.sh`. |
New AP fixtures exercise stale independent SDK state despite successful facade
@@ -106,7 +106,7 @@ Private credentials, scanned network names, and saved secret values are excluded
## Remaining boundaries
- 11–14 mA current consumption is unmeasured. There is no current meter in this
- 11-14 mA current consumption is unmeasured. There is no current meter in this
setup; attached native USB and enabled USB logging inhibit light sleep.
- Requested ESP-NOW state was checked, but actual packet exchange needs a known
second peer.
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""Resolve all tracked ESP32 scan hooks and execute builder fallback without PIO."""
import json
from pathlib import Path
import subprocess
import unittest
from platformio.project.config import ProjectConfig
ROOT = Path(__file__).resolve().parents[1]
HOOK = "pre:scripts/esp32_wifi_scan_fix.py"
class Esp32WiFiScanRecipeTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
# Developer-local overlays must not alter the tracked regression matrix.
cls.config = ProjectConfig(str(ROOT / "platformio.ini"), parse_extra=False)
for path in sorted((ROOT / "variants").glob("*/platformio.ini")):
cls.config.read(str(path), parse_extra=False)
cls.config.read(str(ROOT / "tools/hil/profile_switch.ini"), parse_extra=False)
def test_every_resolved_esp32_recipe_has_exactly_one_direct_scan_hook(self):
tested = []
for section in self.config.sections():
if not section.startswith("env:"):
continue
platform = self.config.get(section, "platform", "").lower()
if "espressif32" not in platform and "pioarduino" not in platform:
continue
with self.subTest(target=section):
hooks = self.config.get(section, "extra_scripts", [])
self.assertEqual(hooks.count(HOOK), 1, hooks)
tested.append(section)
self.assertGreater(len(tested), 400)
for name in ("heltec_v4_partition_migrator", "xiao_s3_partition_legacy_seed",
"esp32_c3_4mb_partition_migrator", "profile_switch_heltec_v4"):
self.assertIn("env:" + name, tested)
def fallback(self, platform, configured, ambient="", disabled=False):
options = [["env:fixture", [["extra_scripts", configured]]]]
shell = r'''
source "$1/build_legacy.sh"
pio() { printf 'FORBIDDEN_PLATFORMIO\n' >&2; return 99; }
PIO_CONFIG_JSON=$2
PLATFORMIO_EXTRA_SCRIPTS=$3
if [ "$5" = 1 ]; then
append_platformio_extra_script() { :; }
fi
ensure_esp32_wifi_scan_fix "$4" fixture
ensure_esp32_wifi_scan_fix "$4" fixture
printf '%s' "$PLATFORMIO_EXTRA_SCRIPTS"
'''
result = subprocess.run(["bash", "-c", shell, "scan-hook", str(ROOT),
json.dumps(options), ambient, platform,
"1" if disabled else "0"], cwd=ROOT,
capture_output=True, text=True, timeout=15)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertNotIn("FORBIDDEN_PLATFORMIO", result.stdout + result.stderr)
return result.stdout
def test_actual_builder_fallback_preserves_overrides_and_is_idempotent(self):
ambient = "pre:scripts/fixture_private.py"
self.assertEqual(self.fallback("ESP32_PLATFORM", [], ambient), ambient + "\n" + HOOK)
self.assertEqual(self.fallback("ESP32_PLATFORM", [HOOK], ambient), ambient)
self.assertEqual(self.fallback("ESP32_PLATFORM", [], ambient + "\n" + HOOK), ambient + "\n" + HOOK)
self.assertEqual(self.fallback("NRF52_PLATFORM", [], ambient), ambient)
def test_disabled_fallback_cannot_satisfy_missing_hook(self):
self.assertNotIn(HOOK, self.fallback("ESP32_PLATFORM", [], disabled=True))
self.assertIn(HOOK, self.fallback("ESP32_PLATFORM", []))
def test_fallback_precedes_recipe_binding_and_suite_is_wired_in_ci(self):
source = (ROOT / "build_legacy.sh").read_text()
body = source[source.index("build_firmware_one_profile() {"):]
self.assertLess(body.index('ensure_esp32_wifi_scan_fix "$env_platform" "$pio_env_name"'),
body.index("BUILD_RECIPE_SHA256=$(compute_build_recipe_digest"))
self.assertIn(" python3 -B test/test_esp32_wifi_scan_recipes.py -v\n",
(ROOT / ".github/workflows/run-unit-tests.yml").read_text())
if __name__ == "__main__":
unittest.main()
+9 -3
View File
@@ -13,7 +13,9 @@ lib_deps =
Wire
https://github.com/jgromes/RadioLib.git#187ef24791c3d844939b2be13a68bd890bd04e4c
rweather/Crypto @ ^0.4.0
extra_scripts = post:scripts/check_firmware_ram.py
extra_scripts =
pre:scripts/esp32_wifi_scan_fix.py
post:scripts/check_firmware_ram.py
build_flags =
${arduino_base.build_flags}
-D ESP32_PLATFORM
@@ -48,7 +50,9 @@ board_upload.flash_size = 8MB
lib_deps =
${env:profile_switch_xiao.lib_deps}
lovyan03/LovyanGFX @ ^1.2.7
extra_scripts = post:scripts/check_firmware_ram.py
extra_scripts =
pre:scripts/esp32_wifi_scan_fix.py
post:scripts/check_firmware_ram.py
build_flags =
${arduino_base.build_flags}
-D ESP32_PLATFORM
@@ -84,7 +88,9 @@ board_upload.flash_size = 16MB
board_build.partitions = default_16MB.csv
upload_speed = 115200
lib_deps = ${env:profile_switch_xiao.lib_deps}
extra_scripts = post:scripts/check_firmware_ram.py
extra_scripts =
pre:scripts/esp32_wifi_scan_fix.py
post:scripts/check_firmware_ram.py
build_flags =
${arduino_base.build_flags}
-D ESP32_PLATFORM
@@ -9,7 +9,9 @@ board_build.partitions = default.csv
board_build.flash_mode = dio
board_upload.flash_size = 4MB
board_upload.maximum_size = 4194304
extra_scripts = post:scripts/check_firmware_ram.py
extra_scripts =
pre:scripts/esp32_wifi_scan_fix.py
post:scripts/check_firmware_ram.py
build_flags =
-D ESP32_PLATFORM
-D ARDUINO_USB_CDC_ON_BOOT=0
+3 -1
View File
@@ -897,7 +897,9 @@ board_build.flash_mode = dio
; repeater build restores its normal USB serial setting.
; This standalone image runs its own Wi-Fi OTA server. Reserve the generic
; ESP32 baseline plus its always-on Wi-Fi budget in the linked-image proof.
extra_scripts = post:scripts/check_firmware_ram.py
extra_scripts =
pre:scripts/esp32_wifi_scan_fix.py
post:scripts/check_firmware_ram.py
build_flags =
-D ESP32_PLATFORM
-D MESH_MIN_RUNTIME_HEAP=100352
+6 -2
View File
@@ -88,7 +88,9 @@ board_build.partitions = default.csv
board_build.flash_mode = dio
; This is a standalone ESP32 utility, but it still hosts a Wi-Fi OTA server.
; Reserve the generic ESP32 baseline plus its always-on Wi-Fi budget.
extra_scripts = post:scripts/check_firmware_ram.py
extra_scripts =
pre:scripts/esp32_wifi_scan_fix.py
post:scripts/check_firmware_ram.py
build_flags =
-D ESP32_PLATFORM
-D MESH_MIN_RUNTIME_HEAP=100352
@@ -118,7 +120,9 @@ framework = arduino
board_build.partitions = default.csv
board_build.flash_mode = dio
; The disposable seed also runs an always-on Wi-Fi OTA server.
extra_scripts = post:scripts/check_firmware_ram.py
extra_scripts =
pre:scripts/esp32_wifi_scan_fix.py
post:scripts/check_firmware_ram.py
build_flags =
-D ESP32_PLATFORM
-D MESH_MIN_RUNTIME_HEAP=100352