diff --git a/.gitignore b/.gitignore index b8fb9f1..e9f15fb 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ target/ run*.csv *.tflite.tmp /backlog +windows.csv diff --git a/docs/eng/bench-assembly.md b/docs/eng/bench-assembly.md index a887de1..a21fb18 100644 --- a/docs/eng/bench-assembly.md +++ b/docs/eng/bench-assembly.md @@ -185,8 +185,45 @@ Why exactly this: the top leg's drop at zero current is 2.5 V → GPIO4 sees headroom on both sides. Rows 20–22 keep the divider as a self-contained block — easier to probe with a multimeter. -A multimeter check before the USB power-up: between the GPIO4 jumper and -(−) — about 1.2–1.3 V with the machine off (that is the ACS712's "zero"). +A multimeter check (USB plugged in — the board powers the module, the +machine off): between the GPIO4 jumper and (−) — about 1.2–1.3 V when the +module runs on 5 V (that is the ACS712's "zero"; on 3V3 it is roughly half +as much — the `zero` norms are in 4b). + +Continuity check (power off): mid↔top ≈ 10 kΩ and mid↔bottom ≈ 10 kΩ +(0 Ω — a resistor is shorted: both legs landed in the same row); +bottom↔the board's GND ≈ 0 Ω. + +Rows 20–22 are not a dogma: any three neighboring rows work, but on one +side of the center groove (the groove splits a row in half — a chain +crossing it is not connected). + +A note from the bring-up bench (2026-09-16): the DevKitC-1 clone's 5V pin +is dead (0.14 V) — the ACS712 module is temporarily powered from 3V3; +the `zero` norms for that case are in 4b. + +### 4b. Diagnosing by `zero=` (one line per boot) + +`a: zero=NNN` is printed at firmware startup — the average of 800 ADC +reads over 0.5 s of rest (`firmware/a/src/main.rs`); the scale: 4095 +counts = 3.1 V (`features-cli/src/calibration.rs`). Bench-refined +guidelines: + +| `zero` | What GPIO4 sees | +| ------------------ | ------------------------------------------------------------------------------------------------------------ | +| 750–850 | the divider midpoint, the module on 3V3 — the norm (the bring-up bench) | +| ~1300–1650 | the divider midpoint, the module on 5 V — the norm | +| 1900–2200, stable | OUT past the divider: the tap is not in the middle row, a resistor is shorted, or the bottom is not grounded | +| ~0 | the divider top does not arrive: no OUT or no module power | +| 4095 | the input is pinned to a supply rail or the pin floats | +| jumps boot to boot | a floating contact | + +The bench case (2026-09-16): `zero=1966` — 1.49 V, exactly the top of the +ladder (OUT). While the pin sees OUT past the divider, the firmware +doubles the amplitudes (`divider = 2.0` in the calibration): a 2 A run +looks like ~4 A, the model confuses run/overload; idle stays honest — +the zero is subtracted at startup. The fix is the 4a continuity check +and the diagnosis table above. ## 5. Node Q — sound and the tapper (DevKitC-1 #2) @@ -360,6 +397,29 @@ Per the shakedown decomposition sessions 5. **S6**: node P — a run of N parts, the count = N, bouncing gives no repeats. +## 9a. The "it does not work — why" cheat sheet + +| Symptom | Cause | Fix | +| ------------------------------------------ | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | +| garbage in UART | wrong baud / the wrong port | 115200, the bridge's port (not OTG) | +| the board won't flash | not in boot mode | hold BOOT while plugging in | +| the servo twitches, the board reboots | no common GND / no capacitor | tie the grounds, add the 470 µF | +| node A's readings drift | ACS712 zero drift | start the firmware with the machine off | +| GPIO4 voltage ≈ OUT, not the midpoint | a resistor shorted (both legs in one row), the tap not in the middle row, or the bottom not grounded | the 4a continuity check; the `zero` diagnosis — 4b | +| node A's readings are garbage | no wire (-) rail → the board's GND | check the ground net (section 3) | +| the machine's motor does not spin | the loop is open: only one wire got into the clamp | check IP+ and IP− — both in the cut | +| the boot log is visible, but no `a: boot` | the cable is in the native "USB" port (by-id `usb-Espressif`) | replug into "COM"; the bridge is by-id `usb-1a86*` | +| `cat` on the port is silent after a replug | the replug reset the baud to the default (9600) | `stty ... && cat` as one line, no pause | +| the gauges are empty with node A alive | the aggregator on the default `--expect a,p,q` | restart with `--expect a` (the watermark at zero) | +| the aggregator died at ~31 s of silence | a pre-2026-09-15-fix binary | rebuild (`PING_AFTER=15 s`); the card in backlog/done | +| P overcounts | bouncing | the 50 ms debounce is in the firmware already; check the pot | + +The step-by-step shakedown order (S0–S7) — +[`HARDWARE-assembly-guide.md`](./HARDWARE-assembly-guide.md) and +[`decompose/firmware-shakedown-runbook.md`](./decompose/firmware-shakedown-runbook.md); +the first bring-up facts (2026-09-14, all three boards alive) — +`firmware/NOTES.md`. + ## 10. Safety - The sensors and the INA226 — only into circuits ≤36 V (the machine/ diff --git a/docs/rus/backlog/done/20260915154542-important-aggregator-idle-disconnect-live-bench-bl.md b/docs/rus/backlog/done/20260915154542-important-aggregator-idle-disconnect-live-bench-bl.md new file mode 100644 index 0000000..3479f5d --- /dev/null +++ b/docs/rus/backlog/done/20260915154542-important-aggregator-idle-disconnect-live-bench-bl.md @@ -0,0 +1,30 @@ +# Агрегатор умирает на 30-секундной тишине живого стенда (ping = idle-cut брокера) + +- **Серьёзность**: важно +- **Компонент**: oee-aggregator (+ mqtt-min — контрагент) +- **Источник**: живой стенд, S1 узла A → фаза 2, 2026-09-15 +- **Локация**: `oee-aggregator/src/aggregator.rs` (`PING_AFTER`), контрагент — `mqtt-min/src/testing.rs` (`serve_connection`, read timeout 30 с) + +## Сценарий + +Живой стенд (не реплей): узел A — событийный, строки `a,bench-a,,` уходят только при подтверждённой смене состояния. Мотор работает ровно → поток молчит дольше 30 секунд → ожидающий сообщений агрегатор не отправляет брокеру ни байта. + +## Поведение + +Ровно на 31-й секунде простоя агрегатор падал: `Error: aggregating from 127.0.0.1:1883: mqtt protocol violation: peer closed the connection`. Воспроизведено изолированно (свежий брокер + агрегатор, `--expect a --minute-ms 10000`, таймаут 60 с): EXIT=1 AFTER 31s — с тем же текстом. + +## Последствие + +Переподключения у агрегатора нет («mid-run disconnects abort the run», так задокументировано в `bin/aggregator.rs`) — конвейер недели 5 против физического стенда рвался при любой паузе длиннее 30 с. В реплей-бенче не всплывало: плотный поток → после каждого сообщения агрегатор сам публикует snapshot → вход брокера никогда не молчит 30 с. + +## Корень + +`PING_AFTER = 30 с` («половина negotiated keepalive 60 с») против фиксированного read timeout 30 с внутри брокера (брокер keepalive из CONNECT не читает). Равные таймеры: чтобы отправить пинг, агрегатор должен сначала проснуться от своего 5-секундного read-таймаута — брокерский cut всегда наступает на микросекунды раньше. Фотофиниш проигран конструктивно. + +Сюда же: дашборд (`oee-dashboard/src/mqtt.rs`) не пингует вовсе — брокер режет его каждые 30 с даже при трафике (важны пакеты от клиента), но reconnect-цикл (1 с) + snapshot-дизайн делают churn незаметным. + +## Как исправлено (2026-09-15) + +`PING_AFTER: 30 с → 15 с` + комментарий у константы с привязкой к 30-с idle-cut брокера. Проверка: 60-секундный idle-тест — EXIT=124 (внешний timeout, ошибок нет, до фикса — Error на 31 с); `cargo test -p oee-aggregator` — 5 passed. + +Отложено: инвариант-тест `PING_AFTER < IDLE_READ_TIMEOUT` (требует выноса литерала таймаута из `serve_connection` в публичную константу mqtt-min); `send_ping` в idle-ветке дашборда. diff --git a/docs/rus/bench-assembly.md b/docs/rus/bench-assembly.md index 4d2211e..1fc612a 100644 --- a/docs/rus/bench-assembly.md +++ b/docs/rus/bench-assembly.md @@ -173,8 +173,43 @@ GPIO4 приходит 1.25 В (ADC 11 дБ, диапазон до ~3.1 В); п будет 2.25 В — запас с обеих сторон. Ряды 20–22 держат делитель отдельным блоком — проще найти мультиметром. -Контроль мультиметром до включения USB: между GPIO4-джампером и (−) — -около 1.2–1.3 В при выключенном станке (это и есть «ноль» ACS712). +Контроль мультиметром (USB воткнут — плата питает модуль, станок выключен): +между GPIO4-джампером и (−) — около 1.2–1.3 В при питании модуля от 5 В +(это и есть «ноль» ACS712; при питании от 3V3 — примерно вдвое меньше, +нормы `zero` — в 4б). + +Прозвонка (питание отключено): середина↔верх ≈ 10 кОм и середина↔низ ≈ +10 кОм (0 Ом — резистор закорочен: обе ножки угодили в один ряд); +низ↔GND платы ≈ 0 Ом. + +Ряды 20–22 — не догма: годятся любые три соседних ряда, но на одной +стороне центральной канавки (канавка разрывает ряд пополам — цепочка, +пересекающая её, не соединена). + +Примечание обкаточного стенда (2026-09-16): у клона DevKitC-1 пин 5V +мёртв (0.14 В) — модуль ACS712 временно питается от 3V3; нормы `zero` +для этого случая — в 4б. + +### 4б. Диагностика по `zero=` (одна строка на boot'е) + +`a: zero=NNN` печатается на старте прошивки — среднее 800 замеров АЦП +за 0.5 с покоя (`firmware/a/src/main.rs`); шкала: 4095 counts = 3.1 В +(`features-cli/src/calibration.rs`). Ориентиры, уточнённые на стенде: + +| `zero` | Что видит GPIO4 | +| ------------------------- | ---------------------------------------------------------------------------------------------- | +| 750–850 | середина делителя, модуль на 3V3 — норма (обкаточный стенд) | +| ~1300–1650 | середина делителя, модуль на 5 В — норма | +| 1900–2200, стаб | OUT напрямую, мимо делителя: отвод не из среднего ряда, резистор закорочен или низ не на земле | +| ~0 | верх делителя не доходит: нет OUT или питания модуля | +| 4095 | вход прижат к шине питания или пин висит | +| скачет от boot'а к boot'у | плавающий контакт | + +Кейс стенда (2026-09-16): `zero=1966` — 1.49 В, ровно верх лестницы +(OUT). Пока пин видит OUT мимо делителя, прошивка удваивает амплитуды +(`divider = 2.0` в калибровке): run 2 А выглядит как ~4 А, модель путает +run/overload; idle остаётся честным — нуль вычитается при старте. +Лечение — прозвонка по 4а и диагнозы таблицы выше. ## 5. Узел Q — звук и таппер (DevKitC-1 №2) @@ -337,6 +372,28 @@ TCRT5000 ×2 (в модуле уже компаратор, резисторы н отдельный); вердикт по звуку на эталонных деталях. 5. **Э6**: узел P — прогон N деталей, счёт = N, дребезг не даёт повторов. +## 9а. Шпаргалка «не работает — почему» + +| Симптом | Причина | Лечение | +| -------------------------------------- | ------------------------------------------------------------------------------------------ | -------------------------------------------------------- | +| мусор в UART | неверный baud / не тот порт | 115200, порт моста (не OTG) | +| плата не шьётся | не в boot-режиме | зажать BOOT при подключении | +| серво дёргается, плата ребутится | нет общего GND / конденсатора | соединить минусы, поставить 470 мкФ | +| показания A плывут | дрейф нуля ACS712 | стартовать прошивку при выключенном станке | +| на GPIO4 напряжение ≈ OUT, не середина | резистор закорочен (обе ножки в одном ряду), отвод не из среднего ряда или низ не на земле | прозвонка по 4а, диагноз по `zero` — 4б | +| показания A — мусор | нет провода шина (−) → GND платы | проверить земляную сеть (разд. 3) | +| мотор станка не крутится | петля разомкнута: в зажим попал только один провод | проверить IP+ и IP− — оба в разрезе цепи | +| boot-лог виден, а `a: boot` нет | кабель в нативном «USB»-порту (by-id `usb-Espressif`) | перетыкнуть в «COM»; мост — by-id `usb-1a86*` | +| `cat` порта молчит после перетыка | перетык сбросил baud в дефолт (9600) | `stty ... && cat` одной строкой, без паузы | +| гейджи пустые при живом узле A | агрегатор с дефолтным `--expect a,p,q` | перезапустить с `--expect a` (водяной знак на нуле) | +| агрегатор упал на ~31-й секунде тишины | старый бинарчик до фикса 2026-09-15 | пересобрать (`PING_AFTER=15 с`); карточка в backlog/done | +| P считает лишнее | дребезг | антидребезг 50 мс уже в прошивке; проверить потенциометр | + +Порядок обкатки по шагам S0–S7 — +[`HARDWARE-assembly-guide.md`](./HARDWARE-assembly-guide.md) и +[`decompose/firmware-shakedown-runbook.md`](./decompose/firmware-shakedown-runbook.md); +факты первой обкатки (2026-09-14, все три платы живы) — `firmware/NOTES.md`. + ## 10. Безопасность - Датчики и INA226 — только в цепи ≤36 В (БП станка/конвейера). Сеть 220 В — diff --git a/firmware/NOTES.md b/firmware/NOTES.md index 068fdbe..0676526 100644 --- a/firmware/NOTES.md +++ b/firmware/NOTES.md @@ -67,3 +67,36 @@ - До S6: сверить по схеме конкретной CAM-платы, что GPIO5 свободен от обвязки камеры (TCRT5000) — CAM-платы разных производителей различаются раскладкой. + +## 2026-09-15 — живой стенд: узел A в контуре; агрегатор умирал на тишине + +- Узел A собран на макетке (ACS712 + делитель 2×10 кОм), в петле IP+/IP− — + мотор конвейера. Живые строки: `a: zero=1966` (делитель честный; + висящий вход на S1 давал 2229), статусы на живом сигнале — + `idle → run → jam → overload`: модель классифицирует бодро, но вне + обученного распределения (конвейерный мотор малоток — реальный вопрос + S2; по схеме проекта датчик должен стоять в цепи станка, не конвейера). +- **Грабля нативного порта — повтор на узле A**: монитор через нативный + «USB» (by-id `usb-Espressif_USB_JTAG_serial_debug_unit_44:1B:F6:FD:EA:CC`) + показывает boot-лог (загрузчик вещает в оба окна) и молчит на + приложение (оно пишет в UART0 → «COM»). Монитор — только порт «COM» + (by-id `usb-1a86_USB_Single_Serial_5C94148486-if00`). MAC в имени + нативного порта (`...FD:EA:CC`) совпадает с MAC из журнала прошивки — + удобный способ опознать плату. +- **Баг агрегатора**: на 31-й секунде событийной тишины — + `mqtt protocol violation: peer closed the connection`. Корень: + `PING_AFTER=30 с` против read timeout 30 с в брокере — равные таймеры, + пинг просыпается на микросекунды позже cut. Фикс: `PING_AFTER=15 с` + (`oee-aggregator/src/aggregator.rs`), 60-с idle-тест чист (EXIT=124, + ошибок нет), `cargo test -p oee-aggregator` — 5 passed. Карточка: + `docs/rus/backlog/done/20260915154542-important-aggregator-idle-disconnect-live-bench-bl.md`. + Отложено: инвариант-тест `PING_AFTER < IDLE_READ_TIMEOUT` (вынос + литерала из `serve_connection`), ping для дашборда. +- **Дашборд**: не пингует — брокер режет его каждые 30 с даже при трафике, + но reconnect 1 с + snapshot-дизайн маскируют churn (незаметно в демо). +- **Одиночный узел**: водяной знак = min по ожидаемым источникам, «never + spoke holds it at 0» — живой стенд из одного узла A требует + `--expect a`, иначе минутные окна не закрываются никогда. Для быстрой + обратной связи — `--minute-ms 10000`. +- Стендовая сессия: stty после перетыкания кабеля сбрасывается — мост + кормить одной строкой `stty -F /dev/ttyACM0 115200 raw -echo && cat ...`. diff --git a/oee-aggregator/src/aggregator.rs b/oee-aggregator/src/aggregator.rs index d06b948..45538a6 100644 --- a/oee-aggregator/src/aggregator.rs +++ b/oee-aggregator/src/aggregator.rs @@ -362,8 +362,12 @@ impl WindowsCsv { /// How long an idle read waits before the keepalive check kicks in. const READ_TIMEOUT: Duration = Duration::from_secs(5); -/// Keepalive: 60 s negotiated, ping at half of that. -const PING_AFTER: Duration = Duration::from_secs(30); +/// Keepalive: 60 s negotiated, ping at a quarter of that — strictly inside +/// the bench broker's 30 s idle-cut (`mqtt-min` enforces a fixed read +/// timeout, not the negotiated keepalive, so a 30 s ping lost the +/// photo-finish to it: an event-driven node A idles longer than 30 s on +/// the live bench and the broker dropped the silent connection, 2026-09-15). +const PING_AFTER: Duration = Duration::from_secs(15); /// Runs the aggregator loop until every expected node has ended: connect, /// subscribe, fold, publish, log. Returns the run summary (with the final