diff --git a/README.md b/README.md index f42d8d6..9c20873 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,191 @@ and Q; 1× ESP32-S3-WROOM-1 N16R8 CAM with OV2640 — node P + the stretch camera (the purchase list — [`docs/eng/equipment.md`](./docs/eng/equipment.md)). +The bench's first flash — 2026-09-14, node A (`firmware-a`, a DevKitC-1 +clone with a CH343 bridge): S0 passed → espflash wrote the 2nd-stage +bootloader, the partition table and the app in one image; the node booted +and calibrated its zero. Log: + +```text +espflash flash --monitor --port /dev/ttyACM0 target/xtensa-esp32s3-none-elf/release/firmware-a +[2026-09-14T03:17:32Z INFO ] Serial port: '/dev/ttyACM0' +[2026-09-14T03:17:32Z INFO ] Connecting... +[2026-09-14T03:17:32Z INFO ] Using flash stub +Chip type: esp32s3 (revision v0.2) +Crystal frequency: 40 MHz +Flash size: 16MB +Features: WiFi, BLE, Embedded Flash +MAC address: 44:1b:f6:fd:ea:cc +App/part. size: 109,856/16,384,000 bytes, 0.67% +[00:00:01] [========================================] 1/1 0x0 Verifying... OK! +[00:00:00] [========================================] 1/1 0x8000 Verifying... OK! +[00:00:03] [========================================] 3/3 0x10000 Verifying... OK! +[2026-09-14T03:17:39Z INFO ] Flashing has completed! +Commands: + CTRL+R Reset chip + CTRL+C Exit + +ESP-ROM:esp32s3-20210327 +Build:Mar 27 2021 +rst:0x1 (POWERON),boot:0x8 (SPI_FAST_FLASH_BOOT) +SPIWP:0xee +mode:DIO, clock div:2 +load:0x3fce2820,len:0x14d0 +load:0x403c8700,len:0xdcc +load:0x403cb700,len:0x2f54 +entry 0x403c8900 +I (29) boot: ESP-IDF v6.1-beta1-497-g14f663f003e 2nd stage bootloader +I (30) boot: Multicore bootloader +I (30) boot: chip revision: v0.2 +I (30) boot: efuse block revision: v1.4 +I (34) boot.esp32s3: Boot SPI Speed : 40MHz +I (38) boot.esp32s3: SPI Mode : DIO +I (42) boot.esp32s3: SPI Flash Size : 16MB +I (45) boot: Enabling RNG early entropy source... +I (50) boot: Partition Table: +I (52) boot: ## Label Usage Type ST Offset Length +I (59) boot: 0 nvs WiFi data 01 02 00009000 00006000 +I (65) boot: 1 phy_init RF data 01 01 0000f000 00001000 +I (72) boot: 2 factory factory app 00 00 00010000 00fa0000 +I (78) boot: End of partition table +I (82) esp_image: segment 0: paddr=00010020 vaddr=3c000020 size=02990h ( 10640) map +I (92) esp_image: segment 1: paddr=000129b8 vaddr=3fc89998 size=009c4h ( 2500) load +I (97) esp_image: segment 2: paddr=00013384 vaddr=40378000 size=01998h ( 6552) load +I (106) esp_image: segment 3: paddr=00014d24 vaddr=00000000 size=0b2f4h (45812) +I (123) esp_image: segment 4: paddr=00020020 vaddr=42010020 size=0acd4h (44244) map +I (135) boot: Loaded app from partition at offset 0x10000 +I (135) boot: Disabling RNG early entropy source... +a: boot, run_id=bench-a +a: zero=2229 +a,bench-a,160,idle +``` + +Node Q (DevKitC-1 #2, `firmware-q`) — reflashed with the final HEAD build +(after the synthetic-window extraction into the lib): `cracked` verdicts +every 400 ms — the test-pinned synthetic behavior until S4 (a loop check, +not a quality label). The bootloader and the partition table were already +in flash — only the app region (0x10000) changed. Log: + +```text +espflash flash --monitor --port /dev/ttyACM0 target/xtensa-esp32s3-none-elf/release/firmware-q +[2026-09-14T05:36:49Z INFO ] Serial port: '/dev/ttyACM0' +[2026-09-14T05:36:49Z INFO ] Connecting... +[2026-09-14T05:36:49Z INFO ] Using flash stub +Chip type: esp32s3 (revision v0.2) +Crystal frequency: 40 MHz +Flash size: 16MB +Features: WiFi, BLE, Embedded Flash +MAC address: 44:1b:f6:fd:fa:60 +App/part. size: 113,456/16,384,000 bytes, 0.69% +[00:00:04] [========================================] 3/3 0x10000 Verifying... OK! +[2026-09-14T05:36:55Z INFO ] Flashing has completed! +Commands: + CTRL+R Reset chip + CTRL+C Exit + +ESP-ROM:esp32s3-20210327 +Build:Mar 27 2021 +rst:0x1 (POWERON),boot:0x8 (SPI_FAST_FLASH_BOOT) +SPIWP:0xee +mode:DIO, clock div:2 +load:0x3fce2820,len:0x14d0 +load:0x403c8700,len:0xdcc +load:0x403cb700,len:0x2f54 +entry 0x403c8900 +I (29) boot: ESP-IDF v6.1-beta1-497-g14f663f003e 2nd stage bootloader +I (30) boot: Multicore bootloader +I (30) boot: chip revision: v0.2 +I (30) boot: efuse block revision: v1.4 +I (34) boot.esp32s3: Boot SPI Speed : 40MHz +I (38) boot.esp32s3: SPI Mode : DIO +I (42) boot.esp32s3: SPI Flash Size : 16MB +I (45) boot: Enabling RNG early entropy source... +I (50) boot: Partition Table: +I (52) boot: ## Label Usage Type ST Offset Length +I (59) boot: 0 nvs WiFi data 01 02 00009000 00006000 +I (65) boot: 1 phy_init RF data 01 01 0000f000 00001000 +I (72) boot: 2 factory factory app 00 00 00010000 00fa0000 +I (78) boot: End of partition table +I (82) esp_image: segment 0: paddr=00010020 vaddr=3c000020 size=04300h ( 17152) map +I (93) esp_image: segment 1: paddr=00014328 vaddr=3fc89998 size=009bch ( 2492) load +I (97) esp_image: segment 2: paddr=00014cec vaddr=40378000 size=01998h ( 6552) load +I (106) esp_image: segment 3: paddr=0001668c vaddr=00000000 size=0998ch ( 39308) +I (123) esp_image: segment 4: paddr=00020020 vaddr=42010020 size=0bae8h ( 47848) map +I (134) boot: Loaded app from partition at offset 0x10000 +I (135) boot: Disabling RNG early entropy source... +q: boot, run_id=bench-q +q,bench-q,30,cracked +q,bench-q,430,cracked +q,bench-q,830,cracked +q,bench-q,1230,cracked +q,bench-q,1630,cracked +q,bench-q,2030,cracked +q,bench-q,2430,cracked +q,bench-q,2830,cracked +q,bench-q,3230,cracked +q,bench-q,3630,cracked +q,bench-q,4030,cracked +``` + +Node P (the CAM board, `firmware-p`) — a 99,040-byte app, the smallest of +the three (no model); after the boot line it stays silent — the norm +without a sensor. A CAM-board quirk: flash and monitor through the bridge +port only (the native USB port does not auto-enter download mode). Log: + +```text +espflash flash --monitor --port /dev/ttyACM0 bin/20260914085743-firmware-p +[2026-09-14T06:09:02Z INFO ] Serial port: '/dev/ttyACM0' +[2026-09-14T06:09:02Z INFO ] Connecting... +[2026-09-14T06:09:02Z INFO ] Using flash stub +Chip type: esp32s3 (revision v0.2) +Crystal frequency: 40 MHz +Flash size: 16MB +Features: WiFi, BLE, Embedded Flash +MAC address: 90:70:69:f8:fc:70 +App/part. size: 99,040/16,384,000 bytes, 0.60% +[00:00:01] [========================================] 1/1 0x0 Verifying... OK! +[00:00:00] [========================================] 1/1 0x8000 Verifying... OK! +[00:00:03] [========================================] 2/2 0x10000 Verifying... OK! +[2026-09-14T06:09:08Z INFO ] Flashing has completed! +Commands: + CTRL+R Reset chip + CTRL+C Exit + +ESP-ROM:esp32s3-20210327 +Build:Mar 27 2021 +rst:0x1 (POWERON),boot:0x8 (SPI_FAST_FLASH_BOOT) +SPIWP:0xee +mode:DIO, clock div:2 +load:0x3fce2820,len:0x14d0 +load:0x403c8700,len:0xdcc +load:0x403cb700,len:0x2f54 +entry 0x403c8900 +I (29) boot: ESP-IDF v6.1-beta1-497-g14f663f003e 2nd stage bootloader +I (30) boot: Multicore bootloader +I (30) boot: chip revision: v0.2 +I (30) boot: efuse block revision: v1.4 +I (34) boot.esp32s3: Boot SPI Speed : 40MHz +I (38) boot.esp32s3: SPI Mode : DIO +I (42) boot.esp32s3: SPI Flash Size : 16MB +I (45) boot: Enabling RNG early entropy source... +I (50) boot: Partition Table: +I (52) boot: ## Label Usage Type ST Offset Length +I (59) boot: 0 nvs WiFi data 01 02 00009000 00006000 +I (65) boot: 1 phy_init RF data 01 01 0000f000 00001000 +I (72) boot: 2 factory factory app 00 00 00010000 00fa0000 +I (78) boot: End of partition table +I (82) esp_image: segment 0: paddr=00010020 vaddr=3c000020 size=01e30h ( 7728) map +I (91) esp_image: segment 1: paddr=00011e58 vaddr=3fc89998 size=009b0h ( 2480) load +I (97) esp_image: segment 2: paddr=00012810 vaddr=40378000 size=01998h ( 6552) load +I (106) esp_image: segment 3: paddr=000141b0 vaddr=00000000 size=0be68h ( 48744) +I (123) esp_image: segment 4: paddr=00020020 vaddr=42010020 size=08298h ( 33432) map +I (133) boot: Loaded app from partition at offset 0x10000 +I (133) boot: Disabling RNG early entropy source... +p: boot, run_id=bench-p +``` + +The shakedown facts — [`firmware/NOTES.md`](./firmware/NOTES.md). + ## Status Done: weeks 1–6 — the Conv1D kernel (optimized in week 6: zero-point diff --git a/README.ru.md b/README.ru.md index e1a1750..c41c6a9 100644 --- a/README.ru.md +++ b/README.ru.md @@ -212,6 +212,191 @@ Conv1D — контракт недель 2–3). [`docs/rus/equipment.md`](./docs/rus/equipment.md)). Декомпозиция обкатки — [`docs/rus/decompose/firmware.md`](./docs/rus/decompose/firmware.md). +Первая прошивка на стенде — 2026-09-14, узел A (`firmware-a`, клон DevKitC-1 +с мостом CH343): этап S0 пройден → espflash залил вторичный загрузчик, +таблицу разделов и приложение одним образом, узел поднялся и откалибровал +нуль. Журнал: + +```text +espflash flash --monitor --port /dev/ttyACM0 target/xtensa-esp32s3-none-elf/release/firmware-a +[2026-09-14T03:17:32Z INFO ] Serial port: '/dev/ttyACM0' +[2026-09-14T03:17:32Z INFO ] Connecting... +[2026-09-14T03:17:32Z INFO ] Using flash stub +Chip type: esp32s3 (revision v0.2) +Crystal frequency: 40 MHz +Flash size: 16MB +Features: WiFi, BLE, Embedded Flash +MAC address: 44:1b:f6:fd:ea:cc +App/part. size: 109,856/16,384,000 bytes, 0.67% +[00:00:01] [========================================] 1/1 0x0 Verifying... OK! +[00:00:00] [========================================] 1/1 0x8000 Verifying... OK! +[00:00:03] [========================================] 3/3 0x10000 Verifying... OK! +[2026-09-14T03:17:39Z INFO ] Flashing has completed! +Commands: + CTRL+R Reset chip + CTRL+C Exit + +ESP-ROM:esp32s3-20210327 +Build:Mar 27 2021 +rst:0x1 (POWERON),boot:0x8 (SPI_FAST_FLASH_BOOT) +SPIWP:0xee +mode:DIO, clock div:2 +load:0x3fce2820,len:0x14d0 +load:0x403c8700,len:0xdcc +load:0x403cb700,len:0x2f54 +entry 0x403c8900 +I (29) boot: ESP-IDF v6.1-beta1-497-g14f663f003e 2nd stage bootloader +I (30) boot: Multicore bootloader +I (30) boot: chip revision: v0.2 +I (30) boot: efuse block revision: v1.4 +I (34) boot.esp32s3: Boot SPI Speed : 40MHz +I (38) boot.esp32s3: SPI Mode : DIO +I (42) boot.esp32s3: SPI Flash Size : 16MB +I (45) boot: Enabling RNG early entropy source... +I (50) boot: Partition Table: +I (52) boot: ## Label Usage Type ST Offset Length +I (59) boot: 0 nvs WiFi data 01 02 00009000 00006000 +I (65) boot: 1 phy_init RF data 01 01 0000f000 00001000 +I (72) boot: 2 factory factory app 00 00 00010000 00fa0000 +I (78) boot: End of partition table +I (82) esp_image: segment 0: paddr=00010020 vaddr=3c000020 size=02990h ( 10640) map +I (92) esp_image: segment 1: paddr=000129b8 vaddr=3fc89998 size=009c4h ( 2500) load +I (97) esp_image: segment 2: paddr=00013384 vaddr=40378000 size=01998h ( 6552) load +I (106) esp_image: segment 3: paddr=00014d24 vaddr=00000000 size=0b2f4h (45812) +I (123) esp_image: segment 4: paddr=00020020 vaddr=42010020 size=0acd4h (44244) map +I (135) boot: Loaded app from partition at offset 0x10000 +I (135) boot: Disabling RNG early entropy source... +a: boot, run_id=bench-a +a: zero=2229 +a,bench-a,160,idle +``` + +Узел Q (DevKitC-1 №2, `firmware-q`) — перепрошит финальной HEAD-сборкой +(после выноса синтетического окна в lib): вердикты `cracked` каждые 400 мс — +закреплённое тестом поведение синтетики до S4 (проверка контура, не метка +качества). Загрузчик и таблица разделов уже во flash — менялось только +приложение (одна область 0x10000). Журнал: + +```text +espflash flash --monitor --port /dev/ttyACM0 target/xtensa-esp32s3-none-elf/release/firmware-q +[2026-09-14T05:36:49Z INFO ] Serial port: '/dev/ttyACM0' +[2026-09-14T05:36:49Z INFO ] Connecting... +[2026-09-14T05:36:49Z INFO ] Using flash stub +Chip type: esp32s3 (revision v0.2) +Crystal frequency: 40 MHz +Flash size: 16MB +Features: WiFi, BLE, Embedded Flash +MAC address: 44:1b:f6:fd:fa:60 +App/part. size: 113,456/16,384,000 bytes, 0.69% +[00:00:04] [========================================] 3/3 0x10000 Verifying... OK! +[2026-09-14T05:36:55Z INFO ] Flashing has completed! +Commands: + CTRL+R Reset chip + CTRL+C Exit + +ESP-ROM:esp32s3-20210327 +Build:Mar 27 2021 +rst:0x1 (POWERON),boot:0x8 (SPI_FAST_FLASH_BOOT) +SPIWP:0xee +mode:DIO, clock div:2 +load:0x3fce2820,len:0x14d0 +load:0x403c8700,len:0xdcc +load:0x403cb700,len:0x2f54 +entry 0x403c8900 +I (29) boot: ESP-IDF v6.1-beta1-497-g14f663f003e 2nd stage bootloader +I (30) boot: Multicore bootloader +I (30) boot: chip revision: v0.2 +I (30) boot: efuse block revision: v1.4 +I (34) boot.esp32s3: Boot SPI Speed : 40MHz +I (38) boot.esp32s3: SPI Mode : DIO +I (42) boot.esp32s3: SPI Flash Size : 16MB +I (45) boot: Enabling RNG early entropy source... +I (50) boot: Partition Table: +I (52) boot: ## Label Usage Type ST Offset Length +I (59) boot: 0 nvs WiFi data 01 02 00009000 00006000 +I (65) boot: 1 phy_init RF data 01 01 0000f000 00001000 +I (72) boot: 2 factory factory app 00 00 00010000 00fa0000 +I (78) boot: End of partition table +I (82) esp_image: segment 0: paddr=00010020 vaddr=3c000020 size=04300h ( 17152) map +I (93) esp_image: segment 1: paddr=00014328 vaddr=3fc89998 size=009bch ( 2492) load +I (97) esp_image: segment 2: paddr=00014cec vaddr=40378000 size=01998h ( 6552) load +I (106) esp_image: segment 3: paddr=0001668c vaddr=00000000 size=0998ch ( 39308) +I (123) esp_image: segment 4: paddr=00020020 vaddr=42010020 size=0bae8h ( 47848) map +I (134) boot: Loaded app from partition at offset 0x10000 +I (135) boot: Disabling RNG early entropy source... +q: boot, run_id=bench-q +q,bench-q,30,cracked +q,bench-q,430,cracked +q,bench-q,830,cracked +q,bench-q,1230,cracked +q,bench-q,1630,cracked +q,bench-q,2030,cracked +q,bench-q,2430,cracked +q,bench-q,2830,cracked +q,bench-q,3230,cracked +q,bench-q,3630,cracked +q,bench-q,4030,cracked +``` + +Узел P (CAM-плата, `firmware-p`) — app 99 040 байт, минимальный из трёх +(нет модели); после boot-строки тишина — норма без датчика. Нюанс CAM-платы: +прошивать и мониторить только через мостовой порт (нативный USB-порт не +даёт авто-download). Журнал: + +```text +espflash flash --monitor --port /dev/ttyACM0 bin/20260914085743-firmware-p +[2026-09-14T06:09:02Z INFO ] Serial port: '/dev/ttyACM0' +[2026-09-14T06:09:02Z INFO ] Connecting... +[2026-09-14T06:09:02Z INFO ] Using flash stub +Chip type: esp32s3 (revision v0.2) +Crystal frequency: 40 MHz +Flash size: 16MB +Features: WiFi, BLE, Embedded Flash +MAC address: 90:70:69:f8:fc:70 +App/part. size: 99,040/16,384,000 bytes, 0.60% +[00:00:01] [========================================] 1/1 0x0 Verifying... OK! +[00:00:00] [========================================] 1/1 0x8000 Verifying... OK! +[00:00:03] [========================================] 2/2 0x10000 Verifying... OK! +[2026-09-14T06:09:08Z INFO ] Flashing has completed! +Commands: + CTRL+R Reset chip + CTRL+C Exit + +ESP-ROM:esp32s3-20210327 +Build:Mar 27 2021 +rst:0x1 (POWERON),boot:0x8 (SPI_FAST_FLASH_BOOT) +SPIWP:0xee +mode:DIO, clock div:2 +load:0x3fce2820,len:0x14d0 +load:0x403c8700,len:0xdcc +load:0x403cb700,len:0x2f54 +entry 0x403c8900 +I (29) boot: ESP-IDF v6.1-beta1-497-g14f663f003e 2nd stage bootloader +I (30) boot: Multicore bootloader +I (30) boot: chip revision: v0.2 +I (30) boot: efuse block revision: v1.4 +I (34) boot.esp32s3: Boot SPI Speed : 40MHz +I (38) boot.esp32s3: SPI Mode : DIO +I (42) boot.esp32s3: SPI Flash Size : 16MB +I (45) boot: Enabling RNG early entropy source... +I (50) boot: Partition Table: +I (52) boot: ## Label Usage Type ST Offset Length +I (59) boot: 0 nvs WiFi data 01 02 00009000 00006000 +I (65) boot: 1 phy_init RF data 01 01 0000f000 00001000 +I (72) boot: 2 factory factory app 00 00 00010000 00fa0000 +I (78) boot: End of partition table +I (82) esp_image: segment 0: paddr=00010020 vaddr=3c000020 size=01e30h ( 7728) map +I (91) esp_image: segment 1: paddr=00011e58 vaddr=3fc89998 size=009b0h ( 2480) load +I (97) esp_image: segment 2: paddr=00012810 vaddr=40378000 size=01998h ( 6552) load +I (106) esp_image: segment 3: paddr=000141b0 vaddr=00000000 size=0be68h ( 48744) +I (123) esp_image: segment 4: paddr=00020020 vaddr=42010020 size=08298h ( 33432) map +I (133) boot: Loaded app from partition at offset 0x10000 +I (133) boot: Disabling RNG early entropy source... +p: boot, run_id=bench-p +``` + +Факты шейкдауна — [`firmware/NOTES.md`](./firmware/NOTES.md). + ## Статус Готово: недели 1–6 — кернел Conv1D (в неделю 6 оптимизирован: вынос diff --git a/docs/eng/backlog/resolved/20260909120005-critical-counts-levels-not-edges-firmware-p-bl.md b/docs/eng/backlog/done/20260909120005-critical-counts-levels-not-edges-firmware-p-bl.md similarity index 100% rename from docs/eng/backlog/resolved/20260909120005-critical-counts-levels-not-edges-firmware-p-bl.md rename to docs/eng/backlog/done/20260909120005-critical-counts-levels-not-edges-firmware-p-bl.md diff --git a/docs/eng/backlog/resolved/20260909120006-critical-tms-always-zero-firmware-a-bl.md b/docs/eng/backlog/done/20260909120006-critical-tms-always-zero-firmware-a-bl.md similarity index 100% rename from docs/eng/backlog/resolved/20260909120006-critical-tms-always-zero-firmware-a-bl.md rename to docs/eng/backlog/done/20260909120006-critical-tms-always-zero-firmware-a-bl.md diff --git a/docs/eng/backlog/resolved/20260909120007-important-features-cli-contract-drift-workspace-bl.md b/docs/eng/backlog/done/20260909120007-important-features-cli-contract-drift-workspace-bl.md similarity index 100% rename from docs/eng/backlog/resolved/20260909120007-important-features-cli-contract-drift-workspace-bl.md rename to docs/eng/backlog/done/20260909120007-important-features-cli-contract-drift-workspace-bl.md diff --git a/docs/eng/backlog/resolved/20260909120031-important-qemu-not-in-ci-ci-bl.md b/docs/eng/backlog/done/20260909120031-important-qemu-not-in-ci-ci-bl.md similarity index 100% rename from docs/eng/backlog/resolved/20260909120031-important-qemu-not-in-ci-ci-bl.md rename to docs/eng/backlog/done/20260909120031-important-qemu-not-in-ci-ci-bl.md diff --git a/docs/eng/backlog/resolved/20260909120032-important-q-stack-overflow-firmware-q-bl.md b/docs/eng/backlog/done/20260909120032-important-q-stack-overflow-firmware-q-bl.md similarity index 100% rename from docs/eng/backlog/resolved/20260909120032-important-q-stack-overflow-firmware-q-bl.md rename to docs/eng/backlog/done/20260909120032-important-q-stack-overflow-firmware-q-bl.md diff --git a/docs/eng/backlog/resolved/20260909120033-important-bridge-dies-on-bad-byte-firmware-tools-bl.md b/docs/eng/backlog/done/20260909120033-important-bridge-dies-on-bad-byte-firmware-tools-bl.md similarity index 100% rename from docs/eng/backlog/resolved/20260909120033-important-bridge-dies-on-bad-byte-firmware-tools-bl.md rename to docs/eng/backlog/done/20260909120033-important-bridge-dies-on-bad-byte-firmware-tools-bl.md diff --git a/docs/eng/backlog/resolved/20260909120041-minor-firmware-bl.md b/docs/eng/backlog/done/20260909120041-minor-firmware-bl.md similarity index 100% rename from docs/eng/backlog/resolved/20260909120041-minor-firmware-bl.md rename to docs/eng/backlog/done/20260909120041-minor-firmware-bl.md diff --git a/docs/eng/backlog/resolved/20260909120047-rec-firmware-bl.md b/docs/eng/backlog/done/20260909120047-rec-firmware-bl.md similarity index 100% rename from docs/eng/backlog/resolved/20260909120047-rec-firmware-bl.md rename to docs/eng/backlog/done/20260909120047-rec-firmware-bl.md diff --git a/docs/eng/backlog/20260909120001-critical-invalid-qmeta-json-nodes-bl.md b/docs/eng/backlog/open/20260909120001-critical-invalid-qmeta-json-nodes-bl.md similarity index 100% rename from docs/eng/backlog/20260909120001-critical-invalid-qmeta-json-nodes-bl.md rename to docs/eng/backlog/open/20260909120001-critical-invalid-qmeta-json-nodes-bl.md diff --git a/docs/eng/backlog/20260909120002-critical-hang-on-lost-end-marker-oee-aggregator-bl.md b/docs/eng/backlog/open/20260909120002-critical-hang-on-lost-end-marker-oee-aggregator-bl.md similarity index 100% rename from docs/eng/backlog/20260909120002-critical-hang-on-lost-end-marker-oee-aggregator-bl.md rename to docs/eng/backlog/open/20260909120002-critical-hang-on-lost-end-marker-oee-aggregator-bl.md diff --git a/docs/eng/backlog/20260909120003-critical-midpacket-retry-desync-mqtt-min-bl.md b/docs/eng/backlog/open/20260909120003-critical-midpacket-retry-desync-mqtt-min-bl.md similarity index 100% rename from docs/eng/backlog/20260909120003-critical-midpacket-retry-desync-mqtt-min-bl.md rename to docs/eng/backlog/open/20260909120003-critical-midpacket-retry-desync-mqtt-min-bl.md diff --git a/docs/eng/backlog/20260909120004-critical-clamp-panic-missing-class-ml-trainer-bl.md b/docs/eng/backlog/open/20260909120004-critical-clamp-panic-missing-class-ml-trainer-bl.md similarity index 100% rename from docs/eng/backlog/20260909120004-critical-clamp-panic-missing-class-ml-trainer-bl.md rename to docs/eng/backlog/open/20260909120004-critical-clamp-panic-missing-class-ml-trainer-bl.md diff --git a/docs/eng/backlog/20260909120008-important-f32-time-degradation-line-simulator-bl.md b/docs/eng/backlog/open/20260909120008-important-f32-time-degradation-line-simulator-bl.md similarity index 100% rename from docs/eng/backlog/20260909120008-important-f32-time-degradation-line-simulator-bl.md rename to docs/eng/backlog/open/20260909120008-important-f32-time-degradation-line-simulator-bl.md diff --git a/docs/eng/backlog/20260909120009-important-toml-validation-panics-line-simulator-bl.md b/docs/eng/backlog/open/20260909120009-important-toml-validation-panics-line-simulator-bl.md similarity index 100% rename from docs/eng/backlog/20260909120009-important-toml-validation-panics-line-simulator-bl.md rename to docs/eng/backlog/open/20260909120009-important-toml-validation-panics-line-simulator-bl.md diff --git a/docs/eng/backlog/20260909120010-important-unconditional-current-stream-line-simulator-bl.md b/docs/eng/backlog/open/20260909120010-important-unconditional-current-stream-line-simulator-bl.md similarity index 100% rename from docs/eng/backlog/20260909120010-important-unconditional-current-stream-line-simulator-bl.md rename to docs/eng/backlog/open/20260909120010-important-unconditional-current-stream-line-simulator-bl.md diff --git a/docs/eng/backlog/20260909120011-important-csv-buffered-in-memory-line-simulator-bl.md b/docs/eng/backlog/open/20260909120011-important-csv-buffered-in-memory-line-simulator-bl.md similarity index 100% rename from docs/eng/backlog/20260909120011-important-csv-buffered-in-memory-line-simulator-bl.md rename to docs/eng/backlog/open/20260909120011-important-csv-buffered-in-memory-line-simulator-bl.md diff --git a/docs/eng/backlog/20260909120012-important-taps-seed-unsalted-line-simulator-bl.md b/docs/eng/backlog/open/20260909120012-important-taps-seed-unsalted-line-simulator-bl.md similarity index 100% rename from docs/eng/backlog/20260909120012-important-taps-seed-unsalted-line-simulator-bl.md rename to docs/eng/backlog/open/20260909120012-important-taps-seed-unsalted-line-simulator-bl.md diff --git a/docs/eng/backlog/20260909120013-important-clock-loop-duplicated-line-simulator-bl.md b/docs/eng/backlog/open/20260909120013-important-clock-loop-duplicated-line-simulator-bl.md similarity index 100% rename from docs/eng/backlog/20260909120013-important-clock-loop-duplicated-line-simulator-bl.md rename to docs/eng/backlog/open/20260909120013-important-clock-loop-duplicated-line-simulator-bl.md diff --git a/docs/eng/backlog/20260909120014-important-determinism-test-gaps-line-simulator-bl.md b/docs/eng/backlog/open/20260909120014-important-determinism-test-gaps-line-simulator-bl.md similarity index 100% rename from docs/eng/backlog/20260909120014-important-determinism-test-gaps-line-simulator-bl.md rename to docs/eng/backlog/open/20260909120014-important-determinism-test-gaps-line-simulator-bl.md diff --git a/docs/eng/backlog/20260909120015-important-end-marker-no-retry-nodes-bl.md b/docs/eng/backlog/open/20260909120015-important-end-marker-no-retry-nodes-bl.md similarity index 100% rename from docs/eng/backlog/20260909120015-important-end-marker-no-retry-nodes-bl.md rename to docs/eng/backlog/open/20260909120015-important-end-marker-no-retry-nodes-bl.md diff --git a/docs/eng/backlog/20260909120016-important-run-id-unvalidated-nodes-bl.md b/docs/eng/backlog/open/20260909120016-important-run-id-unvalidated-nodes-bl.md similarity index 100% rename from docs/eng/backlog/20260909120016-important-run-id-unvalidated-nodes-bl.md rename to docs/eng/backlog/open/20260909120016-important-run-id-unvalidated-nodes-bl.md diff --git a/docs/eng/backlog/20260909120017-important-taps-bad-row-double-loss-nodes-bl.md b/docs/eng/backlog/open/20260909120017-important-taps-bad-row-double-loss-nodes-bl.md similarity index 100% rename from docs/eng/backlog/20260909120017-important-taps-bad-row-double-loss-nodes-bl.md rename to docs/eng/backlog/open/20260909120017-important-taps-bad-row-double-loss-nodes-bl.md diff --git a/docs/eng/backlog/20260909120018-important-status-loss-on-disconnect-nodes-bl.md b/docs/eng/backlog/open/20260909120018-important-status-loss-on-disconnect-nodes-bl.md similarity index 100% rename from docs/eng/backlog/20260909120018-important-status-loss-on-disconnect-nodes-bl.md rename to docs/eng/backlog/open/20260909120018-important-status-loss-on-disconnect-nodes-bl.md diff --git a/docs/eng/backlog/20260909120019-important-terminal-not-restored-oee-dashboard-bl.md b/docs/eng/backlog/open/20260909120019-important-terminal-not-restored-oee-dashboard-bl.md similarity index 100% rename from docs/eng/backlog/20260909120019-important-terminal-not-restored-oee-dashboard-bl.md rename to docs/eng/backlog/open/20260909120019-important-terminal-not-restored-oee-dashboard-bl.md diff --git a/docs/eng/backlog/20260909120020-important-filter-hardcoded-prefix-oee-dashboard-bl.md b/docs/eng/backlog/open/20260909120020-important-filter-hardcoded-prefix-oee-dashboard-bl.md similarity index 100% rename from docs/eng/backlog/20260909120020-important-filter-hardcoded-prefix-oee-dashboard-bl.md rename to docs/eng/backlog/open/20260909120020-important-filter-hardcoded-prefix-oee-dashboard-bl.md diff --git a/docs/eng/backlog/20260909120021-important-zero-err-claim-unpinned-oee-aggregator-bl.md b/docs/eng/backlog/open/20260909120021-important-zero-err-claim-unpinned-oee-aggregator-bl.md similarity index 100% rename from docs/eng/backlog/20260909120021-important-zero-err-claim-unpinned-oee-aggregator-bl.md rename to docs/eng/backlog/open/20260909120021-important-zero-err-claim-unpinned-oee-aggregator-bl.md diff --git a/docs/eng/backlog/20260909120022-important-csv-truncate-vs-append-oee-aggregator-bl.md b/docs/eng/backlog/open/20260909120022-important-csv-truncate-vs-append-oee-aggregator-bl.md similarity index 100% rename from docs/eng/backlog/20260909120022-important-csv-truncate-vs-append-oee-aggregator-bl.md rename to docs/eng/backlog/open/20260909120022-important-csv-truncate-vs-append-oee-aggregator-bl.md diff --git a/docs/eng/backlog/20260909120023-important-no-max-packet-size-mqtt-min-bl.md b/docs/eng/backlog/open/20260909120023-important-no-max-packet-size-mqtt-min-bl.md similarity index 100% rename from docs/eng/backlog/20260909120023-important-no-max-packet-size-mqtt-min-bl.md rename to docs/eng/backlog/open/20260909120023-important-no-max-packet-size-mqtt-min-bl.md diff --git a/docs/eng/backlog/20260909120024-important-capture-channel-leak-mqtt-min-bl.md b/docs/eng/backlog/open/20260909120024-important-capture-channel-leak-mqtt-min-bl.md similarity index 100% rename from docs/eng/backlog/20260909120024-important-capture-channel-leak-mqtt-min-bl.md rename to docs/eng/backlog/open/20260909120024-important-capture-channel-leak-mqtt-min-bl.md diff --git a/docs/eng/backlog/20260909120025-important-keepalive-violated-mqtt-min-bl.md b/docs/eng/backlog/open/20260909120025-important-keepalive-violated-mqtt-min-bl.md similarity index 100% rename from docs/eng/backlog/20260909120025-important-keepalive-violated-mqtt-min-bl.md rename to docs/eng/backlog/open/20260909120025-important-keepalive-violated-mqtt-min-bl.md diff --git a/docs/eng/backlog/20260909120026-important-no-connect-write-timeouts-mqtt-min-bl.md b/docs/eng/backlog/open/20260909120026-important-no-connect-write-timeouts-mqtt-min-bl.md similarity index 100% rename from docs/eng/backlog/20260909120026-important-no-connect-write-timeouts-mqtt-min-bl.md rename to docs/eng/backlog/open/20260909120026-important-no-connect-write-timeouts-mqtt-min-bl.md diff --git a/docs/eng/backlog/20260909120027-important-duplicate-subscribe-mqtt-min-bl.md b/docs/eng/backlog/open/20260909120027-important-duplicate-subscribe-mqtt-min-bl.md similarity index 100% rename from docs/eng/backlog/20260909120027-important-duplicate-subscribe-mqtt-min-bl.md rename to docs/eng/backlog/open/20260909120027-important-duplicate-subscribe-mqtt-min-bl.md diff --git a/docs/eng/backlog/20260909120028-important-interp-panics-foreign-tflite-ml-exporter-bl.md b/docs/eng/backlog/open/20260909120028-important-interp-panics-foreign-tflite-ml-exporter-bl.md similarity index 100% rename from docs/eng/backlog/20260909120028-important-interp-panics-foreign-tflite-ml-exporter-bl.md rename to docs/eng/backlog/open/20260909120028-important-interp-panics-foreign-tflite-ml-exporter-bl.md diff --git a/docs/eng/backlog/20260909120029-important-bit-identical-untested-ml-trainer-bl.md b/docs/eng/backlog/open/20260909120029-important-bit-identical-untested-ml-trainer-bl.md similarity index 100% rename from docs/eng/backlog/20260909120029-important-bit-identical-untested-ml-trainer-bl.md rename to docs/eng/backlog/open/20260909120029-important-bit-identical-untested-ml-trainer-bl.md diff --git a/docs/eng/backlog/20260909120030-important-parity-gen-duplicates-quant-ml-exporter-bl.md b/docs/eng/backlog/open/20260909120030-important-parity-gen-duplicates-quant-ml-exporter-bl.md similarity index 100% rename from docs/eng/backlog/20260909120030-important-parity-gen-duplicates-quant-ml-exporter-bl.md rename to docs/eng/backlog/open/20260909120030-important-parity-gen-duplicates-quant-ml-exporter-bl.md diff --git a/docs/eng/backlog/20260909120034-minor-line-simulator-bl.md b/docs/eng/backlog/open/20260909120034-minor-line-simulator-bl.md similarity index 100% rename from docs/eng/backlog/20260909120034-minor-line-simulator-bl.md rename to docs/eng/backlog/open/20260909120034-minor-line-simulator-bl.md diff --git a/docs/eng/backlog/20260909120035-minor-nodes-bl.md b/docs/eng/backlog/open/20260909120035-minor-nodes-bl.md similarity index 100% rename from docs/eng/backlog/20260909120035-minor-nodes-bl.md rename to docs/eng/backlog/open/20260909120035-minor-nodes-bl.md diff --git a/docs/eng/backlog/20260909120036-minor-oee-aggregator-bl.md b/docs/eng/backlog/open/20260909120036-minor-oee-aggregator-bl.md similarity index 100% rename from docs/eng/backlog/20260909120036-minor-oee-aggregator-bl.md rename to docs/eng/backlog/open/20260909120036-minor-oee-aggregator-bl.md diff --git a/docs/eng/backlog/20260909120037-minor-oee-dashboard-bl.md b/docs/eng/backlog/open/20260909120037-minor-oee-dashboard-bl.md similarity index 100% rename from docs/eng/backlog/20260909120037-minor-oee-dashboard-bl.md rename to docs/eng/backlog/open/20260909120037-minor-oee-dashboard-bl.md diff --git a/docs/eng/backlog/20260909120038-minor-mqtt-min-bl.md b/docs/eng/backlog/open/20260909120038-minor-mqtt-min-bl.md similarity index 100% rename from docs/eng/backlog/20260909120038-minor-mqtt-min-bl.md rename to docs/eng/backlog/open/20260909120038-minor-mqtt-min-bl.md diff --git a/docs/eng/backlog/20260909120039-minor-ml-bl.md b/docs/eng/backlog/open/20260909120039-minor-ml-bl.md similarity index 100% rename from docs/eng/backlog/20260909120039-minor-ml-bl.md rename to docs/eng/backlog/open/20260909120039-minor-ml-bl.md diff --git a/docs/eng/backlog/20260909120040-minor-qemu-bl.md b/docs/eng/backlog/open/20260909120040-minor-qemu-bl.md similarity index 100% rename from docs/eng/backlog/20260909120040-minor-qemu-bl.md rename to docs/eng/backlog/open/20260909120040-minor-qemu-bl.md diff --git a/docs/eng/backlog/20260909120042-rec-line-simulator-bl.md b/docs/eng/backlog/open/20260909120042-rec-line-simulator-bl.md similarity index 100% rename from docs/eng/backlog/20260909120042-rec-line-simulator-bl.md rename to docs/eng/backlog/open/20260909120042-rec-line-simulator-bl.md diff --git a/docs/eng/backlog/20260909120043-rec-nodes-bl.md b/docs/eng/backlog/open/20260909120043-rec-nodes-bl.md similarity index 100% rename from docs/eng/backlog/20260909120043-rec-nodes-bl.md rename to docs/eng/backlog/open/20260909120043-rec-nodes-bl.md diff --git a/docs/eng/backlog/20260909120044-rec-oee-aggregator-bl.md b/docs/eng/backlog/open/20260909120044-rec-oee-aggregator-bl.md similarity index 100% rename from docs/eng/backlog/20260909120044-rec-oee-aggregator-bl.md rename to docs/eng/backlog/open/20260909120044-rec-oee-aggregator-bl.md diff --git a/docs/eng/backlog/20260909120045-rec-mqtt-min-bl.md b/docs/eng/backlog/open/20260909120045-rec-mqtt-min-bl.md similarity index 100% rename from docs/eng/backlog/20260909120045-rec-mqtt-min-bl.md rename to docs/eng/backlog/open/20260909120045-rec-mqtt-min-bl.md diff --git a/docs/eng/backlog/20260909120046-rec-ml-bl.md b/docs/eng/backlog/open/20260909120046-rec-ml-bl.md similarity index 100% rename from docs/eng/backlog/20260909120046-rec-ml-bl.md rename to docs/eng/backlog/open/20260909120046-rec-ml-bl.md diff --git a/docs/eng/backlog/20260909120048-info-rust-analyzer-macro-artifact-nodes-bl.md b/docs/eng/backlog/open/20260909120048-info-rust-analyzer-macro-artifact-nodes-bl.md similarity index 100% rename from docs/eng/backlog/20260909120048-info-rust-analyzer-macro-artifact-nodes-bl.md rename to docs/eng/backlog/open/20260909120048-info-rust-analyzer-macro-artifact-nodes-bl.md diff --git a/docs/eng/backlog/20260910104756-important-flaky-aggregator-pipeline-oee-aggregator-bl.md b/docs/eng/backlog/open/20260910104756-important-flaky-aggregator-pipeline-oee-aggregator-bl.md similarity index 100% rename from docs/eng/backlog/20260910104756-important-flaky-aggregator-pipeline-oee-aggregator-bl.md rename to docs/eng/backlog/open/20260910104756-important-flaky-aggregator-pipeline-oee-aggregator-bl.md diff --git a/docs/eng/decompose/firmware-shakedown-runbook.md b/docs/eng/decompose/firmware-shakedown-runbook.md new file mode 100644 index 0000000..3384ffa --- /dev/null +++ b/docs/eng/decompose/firmware-shakedown-runbook.md @@ -0,0 +1,431 @@ +# Hardware track: the detailed shakedown runbook (updated 2026-09-14) + +Russian original: [firmware-shakedown-runbook.md](../../rus/decompose/firmware-shakedown-runbook.md). + +Context: the host track (weeks 1–6 + the report) is complete; 2026-09-14 +closed S0 — all three boards are flashed and up (the release +`firmware/bin/20260914085743-*`, the logs are in the root READMEs, the +facts are in `firmware/NOTES.md`). The session plan — +`docs/eng/decompose/firmware.md`; the assembly guide — +`docs/eng/HARDWARE-assembly-guide.md`. Bridge serials: node A — +`5C94148486`, node Q — `5C94152266`, node P (CAM) — `5CCC048683`; there +are no `/dev/ttyUSB*` ports on this bench — the actual names are +`/dev/serial/by-id/usb-1a86_USB_Single_Serial_-if00`. + +| Step | Topic | Time | Code needed? | +| ---- | ------------------------------------------- | ------ | ------------------------ | +| 0 | the day's tails: commit the docs, CI | 5 min | — | +| 0.5 | the wire check of P | 5 min | — | +| 1 | S1: node A, ADC and calibration | ~3 h | + the capture mode | +| 2 | S2: node A, statuses across regimes | ~2 h | everything exists | +| 3 | S3: the board vs the host twin cross-check | ~2 h | everything exists | +| 4 | S4: node Q, the INMP441 I2S driver | ~3 h | + the I2S driver | +| 5 | S5: node Q, the servo and the verdicts | ~3 h | everything exists | +| 6 | S6: node P, part counting | ~2 h | everything exists | +| 7 | S7: the track gate and the phase-2 decision | ~2–3 h | + the gate docs | +| 8 | phase 2: the bench in the full OEE loop | ~1–2 h | exists (the uart-bridge) | + +## Step 0 — the day's tails (5 min) + +Commit the day's documentation (the root READMEs ×2, `firmware/README ×2`, +`firmware/NOTES.md`, `firmware/bin/20260914085743-build-info.md`) and +check CI on the `hardware-firmware` branch: the `firmware` job builds the +working tree with the `esp-bootloader-esp-idf` 0.6 dependency for the +first time (it requires rustc 1.95 or newer; CI installs a fresh espup, +expected green). If the job is red exactly on the rustc version — record +the fact in NOTES and roll the dependency back to 0.5 (minimum rustc +1.88). + +## Step 0.5 — the wire check of node P (5 min) + +Short GPIO5 to 3V3 with a jumper wire: hold for at least 60 ms, release, +repeat. The 50 ms debounce ignores closures shorter than 50 ms, so every +correct touch gives exactly one counter increment — the monitor shows a +`p,bench-p,,N` line with the next N. If the counter grows from +short touches (below 50 ms), the `DEBOUNCE_MS` constant in the firmware +does not match the actual parameter — that is a finding for NOTES, not +for a silent fix. + +## Step 1 — S1. Node A: the ADC and calibration (~3 h) + +The goal: prove with numbers that the current the board measures matches +the physical current. At rest the firmware must read zero (within the +tolerance); under a known load it must match the multimeter. + +Wiring: + +- ACS712-20A: VCC — 5 V, GND — common with the board, OUT — through a + 2:1 resistor divider (for example, 10 kΩ + 10 kΩ) to GPIO4 + (`board::node_a::ADC_CURRENT`). The divider is mandatory: the ACS712-20A + output at zero current is 2.5 V, the sensitivity is 100 mV per ampere, + the working range is 0.5–4.5 V; the ESP32-S3 ADC with 11 dB attenuation + measures up to about 3.1 V, and the divider maps the sensor range into + 0.25–2.25 V. +- GPIO4 is chosen because it is an ADC1 channel: the ADC2 channels on the + ESP32-S3 conflict with Wi-Fi, and phase 2 may theoretically use it. + +What is already implemented: at startup the node averages 800 readings +taken every 625 μs (half a second, 1.6 kHz) and prints `a: zero=NNN` — +the startup zero point (`CurrentCalibration::acs712_20a_div2()` plus +`with_zero_counts`). Then every 128 readings (an 80 ms window) the whole +window goes into `model_a`; a status change is printed only after +confirmation by two consecutive windows (the ×2 hysteresis) as an +`a,bench-a,,` line. + +What is missing (this session's code): the capture mode. Today the board +prints only statuses, but the multimeter comparison and step 3 need +capture lines in the `features_cli::capture` schema — +`t_ms,node,run_id,value,state,note`. Add to `firmware-a` behind a +compile-time constant: once per window (80 ms) print a line with the +window-average current in amperes. Traffic estimate: 12.5 lines per +second at ~35 bytes — about 440 bytes per second against the UART's +115200 baud ≈ 11.5 kilobytes per second, a twentyfold margin. Printing +every individual sample is not possible: 1600 lines per second at the +same ~35 bytes give 56 kilobytes per second — the UART will not keep up +and lines will be lost. + +Actions: + +1. Wire the sensor as above; do not power the load yet. +2. Rebuild the image with the capture mode and flash board A (the port by + the `5C94148486` serial), open the monitor recording to a file. +3. Rest: 60 seconds of recording. The criterion: the average `value` is + at most 0.05 A in absolute value, the span (max minus min) is at most + 0.1 A. Tolerance derivation: one ADC count corresponds to about 15 mA + (3.1 V / 4095 counts — that is 0.76 mV per count at the board pin, the + divider doubles the voltage, and the sensor sensitivity is 100 mV per + ampere); 0.05 A is three counts. +4. Linearity: the multimeter in the 10 A current mode in series with the + load. Three levels: 0.5 A, 1.0 A, 2.0 A — resistors on 5 V: 10 Ω rated + at least 5 W (0.5 A), 5.1 Ω at least 10 W (about 1.0 A), 2.7 Ω at + least 25 W (about 1.9 A). Record 30 seconds at each level. +5. For each level compute the average `value` from the capture file and + compare with the multimeter reading. + +Pass criteria: rest — per item 3; linearity — at each level the +board-versus-multimeter deviation is at most the larger of 0.1 A and 5 +percent of the reading; the `a: zero=` value changes by at most 5 counts +between two resets (Ctrl+R in the monitor). + +Failure scenarios and their meaning: the readings are about half the +multimeter — the divider is not 2:1, or the sensor module has a different +sensitivity (the ACS712-5A is 185 mV per ampere; check the module marking +against the `acs712_20a_div2` constant); zero drifted while `zero=` is +stable — the calibration ran before the sensor was connected, reset the +board with the sensor attached; the values jump by ±0.5 A — no common +GND between the sensor supply and the board, or the input is floating. + +Record in NOTES: the `zero` value with the sensor attached, the table +"level, board (A), multimeter (A), deviation", the final tolerance. + +## Step 2 — S2. Node A: statuses across regimes (~2 h) + +The goal: show that the status follows the physical regime within known +delay bounds and does not flip on its own inside a regime. + +Scenario: the node distinguishes regimes by current; physically imitate +them with the same load from step 1 — alternate 0 A (30 seconds, the +idle regime) and 1–2 A (30 seconds, the run regime), at least five +switches each way and no more often than one switch per two seconds. + +Where the delay bounds come from: a window of 128 readings at 1.6 kHz is +80 ms; the hysteresis confirms a change only after two consecutive +windows. So a status change cannot appear earlier than 80 ms and no later +than three windows, i.e. 240 ms. + +Actions: record the monitor to a file for the whole run; for each switch +compute the delay — the status line time minus the actual switch time by +the stopwatch; count spontaneous flips inside the 30-second phases. + +Pass criteria: each of the ten or more switches fits the 80–500 ms +interval (240 ms of theory plus a stopwatch-accuracy margin); spontaneous +flips inside the phases — zero over at least five minutes of recording. + +Failure scenarios: the delay is consistently over a second — the actual +run current is below what the training set maps to the run regime +(compare the `value` during the phase with the week-4 experiment data); +the status flip-flops at the boundary — the phase current sits exactly at +the class-separation threshold, raise the run-current level (the +hysteresis must not be changed — the ×2 contract is pinned by tests). + +Record in NOTES: the number of switches, the minimum, median and maximum +delays, the number of spontaneous flips. + +## Step 3 — S3. The cross-check: the board vs the host twin (~2 h) + +The goal: on one and the same physical run prove that the hardware node A +and the host node A produce the same status sequences, with discrepancies +allowed only at window boundaries. + +Why boundary discrepancies are normal: both sides use the same model (the +rust-born `model_a` with the bit-exact Conv1D kernel), the same ×2 +hysteresis, and the same input — the current in amperes computed on the +board. The only difference is the window phase: the board's windows start +from its boot moment, the host node's windows start from the first line +of the input file, so the samples at a regime-switch boundary fall into +different windows. + +Actions: + +1. Take the board capture file from step 1 or 2 (the monitor recording) — + `tmp/s3.capture.csv`. +2. Convert it with the repository utility: it reads standard input, + renames the `value` column to `current_a`, keeps the node A lines and + writes the host node's ready input: + `cargo run --release -p firmware-tools --bin capture-to-run < tmp/s3.capture.csv > tmp/s3.run.csv` + (the utility prints a counter to stderr: how many lines were taken and + how many skipped — the header, other nodes, bad lines). +3. Run the host node on the same run: + `cargo run --release -p nodes --bin node -- --kind a --input tmp/s3.run.csv --offline tmp/s3.statuses.csv --run-id bench-a` + (without `--mqtt` — an offline comparison, no broker needed). +4. Compare the two status sequences: the `a,bench-a,…` lines from the + board's monitor recording against the lines from `tmp/s3.statuses.csv`. + +Pass criteria: one hundred percent status agreement outside the zones +adjacent to regime switches (a zone is two windows, 160 ms, on each side +of a switch); inside the zones discrepancies are allowed. Any discrepancy +outside the zones is an escalation-level finding from the track plan +("why did the parity test not catch it"): fix the cause and close it with +a test, do not accept the discrepancy. + +Record in NOTES: the table "total switches, agreed, discrepancies, +whether all discrepancies are inside boundary zones". + +## Step 4 — S4. Node Q: the INMP441 I2S driver (~3 h, the only big piece of code) + +The goal: replace the synthetic window with real sound. Today node Q's +verdict is always `cracked` — pinned by a test as the loop check; the +verdicts gain meaning only with the real microphone. + +INMP441 wiring: VDD — strictly 3.3 V (at 5 V the module dies), GND — +common, the L/R channel-select input — to GND (the left channel), SCK — +GPIO12 (`board::node_q::I2S_SCK`), WS — GPIO13 (`I2S_WS`), SD — GPIO14 +(`I2S_SD`). + +Code: + +1. Add an I2S receiver to `firmware-q`: esp-hal, the `i2s::master` module + (the driver modules are already exposed by the `unstable` feature), + the Philips standard mode, the 16 kHz sample rate (the `window_spec(Q)` + contract), a 32-bit slot, one channel (left), reception over DMA into + a 1024-slot buffer (4 kilobytes). The window is read with one blocking + read right after the servo strike. +2. The slot conversion is already written and pinned by host tests: + `i2s_slots_to_f32` takes the top 24 bits of the 32-bit slot, + sign-extends and divides by 2 to the power 23 — the full scale comes + out at about ±1.0. The byte and channel order is exactly the question + the conversion was lifted into a testable library for. +3. In the main loop replace the `synthetic_tap_window(window)` call with + the DMA window read and conversion. Do not delete `synthetic_tap_window` + from the library: its verdict is pinned by a test and stays the + reference point. +4. A temporary dump mode (behind the same compile constant as the capture + in step 1): print the first window after startup — the spectrum is + computed from it. + +The tone check: + +1. Play a 1 kHz tone near the microphone (a generator or a phone tone + app, normal speech loudness). +2. Take a window dump and compute the spectrum on the host by any means + (for example numpy: the discrete Fourier transform of the 1024 + values). +3. Expectation: the peak in bin 64. The math: the bin width is 16000 Hz + / 1024 = 15.625 Hz; 1000 Hz / 15.625 = bin 64; the ±5 percent + tolerance — the 950–1050 Hz band, bins 61–67. +4. The sample rate is confirmed by the same peak: were it half, the 1 kHz + tone would land in bin 32. The window duration is 1024 / 16000 = + exactly 64 ms. +5. Amplitude: in silence the values are around 0.001–0.01, on the tone — + 0.1–0.5. + +Failure scenarios: exact zeros in every window — the L/R channel-select +level is wrong, or SCK and WS are swapped; a raw-slot dump (before +conversion) will show zeros immediately. The peak in bin 32 — the actual +sample rate is 8 kHz, an I2S configuration error. Noise across all bins +instead of a peak — pickup on the data line: shorten the wires, check the +common GND. + +Record in NOTES: the discovered byte and channel order (whether it +matched the conversion contract), the peak bin, the silence and tone +amplitudes. + +After this step — re-release the images into `firmware/bin/` by the same +procedure as 2026-09-14 (a new timestamp, hashes, the old set to +`tmp/trash/`), because the Q firmware behavior changes. + +## Step 5 — S5. Node Q: the servo tapper and the verdicts on reference parts (~3 h) + +The goal: the verdicts depend on the part. A good part yields mostly +`good`, a cracked one — mostly `cracked`. + +Servo wiring: the SG90 signal wire to GPIO11 +(`board::node_q::SERVO_PWM`); the servo power — only from a separate 5 V +supply rated at least 1 A, the supply's minus tied to the board's GND; a +470 μF capacitor across the servo supply terminals. The failure scenario: +if the servo is powered from the board's USB port, at the strike moment +the servo draws an inrush of about 0.5–1 A, the 5 V rail sags below the +processor's brownout threshold, and the board reboots mid-tap — in the +monitor it looks like a sudden repeat of the boot banner. + +What is already implemented: a 50 Hz PWM with 14-bit duty; the strike — +6 percent duty (a ~1.2 ms pulse), rest — 8 percent (~1.6 ms); after the +strike a 30 ms pause (the arm leaves, the part's ring begins), then the +64 ms window; the full cycle is 400 ms. Where the arm strikes and its +length are mechanical-assembly parameters, not code. + +Actions: + +1. Mount the servo over the part holder so that the arm touches the part + on actuation — the strike must be audible. +2. Power the servo from the separate supply and make sure the board does + not reboot across ten consecutive strikes (no repeated boot banners in + the monitor). +3. Five good parts: at least three strikes each, write down all the + verdicts. +4. Five cracked parts: the same. +5. If the dump mode from step 4 is still in the firmware — capture one + window for a good and a cracked part: the spectra must differ + noticeably. + +Pass criteria: for each part at least two thirds of the strikes give its +class verdict; in total at least eight of ten parts are classified +correctly. Every discrepancy — into NOTES with a description of the part. + +Failure scenarios: the verdicts do not depend on the part (for example +all `cracked`, as with the synthetic) — the window missed the ring; a +dump will show the sound starting after the window start, and then the +post-strike pause is increased in the firmware (the `SETTLE_MS` constant, +currently 30 ms, the next sensible step is 50 ms), or the servo +physically misses the part. The verdicts are right but the board reboots +on strikes — the supply is still insufficient: at the strike moment the +servo terminals must stay above roughly 4.5 V by multimeter. + +Record in NOTES: the servo strike current (a multimeter in the supply +break), the per-part verdict table, any `SETTLE_MS` changes. + +## Step 6 — S6. Node P: part counting (~2 h) + +The goal: the counter equals the fact — N passed parts give exactly N +increments, and the bounce at the sensor edge creates no false counts. + +The mandatory pre-mount check: node P lives on the CAM board, the sensor +is assigned to GPIO5 (`board::node_p::IR_OUT`), but the OV2640 camera +wiring on boards from different vendors occupies different pins, and on +some boards GPIO5 belongs to the camera. Check your board's pinout +against its schematic: if GPIO5 is taken, the sensor moves to a free pin +and the fix lands in the `board` crate (the bench-configuration level; +the node contract does not change). The `used_pins_are_free_and_distinct` +test will verify that the new pin is not in the reserved list: 0, 3, 45, +46 — the boot-strapping pins; 19, 20 — the USB lines; 43, 44 — the UART0 +console; 35, 36, 37 — the octal-PSRAM lines of the N16R8 module. + +TCRT5000 wiring: the module with a comparator — VCC per the module's +marking (3.3 or 5 V), GND common, OUT to GPIO5. The sensor is reflective +infrared: it triggers on reflection from an object at roughly 1–25 mm, +the threshold is tuned by the on-module trimmer. + +Actions: + +1. Tune the distance: a part at 5–10 mm from the sensor — the module's + LED lights up. +2. The quick wire check from step 0.5 (every correct touch longer than + 60 ms — exactly one increment). +3. Run 1: 20 slow passes of a part (or a finger) — the final count + exactly 20. +4. Run 2: 20 passes at the pace of the future belt. +5. The bounce test: hold a target at the triggering edge and wiggle it + for five seconds. + +Pass criteria: in both runs the count-versus-fact deviation is zero; the +bounce test gives at most two extra increments in five seconds (the 50 ms +debounce suppresses series shorter than 50 ms; sustained re-crossings +longer than 50 ms are real events already). + +Failure scenarios: the count is double the fact — the signal drops below +the threshold and returns mid-pass (visible on an OUT-level recording): +increase the sensor gap or reduce the sensitivity with the trimmer. The +count is zero although the module's LED triggers — the module has an +inverted output (active low): check the OUT level at rest and at +triggering with a tester; on inversion the fix goes into the pin-reading +driver in the firmware (not the contract) and is recorded in NOTES. + +Record in NOTES: the trigger polarity, the distance, the results of both +runs, the bounce-test result. + +## Step 7 — S7. The track gate and the phase-2 decision (~2–3 h) + +The goal: shape the S0–S6 results into verifiable artifacts and make the +phase-2 decision. + +Actions: + +1. Finalize `firmware/NOTES.md`: each session must have its numbers — S1: + `zero`, the linearity table, the tolerance; S2: the status delays; S3: + the cross-check table; S4: the I2S byte and channel order, the tone + bin; S5: the servo current, the per-part verdicts; S6: the sensor + polarity and distance, the counting results. +2. Write `docs/rus/firmware-gate.md` and `docs/eng/firmware-gate.md` + following the weekly-gate pattern (week1–week6-gate). The track gate + checklist is listed in the decomposition: the A statuses match the + host ones on the same data; the Q verdicts on reference parts match + the model on the same windows; the P count equals the run's fact; the + board's capture CSV is readable by the host tooling and reproducible; + the CI `firmware` job is green; NOTES is filled. For every item — + passed, or a deviation with the reason. The "blinky skipped, the + working firmware-a flashed at once" deviation is already in NOTES — + carry it into the gate too. +3. The phase-2 decision with justification: the UART bridge (already + written and tested, not a single line of network code on the boards) + versus esp-wifi with a ported mqtt-min (a week of extra work plus the + risk of the bench's Wi-Fi environment). Recommendation: the bridge — + it closes the goal today; Wi-Fi — into the report's future work. +4. Check that both CI jobs (`firmware` — the xtensa build with the new + dependency, `firmware-host` — formatting, clippy, the library tests) + are green on the latest commit. + +Pass criterion: both gate docs are written, every checklist item has a +status, the phase-2 decision is recorded with its justification. + +## Step 8 — phase 2: the bench in the full OEE loop (~1–2 h, after the gate) + +The goal: the physical bench replaces the host nodes in the week-5 loop — +the three boards' lines reach MQTT through the UART bridge, the +aggregator folds them into OEE, the dashboard shows live metrics. + +Actions: + +1. Terminal 1 — the loop's tail end (the "The bench loop" section of + `firmware/README.md`): + `./target/debug/broker 1883 &`; + `./target/debug/aggregator --mqtt 127.0.0.1:1883 --ideal-cycle-ms 400 --out windows.csv`; + `./target/debug/oee-dashboard --mqtt 127.0.0.1:1883`. +2. Terminals 2–4 — one bridge per board, the ports by the serials: + `cat /dev/serial/by-id/usb-1a86_USB_Single_Serial_5C94148486-if00 | cargo run -p firmware-tools --bin uart-bridge -- 127.0.0.1:1883` + and two analogous commands for the serials `5C94152266` (Q) and + `5CCC048683` (P). The bridge drops the boot lines (pinned by its + tests), publishes the statuses, verdicts and counts to `oee/line1/*`, + and on input-stream break prints the end marker — the aggregator + closes the window correctly. +3. The no-hardware check (known): feed the bridge four lines via printf — + expect `bridge: 4 messages (a+p+q)` in the output. +4. The live run: turn on the physical stimuli — the current for node A, + the taps for Q, the part passes for P — and watch the dashboard. + +Pass criterion: over a five-minute run the dashboard shows A status +changes, Q verdicts with every tap, the P count equal to the passed +parts, and `windows.csv` fills with window rows without gaps between the +end markers. + +Failure scenarios: the dashboard is empty, the bridge is silent — the +board line does not pass the bridge's whitelist; compare the actual line +from the monitor with the printf-test line format (`a,bench-a,1000,run`). +The OEE does not converge with the expectation — the 400 ms ideal-cycle +parameter is given to the aggregator while the bench's actual tap pace +differs; reconcile `--ideal-cycle-ms` with the fact (it is an aggregator +parameter, the firmware does not change). + +A documentation note: the assembly guide +(`docs/eng/HARDWARE-assembly-guide.md` and its Russian original) names +the ports with the old `/dev/ttyUSB0` — on this bench the actual names +are `/dev/ttyACM*` or by-id; update the guide when convenient. diff --git a/docs/eng/decompose/firmware.md b/docs/eng/decompose/firmware.md index 84c683d..e524107 100644 --- a/docs/eng/decompose/firmware.md +++ b/docs/eng/decompose/firmware.md @@ -12,6 +12,7 @@ > and the stretch camera; the sensors ACS712-20A, INMP441, TCRT5000, a servo > tapper, a separate 5 V supply. Bench details and the bring-up procedure — > [`firmware/README.md`](../../../firmware/README.md). +> The detailed execution runbook (wiring, commands, acceptance criteria) — [firmware-shakedown-runbook.md](firmware-shakedown-runbook.md). > Input: the contracts are already in the repo — `features-cli` > (`window_spec`, the ADC→amps calibration, the `capture` schema), diff --git a/docs/rus/backlog/resolved/20260909120005-critical-counts-levels-not-edges-firmware-p-bl.md b/docs/rus/backlog/done/20260909120005-critical-counts-levels-not-edges-firmware-p-bl.md similarity index 100% rename from docs/rus/backlog/resolved/20260909120005-critical-counts-levels-not-edges-firmware-p-bl.md rename to docs/rus/backlog/done/20260909120005-critical-counts-levels-not-edges-firmware-p-bl.md diff --git a/docs/rus/backlog/resolved/20260909120006-critical-tms-always-zero-firmware-a-bl.md b/docs/rus/backlog/done/20260909120006-critical-tms-always-zero-firmware-a-bl.md similarity index 100% rename from docs/rus/backlog/resolved/20260909120006-critical-tms-always-zero-firmware-a-bl.md rename to docs/rus/backlog/done/20260909120006-critical-tms-always-zero-firmware-a-bl.md diff --git a/docs/rus/backlog/resolved/20260909120007-important-features-cli-contract-drift-workspace-bl.md b/docs/rus/backlog/done/20260909120007-important-features-cli-contract-drift-workspace-bl.md similarity index 100% rename from docs/rus/backlog/resolved/20260909120007-important-features-cli-contract-drift-workspace-bl.md rename to docs/rus/backlog/done/20260909120007-important-features-cli-contract-drift-workspace-bl.md diff --git a/docs/rus/backlog/resolved/20260909120031-important-qemu-not-in-ci-ci-bl.md b/docs/rus/backlog/done/20260909120031-important-qemu-not-in-ci-ci-bl.md similarity index 100% rename from docs/rus/backlog/resolved/20260909120031-important-qemu-not-in-ci-ci-bl.md rename to docs/rus/backlog/done/20260909120031-important-qemu-not-in-ci-ci-bl.md diff --git a/docs/rus/backlog/resolved/20260909120032-important-q-stack-overflow-firmware-q-bl.md b/docs/rus/backlog/done/20260909120032-important-q-stack-overflow-firmware-q-bl.md similarity index 100% rename from docs/rus/backlog/resolved/20260909120032-important-q-stack-overflow-firmware-q-bl.md rename to docs/rus/backlog/done/20260909120032-important-q-stack-overflow-firmware-q-bl.md diff --git a/docs/rus/backlog/resolved/20260909120033-important-bridge-dies-on-bad-byte-firmware-tools-bl.md b/docs/rus/backlog/done/20260909120033-important-bridge-dies-on-bad-byte-firmware-tools-bl.md similarity index 100% rename from docs/rus/backlog/resolved/20260909120033-important-bridge-dies-on-bad-byte-firmware-tools-bl.md rename to docs/rus/backlog/done/20260909120033-important-bridge-dies-on-bad-byte-firmware-tools-bl.md diff --git a/docs/rus/backlog/resolved/20260909120041-minor-firmware-bl.md b/docs/rus/backlog/done/20260909120041-minor-firmware-bl.md similarity index 100% rename from docs/rus/backlog/resolved/20260909120041-minor-firmware-bl.md rename to docs/rus/backlog/done/20260909120041-minor-firmware-bl.md diff --git a/docs/rus/backlog/resolved/20260909120047-rec-firmware-bl.md b/docs/rus/backlog/done/20260909120047-rec-firmware-bl.md similarity index 100% rename from docs/rus/backlog/resolved/20260909120047-rec-firmware-bl.md rename to docs/rus/backlog/done/20260909120047-rec-firmware-bl.md diff --git a/docs/rus/backlog/20260909120001-critical-invalid-qmeta-json-nodes-bl.md b/docs/rus/backlog/open/20260909120001-critical-invalid-qmeta-json-nodes-bl.md similarity index 100% rename from docs/rus/backlog/20260909120001-critical-invalid-qmeta-json-nodes-bl.md rename to docs/rus/backlog/open/20260909120001-critical-invalid-qmeta-json-nodes-bl.md diff --git a/docs/rus/backlog/20260909120002-critical-hang-on-lost-end-marker-oee-aggregator-bl.md b/docs/rus/backlog/open/20260909120002-critical-hang-on-lost-end-marker-oee-aggregator-bl.md similarity index 100% rename from docs/rus/backlog/20260909120002-critical-hang-on-lost-end-marker-oee-aggregator-bl.md rename to docs/rus/backlog/open/20260909120002-critical-hang-on-lost-end-marker-oee-aggregator-bl.md diff --git a/docs/rus/backlog/20260909120003-critical-midpacket-retry-desync-mqtt-min-bl.md b/docs/rus/backlog/open/20260909120003-critical-midpacket-retry-desync-mqtt-min-bl.md similarity index 100% rename from docs/rus/backlog/20260909120003-critical-midpacket-retry-desync-mqtt-min-bl.md rename to docs/rus/backlog/open/20260909120003-critical-midpacket-retry-desync-mqtt-min-bl.md diff --git a/docs/rus/backlog/20260909120004-critical-clamp-panic-missing-class-ml-trainer-bl.md b/docs/rus/backlog/open/20260909120004-critical-clamp-panic-missing-class-ml-trainer-bl.md similarity index 100% rename from docs/rus/backlog/20260909120004-critical-clamp-panic-missing-class-ml-trainer-bl.md rename to docs/rus/backlog/open/20260909120004-critical-clamp-panic-missing-class-ml-trainer-bl.md diff --git a/docs/rus/backlog/20260909120008-important-f32-time-degradation-line-simulator-bl.md b/docs/rus/backlog/open/20260909120008-important-f32-time-degradation-line-simulator-bl.md similarity index 100% rename from docs/rus/backlog/20260909120008-important-f32-time-degradation-line-simulator-bl.md rename to docs/rus/backlog/open/20260909120008-important-f32-time-degradation-line-simulator-bl.md diff --git a/docs/rus/backlog/20260909120009-important-toml-validation-panics-line-simulator-bl.md b/docs/rus/backlog/open/20260909120009-important-toml-validation-panics-line-simulator-bl.md similarity index 100% rename from docs/rus/backlog/20260909120009-important-toml-validation-panics-line-simulator-bl.md rename to docs/rus/backlog/open/20260909120009-important-toml-validation-panics-line-simulator-bl.md diff --git a/docs/rus/backlog/20260909120010-important-unconditional-current-stream-line-simulator-bl.md b/docs/rus/backlog/open/20260909120010-important-unconditional-current-stream-line-simulator-bl.md similarity index 100% rename from docs/rus/backlog/20260909120010-important-unconditional-current-stream-line-simulator-bl.md rename to docs/rus/backlog/open/20260909120010-important-unconditional-current-stream-line-simulator-bl.md diff --git a/docs/rus/backlog/20260909120011-important-csv-buffered-in-memory-line-simulator-bl.md b/docs/rus/backlog/open/20260909120011-important-csv-buffered-in-memory-line-simulator-bl.md similarity index 100% rename from docs/rus/backlog/20260909120011-important-csv-buffered-in-memory-line-simulator-bl.md rename to docs/rus/backlog/open/20260909120011-important-csv-buffered-in-memory-line-simulator-bl.md diff --git a/docs/rus/backlog/20260909120012-important-taps-seed-unsalted-line-simulator-bl.md b/docs/rus/backlog/open/20260909120012-important-taps-seed-unsalted-line-simulator-bl.md similarity index 100% rename from docs/rus/backlog/20260909120012-important-taps-seed-unsalted-line-simulator-bl.md rename to docs/rus/backlog/open/20260909120012-important-taps-seed-unsalted-line-simulator-bl.md diff --git a/docs/rus/backlog/20260909120013-important-clock-loop-duplicated-line-simulator-bl.md b/docs/rus/backlog/open/20260909120013-important-clock-loop-duplicated-line-simulator-bl.md similarity index 100% rename from docs/rus/backlog/20260909120013-important-clock-loop-duplicated-line-simulator-bl.md rename to docs/rus/backlog/open/20260909120013-important-clock-loop-duplicated-line-simulator-bl.md diff --git a/docs/rus/backlog/20260909120014-important-determinism-test-gaps-line-simulator-bl.md b/docs/rus/backlog/open/20260909120014-important-determinism-test-gaps-line-simulator-bl.md similarity index 100% rename from docs/rus/backlog/20260909120014-important-determinism-test-gaps-line-simulator-bl.md rename to docs/rus/backlog/open/20260909120014-important-determinism-test-gaps-line-simulator-bl.md diff --git a/docs/rus/backlog/20260909120015-important-end-marker-no-retry-nodes-bl.md b/docs/rus/backlog/open/20260909120015-important-end-marker-no-retry-nodes-bl.md similarity index 100% rename from docs/rus/backlog/20260909120015-important-end-marker-no-retry-nodes-bl.md rename to docs/rus/backlog/open/20260909120015-important-end-marker-no-retry-nodes-bl.md diff --git a/docs/rus/backlog/20260909120016-important-run-id-unvalidated-nodes-bl.md b/docs/rus/backlog/open/20260909120016-important-run-id-unvalidated-nodes-bl.md similarity index 100% rename from docs/rus/backlog/20260909120016-important-run-id-unvalidated-nodes-bl.md rename to docs/rus/backlog/open/20260909120016-important-run-id-unvalidated-nodes-bl.md diff --git a/docs/rus/backlog/20260909120017-important-taps-bad-row-double-loss-nodes-bl.md b/docs/rus/backlog/open/20260909120017-important-taps-bad-row-double-loss-nodes-bl.md similarity index 100% rename from docs/rus/backlog/20260909120017-important-taps-bad-row-double-loss-nodes-bl.md rename to docs/rus/backlog/open/20260909120017-important-taps-bad-row-double-loss-nodes-bl.md diff --git a/docs/rus/backlog/20260909120018-important-status-loss-on-disconnect-nodes-bl.md b/docs/rus/backlog/open/20260909120018-important-status-loss-on-disconnect-nodes-bl.md similarity index 100% rename from docs/rus/backlog/20260909120018-important-status-loss-on-disconnect-nodes-bl.md rename to docs/rus/backlog/open/20260909120018-important-status-loss-on-disconnect-nodes-bl.md diff --git a/docs/rus/backlog/20260909120019-important-terminal-not-restored-oee-dashboard-bl.md b/docs/rus/backlog/open/20260909120019-important-terminal-not-restored-oee-dashboard-bl.md similarity index 100% rename from docs/rus/backlog/20260909120019-important-terminal-not-restored-oee-dashboard-bl.md rename to docs/rus/backlog/open/20260909120019-important-terminal-not-restored-oee-dashboard-bl.md diff --git a/docs/rus/backlog/20260909120020-important-filter-hardcoded-prefix-oee-dashboard-bl.md b/docs/rus/backlog/open/20260909120020-important-filter-hardcoded-prefix-oee-dashboard-bl.md similarity index 100% rename from docs/rus/backlog/20260909120020-important-filter-hardcoded-prefix-oee-dashboard-bl.md rename to docs/rus/backlog/open/20260909120020-important-filter-hardcoded-prefix-oee-dashboard-bl.md diff --git a/docs/rus/backlog/20260909120021-important-zero-err-claim-unpinned-oee-aggregator-bl.md b/docs/rus/backlog/open/20260909120021-important-zero-err-claim-unpinned-oee-aggregator-bl.md similarity index 100% rename from docs/rus/backlog/20260909120021-important-zero-err-claim-unpinned-oee-aggregator-bl.md rename to docs/rus/backlog/open/20260909120021-important-zero-err-claim-unpinned-oee-aggregator-bl.md diff --git a/docs/rus/backlog/20260909120022-important-csv-truncate-vs-append-oee-aggregator-bl.md b/docs/rus/backlog/open/20260909120022-important-csv-truncate-vs-append-oee-aggregator-bl.md similarity index 100% rename from docs/rus/backlog/20260909120022-important-csv-truncate-vs-append-oee-aggregator-bl.md rename to docs/rus/backlog/open/20260909120022-important-csv-truncate-vs-append-oee-aggregator-bl.md diff --git a/docs/rus/backlog/20260909120023-important-no-max-packet-size-mqtt-min-bl.md b/docs/rus/backlog/open/20260909120023-important-no-max-packet-size-mqtt-min-bl.md similarity index 100% rename from docs/rus/backlog/20260909120023-important-no-max-packet-size-mqtt-min-bl.md rename to docs/rus/backlog/open/20260909120023-important-no-max-packet-size-mqtt-min-bl.md diff --git a/docs/rus/backlog/20260909120024-important-capture-channel-leak-mqtt-min-bl.md b/docs/rus/backlog/open/20260909120024-important-capture-channel-leak-mqtt-min-bl.md similarity index 100% rename from docs/rus/backlog/20260909120024-important-capture-channel-leak-mqtt-min-bl.md rename to docs/rus/backlog/open/20260909120024-important-capture-channel-leak-mqtt-min-bl.md diff --git a/docs/rus/backlog/20260909120025-important-keepalive-violated-mqtt-min-bl.md b/docs/rus/backlog/open/20260909120025-important-keepalive-violated-mqtt-min-bl.md similarity index 100% rename from docs/rus/backlog/20260909120025-important-keepalive-violated-mqtt-min-bl.md rename to docs/rus/backlog/open/20260909120025-important-keepalive-violated-mqtt-min-bl.md diff --git a/docs/rus/backlog/20260909120026-important-no-connect-write-timeouts-mqtt-min-bl.md b/docs/rus/backlog/open/20260909120026-important-no-connect-write-timeouts-mqtt-min-bl.md similarity index 100% rename from docs/rus/backlog/20260909120026-important-no-connect-write-timeouts-mqtt-min-bl.md rename to docs/rus/backlog/open/20260909120026-important-no-connect-write-timeouts-mqtt-min-bl.md diff --git a/docs/rus/backlog/20260909120027-important-duplicate-subscribe-mqtt-min-bl.md b/docs/rus/backlog/open/20260909120027-important-duplicate-subscribe-mqtt-min-bl.md similarity index 100% rename from docs/rus/backlog/20260909120027-important-duplicate-subscribe-mqtt-min-bl.md rename to docs/rus/backlog/open/20260909120027-important-duplicate-subscribe-mqtt-min-bl.md diff --git a/docs/rus/backlog/20260909120028-important-interp-panics-foreign-tflite-ml-exporter-bl.md b/docs/rus/backlog/open/20260909120028-important-interp-panics-foreign-tflite-ml-exporter-bl.md similarity index 100% rename from docs/rus/backlog/20260909120028-important-interp-panics-foreign-tflite-ml-exporter-bl.md rename to docs/rus/backlog/open/20260909120028-important-interp-panics-foreign-tflite-ml-exporter-bl.md diff --git a/docs/rus/backlog/20260909120029-important-bit-identical-untested-ml-trainer-bl.md b/docs/rus/backlog/open/20260909120029-important-bit-identical-untested-ml-trainer-bl.md similarity index 100% rename from docs/rus/backlog/20260909120029-important-bit-identical-untested-ml-trainer-bl.md rename to docs/rus/backlog/open/20260909120029-important-bit-identical-untested-ml-trainer-bl.md diff --git a/docs/rus/backlog/20260909120030-important-parity-gen-duplicates-quant-ml-exporter-bl.md b/docs/rus/backlog/open/20260909120030-important-parity-gen-duplicates-quant-ml-exporter-bl.md similarity index 100% rename from docs/rus/backlog/20260909120030-important-parity-gen-duplicates-quant-ml-exporter-bl.md rename to docs/rus/backlog/open/20260909120030-important-parity-gen-duplicates-quant-ml-exporter-bl.md diff --git a/docs/rus/backlog/20260909120034-minor-line-simulator-bl.md b/docs/rus/backlog/open/20260909120034-minor-line-simulator-bl.md similarity index 100% rename from docs/rus/backlog/20260909120034-minor-line-simulator-bl.md rename to docs/rus/backlog/open/20260909120034-minor-line-simulator-bl.md diff --git a/docs/rus/backlog/20260909120035-minor-nodes-bl.md b/docs/rus/backlog/open/20260909120035-minor-nodes-bl.md similarity index 100% rename from docs/rus/backlog/20260909120035-minor-nodes-bl.md rename to docs/rus/backlog/open/20260909120035-minor-nodes-bl.md diff --git a/docs/rus/backlog/20260909120036-minor-oee-aggregator-bl.md b/docs/rus/backlog/open/20260909120036-minor-oee-aggregator-bl.md similarity index 100% rename from docs/rus/backlog/20260909120036-minor-oee-aggregator-bl.md rename to docs/rus/backlog/open/20260909120036-minor-oee-aggregator-bl.md diff --git a/docs/rus/backlog/20260909120037-minor-oee-dashboard-bl.md b/docs/rus/backlog/open/20260909120037-minor-oee-dashboard-bl.md similarity index 100% rename from docs/rus/backlog/20260909120037-minor-oee-dashboard-bl.md rename to docs/rus/backlog/open/20260909120037-minor-oee-dashboard-bl.md diff --git a/docs/rus/backlog/20260909120038-minor-mqtt-min-bl.md b/docs/rus/backlog/open/20260909120038-minor-mqtt-min-bl.md similarity index 100% rename from docs/rus/backlog/20260909120038-minor-mqtt-min-bl.md rename to docs/rus/backlog/open/20260909120038-minor-mqtt-min-bl.md diff --git a/docs/rus/backlog/20260909120039-minor-ml-bl.md b/docs/rus/backlog/open/20260909120039-minor-ml-bl.md similarity index 100% rename from docs/rus/backlog/20260909120039-minor-ml-bl.md rename to docs/rus/backlog/open/20260909120039-minor-ml-bl.md diff --git a/docs/rus/backlog/20260909120040-minor-qemu-bl.md b/docs/rus/backlog/open/20260909120040-minor-qemu-bl.md similarity index 100% rename from docs/rus/backlog/20260909120040-minor-qemu-bl.md rename to docs/rus/backlog/open/20260909120040-minor-qemu-bl.md diff --git a/docs/rus/backlog/20260909120042-rec-line-simulator-bl.md b/docs/rus/backlog/open/20260909120042-rec-line-simulator-bl.md similarity index 100% rename from docs/rus/backlog/20260909120042-rec-line-simulator-bl.md rename to docs/rus/backlog/open/20260909120042-rec-line-simulator-bl.md diff --git a/docs/rus/backlog/20260909120043-rec-nodes-bl.md b/docs/rus/backlog/open/20260909120043-rec-nodes-bl.md similarity index 100% rename from docs/rus/backlog/20260909120043-rec-nodes-bl.md rename to docs/rus/backlog/open/20260909120043-rec-nodes-bl.md diff --git a/docs/rus/backlog/20260909120044-rec-oee-aggregator-bl.md b/docs/rus/backlog/open/20260909120044-rec-oee-aggregator-bl.md similarity index 100% rename from docs/rus/backlog/20260909120044-rec-oee-aggregator-bl.md rename to docs/rus/backlog/open/20260909120044-rec-oee-aggregator-bl.md diff --git a/docs/rus/backlog/20260909120045-rec-mqtt-min-bl.md b/docs/rus/backlog/open/20260909120045-rec-mqtt-min-bl.md similarity index 100% rename from docs/rus/backlog/20260909120045-rec-mqtt-min-bl.md rename to docs/rus/backlog/open/20260909120045-rec-mqtt-min-bl.md diff --git a/docs/rus/backlog/20260909120046-rec-ml-bl.md b/docs/rus/backlog/open/20260909120046-rec-ml-bl.md similarity index 100% rename from docs/rus/backlog/20260909120046-rec-ml-bl.md rename to docs/rus/backlog/open/20260909120046-rec-ml-bl.md diff --git a/docs/rus/backlog/20260909120048-info-rust-analyzer-macro-artifact-nodes-bl.md b/docs/rus/backlog/open/20260909120048-info-rust-analyzer-macro-artifact-nodes-bl.md similarity index 100% rename from docs/rus/backlog/20260909120048-info-rust-analyzer-macro-artifact-nodes-bl.md rename to docs/rus/backlog/open/20260909120048-info-rust-analyzer-macro-artifact-nodes-bl.md diff --git a/docs/rus/backlog/20260910104756-important-flaky-aggregator-pipeline-oee-aggregator-bl.md b/docs/rus/backlog/open/20260910104756-important-flaky-aggregator-pipeline-oee-aggregator-bl.md similarity index 100% rename from docs/rus/backlog/20260910104756-important-flaky-aggregator-pipeline-oee-aggregator-bl.md rename to docs/rus/backlog/open/20260910104756-important-flaky-aggregator-pipeline-oee-aggregator-bl.md diff --git a/docs/rus/decompose/firmware-shakedown-runbook.md b/docs/rus/decompose/firmware-shakedown-runbook.md new file mode 100644 index 0000000..4a5cdd1 --- /dev/null +++ b/docs/rus/decompose/firmware-shakedown-runbook.md @@ -0,0 +1,421 @@ +# Колея железа: детальный runbook шейкдауна (обновлено 2026-09-14) + +Английская версия: [firmware-shakedown-runbook.md](../../eng/decompose/firmware-shakedown-runbook.md). + +Контекст: хост-трек (недели 1–6 + отчёт) завершён; 2026-09-14 закрыт S0 — +все три платы прошиты и подняты (релиз `firmware/bin/20260914085743-*`, +журналы — в корневых README, факты — в `firmware/NOTES.md`). План сессий — +`docs/rus/decompose/firmware.md`, руководство по сборке — +`docs/rus/HARDWARE-assembly-guide.md`. Серийники мостов: узел A — +`5C94148486`, узел Q — `5C94152266`, узел P (CAM) — `5CCC048683`; порта +`/dev/ttyUSB*` на этом стенде нет, фактические имена — +`/dev/serial/by-id/usb-1a86_USB_Serial_-if00`. + +| Шаг | Тема | Время | Есть код? | +| --- | ---------------------------------------- | ------ | ---------------------- | +| 0 | хвосты дня: коммит доков, CI | 5 мин | — | +| 0.5 | проверка P проводком | 5 мин | — | +| 1 | S1: узел A, АЦП и калибровка | ~3 ч | + capture-режим | +| 2 | S2: узел A, статусы на режимах | ~2 ч | всё есть | +| 3 | S3: кросс-чек платы и хостового двойника | ~2 ч | всё есть | +| 4 | S4: узел Q, драйвер I2S INMP441 | ~3 ч | + драйвер I2S | +| 5 | S5: узел Q, серво и вердикты | ~3 ч | всё есть | +| 6 | S6: узел P, счёт деталей | ~2 ч | всё есть | +| 7 | S7: гейт колеи и решение фазы 2 | ~2–3 ч | + гейт-доки | +| 8 | фаза 2: стенд в полном цикле OEE | ~1–2 ч | всё есть (uart-bridge) | + +## Шаг 0 — хвосты дня (5 мин) + +Закоммитить документацию дня (корневые README×2, `firmware/README×2`, +`firmware/NOTES.md`, `firmware/bin/20260914085743-build-info.md`) и +посмотреть CI на ветке `hardware-firmware`: джоба `firmware` впервые +собирает рабочее дерево с зависимостью `esp-bootloader-esp-idf` 0.6 +(требует rustc не ниже 1.95; CI ставит espup свежей версии, ожидаемо +зелёная). Если джоба красная именно на версии rustc — зафиксировать факт в +NOTES и откатить зависимость на версию 0.5 (минимальный rustc 1.88). + +## Шаг 0.5 — проверка узла P проводком (5 мин) + +Проводником замкнуть GPIO5 на 3V3: держать не меньше 60 мс, отпустить, +повторить. Антидребезг 50 мс игнорирует замыкания короче 50 мс, поэтому +каждое корректное касание даёт ровно один инкремент счётчика — в мониторе +появляется строка `p,bench-p,,N` со следующим значением N. Если +счётчик растёт от коротких касаний (меньше 50 мс), значит константа +`DEBOUNCE_MS` в прошивке не соответствует фактическому параметру — это +находка для NOTES, а не для молчаливой правки. + +## Шаг 1 — S1. Узел A: АЦП и калибровка (~3 ч) + +Цель: доказать числами, что ток, который измеряет плата, совпадает с +физическим током. В покое прошивка обязана показывать ноль (в пределах +допуска), при известной нагрузке — совпадать с мультиметром. + +Подключение: + +- ACS712-20A: VCC — 5 В, GND — общий с платой, OUT — через резисторный + делитель 2:1 (например, 10 кОм + 10 кОм) на GPIO4 + (`board::node_a::ADC_CURRENT`). Делитель обязателен: у ACS712-20A выход + при нуле тока равен 2.5 В, чувствительность 100 мВ на ампер, рабочий + диапазон 0.5–4.5 В; АЦП ESP32-S3 с ослаблением 11 дБ измеряет примерно до + 3.1 В, делитель переводит диапазон датчика в 0.25–2.25 В. +- GPIO4 выбран потому, что это канал ADC1: каналы ADC2 на ESP32-S3 + конфликтуют с Wi-Fi, а фаза 2 теоретически может его задействовать. + +Что уже реализовано: при старте узел усредняет 800 замеров с периодом +625 мкс (полсекунды, частота 1.6 кГц) и печатает `a: zero=NNN` — +стартовую точку нуля (`CurrentCalibration::acs712_20a_div2()` плюс +`with_zero_counts`). Дальше каждые 128 замеров (окно 80 мс) окно целиком +подается в `model_a`; смена статуса печатается только после подтверждения +двумя окнами подряд (гистерезис ×2) строкой `a,bench-a,,`. + +Чего не хватает (код этой сессии): capture-режима. Сейчас плата печатает +только статусы, а для сверки с мультиметром и для шага 3 нужны строки +захвата по схеме `features_cli::capture` — `t_ms,node,run_id,value,state,note`. +Добавить в `firmware-a` за константой времени компиляции: раз в окно (80 мс) +печатать строку со средним током окна в амперах. Оценка трафика: 12.5 +строки в секунду по ~35 байт — это около 440 байт в секунду при +пропускной способности UART 115200 бод примерно 11.5 килобайт в секунду, +запас двадцатикратный. Печатать каждый отдельный замер нельзя: 1600 строк +в секунду теми же ~35 байтами дают 56 килобайт в секунду, UART не успеет и +часть строк потеряется. + +Действия: + +1. Подключить датчик по схеме выше; нагрузку не подавать. +2. Пересобрать образ с capture-режимом и прошить плату A (порт по + серийнику `5C94148486`), открыть монитор с записью в файл. +3. Покой: 60 секунд записи. Критерий: среднее `value` не больше 0.05 А по + модулю, размах (максимум минус минимум) не больше 0.1 А. Обоснование + допуска: один отсчёт АЦП соответствует примерно 15 мА (3.1 В / 4095 + отсчётов — это 0.76 мВ на отсчёт на пине платы, делитель удваивает + напряжение, а чувствительность датчика 100 мВ на ампер); 0.05 А — это + три отсчёта. +4. Линейность: мультиметр в режиме измерения тока 10 А последовательно с + нагрузкой. Три уровня: 0.5 А, 1.0 А, 2.0 А — резисторы на 5 В: 10 Ом + мощностью не меньше 5 Вт (ток 0.5 А), 5.1 Ом не меньше 10 Вт (около + 1.0 А), 2.7 Ом не меньше 25 Вт (около 1.9 А). На каждом уровне 30 + секунд записи. +5. Для каждого уровня вычислить среднее `value` из файла захвата и + сравнить с показанием мультиметра. + +Критерии пройдено: покой — по пункту 3; линейность — на каждом уровне +расхождение платы с мультиметром не больше, чем большая из величин 0.1 А +и 5 процентов показания; значение `a: zero=` между двумя перезагрузками +(Ctrl+R в мониторе) меняется не больше чем на 5 отсчётов. + +Сценарии неудач и их смысл: показания примерно вдвое меньше мультиметра — +делитель не 2:1 или модуль датчика другой чувствительности (у ACS712-5A +это 185 мВ на ампер; сверить маркировку модуля с константой +`acs712_20a_div2`); ноль уехал при стабильном `zero=` — калибровка +выполнялась до подключения датчика, перезагрузить плату уже с датчиком; +значения скачут на ±0.5 А — нет общего провода GND между питанием датчика +и платой либо вход висит. + +Записать в NOTES: значение `zero` с подключённым датчиком, таблицу +«уровень, плата (А), мультиметр (А), отклонение», итоговый допуск. + +## Шаг 2 — S2. Узел A: статусы на режимах (~2 ч) + +Цель: показать, что статус следует за физическим режимом с задержкой в +известных границах и не меняется сам по себе внутри режима. + +Сценарий: узел различает режимы по току; физически имитируем их той же +нагрузкой из шага 1 — чередуем 0 А (30 секунд, режим покоя) и 1–2 А (30 +секунд, рабочий режим), не меньше пяти переключений в каждую сторону и не +чаще одного переключения в две секунды. + +Откуда берутся границы задержки: окно 128 замеров на частоте 1.6 кГц — это +80 мс; гистерезис подтверждает смену только после двух окон подряд. Значит +смена статуса физически не может появиться раньше чем через 80 мс и не +позже чем через три окна, то есть 240 мс. + +Действия: записать монитор в файл на весь прогон; для каждой смены +посчитать задержку — время строки статуса минус фактическое время +переключения по секундомеру; внутри 30-секундных фаз посчитать +самопроизвольные смены. + +Критерии пройдено: каждая из десяти и более смен укладывается в интервал +80–500 мс (240 мс теории плюс запас на погрешность секундомера); +самопроизвольных смен внутри фаз — ноль на не менее чем пяти минутах +записи. + +Сценарии неудач: задержка стабильно больше секунды — фактический рабочий +ток ниже того, что в обучающей выборке соответствует рабочему режиму +(сравнить `value` во время фазы с данными эксперимента недели 4); статус +меняется туда-обратно на границе — ток фазы сидит ровно на пороге +разделения классов, поднять уровень рабочего тока (менять гистерезис +нельзя — контракт ×2 закреплен тестами). + +Записать в NOTES: количество смен, минимальную, медианную и максимальную +задержки, количество самопроизвольных смен. + +## Шаг 3 — S3. Кросс-чек: плата против хостового двойника (~2 ч) + +Цель: на одном и том же физическом прогоне доказать, что железный узел A +и хостовый узел A выдают одинаковые последовательности статусов, а +расхождения возможны только у границ окон. + +Почему расхождения у границ — это нормально: обе стороны используют одну +и ту же модель (rust-born `model_a` с бит-точным кернелем Conv1D), один и +тот же гистерезис ×2 и один и тот же вход — ток в амперах, посчитанный на +плате. Единственное отличие — фаза окон: окна платы начинаются от момента +её старта, окна хостового узла — от первой строки входного файла, поэтому +замеры на границе переключения режима попадают в разные окна. + +Действия: + +1. Взять файл захвата с платы из шага 1 или 2 (запись монитора) — + `tmp/s3.capture.csv`. +2. Конвертировать его утилитой из репозитория: она читает стандартный + ввод, переименовывает колонку `value` в `current_a`, отбирает строки + узла A и пишет готовый вход хостового узла: + `cargo run --release -p firmware-tools --bin capture-to-run < tmp/s3.capture.csv > tmp/s3.run.csv` + (в stderr утилита печатает счётчик: сколько строк взято и сколько + пропущено — заголовок, чужие узлы, битые строки). +3. Прогнать хостовый узел на том же прогоне: + `cargo run --release -p nodes --bin node -- --kind a --input tmp/s3.run.csv --offline tmp/s3.statuses.csv --run-id bench-a` + (без `--mqtt` — сверка офлайн, брокер не нужен). +4. Сравнить две последовательности статусов: строки `a,bench-a,…` из + записи монитора платы против строк из `tmp/s3.statuses.csv`. + +Критерии пройдено: сто процентов совпадения статусов вне зон, +прилегающих к переключениям режима (зона — два окна, 160 мс, с каждой +стороны от переключения); внутри зон расхождения допустимы. Любое +расхождение вне зон — находка уровня эскалации из плана колеи («почему +паритет-тест это не поймал»): причину чинить и закрывать тестом, а не +соглашаться с расхождением. + +Записать в NOTES: таблицу «всего смен, совпало, расхождения, все ли +расхождения в пограничных зонах». + +## Шаг 4 — S4. Узел Q: драйвер I2S INMP441 (~3 ч, единственный большой кусок кода) + +Цель: заменить синтетическое окно реальным звуком. Сейчас вердикт узла Q +всегда `cracked` — это закреплено тестом как проверка контура; смысл +вердикты обретут только на реальном микрофоне. + +Подключение INMP441: VDD — строго 3.3 В (от 5 В модуль выходит из строя), +GND — общий, вход выбора канала L/R — на GND (левый канал), SCK — GPIO12 +(`board::node_q::I2S_SCK`), WS — GPIO13 (`I2S_WS`), SD — GPIO14 +(`I2S_SD`). + +Код: + +1. В `firmware-q` добавить приёмник I2S: esp-hal, модуль `i2s::master` + (драйверные модули уже открыты фичей `unstable`), стандартный режим + Philips, частота дискретизации 16 кГц (это контракт + `window_spec(Q)`), слот 32 бита, один канал (левый), приём через DMA в + буфер на 1024 слота (4 килобайта). Окно читается одним блокирующим + чтением сразу после удара серво. +2. Конвертация слотов уже написана и закреплена хостовыми тестами: + `i2s_slots_to_f32` берёт старшие 24 бита 32-битного слота, растягивает + по знаку и делит на 2 в степени 23 — полный масштаб получается около + ±1.0. Порядок байтов и каналов — именно тот вопрос, ради которого + конвертация вынесена в тестируемую библиотеку. +3. В главном цикле заменить вызов `synthetic_tap_window(window)` на чтение + DMA-окна и конвертацию. Саму функцию `synthetic_tap_window` из + библиотеки не удалять: её вердикт закреплён тестом и остаётся + контрольным ориентиром. +4. Временный дамп-режим (за той же константой компиляции, что и capture в + шаге 1): напечатать значения первого окна после старта — по нему + считается спектр. + +Проверка тоном: + +1. Рядом с микрофоном включить тон 1 кГц (генератор или приложение тона + на телефоне, громкость обычной речи). +2. Снять дамп окна и посчитать спектр на хосте любым способом (например, + numpy: модуль дискретного преобразования Фурье от 1024 значений). +3. Ожидание: пик в бине 64. Расчёт: ширина бина 16000 Гц / 1024 = 15.625 + Гц; 1000 Гц / 15.625 = бин 64; допуск ±5 процентов — полоса 950–1050 Гц, + бины 61–67. +4. Частота дискретизации подтверждается тем же пиком: если бы она была + вдвое ниже, тон 1 кГц оказался бы в бине 32. Длительность окна 1024 / + 16000 = ровно 64 мс. +5. Амплитуда: в тишине значения порядка 0.001–0.01, на тоне — 0.1–0.5. + +Сценарии неудач: на всех окнах ровно ноль — не тот уровень на входе +выбора канала L/R либо SCK и WS переставлены местами; дамп сырых слотов +(до конвертации) сразу покажет нули. Пик в бине 32 — фактическая частота +дискретизации 8 кГц, ошибка в конфигурации I2S. Вместо пика шум по всем +бинам — наводка на линию данных: укоротить провода, проверить общий GND. + +Записать в NOTES: найденный порядок байтов и каналов (совпал ли с +контрактом конвертации), бин пика, амплитуды тишины и тона. + +После этого шага — перевыпуск образов в `firmware/bin/` по той же +процедуре, что 2026-09-14 (новая метка времени, хеши, старый сет в +`tmp/trash/`), потому что меняется поведение прошивки Q. + +## Шаг 5 — S5. Узел Q: серво-тапер и вердикты на эталонных деталях (~3 ч) + +Цель: вердикты зависят от детали. Годная деталь даёт преимущественно +`good`, треснувшая — преимущественно `cracked`. + +Подключение серво: сигнальный провод SG90 на GPIO11 +(`board::node_q::SERVO_PWM`); питание серво — только от отдельного блока +5 В на ток не меньше 1 А, минус блока соединить с GND платы; конденсатор +470 мкФ на клеммах питания серво. Сценарий ошибки: если запитать серво от +USB-порта платы, то в момент удара серво потребляет бросок порядка +0.5–1 А, шина 5 В проседает ниже порога срабатывания защиты процессора, +и плата перезагружается посреди такта — в мониторе это выглядит как +внезапный повтор загрузочного баннера. + +Что уже реализовано: ШИМ 50 Гц с 14-битным заполнением; удар — заполнение +6 процентов (импульс примерно 1.2 мс), покой — 8 процентов (примерно +1.6 мс); после удара пауза 30 мс (рычаг отходит, звон детали начинается), +затем окно 64 мс; полный такт 400 мс. Где бьёт рычаг и какой он длины — +параметр механической сборки, кода это не касается. + +Действия: + +1. Смонтировать серво над держателем детали так, чтобы рычаг касался + детали при срабатывании — удар должен быть слышен. +2. Включить питание серво от отдельного блока и убедиться, что на десяти + подряд ударах плата не перезагружается (в мониторе нет повторных + загрузочных баннеров). +3. Пять годных деталей: для каждой не меньше трёх ударов, записать все + вердикты. +4. Пять треснувших деталей: аналогично. +5. Если дамп-режим из шага 4 ещё в прошивке — снять по одному окну для + годной и треснувшей детали: спектры должны заметно отличаться. + +Критерии пройдено: для каждой детали не меньше двух третей ударов дают +вердикт её класса; в сумме не меньше восьми из десяти деталей +классифицированы верно. Каждое расхождение — в NOTES с описанием детали. + +Сценарии неудач: вердикты не зависят от детали (например, все `cracked`, +как на синтетике) — окно не попало на звон детали; дамп покажет, что звук +начинается позже начала окна, и тогда пауза после удара увеличивается в +прошивке (константа `SETTLE_MS`, сейчас 30 мс, следующий разумный шаг — +50 мс), либо серво физически не касается детали. Вердикты верные, но +плата перезагружается на ударах — питания всё ещё не хватает: +мультиметром в момент удара на клеммах серво должно оставаться не ниже +примерно 4.5 В. + +Записать в NOTES: ток серво при ударе (мультиметр в разрыв питания), +таблицу вердиктов по деталям, правки `SETTLE_MS`, если были. + +## Шаг 6 — S6. Узел P: счёт деталей (~2 ч) + +Цель: счётчик равен факту — N прошедших деталей дают ровно N инкрементов, +дребезг на краю датчика не создаёт ложных. + +Обязательная проверка до монтажа: узел P живет на CAM-плате, датчик +назначен на GPIO5 (`board::node_p::IR_OUT`), но обвязка камеры OV2640 на +платах разных производителей занимает разные пины, и на части плат GPIO5 +принадлежит камере. Сверить распиновку вашей платы со схемой: если GPIO5 +занят, датчик переносится на свободный пин, а правка делается в крейте +`board` (уровень конфигурации стенда; контракт узла не меняется). Тест +`used_pins_are_free_and_distinct` проверит, что новый пин не входит в +зарезервированный список: 0, 3, 45, 46 — пины режимов загрузки; 19, 20 — +линии USB; 43, 44 — консоль UART0; 35, 36, 37 — линии восьмиразрядной +PSRAM модуля N16R8. + +Подключение TCRT5000: модуль с компаратором — VCC по маркировке модуля +(3.3 или 5 В), GND общий, OUT на GPIO5. Датчик рефлективный +инфракрасный: срабатывает на отражение от объекта на расстоянии +примерно 1–25 мм, порог подстраивается резистором на модуле. + +Действия: + +1. Подстроить расстояние: деталь на 5–10 мм от датчика — светодиод + модуля загорается. +2. Быстрая проверка из шага 0.5 проводником (каждое корректное касание + длиннее 60 мс — ровно один инкремент). +3. Прогон 1: 20 медленных проходов детали (или пальца) — итоговый счёт + ровно 20. +4. Прогон 2: 20 проходов в темпе будущей ленты. +5. Тест дребезга: держать цель на краю срабатывания и шевелить её пять + секунд. + +Критерии пройдено: в обоих прогонах расхождение счёта с фактом — ноль; +тест дребезга даёт не больше двух лишних инкрементов за пять секунд +(антидребезг 50 мс подавляет серии короче 50 мс; устойчивые повторные +пересечения порога длиннее 50 мс — это уже реальные события). + +Сценарии неудач: счёт вдвое больше факта — сигнал во время прохода +уходит за порог и возвращается (видно на записи уровня OUT): увеличить +зазор датчика или уменьшить чувствительность резистором. Счёт ноль, хотя +светодиод модуля срабатывает, — у модуля инвертированный выход +(активный низкий уровень): проверить тестером уровень OUT в покое и при +срабатывании; при инверсии правка делается в драйвере чтения пина в +прошивке (не в контракте) и фиксируется в NOTES. + +Записать в NOTES: полярность срабатывания, дистанцию, итоги обоих +прогонов, результат теста дребезга. + +## Шаг 7 — S7. Гейт колеи и решение фазы 2 (~2–3 ч) + +Цель: оформить результаты S0–S6 в проверяемые артефакты и принять решение +по фазе 2. + +Действия: + +1. Финализировать `firmware/NOTES.md`: по каждой сессии должны стоять + числа — S1: `zero`, таблица линейности, допуск; S2: задержки статусов; + S3: таблица кросс-чека; S4: порядок байтов и каналов I2S, бин тона; + S5: ток серво, вердикты по деталям; S6: полярность и дистанция + датчика, итоги счёта. +2. Написать `docs/rus/firmware-gate.md` и `docs/eng/firmware-gate.md` по + образцу недельных гейтов (week1–week6-gate). Чеклист гейта трека + перечислен в декомпозиции: статусы A совпадают с хостовыми на тех же + данных; вердикты Q на эталонных деталях совпадают с моделью на тех же + окнах; счёт P равен факту прогона; capture-CSV с платы читается + хостовыми инструментами и воспроизводим; джоба CI `firmware` зелёная; + NOTES заполнен. По каждому пункту — пройдено или отступление с + причиной. Отклонение «пропущен blinky, сразу прошит рабочий firmware-a» + уже зафиксировано в NOTES — перенести его и в гейт. +3. Решение фазы 2 с обоснованием: UART-мост (уже написан и протестирован, + на платах нет ни строчки сетевого кода) против esp-wifi с + портированием mqtt-min (неделя дополнительной работы плюс риск + Wi-Fi-окружения на стенде). Рекомендация: мост — он закрывает цель + уже сегодня; Wi-Fi — в будущую работу отчёта. +4. Проверить, что обе джобы CI (`firmware` — сборка под xtensa с новой + зависимостью, `firmware-host` — форматирование, clippy, тесты + библиотек) зелёные на последнем коммите. + +Критерий пройдено: оба гейт-дока написаны, у каждого пункта чеклиста +есть статус, решение фазы 2 зафиксировано с обоснованием. + +## Шаг 8 — фаза 2: стенд в полном цикле OEE (~1–2 ч, после гейта) + +Цель: физический стенд заменяет хостовые узлы в цикле недели 5 — строки +трёх плат попадают через UART-мост в MQTT, агрегатор сворачивает их в +OEE, дашборд показывает живые метрики. + +Действия: + +1. Терминал 1 — хвостовая часть цикла (раздел «Цикл стенда» в + `firmware/README.ru.md`): + `./target/debug/broker 1883 &`; + `./target/debug/aggregator --mqtt 127.0.0.1:1883 --ideal-cycle-ms 400 --out windows.csv`; + `./target/debug/oee-dashboard --mqtt 127.0.0.1:1883`. +2. Терминалы 2–4 — по одному мосту на плату, порта по серийникам: + `cat /dev/serial/by-id/usb-1a86_USB_Serial_5C94148486-if00 | cargo run -p firmware-tools --bin uart-bridge -- 127.0.0.1:1883` + и две аналогичные команды для серийников `5C94152266` (Q) и + `5CCC048683` (P). Мост отбрасывает загрузочные строки (закреплено его + тестами), публикует статусы, вердикты и счёт в `oee/line1/*`, а при + обрыве входного потока печатает конечный маркер — агрегатор корректно + закрывает окно. +3. Проверка без железа (известная): подать на вход моста четыре строки + через printf — ожидать в выводе `bridge: 4 messages (a+p+q)`. +4. Живой прогон: включить физические воздействия — ток для узла A, тапы + для Q, проходы деталей для P — и наблюдать дашборд. + +Критерий пройдено: за пять минут прогона дашборд показывает смену статусов +A, вердикты Q с каждым тапом, счёт P равен числу прошедших деталей, а +файл `windows.csv` наполняется строками окон без пропусков между +конечными маркерами. + +Сценарии неудач: дашборд пуст, мост молчит — строка платы не проходит +белый список моста; сверить фактическую строку из монитора с форматом +строк из printf-теста (`a,bench-a,1000,run`). OEE не сходится с +ожиданием — параметр идеального цикла 400 мс задан агрегатору, а +фактический темп тапов стенда другой; согласовать `--ideal-cycle-ms` с +фактом (это параметр агрегатора, прошивка не меняется). + +Примечание к документации: в руководстве по сборке +(`docs/rus/HARDWARE-assembly-guide.md` и английский аналог) порты указаны +старым именем `/dev/ttyUSB0` — на этом стенде фактические имена +`/dev/ttyACM*` или by-id; при случае обновить руководство. diff --git a/docs/rus/decompose/firmware.md b/docs/rus/decompose/firmware.md index 93f99c1..419cffe 100644 --- a/docs/rus/decompose/firmware.md +++ b/docs/rus/decompose/firmware.md @@ -11,6 +11,7 @@ > stretch-камера; сенсоры ACS712-20A, INMP441, TCRT5000, серво-таппер, > отдельный БП 5 В. Подробности стенда и процедура оживления — > [`firmware/README.md`](../../../firmware/README.md). +> Детальный runbook выполнения (подключение, команды, критерии приёмки) — [firmware-shakedown-runbook.md](firmware-shakedown-runbook.md). > Вход: контракты уже в репо — `features-cli` (`window_spec`, калибровка > ADC→амперы, схема `capture`), `nodes::source::SensorSource` diff --git a/firmware/Cargo.lock b/firmware/Cargo.lock index c2b6bc4..c4fbffa 100644 --- a/firmware/Cargo.lock +++ b/firmware/Cargo.lock @@ -119,6 +119,15 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + [[package]] name = "critical-section" version = "1.2.0" @@ -212,6 +221,37 @@ dependencies = [ "syn 3.0.4", ] +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + [[package]] name = "delegate" version = "0.13.5" @@ -480,6 +520,23 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "esp-bootloader-esp-idf" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba0e487f6843419bb085d871f6e860da1bb8c267ce1c5db1051b83882e943054" +dependencies = [ + "document-features", + "esp-config", + "esp-hal-procmacros", + "esp-metadata-generated", + "esp-rom-sys", + "esp-storage", + "jiff", + "sha2", + "strum", +] + [[package]] name = "esp-config" version = "0.8.0" @@ -597,6 +654,21 @@ dependencies = [ "esp32s3", ] +[[package]] +name = "esp-storage" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef0e785f54e316f64a1e14a7a2f061a2ea0b038b284902ca71e1e1f5574cf47" +dependencies = [ + "document-features", + "esp-hal", + "esp-hal-procmacros", + "esp-metadata-generated", + "esp-rom-sys", + "esp-sync", + "esp32s3", +] + [[package]] name = "esp-sync" version = "0.3.0" @@ -725,6 +797,7 @@ name = "firmware-a" version = "0.1.0" dependencies = [ "board", + "esp-bootloader-esp-idf", "esp-hal", "features-cli", "fmt-util", @@ -737,6 +810,7 @@ name = "firmware-p" version = "0.1.0" dependencies = [ "board", + "esp-bootloader-esp-idf", "esp-hal", "fmt-util", ] @@ -746,6 +820,7 @@ name = "firmware-q" version = "0.1.0" dependencies = [ "board", + "esp-bootloader-esp-idf", "esp-hal", "features-cli", "fmt-util", @@ -929,6 +1004,49 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jiff" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" +dependencies = [ + "defmt", + "log", +] + +[[package]] +name = "jiff-static" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + [[package]] name = "libm" version = "0.2.16" @@ -941,6 +1059,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + [[package]] name = "matrixmultiply" version = "0.3.11" @@ -1090,6 +1214,15 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + [[package]] name = "proc-macro-crate" version = "3.5.0" @@ -1315,6 +1448,17 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.11.3", +] + [[package]] name = "simba" version = "0.8.1" @@ -1461,6 +1605,26 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + [[package]] name = "toml_datetime" version = "1.1.1+spec-1.1.0" diff --git a/firmware/NOTES.md b/firmware/NOTES.md new file mode 100644 index 0000000..068fdbe --- /dev/null +++ b/firmware/NOTES.md @@ -0,0 +1,69 @@ +# NOTES — факты шейкдауна (колея железа) + +Рабочий журнал обкатки на стенде. План сессий S0–S7 — +[docs/rus/decompose/firmware.md](../docs/rus/decompose/firmware.md); итоговый +гейт (`firmware-gate.md`, рус/англ) оформляется на S7. Пополняется по ходу +сессий. + +## 2026-09-14 — S0/S1: первая прошивка, узел A + +- **Плата — клон DevKitC-1**: мост USB-UART — **CH343** (QinHeng + `1a86:55d3`), не CP2102 → порт `/dev/ttyACM0`, а не `/dev/ttyUSB*`. + Маркировка портов «COM»/«USB» (USB-C), а не «UART»/«USB» как у v1.0 с + micro-USB. Нативный «USB»-порт платы (Espressif `303a:4001`) тоже даёт + `ttyACM*` — для монитора бесполезен (вывод идёт в аппаратный UART). + Различение плат при нескольких подключённых — `/dev/serial/by-id/` + (CH343 не отдаёт строку производителя, udev строит имя от VID — + `usb-1a86_USB_Single_Serial_-if00`; серийник уникален). Серийники + стенда: DevKitC-1 №1 (узел A) — `5C94148486`, №2 (узел Q) — + `5C94152266`, CAM (узел P) — `5CCC048683`. +- **espflash ≥ 4.6** отказывается прошивать образ без ESP-IDF App + Descriptor → в `firmware-{a,q,p}` добавлены target-gated зависимость + `esp-bootloader-esp-idf = "0.6"` (feature `esp32s3`, MSRV rustc 1.95) и + вызов `esp_bootloader_esp_idf::esp_app_desc!()` в `mod app`. +- espflash заливает **три региона**: вторичный загрузчик @0x0, таблица + разделов @0x8000, приложение @0x10000 — образ самодостаточен, фабричный + загрузчик в flash не нужен (загрузчик ESP-IDF v6.1-beta1 идёт в бандле + espflash). +- Чистая машина: `cargo install espup` + `cargo install espflash`; с + asdf-рустом бинарники попадают в `~/.asdf/installs/rust//bin` → + после установки `asdf reshim rust`. Права на порт — группа `dialout` + (`sudo usermod -aG dialout $USER` + перелогин; в текущем шелле — + `newgrp dialout`). +- **Железо узла A**: esp32s3 rev v0.2, 16 MB flash, кварц 40 МГц. + Стартовая калибровка нуля на висящем входе ACS712: **zero=2229** отсчётов + (середина шкалы 12-битного АЦП ≈ 2048; допуск калибровки — вопрос S1). +- Первые строки живого образа — в точности как в README: `a: boot, + run_id=bench-a` → `a: zero=2229` → `a,bench-a,160,idle`. +- Отклонение от плана: blinky (S0) пропущен — сразу прошит рабочий + `firmware-a`; полный журнал первой загрузки — в корневом README. + +## 2026-09-14 — узел Q: прошит; вердикт синтетики — `cracked` + +- Прошит вторым (`bin/20260914085743-firmware-q`, DevKitC-1 №2): boot, строки + `q,bench-q,,` каждые ровно 400 мс — конвейер + «серво → окно → модель → UART» подтверждён. +- **Расхождение**: синтетическое окно (quiet decay ~940 Гц, амплитуда 0.05) + модель классифицирует как `cracked`, хотя комментарий в `main.rs` обещал + «the good-part family». Причина: формула жила в xtensa-гейтед `mod app` — + хостовые тесты её не видели, ожидание не было закреплено. +- Закрытие: формула вынесена в lib (`synthetic_tap_window`), фактический + вердикт `cracked` закреплён хостовым тестом `synthetic_tap_verdict_is_pinned`, + комментарий исправлен. Смысл вердикты обретут на S4 (реальный I2S/INMP441); + до тех пор константность вердикта — норма (проверка контура, не метки). +- Замечание: сет `bin/20260914085743-*` собран до этого рефакторинга; + поведение идентично (та же формула, тот же вердикт). + +## 2026-09-14 — узел P: прошит; S0 закрыт по всему стенду + +- CAM-плата прошита третьей (`bin/20260914085743-firmware-p`): esp32s3 v0.2, + 16 MB, app 99 040 байт — минимальный из трёх узлов (нет модели, только + счётчик с антидребезгом); boot-строка `p: boot, run_id=bench-p` получена, + далее тишина — норма без датчика (у P нет периодики, только события). +- **Грабля нативного порта**: первая попытка прошивки шла через нативный + USB-порт CAM-платы (Espressif `303a:4001`) — «Failed to connect», + авто-вход в download-режим не сработал. Прошивать и мониторить — только + через мостовой порт: на CAM-плате тоже CH343, серийник `5CCC048683`. +- До S6: сверить по схеме конкретной CAM-платы, что GPIO5 свободен от + обвязки камеры (TCRT5000) — CAM-платы разных производителей различаются + раскладкой. diff --git a/firmware/README.md b/firmware/README.md index c4ebe2b..402f25d 100644 --- a/firmware/README.md +++ b/firmware/README.md @@ -2,6 +2,7 @@ Russian version: [README.ru.md](README.ru.md). The shakedown plan (sessions S0–S7, the gate): [docs/eng/decompose/firmware.md](../docs/eng/decompose/firmware.md). +The detailed runbook (criteria, commands, tolerances) — [docs/eng/decompose/firmware-shakedown-runbook.md](../docs/eng/decompose/firmware-shakedown-runbook.md). ESP32-S3 firmwares for nodes A/P/Q. The bench: 2× ESP32-S3-DevKitC-1 (N16R8) — nodes A and Q; 1× ESP32-S3-WROOM-1 N16R8 **CAM** with an on-board OV2640 — @@ -9,10 +10,11 @@ node P plus the stretch camera (its camera wiring takes some pins). A separate workspace, like `fork/microflow`: the target toolchain (Xtensa, `espup`) must not affect the host CI of the root workspace. -## Status: implemented, awaiting bench bring-up +## Status: implemented; bench bring-up started (S0 — 2026-09-14) The track is implemented in code; the physical bring-up (S0 blinky → S6 -counting) is the remaining human-on-hardware part: +counting) is the human-on-hardware part, started 2026-09-14. The shakedown +facts — [NOTES.md](NOTES.md). - **`firmware-{a,q,p}`** build for `xtensa-esp32s3-none-elf` (esp-hal 1.2, the `unstable` driver modules) **and** on the host (an empty stub binary; @@ -47,6 +49,10 @@ cargo install espup && espup install # the patched Xtensa toolchain . $HOME/export-esp.sh # the linker (xtensa-esp-elf-gcc) PATH ``` +The flasher (host, once): `cargo install espflash`. With asdf-managed Rust +the binary lands in `~/.asdf/installs/rust//bin` — run +`asdf reshim rust` after installing. + The esp toolchain lives in `~/.rustup/toolchains/esp` (espup's default), which the asdf-managed shell rustup does not see. Prepend its `bin` to `PATH` — called by full path, esp's cargo still resolves `rustc` from @@ -74,7 +80,8 @@ the board's wiring (`board` is the single source of truth): | DevKitC-1 #2 (servo GPIO11 + INMP441) | Q | `firmware-q` | | CAM board (TCRT5000 on GPIO5) | P | `firmware-p` | -The boards differ by their USB serial port — check `/dev/ttyUSB*`: +The boards differ by their USB serial port — check `/dev/ttyUSB*` (a +CP2102 bridge) or `/dev/ttyACM*` (a CH343 — see the note below): ```bash espflash flash --port /dev/ttyUSB0 target/xtensa-esp32s3-none-elf/release/firmware-a @@ -86,6 +93,24 @@ espflash flash --port /dev/ttyUSB2 target/xtensa-esp32s3-none-elf/release/firmwa board is not seen, hold **BOOT** while plugging it in (the USB download mode). +The device name depends on the board's USB-UART bridge: the original +DevKitC-1 carries a CP2102 → `/dev/ttyUSB*`; clones often replace it with +a CH343 → `/dev/ttyACM*` (on USB-C revisions the bridge connector is +labeled "COM", on the v1.0 micro-USB one — "UART"). espflash does not +care whether it opens a `ttyUSB` or a `ttyACM`. The board's native "USB" +port also shows up as `/dev/ttyACM*`, but it is useless for monitoring: +the output goes to the hardware UART. With several boards all on +`ttyACM*`, tell them apart by the stable by-id names (the CH343 exposes no +manufacturer string, so udev builds the name from the VID — +`usb-1a86_USB_Single_Serial_-if00`; the serial is unique per +board): + +```bash +ls -l /dev/serial/by-id/ +espflash flash --port /dev/serial/by-id/usb-1a86_USB_Single_Serial_-if00 \ + target/xtensa-esp32s3-none-elf/release/firmware-a +``` + What a live image prints over UART (115200) — the first bring-up check: - A: `a: boot, run_id=bench-a` → `a: zero=NNN` (the startup zero @@ -114,9 +139,11 @@ before the real I2S driver lands (DMA buffers will add on top). ## Checking the UART output -Plug the board's **UART** micro-USB port (the one by the buttons, -labeled "UART", through the CP2102 bridge → `/dev/ttyUSB*`) — not the -OTG port: the firmware does not use the native USB. +Plug the board's **bridge port** — the connector by the buttons: on v1.0 +a micro-USB labeled "UART", on USB-C revisions and clones a USB-C +labeled "COM". Not the OTG "USB" port: the firmware does not use the +native USB. A CP2102 bridge gives `/dev/ttyUSB*`, a CH343 on clones +`/dev/ttyACM0` (the examples below use `/dev/ttyUSB0`). ```bash # option 1: espflash (already installed for flashing) — sets 115200 and diff --git a/firmware/README.ru.md b/firmware/README.ru.md index 8bcddce..2130174 100644 --- a/firmware/README.ru.md +++ b/firmware/README.ru.md @@ -2,6 +2,7 @@ Английская версия: [README.md](README.md). План шейкдауна (сессии S0–S7, гейт): [docs/rus/../eng/decompose/firmware.md](../docs/eng/decompose/firmware.md). +Детальный runbook (критерии, команды, допуски) — [docs/rus/decompose/firmware-shakedown-runbook.md](../docs/rus/decompose/firmware-shakedown-runbook.md). Прошивки ESP32-S3 для узлов A/P/Q. Стенд: 2× ESP32-S3-DevKitC-1 (N16R8) — узлы A и Q; 1× ESP32-S3-WROOM-1 N16R8 **CAM** с OV2640 на борту — узел P и @@ -9,10 +10,12 @@ как `fork/microflow`: целевой тулчейн (Xtensa, `espup`) не должен влиять на хостовый CI корневого workspace. -## Статус: реализовано, ожидает шейкдауна на стенде +## Статус: реализовано; шейкдаун начат (S0 — 2026-09-14) Колея реализована в коде; физический подъём (S0 blinky → S6 счёт) — -оставшаяся человеческая часть на железе: +человеческая часть на железе, начата 2026-09-14. + +Факты шейкдауна — [NOTES.md](NOTES.md). - **`firmware-{a,q,p}`** собираются под `xtensa-esp32s3-none-elf` (esp-hal 1.2, драйверные модули `unstable`) **и** на хосте (бинарь — @@ -48,6 +51,10 @@ cargo install espup && espup install # пропатченный Xtensa-тул . $HOME/export-esp.sh # PATH линкера (xtensa-esp-elf-gcc) ``` +Прошивальщик (хост, один раз): `cargo install espflash`. С asdf-рустом +бинарник встаёт в `~/.asdf/installs/rust//bin` — после установки +сделайте `asdf reshim rust`. + Esp-тулчейн стоит в `~/.rustup/toolchains/esp` (дефолт espup), который asdf-шелловый rustup не видит. Добавьте его `bin` в начало `PATH` — при вызове по полному пути cargo из esp всё равно берёт `rustc` из `PATH` @@ -75,7 +82,8 @@ cargo build --release \ | DevKitC-1 №2 (серво GPIO11 + INMP441) | Q | `firmware-q` | | CAM-плата (TCRT5000 на GPIO5) | P | `firmware-p` | -Платы различаются по USB-последовательному порту — смотрите `/dev/ttyUSB*`: +Платы различаются по USB-последовательному порту — смотрите `/dev/ttyUSB*` +(мост CP2102) или `/dev/ttyACM*` (мост CH343 — см. примечание ниже): ```bash espflash flash --port /dev/ttyUSB0 target/xtensa-esp32s3-none-elf/release/firmware-a @@ -86,6 +94,23 @@ espflash flash --port /dev/ttyUSB2 target/xtensa-esp32s3-none-elf/release/firmwa `--monitor` сразу после прошивки покажет консоль. Если плата не видится — зажмите **BOOT** при подключении (режим прошивки по USB). +Имя устройства зависит от моста платы: оригинальный DevKitC-1 несёт CP2102 +→ `/dev/ttyUSB*`; на клонах его часто заменяет CH343 → `/dev/ttyACM*` +(на ревизиях с USB-C разъём моста подписан «COM», на v1.0 с micro-USB — +«UART»). espflash'у безразлично, `ttyUSB` это или `ttyACM`. Нативный +«USB»-порт платы тоже даёт `/dev/ttyACM*`, но для монитора бесполезен: +вывод идёт в аппаратный UART. Если подключено несколько плат и все они +`ttyACM*`, различайте их по стабильным by-id-именам (CH343 не отдаёт +строку производителя, udev строит имя от VID — +`usb-1a86_USB_Single_Serial_-if00`; серийник уникален у каждой +платы): + +```bash +ls -l /dev/serial/by-id/ +espflash flash --port /dev/serial/by-id/usb-1a86_USB_Single_Serial_-if00 \ + target/xtensa-esp32s3-none-elf/release/firmware-a +``` + Что печатает живой образ в UART (115200) — первая проверка шейкдауна: - A: `a: boot, run_id=bench-a` → `a: zero=NNN` (стартовая калибровка @@ -114,9 +139,11 @@ Bring-up: всё равно проверить высокую воду до пе ## Проверка вывода UART -Подключайте **UART**-порт платы (micro-USB со стороны кнопок, подписан -«UART», через мост CP2102 → `/dev/ttyUSB*`) — не OTG-порт: прошивка -нативный USB не использует. +Подключайте порт моста платы — разъём со стороны кнопок: на v1.0 это +micro-USB с маркировкой «UART», на ревизиях с USB-C и клонах — USB-C с +маркировкой «COM». Не OTG-порт «USB»: прошивка нативный USB не +использует. CP2102 даёт `/dev/ttyUSB*`, CH343 на клонах — `/dev/ttyACM0` +(примеры ниже — для `/dev/ttyUSB0`). ```bash # вариант 1: espflash (уже стоит для прошивки) — сам ставит 115200 diff --git a/firmware/a/Cargo.toml b/firmware/a/Cargo.toml index 9bb4609..6e683f4 100644 --- a/firmware/a/Cargo.toml +++ b/firmware/a/Cargo.toml @@ -19,3 +19,6 @@ nalgebra = { workspace = true } # the driver modules (uart/adc/gpio/delay) are public only under it. [target.'cfg(target_arch = "xtensa")'.dependencies] esp-hal = { version = "1.2", features = ["esp32s3", "unstable"] } +# The ESP-IDF app descriptor (esp_app_desc! in main.rs): required by the +# 2nd-stage bootloader; espflash >= 4.6 refuses a descriptor-less image. +esp-bootloader-esp-idf = { version = "0.6", features = ["esp32s3"] } diff --git a/firmware/a/src/main.rs b/firmware/a/src/main.rs index 0d98082..8e73140 100644 --- a/firmware/a/src/main.rs +++ b/firmware/a/src/main.rs @@ -36,6 +36,11 @@ mod app { CONFIRM_AFTER, SAMPLE_US, }; + // The ESP-IDF app descriptor at the image head: the 2nd-stage + // bootloader requires it, and espflash >= 4.6 refuses to flash an + // image without it. + esp_bootloader_esp_idf::esp_app_desc!(); + /// Run id of this firmware image (the offline-CSV family uses it verbatim). const RUN_ID: &str = "bench-a"; diff --git a/firmware/bin/20260910101457-build-info.md b/firmware/bin/20260910101457-build-info.md deleted file mode 100644 index 4053973..0000000 --- a/firmware/bin/20260910101457-build-info.md +++ /dev/null @@ -1,34 +0,0 @@ -# Firmware images — build 20260910101457 - -- **Target**: `xtensa-esp32s3-none-elf`, release (optimized, LTO) -- **Toolchain**: rustc 1.97.0-nightly (8ea53bcd7 2026-07-08) — espup, `~/.rustup/toolchains/esp` -- **Built**: 2026-09-10 10:14 (+0500), from the `firmware/` workspace -- **git HEAD**: `e497501ad07f43b4743bacfdffa39b71d1aa4dad` + uncommitted firmware review fixes (this build carries them) -- **Fixes vs the 20260910072946 set** (plan `docs/rus/backlog/plan/20260910095045-bl-plan.md`): - - `firmware-p`: EdgeCounter mirrors the host semantics — rising-edge detection (no phantom count on a boot-blocked barrier), anti-double window anchored to the last counted part (a glitch storm no longer freezes the counter); pinned by a shared-trace test with `nodes::p` (card 05) - - `firmware-a`: `t_ms` advances (`u64` microseconds, was `SAMPLE_US/1000 == 0`) (card 06) - - `firmware-q`: the 4 KiB window is static, not on the stack; measured headroom ~294 KiB vs ~25 KiB peak (card 32, see firmware/README "Stack headroom") - - `firmware-tools`: uart-bridge survives serial garbage (lossy UTF-8) and publish errors; state/verdict whitelisted against JSON injection; end markers on every exit path (card 33) - - host-only: `fmt-util` crate (shared Cursor, NaN-safe argmax), contract tests pin WINDOW to `features_cli::window_spec`, real label=good fixture for the Q parity test (cards 07-fw, 41) - -## Files - -One ELF per board (espflash converts it into a bootable image itself). Board mapping per `firmware/README.md`: - -| File | Node | Board | sha256 | -| --------------------------- | ---- | -------------------------------------- | ---------------------------------------------------------------- | -| `20260910101457-firmware-a` | A | DevKitC-1 #1 (ACS712 on GPIO4) | `a9246eea700f98245feea29dc9d69de6430d013338f47eb2586dc6b3d56c937e` | -| `20260910101457-firmware-q` | Q | DevKitC-1 #2 (servo GPIO11 + INMP441) | `2d8326a23f159117e64eceb7aaf80e6940b784323e0ff5141dca7f31fff834a7` | -| `20260910101457-firmware-p` | P | CAM board (TCRT5000 on GPIO5) | `53382eca2396c9b4082caf44df756c9596d0352868c984c450de18d7933ac768` | - -## Flashing - -From the `firmware/` directory (ports per `firmware/README.md` — boards differ by `/dev/ttyUSB*`): - -```bash -espflash flash --port /dev/ttyUSB0 bin/20260910101457-firmware-a -espflash flash --port /dev/ttyUSB1 bin/20260910101457-firmware-q -espflash flash --port /dev/ttyUSB2 bin/20260910101457-firmware-p -``` - -Do NOT flash `qemu/target/.../oee-qemu` — a different target (LM3S6965/Cortex-M3); it will not boot on an ESP32-S3. diff --git a/firmware/bin/20260914085743-build-info.md b/firmware/bin/20260914085743-build-info.md new file mode 100644 index 0000000..22370fb --- /dev/null +++ b/firmware/bin/20260914085743-build-info.md @@ -0,0 +1,38 @@ +# Firmware images — build 20260914085743 + +- **Target**: `xtensa-esp32s3-none-elf`, release (optimized, LTO) +- **Toolchain**: rustc 1.97.0-nightly (8ea53bcd7 2026-07-08) (1.97.0.0) — espup, `~/.rustup/toolchains/esp` +- **Built**: 2026-09-14 (local, +0500), from the `firmware/` workspace +- **git HEAD**: `de022a45363665a45f10cc56d802e7379ea01153` + uncommitted working-tree changes on `hardware-firmware` (this build carries them) +- **Changes vs the 20260910101457 set**: + - all nodes: the ESP-IDF app descriptor is embedded (`esp-bootloader-esp-idf` 0.6, + feature `esp32s3`, `esp_app_desc!()` in each `mod app`) — required by espflash >= 4.6 + (refuses a descriptor-less image) and accepted by the 2nd-stage bootloader; first + bench flash verified 2026-09-14 (node A on a CH343-bridge clone, S0 — see + `firmware/NOTES.md` and the root README) + - espflash writes bootloader @0x0 + partition table @0x8000 + app @0x10000 — the + image is self-contained + - docs only (no code): CH343/`ttyACM*` port naming, `/dev/serial/by-id`, espflash + install notes + +## Files + +One ELF per board (espflash converts it into a bootable image itself). Board mapping per `firmware/README.md`: + +| File | Node | Board | sha256 | +| --------------------------- | ---- | -------------------------------------- | ------------------------------------------------------------------ | +| `20260914085743-firmware-a` | A | DevKitC-1 #1 (ACS712 on GPIO4) | `b4b79ed303d00ac8559e81ea10d50333a3935ee2c8c5a1a01f8dfc368ffd5ad4` | +| `20260914085743-firmware-q` | Q | DevKitC-1 #2 (servo GPIO11 + INMP441) | `4cebffe6fcd72acb6de2d12c07a6ea6898ac538c6665150ae252c25cfded7f5d` | +| `20260914085743-firmware-p` | P | CAM board (TCRT5000 on GPIO5) | `bc301f1be9f9b89167b3d5f306ea0ad424f1a9cc4bff80db350b3e6f79990dd6` | + +## Flashing + +From the `firmware/` directory (ports per `firmware/README.md`; on CH343-bridge clones the port is `/dev/ttyACM*`, see NOTES): + +```bash +espflash flash --port /dev/ttyACM0 bin/20260914085743-firmware-a +espflash flash --port /dev/ttyACM1 bin/20260914085743-firmware-q +espflash flash --port /dev/ttyACM2 bin/20260914085743-firmware-p +``` + +Do NOT flash `qemu/target/.../oee-qemu` — a different target (LM3S6965/Cortex-M3); it will not boot on an ESP32-S3. diff --git a/firmware/bin/20260910101457-firmware-a b/firmware/bin/20260914085743-firmware-a similarity index 59% rename from firmware/bin/20260910101457-firmware-a rename to firmware/bin/20260914085743-firmware-a index 2a029ef..45c9a20 100755 Binary files a/firmware/bin/20260910101457-firmware-a and b/firmware/bin/20260914085743-firmware-a differ diff --git a/firmware/bin/20260910101457-firmware-p b/firmware/bin/20260914085743-firmware-p similarity index 55% rename from firmware/bin/20260910101457-firmware-p rename to firmware/bin/20260914085743-firmware-p index adc8e95..2dbd253 100755 Binary files a/firmware/bin/20260910101457-firmware-p and b/firmware/bin/20260914085743-firmware-p differ diff --git a/firmware/bin/20260910101457-firmware-q b/firmware/bin/20260914085743-firmware-q similarity index 59% rename from firmware/bin/20260910101457-firmware-q rename to firmware/bin/20260914085743-firmware-q index 0d08d21..4b935ba 100755 Binary files a/firmware/bin/20260910101457-firmware-q and b/firmware/bin/20260914085743-firmware-q differ diff --git a/firmware/p/Cargo.toml b/firmware/p/Cargo.toml index 0b541be..6c98b83 100644 --- a/firmware/p/Cargo.toml +++ b/firmware/p/Cargo.toml @@ -13,3 +13,6 @@ fmt-util = { workspace = true } # The on-target half (see a/Cargo.toml). No model on this node: pure GPIO. [target.'cfg(target_arch = "xtensa")'.dependencies] esp-hal = { version = "1.2", features = ["esp32s3", "unstable"] } +# The ESP-IDF app descriptor (esp_app_desc! in main.rs): required by the +# 2nd-stage bootloader; espflash >= 4.6 refuses a descriptor-less image. +esp-bootloader-esp-idf = { version = "0.6", features = ["esp32s3"] } diff --git a/firmware/p/src/main.rs b/firmware/p/src/main.rs index e6a5605..211b4ac 100644 --- a/firmware/p/src/main.rs +++ b/firmware/p/src/main.rs @@ -24,6 +24,11 @@ mod app { use firmware_p::{format_count, EdgeCounter, DEBOUNCE_MS}; + // The ESP-IDF app descriptor at the image head: the 2nd-stage + // bootloader requires it, and espflash >= 4.6 refuses to flash an + // image without it. + esp_bootloader_esp_idf::esp_app_desc!(); + /// Run id of this firmware image. const RUN_ID: &str = "bench-p"; diff --git a/firmware/q/Cargo.toml b/firmware/q/Cargo.toml index 33f221e..89acd8f 100644 --- a/firmware/q/Cargo.toml +++ b/firmware/q/Cargo.toml @@ -17,3 +17,6 @@ libm = "0.2" # The on-target half (see a/Cargo.toml). [target.'cfg(target_arch = "xtensa")'.dependencies] esp-hal = { version = "1.2", features = ["esp32s3", "unstable"] } +# The ESP-IDF app descriptor (esp_app_desc! in main.rs): required by the +# 2nd-stage bootloader; espflash >= 4.6 refuses a descriptor-less image. +esp-bootloader-esp-idf = { version = "0.6", features = ["esp32s3"] } diff --git a/firmware/q/src/lib.rs b/firmware/q/src/lib.rs index 5c3caf4..a77f182 100644 --- a/firmware/q/src/lib.rs +++ b/firmware/q/src/lib.rs @@ -64,6 +64,20 @@ pub fn i2s_slots_to_f32(slots: &[u32], out: &mut [f32]) -> usize { written } +/// Fills the window with the deterministic synthetic tap (the S4 stand-in +/// until the I2S/INMP441 driver lands): a quiet decay +/// `sin(120·π·t)·(1−t)·0.05`, `t = i/WINDOW`. +/// +/// Out of the model's training distribution by construction: the pinned +/// verdict on it is `cracked` (the bench fact, 2026-09-14) — a constant +/// verdict proves the tap→window→model→UART loop, not a part label. +pub fn synthetic_tap_window(out: &mut [f32]) { + for (i, slot) in out.iter_mut().enumerate() { + let t = i as f32 / WINDOW as f32; + *slot = libm::sinf(t * 120.0 * core::f32::consts::PI) * (1.0 - t) * 0.05; + } +} + /// Formats a verdict line for the UART bridge / capture log: /// `q,,,`. pub fn format_verdict(out: &mut [u8], run_id: &str, t_ms: u32, verdict: usize) -> Option { @@ -111,6 +125,18 @@ mod tests { assert_eq!(verdict, 0, "the good class: probs={probs:?}"); } + /// The synthetic tap is out of the training distribution, so its verdict + /// is a pinned convention, not a truth: `cracked`, the bench-observed + /// fact (2026-09-14, board #2). Meaningful verdicts arrive with the real + /// I2S input (S4); until then the constancy is the loop check. + #[test] + fn synthetic_tap_verdict_is_pinned() { + let mut window = [0.0f32; WINDOW]; + synthetic_tap_window(&mut window); + let (probs, verdict) = classify(&window); + assert_eq!(verdict, 1, "the synthetic quiet decay: probs={probs:?}"); + } + /// Review card 20260909120007 (firmware half): the WINDOW constant is /// a hand copy — pin it to the single source of truth. The rate /// (16 kHz) is fixed by the I2S driver on the target; the window size diff --git a/firmware/q/src/main.rs b/firmware/q/src/main.rs index 3b974a2..98aa501 100644 --- a/firmware/q/src/main.rs +++ b/firmware/q/src/main.rs @@ -39,7 +39,12 @@ mod app { uart::{Config as UartConfig, UartTx}, }; - use firmware_q::{classify, format_verdict, WINDOW}; + use firmware_q::{classify, format_verdict, synthetic_tap_window, WINDOW}; + + // The ESP-IDF app descriptor at the image head: the 2nd-stage + // bootloader requires it, and espflash >= 4.6 refuses to flash an + // image without it. + esp_bootloader_esp_idf::esp_app_desc!(); /// Run id of this firmware image. const RUN_ID: &str = "bench-q"; @@ -102,14 +107,13 @@ mod app { set_duty(&mut servo, REST_DUTY_PCT); t_ms = t_ms.wrapping_add(SETTLE_MS); - // The window (S4 TODO: I2S INMP441; a deterministic synthetic - // tap until then — quiet decay, the good-part family). + // The window (S4 TODO: I2S INMP441; the deterministic synthetic + // tap until then — `synthetic_tap_window` in the lib: a quiet + // decay out of the training distribution, the pinned verdict + // is `cracked` — the loop check, not a label). // SAFETY: see the WINDOW_BUF declaration above. let window = unsafe { &mut *core::ptr::addr_of_mut!(WINDOW_BUF) }; - for (i, slot) in window.iter_mut().enumerate() { - let t = i as f32 / WINDOW as f32; - *slot = libm::sinf(t * 120.0 * core::f32::consts::PI) * (1.0 - t) * 0.05; - } + synthetic_tap_window(window); let verdict = classify(&window).1; if let Some(n) = format_verdict(&mut line, RUN_ID, t_ms, verdict) {