Qualify Arduino 3.3.12 compatibility hooks

This commit is contained in:
mikecarper committed 2026-11-01 19:58:47 -08:00
1 parent f121704a5a
commit f056ea688d
9 files changed
+81 -44

No files matched your search

+15 -4
View File
@@ -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()
+4
View File
@@ -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'),
}
+2
View File
@@ -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'
+1 -1
View File
@@ -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):
+42 -32
View File
@@ -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__":
+7 -3
View File
@@ -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)
+2 -1
View File
@@ -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")
+2 -2
View File
@@ -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)
+6 -1
View File
@@ -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):