← Commit history
design/stm32-bringup-build.json+248−51
@@ -29,33 +29,38 @@   "sources": {     "firmware/fan_tach.c": "844f00c1f77e39529bd8f667d543ecee187ff4fe91108aedb7a2c36eb22846fa",     "firmware/fan_tach.h": "20fc7b0bbb1a040dfd0453e3af22cf62e7a564241c28cf9cd08dd9b6d87ddf80",+    "firmware/instruments.c": "8d14877861dc04ea88c21e2047df81dc6291e340783515c36bf417c32763b995",     "firmware/instruments.h": "47756a2358e94e143b01bc1f99e5987b9fd8a01d81d762b0b490e96cebeb3151",     "firmware/load_core.c": "85017ff0a3e37e7b5e41604989326fe623e7f73427ecd8e1a29c63a38d003638",     "firmware/load_core.h": "408263af21e6c15e82f4118343a2b7d0dd2db61808754c0b91021acd1146fadf",     "firmware/load_protocol.c": "63b4404a0e510eb2e472d8f7f93b1aa78155fd171312b941948ab85346fc2b61",     "firmware/load_protocol.h": "f64f6b689d57501031515e8ee9f8c8349a2cea63738e73db16f95e747caf91fb",-    "firmware/stm32/README.md": "3b899060dee0a9e74e0841d6c220dd68d3211479b16ccaf3a700e6668c6526b0",+    "firmware/measurement_cache.c": "5f2c1cee84c5118b7647e1b833924673b132c4d76a95b790671d2747209b9733",+    "firmware/measurement_cache.h": "5de6ed33032a25f3a1b80331461b6ad43aa26374d18c03d4e458f13036694262",+    "firmware/stm32/README.md": "be8404a69783f1f5b359851b987fab36f92edd4a51ce881e3696f94f1a7c5e23",     "firmware/stm32/clock.c": "f42f5c1ff229b065a048218ecc575624276aaa229e0bc340d80c80eec35416d2",     "firmware/stm32/dependencies.json": "7e8f3c17a59c856371a9f411dce6a9dadafbb8c8edd5ede5e4e5cf46b1567e92",     "firmware/stm32/fan_input.c": "d4ebe5572dbbcd1a3ea84c4e31442732a7237e94d554ec8acdc07142486e92f8",     "firmware/stm32/fan_input.h": "6dc99727c0e3aa5f711cdfd00b4c53d7bf123ad6698bbe842a64ec6b7d87e77b",+    "firmware/stm32/instrument_input.c": "26f7269d956387b9a44328802fccdd60b4d4fd18e5eeaaa8e0db2621e4a8a35a",+    "firmware/stm32/instrument_input.h": "e50485f7725a47ac807758901acd79dd41bff27331d54d10714a62225da4ea05",     "firmware/stm32/licenses/CMSIS_5-LICENSE.txt": "b40930bbcf80744c86c46a12bc9da056641d722716c378f5659b9e555ef833e1",     "firmware/stm32/licenses/cmsis_device_g0-LICENSE.txt": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4",     "firmware/stm32/licenses/stm32g0xx_hal_driver-LICENSE.txt": "3564c849dfd5e628732afb7933ccb1c983b53f7ac6bb237b6177691339082671",     "firmware/stm32/licenses/tinyusb-LICENSE.txt": "b01185698f530f4f61b28f42d5a4d6bbe7834bde6a63f00773235628a7bdce22",-    "firmware/stm32/main.c": "522b0960564c91dca05b876f0ab05aad4707ac57fb5190b623f7e8e92f3ef84d",+    "firmware/stm32/main.c": "73b6deeffceedc87efcc8787a2ed17069f76d434c48413d7f7ebd9a50e2f0783",     "firmware/stm32/stm32g0b1kc.ld": "1c938076e760300379115ee955589434dba7b84d1bb945dd41cf39abd342fd13",     "firmware/stm32/stm32g0xx_hal_conf.h": "2e2a3aed4bf01ce3f890df69061e740369ed8839bb53d5b1be745be4de185b6f",     "firmware/stm32/syscalls.c": "bbfcedecfe5f33380424ab90cdb89c842c6a5717e01298a29ec43aef24c1524d",     "firmware/stm32/tusb_config.h": "dfbb343cc609aa2693058b7d7e6e6c487524222cbe3eab5296568eca0b7d3970",     "firmware/stm32/usb_descriptors.c": "ed2666d7115f05a07088dd92ebf35a600aadd8d9878fae2eb7635201b8f0b2a9",-    "tools/build_stm32_bringup.py": "20f8d74fa7bfc8d3ea9a4ba5fce1b912b98a88389285111ff70d58fde4aa93f2"+    "tools/build_stm32_bringup.py": "2e7bb3c5482e0807ae138a6b70b0bdbce9c3b639cd68e7164b92c6af00f43fb4"   },   "artifacts": {-    "adom-load-bringup.hex": "41ddfee1467efabf8a871ed952a7b7418ded0120a28f9d31ef6c5fe1a46e50a6",-    "adom-load-bringup.bin": "839b1ec5e860f0b56dbba6362918d45148132c44dd86f37a9dcc4732fda42c97",-    "adom-load-bringup.map": "d74c18353387c363be04b9173ab34906ed8ffee994d1024855b2a2da0f2ec8fc",-    "adom-load-bringup.elf": "a83740bc95215f972e2035e75276d59bc9cef8026a33055e2e5ef7dbea509d5e"+    "adom-load-bringup.hex": "4b0aa51d83e690e2e06ce89593660ad0017a78f0b2c70c8979870ed0387517d0",+    "adom-load-bringup.bin": "f152f6fbc0f937a7c9a07697e4f2cd580a3f96a748dfa9e7bc3a8015d67c3b62",+    "adom-load-bringup.map": "30380769911a514216121a4e00ddb296f420f7eaf7b06e1775e910d754ca35a5",+    "adom-load-bringup.elf": "719ac2dff3c397a92c866595abfae7bdbf74864fb652a93b54ed18e401a623fb"   },   "commands": [     [@@ -90,6 +95,102 @@       "-o",       "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/00-load_core.o"     ],+    [+      "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",+      "-mcpu=cortex-m0plus",+      "-mthumb",+      "-Os",+      "-g3",+      "-ffunction-sections",+      "-fdata-sections",+      "-fno-common",+      "-fstack-usage",+      "-Wall",+      "-Wextra",+      "-DSTM32G0B1xx",+      "-DUSE_HAL_DRIVER",+      "-DADOM_BRINGUP_OUTPUT_LOCK=1",+      "-DADOM_USB_VID=51966",+      "-DADOM_USB_PID=16385",+      "-DADOM_LAB_BOOT_ID=\"lab20261009bringup\"",+      "-I/home/adom/project/adom-custom-electronic-load/firmware",+      "-I/home/adom/project/adom-custom-electronic-load/firmware/stm32",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/src",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/lib/CMSIS_5/CMSIS/Core/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/cmsis_device_g0/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Inc",+      "-I/home/adom/project/adom-load-firmware-toolchain/root/usr/include/newlib",+      "-std=c11",+      "-Werror",+      "-c",+      "/home/adom/project/adom-custom-electronic-load/firmware/instruments.c",+      "-o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/01-instruments.o"+    ],+    [+      "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",+      "-mcpu=cortex-m0plus",+      "-mthumb",+      "-Os",+      "-g3",+      "-ffunction-sections",+      "-fdata-sections",+      "-fno-common",+      "-fstack-usage",+      "-Wall",+      "-Wextra",+      "-DSTM32G0B1xx",+      "-DUSE_HAL_DRIVER",+      "-DADOM_BRINGUP_OUTPUT_LOCK=1",+      "-DADOM_USB_VID=51966",+      "-DADOM_USB_PID=16385",+      "-DADOM_LAB_BOOT_ID=\"lab20261009bringup\"",+      "-I/home/adom/project/adom-custom-electronic-load/firmware",+      "-I/home/adom/project/adom-custom-electronic-load/firmware/stm32",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/src",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/lib/CMSIS_5/CMSIS/Core/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/cmsis_device_g0/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Inc",+      "-I/home/adom/project/adom-load-firmware-toolchain/root/usr/include/newlib",+      "-std=c11",+      "-Werror",+      "-c",+      "/home/adom/project/adom-custom-electronic-load/firmware/measurement_cache.c",+      "-o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/02-measurement_cache.o"+    ],+    [+      "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",+      "-mcpu=cortex-m0plus",+      "-mthumb",+      "-Os",+      "-g3",+      "-ffunction-sections",+      "-fdata-sections",+      "-fno-common",+      "-fstack-usage",+      "-Wall",+      "-Wextra",+      "-DSTM32G0B1xx",+      "-DUSE_HAL_DRIVER",+      "-DADOM_BRINGUP_OUTPUT_LOCK=1",+      "-DADOM_USB_VID=51966",+      "-DADOM_USB_PID=16385",+      "-DADOM_LAB_BOOT_ID=\"lab20261009bringup\"",+      "-I/home/adom/project/adom-custom-electronic-load/firmware",+      "-I/home/adom/project/adom-custom-electronic-load/firmware/stm32",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/src",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/lib/CMSIS_5/CMSIS/Core/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/cmsis_device_g0/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Inc",+      "-I/home/adom/project/adom-load-firmware-toolchain/root/usr/include/newlib",+      "-std=c11",+      "-Werror",+      "-c",+      "/home/adom/project/adom-custom-electronic-load/firmware/stm32/instrument_input.c",+      "-o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/03-instrument_input.o"+    ],     [       "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",       "-mcpu=cortex-m0plus",@@ -120,7 +221,7 @@       "-c",       "/home/adom/project/adom-custom-electronic-load/firmware/load_protocol.c",       "-o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/01-load_protocol.o"+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/04-load_protocol.o"     ],     [       "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",@@ -152,7 +253,7 @@       "-c",       "/home/adom/project/adom-custom-electronic-load/firmware/fan_tach.c",       "-o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/02-fan_tach.o"+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/05-fan_tach.o"     ],     [       "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",@@ -184,7 +285,7 @@       "-c",       "/home/adom/project/adom-custom-electronic-load/firmware/stm32/fan_input.c",       "-o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/03-fan_input.o"+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/06-fan_input.o"     ],     [       "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",@@ -216,7 +317,7 @@       "-c",       "/home/adom/project/adom-custom-electronic-load/firmware/stm32/main.c",       "-o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/04-main.o"+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/07-main.o"     ],     [       "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",@@ -248,7 +349,7 @@       "-c",       "/home/adom/project/adom-custom-electronic-load/firmware/stm32/clock.c",       "-o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/05-clock.o"+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/08-clock.o"     ],     [       "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",@@ -280,7 +381,7 @@       "-c",       "/home/adom/project/adom-custom-electronic-load/firmware/stm32/usb_descriptors.c",       "-o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/06-usb_descriptors.o"+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/09-usb_descriptors.o"     ],     [       "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",@@ -312,7 +413,7 @@       "-c",       "/home/adom/project/adom-custom-electronic-load/firmware/stm32/syscalls.c",       "-o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/07-syscalls.o"+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/10-syscalls.o"     ],     [       "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",@@ -342,7 +443,7 @@       "-c",       "/home/adom/project/adom-load-firmware-deps/tinyusb/src/tusb.c",       "-o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/08-tusb.o"+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/11-tusb.o"     ],     [       "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",@@ -372,7 +473,7 @@       "-c",       "/home/adom/project/adom-load-firmware-deps/tinyusb/src/common/tusb_fifo.c",       "-o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/09-tusb_fifo.o"+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/12-tusb_fifo.o"     ],     [       "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",@@ -402,7 +503,7 @@       "-c",       "/home/adom/project/adom-load-firmware-deps/tinyusb/src/device/usbd.c",       "-o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/10-usbd.o"+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/13-usbd.o"     ],     [       "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",@@ -432,7 +533,7 @@       "-c",       "/home/adom/project/adom-load-firmware-deps/tinyusb/src/class/cdc/cdc_device.c",       "-o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/11-cdc_device.o"+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/14-cdc_device.o"     ],     [       "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",@@ -462,7 +563,7 @@       "-c",       "/home/adom/project/adom-load-firmware-deps/tinyusb/src/portable/st/stm32_fsdev/dcd_stm32_fsdev.c",       "-o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/12-dcd_stm32_fsdev.o"+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/15-dcd_stm32_fsdev.o"     ],     [       "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",@@ -492,7 +593,7 @@       "-c",       "/home/adom/project/adom-load-firmware-deps/tinyusb/src/portable/st/stm32_fsdev/fsdev_common.c",       "-o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/13-fsdev_common.o"+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/16-fsdev_common.o"     ],     [       "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",@@ -522,7 +623,7 @@       "-c",       "/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Src/stm32g0xx_hal.c",       "-o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/14-stm32g0xx_hal.o"+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/17-stm32g0xx_hal.o"     ],     [       "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",@@ -552,7 +653,7 @@       "-c",       "/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Src/stm32g0xx_hal_cortex.c",       "-o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/15-stm32g0xx_hal_cortex.o"+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/18-stm32g0xx_hal_cortex.o"     ],     [       "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",@@ -582,7 +683,7 @@       "-c",       "/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Src/stm32g0xx_hal_pwr.c",       "-o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/16-stm32g0xx_hal_pwr.o"+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/19-stm32g0xx_hal_pwr.o"     ],     [       "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",@@ -612,7 +713,7 @@       "-c",       "/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Src/stm32g0xx_hal_pwr_ex.c",       "-o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/17-stm32g0xx_hal_pwr_ex.o"+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/20-stm32g0xx_hal_pwr_ex.o"     ],     [       "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",@@ -642,7 +743,7 @@       "-c",       "/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Src/stm32g0xx_hal_rcc.c",       "-o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/18-stm32g0xx_hal_rcc.o"+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/21-stm32g0xx_hal_rcc.o"     ],     [       "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",@@ -672,7 +773,7 @@       "-c",       "/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Src/stm32g0xx_hal_rcc_ex.c",       "-o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/19-stm32g0xx_hal_rcc_ex.o"+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/22-stm32g0xx_hal_rcc_ex.o"     ],     [       "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",@@ -702,7 +803,97 @@       "-c",       "/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Src/stm32g0xx_hal_gpio.c",       "-o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/20-stm32g0xx_hal_gpio.o"+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/23-stm32g0xx_hal_gpio.o"+    ],+    [+      "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",+      "-mcpu=cortex-m0plus",+      "-mthumb",+      "-Os",+      "-g3",+      "-ffunction-sections",+      "-fdata-sections",+      "-fno-common",+      "-fstack-usage",+      "-Wall",+      "-Wextra",+      "-DSTM32G0B1xx",+      "-DUSE_HAL_DRIVER",+      "-DADOM_BRINGUP_OUTPUT_LOCK=1",+      "-DADOM_USB_VID=51966",+      "-DADOM_USB_PID=16385",+      "-DADOM_LAB_BOOT_ID=\"lab20261009bringup\"",+      "-I/home/adom/project/adom-custom-electronic-load/firmware",+      "-I/home/adom/project/adom-custom-electronic-load/firmware/stm32",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/src",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/lib/CMSIS_5/CMSIS/Core/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/cmsis_device_g0/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Inc",+      "-I/home/adom/project/adom-load-firmware-toolchain/root/usr/include/newlib",+      "-c",+      "/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Src/stm32g0xx_hal_i2c.c",+      "-o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/24-stm32g0xx_hal_i2c.o"+    ],+    [+      "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",+      "-mcpu=cortex-m0plus",+      "-mthumb",+      "-Os",+      "-g3",+      "-ffunction-sections",+      "-fdata-sections",+      "-fno-common",+      "-fstack-usage",+      "-Wall",+      "-Wextra",+      "-DSTM32G0B1xx",+      "-DUSE_HAL_DRIVER",+      "-DADOM_BRINGUP_OUTPUT_LOCK=1",+      "-DADOM_USB_VID=51966",+      "-DADOM_USB_PID=16385",+      "-DADOM_LAB_BOOT_ID=\"lab20261009bringup\"",+      "-I/home/adom/project/adom-custom-electronic-load/firmware",+      "-I/home/adom/project/adom-custom-electronic-load/firmware/stm32",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/src",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/lib/CMSIS_5/CMSIS/Core/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/cmsis_device_g0/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Inc",+      "-I/home/adom/project/adom-load-firmware-toolchain/root/usr/include/newlib",+      "-c",+      "/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Src/stm32g0xx_hal_i2c_ex.c",+      "-o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/25-stm32g0xx_hal_i2c_ex.o"+    ],+    [+      "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",+      "-mcpu=cortex-m0plus",+      "-mthumb",+      "-Os",+      "-g3",+      "-ffunction-sections",+      "-fdata-sections",+      "-fno-common",+      "-fstack-usage",+      "-Wall",+      "-Wextra",+      "-DSTM32G0B1xx",+      "-DUSE_HAL_DRIVER",+      "-DADOM_BRINGUP_OUTPUT_LOCK=1",+      "-DADOM_USB_VID=51966",+      "-DADOM_USB_PID=16385",+      "-DADOM_LAB_BOOT_ID=\"lab20261009bringup\"",+      "-I/home/adom/project/adom-custom-electronic-load/firmware",+      "-I/home/adom/project/adom-custom-electronic-load/firmware/stm32",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/src",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/lib/CMSIS_5/CMSIS/Core/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/cmsis_device_g0/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Inc",+      "-I/home/adom/project/adom-load-firmware-toolchain/root/usr/include/newlib",+      "-c",+      "/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Src/stm32g0xx_hal_dma.c",+      "-o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/26-stm32g0xx_hal_dma.o"     ],     [       "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",@@ -732,7 +923,7 @@       "-c",       "/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/cmsis_device_g0/Source/Templates/system_stm32g0xx.c",       "-o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/21-system_stm32g0xx.o"+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/27-system_stm32g0xx.o"     ],     [       "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",@@ -762,7 +953,7 @@       "-c",       "/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/cmsis_device_g0/Source/Templates/gcc/startup_stm32g0b1xx.s",       "-o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/22-startup_stm32g0b1xx.o"+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/28-startup_stm32g0b1xx.o"     ],     [       "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",@@ -770,28 +961,34 @@       "-mthumb",       "-nostartfiles",       "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/00-load_core.o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/01-load_protocol.o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/02-fan_tach.o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/03-fan_input.o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/04-main.o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/05-clock.o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/06-usb_descriptors.o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/07-syscalls.o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/08-tusb.o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/09-tusb_fifo.o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/10-usbd.o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/11-cdc_device.o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/12-dcd_stm32_fsdev.o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/13-fsdev_common.o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/14-stm32g0xx_hal.o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/15-stm32g0xx_hal_cortex.o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/16-stm32g0xx_hal_pwr.o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/17-stm32g0xx_hal_pwr_ex.o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/18-stm32g0xx_hal_rcc.o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/19-stm32g0xx_hal_rcc_ex.o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/20-stm32g0xx_hal_gpio.o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/21-system_stm32g0xx.o",-      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/22-startup_stm32g0b1xx.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/01-instruments.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/02-measurement_cache.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/03-instrument_input.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/04-load_protocol.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/05-fan_tach.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/06-fan_input.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/07-main.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/08-clock.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/09-usb_descriptors.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/10-syscalls.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/11-tusb.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/12-tusb_fifo.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/13-usbd.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/14-cdc_device.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/15-dcd_stm32_fsdev.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/16-fsdev_common.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/17-stm32g0xx_hal.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/18-stm32g0xx_hal_cortex.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/19-stm32g0xx_hal_pwr.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/20-stm32g0xx_hal_pwr_ex.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/21-stm32g0xx_hal_rcc.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/22-stm32g0xx_hal_rcc_ex.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/23-stm32g0xx_hal_gpio.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/24-stm32g0xx_hal_i2c.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/25-stm32g0xx_hal_i2c_ex.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/26-stm32g0xx_hal_dma.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/27-system_stm32g0xx.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/28-startup_stm32g0b1xx.o",       "-T",       "/home/adom/project/adom-custom-electronic-load/firmware/stm32/stm32g0b1kc.ld",       "-Wl,--gc-sections,--print-memory-usage",
design/stm32-bringup-build.log+3−3
@@ -1,6 +1,6 @@ Memory region         Used Size  Region Size  %age Used-             RAM:       23968 B       144 KB     16.25%-           FLASH:       64688 B       256 KB     24.68%+             RAM:       24072 B       144 KB     16.32%+           FLASH:       68196 B       256 KB     26.01%    text	   data	    bss	    dec	    hex	filename-  62936	   1752	  22216	  86904	  15378	/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/adom-load-bringup.elf+  66440	   1752	  22320	  90512	  16190	/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/adom-load-bringup.elf COMPILED ONLY: output permanently locked OFF; no flash or hardware validation performed.
design/stm32-i2c-host-tests.logadded+11
@@ -0,0 +1,11 @@+STM32 adapter fake-HAL: pins, endian/address encoding, zero-only DAC, age and sticky faults passed.+load_core: safety policy tests passed (host C build; no MCU/HAL validation)+fan_tach: qualification, RPM, stale input, noise, slow fan, timer wrap and stale-history expiry passed+load_protocol: host safety/framing tests passed; no MCU or USB validation+Measurement cache: conversion age, WAIT expiry, fault latch and clock wrap passed.+Instrument register, readback, signed conversion and transaction-failure tests passed.+.........................+----------------------------------------------------------------------+Ran 25 tests in 0.049s++OK
docs/completion-plan.md+1−1
@@ -25,7 +25,7 @@ Sunon MF40200V1-1000U-G99 version 0.2.3 is published in source and registry: fac  ## Firmware continuation -Fresh latch acknowledgement, instrument readback/fault injection, and null-sensor diagnostic host connections are implemented and host-tested (25 Python tests plus C suites). MCU output stays locked; no flash occurred. Continue with bounded I2C/ADC adaptation and hardware-independent integration tests. Do not mistake these checks for physical qualification or use them to close thermal/finish gates. The reviewed fan-stall replay brings the raw inventory to 62.+Fresh latch acknowledgement, instrument readback/fault injection, and null-sensor diagnostic host connections are implemented and host-tested (25 Python tests plus C suites). MCU output stays locked; no flash occurred. Diagnostic I2C integration, conversion-age cache and fake-HAL tests now pass; continue with ADC/temperature adaptation and physical timing tests. Do not mistake these checks for physical qualification or use them to close thermal/finish gates. The reviewed fan-stall replay brings the raw inventory to 63 (the latest static fastener-page take is excluded from the action edit).  ## Browser recovery and fastener review 
docs/cooling-fastener-review.md+4
@@ -19,3 +19,7 @@ Boyd recommends 0.452 N·m for the -7 washer, below the transistor's 1.13 N·m m Next: choose a toleranced sleeve/fastener or insulated clamp that fits the exact tab, preserve the existing PCB drill, then check the assembled screw/head/nut and tooling volumes in CAD. No washer is added to the released BOM by this screen.  [Reproducible dimensional screen](mechanical/cooling-fastener-screen.json). Run `python3 tools/screen_cooling_fasteners.py`.++## Short-sleeve centering sensitivity++A rigid 2D cylinder model gives 0.176 mm remaining nominal radial clearance at the far side of a 1.40 mm tab with a 2.8448 mm screw, the 0.81 mm sleeve and nominal catalog diameters. Smaller illustrative screw diameters of 2.75 and 2.60 mm reduce this to 0.129 and 0.066 mm. Thus full-depth coverage is not inherently necessary, and this washer remains a candidate. These are sensitivity calculations, not tolerance limits: real thread roots, discontinuous crests, eccentricity, deformation and molding variation are absent. Do not use the result as insulation qualification. [Model and assumptions](mechanical/short-sleeve-sensitivity.json), reproduced with `python3 tools/check_short_sleeve.py`.
docs/firmware-contract.md+3−1
@@ -42,8 +42,10 @@ Request sequence numbers remain monotonic across all owners during one device bo  ## Instrument readback and bring-up monitoring — 9 October -The driver checks DAC gain, configuration, synchronization, command-register readback and reference status. INA226 initialization verifies configuration and calibration readback. Paired shunt/bus measurements now use triggered conversions: `load_ina_poll` distinguishes WAIT, READY and ERROR; only READY replaces outputs and starts the next conversion. WAIT must not refresh sample age. ERROR requires immediate output disable and reinitialization by the future adapter. This API is not yet connected to the MCU I2C peripheral.+The driver checks DAC gain, configuration, synchronization, command-register readback and reference status. INA226 initialization verifies configuration and calibration readback. Paired shunt/bus measurements now use triggered conversions: `load_ina_poll` distinguishes WAIT, READY and ERROR; only READY replaces outputs and starts the next conversion. WAIT must not refresh sample age. ERROR requires immediate output disable and reinitialization by the future adapter. This API is connected to I2C1 in the permanently output-locked diagnostic image; hardware operation is unverified.  The register model tests inject failure at every transaction in initialization, DAC writes and measurement reads. They also cover acknowledged-but-ignored setup/command writes, wrong IDs, reference fault, configuration reset, signed current, overflow and invalid arguments. These are host tests; they do not verify physical I2C timing or analog accuracy. Register definitions follow [TI DAC60501 Rev E](https://www.ti.com/lit/ds/symlink/dac60501.pdf) and [TI INA226 Rev C](https://www.ti.com/lit/ds/symlink/ina226.pdf).  Replies now include mandatory Boolean `sensors_valid` and `assembly_qualified`. Missing sensors may use null only when validity is false. The host accepts such a diagnostic connection only with OFF/FAULT, enable false, DAC zero and applied current zero. It preserves unavailable readings as null, does not calculate power, and refuses ARM in both server and UI. Valid telemetry still requires the existing 50 ms freshness bound. These fields are required by this draft protocol revision; update host and firmware together. A compiled C peer with missing sensors verifies the actual Python transport connection and rejected ARM.++The diagnostic I2C adapter and conservative measurement cache are compiled and host-tested together using a fake HAL. Current is available only from fresh successful conversions; post-diode voltage is not mislabeled as DUT voltage. The MCU still requires ADC/temperature integration and physical timing validation. See the STM32 README for configured timeout limits, including the HAL internal BUSY wait.
firmware/measurement_cache.cadded+23
@@ -0,0 +1,23 @@+#include "measurement_cache.h"+#include <math.h>+#include <stddef.h>+void load_measurement_begin(load_measurement_cache *c,uint32_t now,bool ok){+ *c=(load_measurement_cache){.healthy=ok,.trigger_ms=now};+}+load_ina_result load_measurement_poll(load_measurement_cache *c,const load_i2c *b,uint32_t now){+ if(!c->healthy)return LOAD_INA_ERROR;+ if(c->have_sample&&(uint32_t)(now-c->sample_ms)>50)c->have_sample=false;+ float v,a;+ load_ina_result r=load_ina_poll(b,&v,&a);+ if(r==LOAD_INA_ERROR){c->healthy=false;c->have_sample=false;return r;}+ if(r==LOAD_INA_READY){+  c->sample_ms=c->trigger_ms;c->trigger_ms=now;+  c->load_terminal_v=v;c->amps=a;c->have_sample=isfinite(v)&&isfinite(a);+  if(!c->have_sample){c->healthy=false;return LOAD_INA_ERROR;}+ }+ return r;+}+bool load_measurement_get(const load_measurement_cache *c,uint32_t now,float *v,float *a,uint32_t *stamp){+ if(!c||!v||!a||!stamp||v==a||!c->healthy||!c->have_sample||(uint32_t)(now-c->sample_ms)>50)return false;+ *v=c->load_terminal_v;*a=c->amps;*stamp=c->sample_ms;return true;+}
firmware/measurement_cache.hadded+15
@@ -0,0 +1,15 @@+#ifndef LOAD_MEASUREMENT_CACHE_H+#define LOAD_MEASUREMENT_CACHE_H+#include "instruments.h"+typedef struct {+ bool healthy, have_sample;+ uint32_t trigger_ms, sample_ms;+ float load_terminal_v, amps;+} load_measurement_cache;+/* Caller initializes the devices with output OFF, then starts this cache with+ * a timestamp captured BEFORE initialization. All bus callbacks must be bounded. */+void load_measurement_begin(load_measurement_cache *,uint32_t before_init_ms,bool init_ok);+load_ina_result load_measurement_poll(load_measurement_cache *,const load_i2c *,uint32_t before_poll_ms);+/* Timestamp is the beginning of the averaging interval, never the read time. */+bool load_measurement_get(const load_measurement_cache *,uint32_t now_ms,float *,float *,uint32_t *);+#endif
firmware/stm32/README.md+14−3
@@ -4,7 +4,7 @@ This adapter compiles for the routed LQFP32 GP package. **Output is permanently  The adapter sets PA5 (MCU_ENABLE), PA4 (ARM_CLK), and PA6 (WDI) low before clock setup. PB3 (FAULT_CLEAR) is open drain and held low. It does not import the Nucleo LED code, because that example toggles our enable pin. Hard-fault and fatal initialization paths deassert enable. USB disconnect/suspend/DTR loss drops protocol ownership. USB attachment follows PB0 VBUS detection. The image never releases the hardware latch or writes a nonzero DAC command. -Voltage, current and temperature telemetry remain unavailable (`null`, invalid sensors), and assembly qualification is false. Fan RPM is supplied by the PA7/TIM2 input once its pulse history qualifies. The portable protocol and ownership/replay checks run over CDC, with bounded input work and bounded reply buffering. This proves a linkable implementation, not boot timing, interrupt latency, USB compliance, or operation of the board's interlocks. Reset-time pulldowns and physical faults still require bench verification.+DUT voltage and case temperature remain unavailable (`null`, invalid sensors), and assembly qualification is false. Current can be reported only after a successful INA conversion and while its conservative age is at most 50 ms. Fan RPM is supplied by the PA7/TIM2 input once its pulse history qualifies. The portable protocol and ownership/replay checks run over CDC, with bounded input work and bounded reply buffering. This proves a linkable implementation, not boot timing, interrupt latency, USB compliance, or operation of the board's interlocks. Reset-time pulldowns and physical faults still require bench verification.  ## Reproduce @@ -27,7 +27,7 @@ The explicitly supplied lab boot ID is **not fresh across resets**. Production f  ## Remaining adapter work -ADC calibration and temperature conversion; INA226/DAC60501 I2C integration and readback; physical tach timing and ISR-latency verification; physical watchdog/latch handshake; reset/brownout and auxiliary/USB power sequencing; production boot identity; integration of qualified assembly configuration; fault injection and USB reconnect tests on the actual PCB. Only then can a separately reviewed functional adapter enable the load.+ADC calibration and temperature conversion; physical INA226/DAC60501 I2C validation and timing; physical tach timing and ISR-latency verification; physical watchdog/latch handshake; reset/brownout and auxiliary/USB power sequencing; production boot identity; integration of qualified assembly configuration; fault injection and USB reconnect tests on the actual PCB. Only then can a separately reviewed functional adapter enable the load.  ## Fan tachometer bring-up @@ -49,4 +49,15 @@ The portable core refuses ARM if the sampled hardware latch is already high. A n  ## Host bring-up monitoring -The control panel can retain a confirmed-OFF connection while voltage/current/temperature are unavailable. Firmware replies explicitly report sensor and assembly qualification; the host keeps unknown readings blank and Enable load disabled. Host-to-compiled-C interoperability covers this path. It does not establish USB enumeration on this board. The register driver is tested separately and is not yet linked to the I2C peripheral.+The control panel can retain a confirmed-OFF connection while voltage/current/temperature are unavailable. Firmware replies explicitly report sensor and assembly qualification; the host keeps unknown readings blank and Enable load disabled. Host-to-compiled-C interoperability covers this path. It does not establish USB enumeration on this board. The register driver is now linked to I2C1 through an output-locked diagnostic adapter. Host fake-HAL tests exercise that adapter; physical bus operation remains unverified.+++## I2C diagnostic acquisition++PB6/PB7 (GP package pins 30/31) use AF6 I2C1 with external pull-ups, matching [ST's pin table](https://www.st.com/resource/en/datasheet/stm32g0b1kc.pdf). A dedicated HSI16 peripheral clock and TIMINGR `0x30444f4f` target no more than about 25 kHz before edge/filter effects. This is an initial timing configuration, not verified I2C compliance. Check rise/fall time, capacitance, analog filtering and the actual clock on hardware.++The adapter performs register ID/config/readback checks at startup and permits only zero writes to the DAC data register. It samples the INA periodically; failure is sticky until reset. Main-loop output disable remains unconditional. Transfer timeout arguments are 5 ms, but the pinned ST HAL also has an internal 25 ms BUSY wait. A pre-check rejects an already busy bus. This does not establish a 5 ms overall deadline or measured worst-case execution time; production enabling requires a separately verified nonblocking/bounded control schedule.++The cache timestamps readings from before the averaging interval began, not when they were read. WAIT cannot refresh their age; errors invalidate the cache. Polling and reading expire values older than 50 ms, with unsigned timer-wrap handling. INA voltage is after the protection diode and is deliberately not sent as DUT voltage. The future PA1 ADC adapter must supply the DUT measurement, and PA0 must supply calibrated case temperature before sensors can be valid.++Fake-HAL tests execute the real adapter, register driver and cache together: GPIO mapping, shifted bus addresses, byte order, timeout arguments, zero-only DAC, invalid startup, stale samples and sticky faults. They do not prove electrical timing, boot behavior or analog accuracy. Current image: 68,196 bytes flash; 24,072 bytes RAM including reservations. No device was flashed.
firmware/stm32/instrument_input.cadded+62
@@ -0,0 +1,62 @@+/* Diagnostic acquisition only; never writes a nonzero DAC or enables the load.+ * PB6/PB7, pins 30/31 of STM32G0B1KCT6 GP, AF6 I2C1.+ * HAL transfer timeout is 5 ms; upstream BUSY wait can take 25 ms.+ * These are configured timeouts, not measured WCET or a production control loop.+ */+#include "stm32g0xx_hal.h"+#include "instrument_input.h"+#include "measurement_cache.h"+#if !defined(ADOM_BRINGUP_OUTPUT_LOCK) || ADOM_BRINGUP_OUTPUT_LOCK != 1+#error Diagnostic I2C adapter requires permanent output lock+#endif+static I2C_HandleTypeDef bus;+static load_measurement_cache cache;+static uint32_t last_poll;+static bool write16(uint8_t addr,uint8_t reg,uint16_t value){+ if(addr==0x48&&reg==8&&value!=0)return false; /* local zero-only defense */+ uint8_t bytes[2]={(uint8_t)(value>>8),(uint8_t)value};+ if(__HAL_I2C_GET_FLAG(&bus,I2C_FLAG_BUSY))return false;+ return HAL_I2C_Mem_Write(&bus,(uint16_t)addr<<1,reg,I2C_MEMADD_SIZE_8BIT,bytes,2,5)==HAL_OK;+}+static bool read16(uint8_t addr,uint8_t reg,uint16_t *value){+ uint8_t bytes[2];+ if(__HAL_I2C_GET_FLAG(&bus,I2C_FLAG_BUSY))return false;+ if(HAL_I2C_Mem_Read(&bus,(uint16_t)addr<<1,reg,I2C_MEMADD_SIZE_8BIT,bytes,2,5)!=HAL_OK)return false;+ *value=((uint16_t)bytes[0]<<8)|bytes[1];return true;+}+static const load_i2c io={write16,read16,HAL_Delay};+bool load_instrument_input_init(void){+ uint32_t start=HAL_GetTick();+ load_measurement_begin(&cache,start,false);+ RCC_PeriphCLKInitTypeDef clock={0};+ clock.PeriphClockSelection=RCC_PERIPHCLK_I2C1;+ clock.I2c1ClockSelection=RCC_I2C1CLKSOURCE_HSI;+ if(HAL_RCCEx_PeriphCLKConfig(&clock)!=HAL_OK)return false;+ __HAL_RCC_GPIOB_CLK_ENABLE();__HAL_RCC_I2C1_CLK_ENABLE();+ GPIO_InitTypeDef pins={0};pins.Pin=GPIO_PIN_6|GPIO_PIN_7;+ pins.Mode=GPIO_MODE_AF_OD;pins.Pull=GPIO_NOPULL;+ pins.Speed=GPIO_SPEED_FREQ_LOW;pins.Alternate=GPIO_AF6_I2C1;+ HAL_GPIO_Init(GPIOB,&pins); /* external pullups; never push-pull */+ bus.Instance=I2C1;+ /* HSI16: prescaler /4, SCLL/H 80 ticks each -> <=25 kHz.+    SCLDEL 5 ticks, SDADEL 4 ticks. Rise/fall and filter timing need bench checks. */+ bus.Init.Timing=0x30444f4f;+ bus.Init.AddressingMode=I2C_ADDRESSINGMODE_7BIT;+ bus.Init.DualAddressMode=I2C_DUALADDRESS_DISABLE;+ bus.Init.GeneralCallMode=I2C_GENERALCALL_DISABLE;+ bus.Init.NoStretchMode=I2C_NOSTRETCH_DISABLE;+ if(HAL_I2C_Init(&bus)!=HAL_OK||HAL_I2CEx_ConfigAnalogFilter(&bus,I2C_ANALOGFILTER_ENABLE)!=HAL_OK||+    HAL_I2CEx_ConfigDigitalFilter(&bus,0)!=HAL_OK)return false;+ start=HAL_GetTick();+ bool ok=load_instruments_init(&io);+ load_measurement_begin(&cache,start,ok);last_poll=HAL_GetTick();return ok;+}+void load_instrument_input_poll(void){+ uint32_t now=HAL_GetTick();if((uint32_t)(now-last_poll)<5)return;+ last_poll=now;(void)load_measurement_poll(&cache,&io,now);+}+bool load_instrument_input_current(float *amps,uint32_t *sample_ms){+ float post_diode_v;+ return load_measurement_get(&cache,HAL_GetTick(),&post_diode_v,amps,sample_ms);+ /* Post-diode INA voltage is deliberately NOT published as DUT voltage. */+}
firmware/stm32/instrument_input.hadded+9
@@ -0,0 +1,9 @@+#ifndef LOAD_INSTRUMENT_INPUT_H+#define LOAD_INSTRUMENT_INPUT_H+#include <stdbool.h>+#include <stdint.h>+/* Output-locked bring-up only. Failure is sticky until reset. */+bool load_instrument_input_init(void);+void load_instrument_input_poll(void);+bool load_instrument_input_current(float *amps,uint32_t *sample_ms);+#endif
firmware/stm32/main.c+4
@@ -4,6 +4,7 @@ #include "tusb.h" #include "load_protocol.h" #include "fan_input.h"+#include "instrument_input.h" #include <stdio.h> #include <math.h> #if !defined(ADOM_BRINGUP_OUTPUT_LOCK) || ADOM_BRINGUP_OUTPUT_LOCK != 1@@ -62,6 +63,7 @@ int main(void) {   safe_gpio_init(); /* do not wait for oscillator or USB setup to deassert enable */   if (HAL_Init() != HAL_OK || !load_clock_init()) load_fatal();   if (!load_fan_input_init()) load_fatal();+  (void)load_instrument_input_init(); /* stay diagnostic/OFF if devices absent */   __HAL_RCC_SYSCFG_CLK_ENABLE(); __HAL_RCC_PWR_CLK_ENABLE();   GPIO_InitTypeDef gpio = {0};   gpio.Pin = GPIO_PIN_11 | GPIO_PIN_12;@@ -85,12 +87,14 @@ int main(void) {       if (vbus) tud_connect(); else { tud_disconnect(); disconnect(); }     }     tud_task();+    load_instrument_input_poll();     bool online = vbus && tud_cdc_connected();     if (!online && connected) disconnect();     connected = online;     load_sample sample = {.now_ms = HAL_GetTick(), .volts = NAN, .amps = NAN,       .case_c = NAN, .fan_rpm = NAN, .assembly_qualified = false,       .sensors_valid = false, .hw_ok = false, .latch_q = false};+    (void)load_instrument_input_current(&sample.amps,&sample.sample_ms);     sample.fan_rpm_valid=load_fan_input_read(&sample.fan_rpm,&sample.last_tach_ms);     sample.now_ms=HAL_GetTick();     load_protocol_poll(&protocol, &sample);
mechanical/short-sleeve-sensitivity.jsonadded+35
@@ -0,0 +1,35 @@+{+  "status": "nominal-centering-sensitivity-only",+  "acceptance": false,+  "washer": "7721-7PPSG",+  "assumptions": {+    "tabMaxMm": 1.4,+    "stemLengthMm": 0.81,+    "boreMm": 2.95,+    "stemODMm": 3.43,+    "screwDiameters": "2.8448 mm is nominal #4 diameter; smaller values are sensitivity inputs, not sourced thread tolerances"+  },+  "model": "Rigid straight cylindrical screw in a straight concentric sleeve; sleeve translates to tab-hole wall; full angular fit includes screw diameter / cos(theta).",+  "rows": [+    {+      "screwDiameterMm": 2.8448,+      "maximumTiltDegrees": 6.223737950440215,+      "remainingRadialMarginMm": 0.17565815293782633+    },+    {+      "screwDiameterMm": 2.75,+      "maximumTiltDegrees": 10.62817441998488,+      "remainingRadialMarginMm": 0.12928418797722285+    },+    {+      "screwDiameterMm": 2.6,+      "maximumTiltDegrees": 16.445511453185023,+      "remainingRadialMarginMm": 0.06584419059654684+    }+  ],+  "limitations": [+    "Thread roots and discontinuous crests are not a solid cylinder",+    "No washer tolerances, deformation, wear, molding flash or eccentricity",+    "No flange/tool access, pad insulation, pressure or torque verification"+  ]+}
tests/hal_stub/stm32g0xx_hal.hadded+41
@@ -0,0 +1,41 @@+#ifndef LOAD_TEST_HAL_H+#define LOAD_TEST_HAL_H+#include <stdint.h>+typedef int HAL_StatusTypeDef;+#define HAL_OK 0+#define HAL_ERROR 1+typedef struct {uint32_t Timing,AddressingMode,DualAddressMode,GeneralCallMode,NoStretchMode;} TestI2CInit;+typedef struct {void *Instance;TestI2CInit Init;} I2C_HandleTypeDef;+typedef struct {uint32_t PeriphClockSelection,I2c1ClockSelection;} RCC_PeriphCLKInitTypeDef;+typedef struct {uint32_t Pin,Mode,Pull,Speed,Alternate;} GPIO_InitTypeDef;+#define I2C1 ((void *)1)+#define GPIOB ((void *)2)+#define I2C_FLAG_BUSY 1+#define RCC_PERIPHCLK_I2C1 1+#define RCC_I2C1CLKSOURCE_HSI 2+#define GPIO_PIN_6 64+#define GPIO_PIN_7 128+#define GPIO_MODE_AF_OD 1+#define GPIO_NOPULL 0+#define GPIO_SPEED_FREQ_LOW 0+#define GPIO_AF6_I2C1 6+#define I2C_ADDRESSINGMODE_7BIT 7+#define I2C_DUALADDRESS_DISABLE 0+#define I2C_GENERALCALL_DISABLE 0+#define I2C_NOSTRETCH_DISABLE 0+#define I2C_ANALOGFILTER_ENABLE 1+#define I2C_MEMADD_SIZE_8BIT 1+#define __HAL_RCC_GPIOB_CLK_ENABLE() ((void)0)+#define __HAL_RCC_I2C1_CLK_ENABLE() ((void)0)+extern int test_busy;+#define __HAL_I2C_GET_FLAG(bus,flag) (test_busy)+uint32_t HAL_GetTick(void);+void HAL_Delay(uint32_t);+HAL_StatusTypeDef HAL_RCCEx_PeriphCLKConfig(RCC_PeriphCLKInitTypeDef *);+void HAL_GPIO_Init(void *,GPIO_InitTypeDef *);+HAL_StatusTypeDef HAL_I2C_Init(I2C_HandleTypeDef *);+HAL_StatusTypeDef HAL_I2CEx_ConfigAnalogFilter(I2C_HandleTypeDef *,uint32_t);+HAL_StatusTypeDef HAL_I2CEx_ConfigDigitalFilter(I2C_HandleTypeDef *,uint32_t);+HAL_StatusTypeDef HAL_I2C_Mem_Write(I2C_HandleTypeDef *,uint16_t,uint16_t,uint16_t,uint8_t *,uint16_t,uint32_t);+HAL_StatusTypeDef HAL_I2C_Mem_Read(I2C_HandleTypeDef *,uint16_t,uint16_t,uint16_t,uint8_t *,uint16_t,uint32_t);+#endif
tests/test_instrument_input.cadded+45
@@ -0,0 +1,45 @@+/* Fake HAL exercises the real adapter/driver/cache boundary, not physical I2C. */+#include "hal_stub/stm32g0xx_hal.h"+#include <assert.h>+#include <math.h>+#include <stdio.h>+#include <string.h>+#define ADOM_BRINGUP_OUTPUT_LOCK 1+#include "../firmware/stm32/instrument_input.c"+static uint32_t tick;+static uint16_t dac[256],ina[256];+static unsigned transactions;+static bool fail;+int test_busy;+uint32_t HAL_GetTick(void){return tick;}+void HAL_Delay(uint32_t n){tick+=n;}+HAL_StatusTypeDef HAL_RCCEx_PeriphCLKConfig(RCC_PeriphCLKInitTypeDef *c){assert(c->I2c1ClockSelection==RCC_I2C1CLKSOURCE_HSI);return HAL_OK;}+void HAL_GPIO_Init(void *port,GPIO_InitTypeDef *p){assert(port==GPIOB&&p->Pin==(GPIO_PIN_6|GPIO_PIN_7)&&p->Mode==GPIO_MODE_AF_OD&&p->Alternate==6&&p->Pull==GPIO_NOPULL);}+HAL_StatusTypeDef HAL_I2C_Init(I2C_HandleTypeDef *h){assert(h->Instance==I2C1&&h->Init.AddressingMode==7);return HAL_OK;}+HAL_StatusTypeDef HAL_I2CEx_ConfigAnalogFilter(I2C_HandleTypeDef *h,uint32_t n){(void)h;assert(n==1);return HAL_OK;}+HAL_StatusTypeDef HAL_I2CEx_ConfigDigitalFilter(I2C_HandleTypeDef *h,uint32_t n){(void)h;assert(n==0);return HAL_OK;}+static uint16_t *regs(uint16_t addr){assert(addr==0x90||addr==0x80);return addr==0x90?dac:ina;}+HAL_StatusTypeDef HAL_I2C_Mem_Write(I2C_HandleTypeDef *h,uint16_t addr,uint16_t reg,uint16_t size,uint8_t *p,uint16_t n,uint32_t timeout){+ (void)h;assert(size==1&&n==2&&timeout==5);transactions++;if(fail)return HAL_ERROR;+ regs(addr)[reg]=((uint16_t)p[0]<<8)|p[1];if(addr==0x80&&reg==0)ina[6]&=~8u;+ if(addr==0x90&&reg==8)assert(p[0]==0&&p[1]==0);+ return HAL_OK;+}+HAL_StatusTypeDef HAL_I2C_Mem_Read(I2C_HandleTypeDef *h,uint16_t addr,uint16_t reg,uint16_t size,uint8_t *p,uint16_t n,uint32_t timeout){+ (void)h;assert(size==1&&n==2&&timeout==5);transactions++;if(fail)return HAL_ERROR;+ uint16_t v=regs(addr)[reg];p[0]=v>>8;p[1]=v;if(addr==0x80&&reg==6)ina[6]&=~8u;return HAL_OK;+}+static void reset(void){tick=0;test_busy=0;fail=false;transactions=0;memset(dac,0,sizeof dac);memset(ina,0,sizeof ina);dac[1]=0x2115;ina[0xfe]=0x5449;ina[0xff]=0x2260;}+int main(void){+ reset();assert(load_instrument_input_init());assert(dac[4]==0x0101&&ina[0]==0x4523);+ unsigned before=transactions;assert(!write16(0x48,8,1)&&transactions==before);+ float a=-1;uint32_t stamp=99;assert(!load_instrument_input_current(&a,&stamp));+ ina[1]=8000;ina[2]=4000;ina[6]=8;tick=36;load_instrument_input_poll();+ assert(load_instrument_input_current(&a,&stamp)&&fabsf(a-1)<.00001f&&stamp==0);+ tick=51;load_instrument_input_poll();assert(!load_instrument_input_current(&a,&stamp));+ tick=72;ina[6]=8;load_instrument_input_poll();assert(load_instrument_input_current(&a,&stamp)&&stamp==36);+ fail=true;tick=77;load_instrument_input_poll();fail=false;tick=82;ina[6]=8;+ before=transactions;load_instrument_input_poll();assert(transactions==before&&!load_instrument_input_current(&a,&stamp));+ reset();test_busy=1;assert(!load_instrument_input_init()&&transactions==0);+ puts("STM32 adapter fake-HAL: pins, endian/address encoding, zero-only DAC, age and sticky faults passed.");+}
tests/test_measurement_cache.cadded+36
@@ -0,0 +1,36 @@+#include "../firmware/measurement_cache.h"+#include <assert.h>+#include <math.h>+#include <stdio.h>+static load_ina_result result;+static unsigned calls;+static float reading_v=5,reading_a=1;+load_ina_result load_ina_poll(const load_i2c *unused,float *v,float *a){+ (void)unused;calls++;if(result==LOAD_INA_READY){*v=reading_v;*a=reading_a;}return result;+}+int main(void){+ load_measurement_cache c;float v=-1,a=-1;uint32_t stamp=0;+ load_measurement_begin(&c,100,true);+ assert(!load_measurement_get(&c,100,&v,&a,&stamp));+ result=LOAD_INA_WAIT;assert(load_measurement_poll(&c,0,110)==LOAD_INA_WAIT);+ assert(!load_measurement_get(&c,110,&v,&a,&stamp));+ result=LOAD_INA_READY;assert(load_measurement_poll(&c,0,136)==LOAD_INA_READY);+ assert(load_measurement_get(&c,136,&v,&a,&stamp)&&stamp==100&&v==5&&a==1);+ result=LOAD_INA_WAIT;assert(load_measurement_poll(&c,0,149)==LOAD_INA_WAIT);+ assert(load_measurement_get(&c,150,&v,&a,&stamp)&&stamp==100);+ v=a=-1;assert(!load_measurement_get(&c,151,&v,&a,&stamp)&&v==-1&&a==-1);+ result=LOAD_INA_READY;assert(load_measurement_poll(&c,0,172)==LOAD_INA_READY);+ assert(load_measurement_get(&c,172,&v,&a,&stamp)&&stamp==136);+ result=LOAD_INA_ERROR;assert(load_measurement_poll(&c,0,173)==LOAD_INA_ERROR);+ unsigned n=calls;result=LOAD_INA_READY;+ assert(load_measurement_poll(&c,0,174)==LOAD_INA_ERROR&&calls==n);+ assert(!load_measurement_get(&c,174,&v,&a,&stamp));+ load_measurement_begin(&c,UINT32_MAX-20,true);+ assert(load_measurement_poll(&c,0,15)==LOAD_INA_READY);+ assert(load_measurement_get(&c,29,&v,&a,&stamp));+ assert(!load_measurement_get(&c,30,&v,&a,&stamp));+ load_measurement_begin(&c,200,false);assert(load_measurement_poll(&c,0,201)==LOAD_INA_ERROR);+ load_measurement_begin(&c,300,true);reading_v=NAN;+ assert(load_measurement_poll(&c,0,336)==LOAD_INA_ERROR);+ puts("Measurement cache: conversion age, WAIT expiry, fault latch and clock wrap passed.");+}
tools/build_stm32_bringup.py+2−2
@@ -24,9 +24,9 @@ for name,path in paths.items(): cc=root/'usr/bin/arm-none-eabi-gcc'; prefix=root/'usr/bin/arm-none-eabi-' newlib=root/'usr/lib/arm-none-eabi/newlib/thumb/v6-m/nofp' h=paths['stm32g0xx_hal_driver']; c=paths['cmsis_device_g0']-sources=[P/'firmware'/s for s in ['load_core.c','load_protocol.c','fan_tach.c','stm32/fan_input.c','stm32/main.c','stm32/clock.c','stm32/usb_descriptors.c','stm32/syscalls.c']]+sources=[P/'firmware'/s for s in ['load_core.c','instruments.c','measurement_cache.c','stm32/instrument_input.c','load_protocol.c','fan_tach.c','stm32/fan_input.c','stm32/main.c','stm32/clock.c','stm32/usb_descriptors.c','stm32/syscalls.c']] sources += [t/'src'/s for s in ['tusb.c','common/tusb_fifo.c','device/usbd.c','class/cdc/cdc_device.c','portable/st/stm32_fsdev/dcd_stm32_fsdev.c','portable/st/stm32_fsdev/fsdev_common.c']]-sources += [h/'Src'/('stm32g0xx_hal'+s+'.c') for s in ['', '_cortex','_pwr','_pwr_ex','_rcc','_rcc_ex','_gpio']]+sources += [h/'Src'/('stm32g0xx_hal'+s+'.c') for s in ['', '_cortex','_pwr','_pwr_ex','_rcc','_rcc_ex','_gpio','_i2c','_i2c_ex','_dma']] sources += [c/'Source/Templates/system_stm32g0xx.c',c/'Source/Templates/gcc/startup_stm32g0b1xx.s'] includes=[P/'firmware',P/'firmware/stm32',t/'src',paths['CMSIS_5']/'CMSIS/Core/Include',c/'Include',h/'Inc',root/'usr/include/newlib'] flags=['-mcpu=cortex-m0plus','-mthumb','-Os','-g3','-ffunction-sections','-fdata-sections','-fno-common','-fstack-usage','-Wall','-Wextra',
tools/check_firmware.sh+4
@@ -4,6 +4,8 @@ cd "$(dirname "$0")/.." load_check_dir=$(mktemp -d) trap 'rm -rf "$load_check_dir"' EXIT cc_args=(-std=c11 -Wall -Wextra -Werror -fsanitize=address,undefined -g)+gcc "${cc_args[@]}" -Itests/hal_stub -Ifirmware firmware/instruments.c firmware/measurement_cache.c tests/test_instrument_input.c -lm -o "$load_check_dir/adapter"+"$load_check_dir/adapter" gcc "${cc_args[@]}" firmware/load_core.c tests/test_load_core.c -lm -o "$load_check_dir/core" "$load_check_dir/core" gcc "${cc_args[@]}" firmware/fan_tach.c tests/test_fan_tach.c -lm -o "$load_check_dir/tach"@@ -12,6 +14,8 @@ gcc "${cc_args[@]}" firmware/load_core.c firmware/load_protocol.c tests/test_loa "$load_check_dir/protocol" gcc "${cc_args[@]}" demos/fan-safety/trace.c firmware/load_core.c firmware/fan_tach.c -lm -o "$load_check_dir/fan-demo" "$load_check_dir/fan-demo" > "$load_check_dir/fan-trace.json"+gcc "${cc_args[@]}" firmware/measurement_cache.c tests/test_measurement_cache.c -lm -o "$load_check_dir/measurement-cache"+"$load_check_dir/measurement-cache" gcc "${cc_args[@]}" firmware/instruments.c tests/test_instruments.c -lm -o "$load_check_dir/instruments" "$load_check_dir/instruments" gcc "${cc_args[@]}" firmware/load_core.c firmware/load_protocol.c tests/protocol_peer.c -lm -o "$load_check_dir/peer"
tools/check_short_sleeve.pyadded+27
@@ -0,0 +1,27 @@+"""Rigid 2D tilted-screw sensitivity; not a toleranced assembly qualification."""+import math,json+from pathlib import Path++def clearance(tab,stem,bore,od,screw):+ if min(tab,stem,bore,od,screw)<=0 or bore<=screw or od<=bore:raise ValueError('invalid geometry')+ lo,hi=0.,math.pi/2-1e-8+ for _ in range(80):+  a=(lo+hi)/2+  if stem*math.tan(a)+screw/math.cos(a)>bore:hi=a+  else:lo=a+ angle=(lo+hi)/2+ # Sleeve can translate until its OD contacts the tab hole. Screw then tilts+ # within the sleeve's bore. Hole diameter cancels from the radial margin.+ margin=(od-bore)/2-max(0,tab-stem)*math.tan(angle)+ return {'maximumTiltDegrees':math.degrees(angle),'remainingRadialMarginMm':margin}+if __name__=='__main__':+ rows=[]+ for screw in [2.8448,2.75,2.6]:+  rows.append({'screwDiameterMm':screw,**clearance(1.4,.81,2.95,3.43,screw)})+ r={'status':'nominal-centering-sensitivity-only','acceptance':False,'washer':'7721-7PPSG',+ 'assumptions':{'tabMaxMm':1.4,'stemLengthMm':.81,'boreMm':2.95,'stemODMm':3.43,+ 'screwDiameters':'2.8448 mm is nominal #4 diameter; smaller values are sensitivity inputs, not sourced thread tolerances'},+ 'model':'Rigid straight cylindrical screw in a straight concentric sleeve; sleeve translates to tab-hole wall; full angular fit includes screw diameter / cos(theta).',+ 'rows':rows,'limitations':['Thread roots and discontinuous crests are not a solid cylinder','No washer tolerances, deformation, wear, molding flash or eccentricity','No flange/tool access, pad insulation, pressure or torque verification']}+ Path(__file__).resolve().parents[1].joinpath('mechanical/short-sleeve-sensitivity.json').write_text(json.dumps(r,indent=2)+'\n')+ print(json.dumps(rows,indent=2))
videos/fastener-review/recording-stop.jsonadded+37
@@ -0,0 +1,37 @@+{+  "_hint": "The MP4 is finalized (moov written). pull_file {path} brings it to the caller; the janitor prunes the recordings dir by age/size, so pull it promptly.",+  "abVersion": "2.1.173",+  "audio": false,+  "audioFrames": 0,+  "autoStopped": false,+  "bytes": 4440765,+  "captureItemClosed": false,+  "captureQuiet": null,+  "droppedFrames": 0,+  "durationMs": 464,+  "elapsedMs": 17753,+  "encodedFrames": 0,+  "error": "",+  "fileExists": true,+  "finaliseMs": 229,+  "framePath": "gpu",+  "frames": 27,+  "gpuZeroCopy": true,+  "height": 1528,+  "kind": "window",+  "lastFrameAgoMs": 4188,+  "output": "{\"data\":{\"bytes\":4440765,\"elapsedMs\":17753,\"frames\":27,\"path\":\"C:\\\\Users\\\\john\\\\AppData\\\\Local\\\\Temp\\\\adom-bridge-recordings\\\\window-5969372-20261009-204305.mp4\",\"recordingId\":\"rec-b8e8b39b338747c5b694407bc3dd8403\"},\"message\":\"Stopped recording rec-b8e8b39b338747c5b694407bc3dd8403: 27 frames, 17753 ms, 4440765 bytes -> C:\\\\Users\\\\john\\\\AppData\\\\Local\\\\Temp\\\\adom-bridge-recordings\\\\window-5969372-20261009-204305.mp4\"}",+  "path": "C:/Users/john/AppData/Local/Temp/adom-bridge-recordings/window-5969372-20261009-204305.mp4",+  "rateControl": "vbr",+  "recordingId": "rec-b8e8b39b338747c5b694407bc3dd8403",+  "schemaVersion": 1,+  "state": "finalised",+  "status": "ok",+  "success": true,+  "timeline": {+    "reason": "file has more video samples than were written (27 written, fragment 1 needs 60)",+    "repaired": false+  },+  "timeoutSeconds": 30,+  "width": 2560+}
videos/inventory.json+7−1
@@ -1,5 +1,5 @@ {-  "rawTakeCount": 62,+  "rawTakeCount": 63,   "reviewedExcerptCount": 16,   "countingRule": "Unique raw window recording filenames in this project; excludes derivative action/speed exports and reference projects. Raw count includes rejected takes.",   "takes": [@@ -374,6 +374,12 @@       "durationSeconds": 16.233333,       "topic": "Review of successful official Parts Search API result",       "review": "Full decode and contact sheet reviewed; mostly static, use only a short price/stock cutaway; no new search shown."+    },+    {+      "file": "videos/fastener-review/window-5969372-20261009-204305.mp4",+      "durationSeconds": 19.6,+      "topic": "Published fastener review",+      "review": "Exclude from action edit: static page; attempted scroll moved zero pixels. Full decode passed. Recorder timing mismatch retained; not a duration measurement of engineering work."     }   ] }