From f056ea688dbc669df48a284896e3597a6facff81 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Sun, 1 Nov 2026 19:58:47 -0800 Subject: [PATCH] Qualify Arduino 3.3.12 compatibility hooks --- scripts/esp32_ble_lifecycle_fix.py | 19 +++++-- scripts/esp32_file_buffer_fix.py | 4 ++ scripts/esp32_usb_session_fix.py | 2 + scripts/esp32_wifi_scan_fix.py | 2 +- test/test_esp32_ble_lifecycle_fix.py | 74 ++++++++++++++----------- test/test_esp32_file_buffer_fix.py | 10 +++- test/test_esp32_usb_session_fix.py | 3 +- test/test_esp32_wifi_scan_fix.py | 4 +- test/test_hwcdc_3311_startup_session.py | 7 ++- 9 files changed, 81 insertions(+), 44 deletions(-) diff --git a/scripts/esp32_ble_lifecycle_fix.py b/scripts/esp32_ble_lifecycle_fix.py index 85337ac10..678abcde8 100644 --- a/scripts/esp32_ble_lifecycle_fix.py +++ b/scripts/esp32_ble_lifecycle_fix.py @@ -1,4 +1,4 @@ -"""Restore pinned Arduino 3.3.11 BLE service ownership for reversible shutdown. +"""Restore pinned Arduino 3.3.11/3.3.12 BLE ownership for reversible shutdown. BLEDevice owns the new wrapper's server, but its implicit destructor leaves raw service pointers behind. BLEService's destructor is private; delete it @@ -11,7 +11,6 @@ import re FRAMEWORKS_UNCHANGED = {(2, 0, 17), (3, 1, 3)} -FRAMEWORK_PATCHED = (3, 3, 11) PINNED_SHA256 = "ec3735051f3263fc0101986fe33602a2593c294c6ccbd9f7b1b416c15529c3c9" PATCHED_SHA256 = "bf8fecfd6367eb0a2c23dd610336fefa17687d1f7bbc38e9d8f8642835763e87" SOURCE_DIRECTORY = ("libraries", "BLE", "src") @@ -43,16 +42,28 @@ SOURCE_PINS = { "2b71699e1962cdc37a54ace429ebdd408b0e40b379d621d427add5bb5a93b252", "8c4d8917ffb768a9eab468b1be06be209b4b8fc065b125c821682b6579a30267", SERVICE_ORIGINAL, SERVICE_REPAIRED), } +FRAMEWORK_PINS = { + (3, 3, 11): SOURCE_PINS, + (3, 3, 12): { + # 3.3.12 adds pairing-result logs; retain them while repairing only the + # unchanged server/service ownership and array destruction below. + "BLEDevice.cpp": ( + "212c62b3575f4ca8aff3be57a066291e1b1522f0d609b9e727bb547ab600371d", + "8790bb0f75b177a3c47bdfb03e436a705f7e1f0b21868039244242cc1f3d08fc", + ORIGINAL, REPAIRED), + "BLEService.cpp": SOURCE_PINS["BLEService.cpp"], + }, +} def patched_ble_source(source, framework_version, name="BLEDevice.cpp"): if framework_version in FRAMEWORKS_UNCHANGED: return source - if framework_version != FRAMEWORK_PATCHED: + if framework_version not in FRAMEWORK_PINS: raise RuntimeError("ESP32 BLE lifecycle fix: unreviewed framework version") source = source.replace("\r\n", "\n") try: - original_digest, patched_digest, original, repaired = SOURCE_PINS[name] + original_digest, patched_digest, original, repaired = FRAMEWORK_PINS[framework_version][name] except KeyError as error: raise RuntimeError("ESP32 BLE lifecycle fix: unreviewed source") from error digest = hashlib.sha256(source.encode("utf-8")).hexdigest() diff --git a/scripts/esp32_file_buffer_fix.py b/scripts/esp32_file_buffer_fix.py index 08ab8aeba..1236a517c 100644 --- a/scripts/esp32_file_buffer_fix.py +++ b/scripts/esp32_file_buffer_fix.py @@ -68,6 +68,10 @@ FRAMEWORK_PINS = { (3, 3, 11): ( '3d345bcb3b764f08c6e923dbacc96f27da10edb12beb774de439228057d14a54', '0cfa80cc29a38e854fa17401605dbe9adbd6157195ec5444f0a90d68c344cd0c'), + # The FS source and compiler/newlib ABI are unchanged in 3.3.12. + (3, 3, 12): ( + '3d345bcb3b764f08c6e923dbacc96f27da10edb12beb774de439228057d14a54', + '0cfa80cc29a38e854fa17401605dbe9adbd6157195ec5444f0a90d68c344cd0c'), } diff --git a/scripts/esp32_usb_session_fix.py b/scripts/esp32_usb_session_fix.py index c30a4326a..8a2e6b1b3 100644 --- a/scripts/esp32_usb_session_fix.py +++ b/scripts/esp32_usb_session_fix.py @@ -368,6 +368,8 @@ PATCHED_HWCDC_3311_SHA256 = "7b693bf5d72df97336360969ff429c67f3783d472123aeb79b3 HWCDC_STARTUP_PINS = { (3, 1, 3): (PINNED_HWCDC_313_SHA256, PATCHED_HWCDC_313_SHA256), (3, 3, 11): (PINNED_HWCDC_3311_SHA256, PATCHED_HWCDC_3311_SHA256), + # The 3.3.12 HWCDC driver is byte-identical to the reviewed 3.3.11 driver. + (3, 3, 12): (PINNED_HWCDC_3311_SHA256, PATCHED_HWCDC_3311_SHA256), } HWCDC_STARTUP_INCLUDE = '#include "hal/usb_serial_jtag_ll.h"\n' HWCDC_STARTUP_HOOK = 'extern "C" bool meshEsp32HwcdcShouldReportBusReset() __attribute__((weak));\n' diff --git a/scripts/esp32_wifi_scan_fix.py b/scripts/esp32_wifi_scan_fix.py index b740ba3cf..843a3c7ea 100644 --- a/scripts/esp32_wifi_scan_fix.py +++ b/scripts/esp32_wifi_scan_fix.py @@ -18,7 +18,7 @@ ZEROED_DECLARATION = "wifi_scan_config_t config = {};" PATCHED_VERSION = (2, 0, 17) # These existing recipes keep their own framework sources. This narrowly # qualified Arduino 2 fix does not claim to qualify Arduino 3 scanning. -KNOWN_UNCHANGED_VERSIONS = {(3, 1, 3), (3, 3, 11)} +KNOWN_UNCHANGED_VERSIONS = {(3, 1, 3), (3, 3, 11), (3, 3, 12)} def patched_scan_source(source, framework_version=PATCHED_VERSION): diff --git a/test/test_esp32_ble_lifecycle_fix.py b/test/test_esp32_ble_lifecycle_fix.py index a7e9f8577..cf838c9b8 100644 --- a/test/test_esp32_ble_lifecycle_fix.py +++ b/test/test_esp32_ble_lifecycle_fix.py @@ -145,24 +145,25 @@ class BleLifecycleFixTests(unittest.TestCase): self.assertIn("alloc-dealloc-mismatch", run.stderr) def test_hash_guards_idempotence_and_old_sdk_unchanged(self): - for name, (_, _, original, repaired) in FIX.SOURCE_PINS.items(): - with self.subTest(name=name): - source = "// reviewed source fixture\n" + original - expected = source.replace(original, repaired) - pins = {name: (hashlib.sha256(source.encode()).hexdigest(), - hashlib.sha256(expected.encode()).hexdigest(), original, repaired)} - with patch.object(FIX, "SOURCE_PINS", pins): - self.assertEqual(FIX.patched_ble_source(source, (3, 3, 11), name), expected) - self.assertEqual(FIX.patched_ble_source(expected, (3, 3, 11), name), expected) - self.assertEqual(FIX.patched_ble_source(source.replace("\n", "\r\n"), (3, 3, 11), name), expected) - for version in ((2, 0, 17), (3, 1, 3)): - self.assertEqual(FIX.patched_ble_source(source, version, name), source) - with self.assertRaisesRegex(RuntimeError, "changed pinned source"): - FIX.patched_ble_source(source + "// changed\n", (3, 3, 11), name) - with self.assertRaisesRegex(RuntimeError, "unreviewed framework"): - FIX.patched_ble_source(source, (3, 3, 12), name) - with self.assertRaisesRegex(RuntimeError, "unreviewed source"): - FIX.patched_ble_source(source, (3, 3, 11), "BLEOther.cpp") + for version, sources in FIX.FRAMEWORK_PINS.items(): + for name, (_, _, original, repaired) in sources.items(): + with self.subTest(version=version, name=name): + source = "// reviewed source fixture\n" + original + expected = source.replace(original, repaired) + pins = {name: (hashlib.sha256(source.encode()).hexdigest(), + hashlib.sha256(expected.encode()).hexdigest(), original, repaired)} + with patch.object(FIX, "FRAMEWORK_PINS", {version: pins}): + self.assertEqual(FIX.patched_ble_source(source, version, name), expected) + self.assertEqual(FIX.patched_ble_source(expected, version, name), expected) + self.assertEqual(FIX.patched_ble_source(source.replace("\n", "\r\n"), version, name), expected) + for old in ((2, 0, 17), (3, 1, 3)): + self.assertEqual(FIX.patched_ble_source(source, old, name), source) + with self.assertRaisesRegex(RuntimeError, "changed pinned source"): + FIX.patched_ble_source(source + "// changed\n", version, name) + with self.assertRaisesRegex(RuntimeError, "unreviewed framework"): + FIX.patched_ble_source(source, (3, 3, 13), name) + with self.assertRaisesRegex(RuntimeError, "unreviewed source"): + FIX.patched_ble_source(source, version, "BLEOther.cpp") def test_middleware_copies_only_pinned_sources_without_touching_shared_sdk(self): with tempfile.TemporaryDirectory(prefix="ble-sdk-middleware-") as directory: @@ -179,7 +180,7 @@ class BleLifecycleFixTests(unittest.TestCase): pins = {"BLEDevice.cpp": (hashlib.sha256(original.encode()).hexdigest(), hashlib.sha256(expected.encode()).hexdigest(), FIX.ORIGINAL, FIX.REPAIRED)} node, env = FakeNode(source), FakeEnvironment(root / "build") - with patch.object(FIX, "SOURCE_PINS", pins): + with patch.object(FIX, "FRAMEWORK_PINS", {(3, 3, 11): pins, (3, 3, 12): pins}): destination = FIX.replace_ble_source(env, node) self.assertEqual(source.read_text(), original) self.assertEqual(destination.read_text(), expected) @@ -188,25 +189,34 @@ class BleLifecycleFixTests(unittest.TestCase): self.assertEqual(FIX.replace_ble_source(env, node), destination) self.assertEqual(destination.stat().st_mtime_ns, timestamp) version.write_text(version.read_text().replace("PATCH 11", "PATCH 12")) + self.assertEqual(FIX.replace_ble_source(env, node), destination) + version.write_text(version.read_text().replace("PATCH 12", "PATCH 13")) with self.assertRaisesRegex(RuntimeError, "unreviewed framework"): FIX.replace_ble_source(env, node) def test_installed_exact_sdk_pins_when_available(self): - packages = Path.home() / ".platformio/packages" - candidates = list(packages.glob("framework-arduinoespressif32*")) - sdk = None + core_dir = Path(os.environ.get("PLATFORMIO_CORE_DIR", str(Path.home() / ".platformio"))) + candidates = list((core_dir / "packages").glob("framework-arduinoespressif32*")) + if os.environ.get("MESH_ARDUINO_SDK_TEST_DIR"): + candidates.append(Path(os.environ["MESH_ARDUINO_SDK_TEST_DIR"])) + tested = 0 for candidate in candidates: source = candidate / "libraries/BLE/src/BLEDevice.cpp" - if source.exists() and FIX.framework_version(source) == (3, 3, 11): - sdk = source.parent - break - if sdk is None: - self.skipTest("Pinned Arduino3.3.11 SDK is not installed") - for name in FIX.SOURCE_PINS: - source = (sdk / name).read_text() - patched = FIX.patched_ble_source(source, (3, 3, 11), name) - self.assertNotEqual(source, patched) - self.assertEqual(FIX.patched_ble_source(patched, (3, 3, 11), name), patched) + if not source.exists(): + continue + version = FIX.framework_version(source) + if version not in FIX.FRAMEWORK_PINS: + continue + for name in FIX.FRAMEWORK_PINS[version]: + original = (source.parent / name).read_text(encoding="utf-8") + patched = FIX.patched_ble_source(original, version, name) + self.assertNotEqual(original, patched) + self.assertEqual(FIX.patched_ble_source(patched, version, name), patched) + old, new = FIX.FRAMEWORK_PINS[version][name][2:] + self.assertEqual(patched.replace(new, old, 1), original) + tested += 1 + if not tested: + self.skipTest("Pinned Arduino3.3.11/3.3.12 SDK is not installed") if __name__ == "__main__": diff --git a/test/test_esp32_file_buffer_fix.py b/test/test_esp32_file_buffer_fix.py index 43c2e8664..c923d6230 100644 --- a/test/test_esp32_file_buffer_fix.py +++ b/test/test_esp32_file_buffer_fix.py @@ -20,6 +20,8 @@ for version in ((3, 1, 3), (3, 3, 11)): fixture = FIXTURES / ".".join(map(str, version)) raw = json.loads((fixture / "vfs_api.json").read_text(encoding="ascii"))["source"] SDK_VARIANTS.append((version, raw, FIX.patched_vfs_source(raw, version), fixture)) +# The reviewed 3.3.12 FS source is byte-identical; share its source witness. +SDK_VARIANTS.append(((3, 3, 12), raw, FIX.patched_vfs_source(raw, (3, 3, 12)), fixture)) def method(source, signature): @@ -276,6 +278,8 @@ class FileBufferFixTest(unittest.TestCase): for version, raw, patched, fixture in SDK_VARIANTS: with self.subTest(version=version): original_header = (fixture / "esp_arduino_version.h").read_bytes() + if version == (3, 3, 12): + original_header = original_header.replace(b"VERSION_PATCH 11", b"VERSION_PATCH 12") source.write_text(raw, encoding="utf-8") header.write_bytes(original_header) self.assertEqual(FIX.require_pinned_framework(source), version) @@ -296,12 +300,12 @@ class FileBufferFixTest(unittest.TestCase): for changed in (raw + "\n", patched + "\n", raw.replace("return res == 0;", "return true;", 1)): with self.assertRaises(RuntimeError): FIX.patched_vfs_source(changed, version) - for other_version, _, _, _ in SDK_VARIANTS: - if other_version != version: + for other_version, other_raw, _, _ in SDK_VARIANTS: + if other_version != version and other_raw != raw: with self.assertRaises(RuntimeError): FIX.patched_vfs_source(raw, other_version) header.write_bytes(original_header + b"\n#define ESP_ARDUINO_VERSION_MAJOR 9\n") with self.assertRaises(RuntimeError): FIX.replace_vfs_source(env, node) - with self.assertRaises(RuntimeError): FIX.patched_vfs_source(RAW, (3, 3, 12)) + with self.assertRaises(RuntimeError): FIX.patched_vfs_source(RAW, (3, 3, 13)) def test_exact_fingerprints_shape_normalization_and_idempotence(self): self.assertEqual(hashlib.sha256((FIXTURES / "vfs_api.cpp").read_bytes()).hexdigest(), FIX.PINNED_VFS_SHA256) diff --git a/test/test_esp32_usb_session_fix.py b/test/test_esp32_usb_session_fix.py index 40d82f944..c5233f3c4 100644 --- a/test/test_esp32_usb_session_fix.py +++ b/test/test_esp32_usb_session_fix.py @@ -119,7 +119,8 @@ class Esp32UsbSessionFixTests(unittest.TestCase): cores = {} for name, version in (("framework-arduinoespressif32", (3, 1, 3)), ("framework-arduinoespressif32@src-old", (2, 0, 17)), - ("framework-arduinoespressif32@src-new", (3, 3, 11))): + ("framework-arduinoespressif32@src-new", (3, 3, 11)), + ("framework-arduinoespressif32@src-experiment", (3, 3, 12))): core = packages / name / "cores/esp32" core.mkdir(parents=True) (core / "USBCDC.cpp").write_text(CDC, encoding="ascii") diff --git a/test/test_esp32_wifi_scan_fix.py b/test/test_esp32_wifi_scan_fix.py index 2380f9ee6..a88d09f66 100644 --- a/test/test_esp32_wifi_scan_fix.py +++ b/test/test_esp32_wifi_scan_fix.py @@ -224,12 +224,12 @@ class WiFiScanFixTest(unittest.TestCase): def version_text(version): return "".join(f"#define ESP_ARDUINO_VERSION_{key} {value}\n" for key, value in zip(("MAJOR", "MINOR", "PATCH"), version)) - for version in ((3, 1, 3), (3, 3, 11)): + for version in ((3, 1, 3), (3, 3, 11), (3, 3, 12)): header.write_text(version_text(version)) self.assertEqual(FIX.require_known_framework(source), version) self.assertIs(FIX.replace_scan_source(env, node), node) self.assertFalse(env.build.exists()) - for changed in ("", version_text((2, 0, 18)), version_text((3, 3, 12)), + for changed in ("", version_text((2, 0, 18)), version_text((3, 3, 13)), version_text((2, 0, 17)) + "#define ESP_ARDUINO_VERSION_MAJOR 2\n"): header.write_text(changed) with self.assertRaises(RuntimeError): FIX.replace_scan_source(env, node) diff --git a/test/test_hwcdc_3311_startup_session.py b/test/test_hwcdc_3311_startup_session.py index fb0b709cf..420748e17 100644 --- a/test/test_hwcdc_3311_startup_session.py +++ b/test/test_hwcdc_3311_startup_session.py @@ -167,10 +167,15 @@ class Hwcdc3311StartupSessionTests(startup.HwcdcStartupSessionTests): encoding="ascii") with self.assertRaises(RuntimeError): FIX.replace_hwcdc_source(env, node) - # An unreviewed framework version keeps its existing driver. + # 3.3.12 retains this exact driver and uses the same capture hook. version.write_text("#define ESP_ARDUINO_VERSION_MAJOR 3\n" "#define ESP_ARDUINO_VERSION_MINOR 3\n" "#define ESP_ARDUINO_VERSION_PATCH 12\n", encoding="ascii") + self.assertEqual(Path(FIX.replace_hwcdc_source(env, node)).read_text(), self.patched) + # An unreviewed framework version keeps its existing driver. + version.write_text("#define ESP_ARDUINO_VERSION_MAJOR 3\n" + "#define ESP_ARDUINO_VERSION_MINOR 3\n" + "#define ESP_ARDUINO_VERSION_PATCH 13\n", encoding="ascii") self.assertIs(FIX.replace_hwcdc_source(env, node), node) def test_shared_predicate_is_exported_without_the_217_tx_backport(self):