# Known Issues / Известные проблемы

**🇷🇺 Русская версия** · [🇬🇧 English](KNOWN_ISSUES.en.md)

Список известных багов, ограничений и исправленных проблем AtomSpectra ESP32 Gateway.

## Открытые

### BUG-AS-08: ⚠ Шлюз не резервирует заводскую DSP-настройку прибора

**Статус:** ограничение by design + предупреждение (не баг прошивки шлюза).

Прибор «Atom Spectra» хранит **внутри себя** настройку цифровой обработки импульсов
(DSP-tuning): пороги формы импульса `RISE` / `FALL` / `NOISE`, `MAX` / `HYST` / `MODE` /
`STEP`, частоту дискретизации `F`, аппаратные потенциометры HV/усиления `POT` / `POT2`,
таблицу pile-up и термокомпенсацию.

Бо́льшая часть этих значений видна в ответе прибора на команду `-inf` — включая таблицу
pile-up (`PileUp […]` / `PileUpThr`) и таблицу термокомпенсации **MAX** (`Tco […]`). **Но
таблица термокомпенсации baseline** (зависимость `POT2` / `V` от температуры, точки
`-t_pot`) **в `-inf` НЕ возвращается** — там присутствует лишь флаг `TCpot ON/OFF`. Полный
слепок baseline-термокомпенсации даёт отдельная команда **`-tc_pot?`**.

**Что наблюдалось.** Зафиксирован случай, когда у прибора **обнулилась вся эта настройка**:
`-inf` стал отдавать `RISE 0 FALL 0 NOISE 0 F 1.00 MAX 0 HYST 0 ... POT 0 POT2 0` при том,
что сам прибор остаётся на связи и отвечает `VERSION`. С нулевыми порогами прибор физически
не выделяет импульсы — счёт пропадает (`total_counts = 0`, `cps = 0`), хотя соединение
USB/CDC исправно. **Переподключение USB настройку НЕ восстанавливает.**

**Влияние:** до восстановления DSP-настройки прибор не набирает спектр (ноль отсчётов).
Шлюз, WiFi, веб-интерфейс, TCP-мост и **energy-калибровка** (полином `E(ch)`, хранится
на шлюзе в `calib.bin`) при этом **не затронуты** — это состояние самого прибора, а не
прошивки шлюза.

**⚠ Предупреждение — НЕ восстанавливать «вслепую» командами тюнинга.**
Хотя протокол прибора имеет команды настройки (`-ris`, `-fall`, `-nos`, `-max`, `-hyst`,
`-U`, `-V`, `-step` и др.), **записывать их наугад нельзя**:
- `POT` / `POT2` — аппаратные потенциометры HV/усиления, **индивидуальны для каждого
  экземпляра** детектора. Чужие значения (в т.ч. из примеров в документации) могут задать
  неверное смещение/усиление и повредить прибор.
- Корректные значения порогов и HV знает только заводская процедура калибровки под
  конкретный экземпляр.

**Обходной путь / восстановление:** обратиться к **производителю прибора (KB Radar)** —
восстановление заводского профиля детектора выполняет производитель. Самостоятельно,
«вслепую» записывать параметры настройки нельзя (см. предупреждение выше).

**Рекомендация на будущее:** шлюз умеет **читать** `-inf` (показывает поля в Web UI), но
не делает их резервную копию. Имеет смысл при настроенном приборе один раз сохранить **два**
ответа прибора как эталонный «слепок» DSP-конфигурации:

1. ответ на **`-inf`** — основные пороги, `POT` / `POT2`, таблицу термокомпенсации MAX
   (`Tco […]`) и pile-up;
2. ответ на **`-tc_pot?`** — таблицу термокомпенсации baseline (`POT2` / `V` от
   температуры); **её в `-inf` нет** (см. выше).

Тогда повторный сброс можно будет зафиксировать и передать производителю (KB Radar) оба
слепка как референс заводской настройки.

### BUG-AS-03: Серийный номер не считывается

**Статус:** открыт

Серийный номер прибора (`serial_number`) остаётся пустым после подключения.

**Причина:** прибор в ответ на команду `-inf` возвращает менее 40 строк текста. Код ожидает серийный номер в строке 39 (`process_info_response`), но реальный ответ короче. Калибровка (строки 0–10) считывается корректно; серийный номер — нет.

**Влияние:** в Web UI, XML и CSV экспорте поле серийного номера пустое. Функциональность спектрометра не затронута.

**Обходной путь:** серийный номер можно задать вручную через Web UI (панель калибровки).

---

## Исправленные

### #UI-43: тумблер шкалы X (с/м/ч) на «Мониторинге» не давал видимого эффекта

**Статус:** исправлен, прошивка [`firmware-v1.0.11`](https://github.com/VibeEngineering-LLC/atomspectra-waterfall-esp32/releases/tag/firmware-v1.0.11).

На графике «Мониторинг» переключатель единиц оси времени (секунды / минуты / часы) не
менял видимый шаг подписей — все три положения давали одинаковую сетку.

**Причина:** старый `xTickStep` (`web/monitor.html`) вычислял целевой шаг как
`target = spanMs/6`, игнорируя выбранную единицу — на реальном спане (напр. 199 мин) все
единицы округлялись к одному и тому же «красивому» шагу (3 600 000 мс), сетка получалась
идентичной (`all_equal`).

**Исправление:** новый density-based `xTickStep` — целевая плотность тиков задаётся по
единице (`k = 15 / 8 / 4` для с / м / ч) на едином nice-list шагов; удалён guard-частокол
округления. Теперь с / м / ч дают три разных шага на любом спане.

**Проверка (2026-07-11):** на живой плате (`/api/system` fw=v1.0.11) на спанах 1 мин…12 ч
единицы дают `distinct=3` разных шага; на 199-мин спане с=900 с / м=1800 с / ч=3600 с →
13 / 7 / 3 тика (было — все одинаково).

### #UI-42 + #FW-49: адаптивная ширина Web UI и перенос поля префикса на вкладки экспорта

**Статус:** исправлены, прошивка `firmware-v1.0.11`.

- **#UI-42:** все 6 страниц Web UI (Спектр / Водопад / Сохранённые / Система / Сервис /
  Мониторинг) не растягивались на широких экранах — контент был ограничен фиксированной
  `max-width` контейнера `.wrap`. `max-width` снята, сетки переведены на `auto-fit` —
  интерфейс занимает всю ширину окна.
- **#FW-49:** поле «префикс имени файла» стояло на странице «Система», в стороне от самого
  экспорта. Перенесено на каждую вкладку экспорта (Спектр / Водопад / Мониторинг) сразу
  после кнопок экспорта; панели «Системы» выровнены.

### #FW-41: температура детектора добавлена в формат водопада (ASWF v5)

**Статус:** реализовано, прошивка `firmware-v1.0.11` (внутренний бамп формата на стендовой
сборке v1.0.7).

В сегментный формат водопада `.aswf` добавлено поле температуры детектора (`t1` из ответа
прибора `-inf`). Версия формата поднята **v4 → v5**: заголовок `version:5`,
`row_stride:16406`, новое поле `temperature` со смещением `16402` (float32, °C). При
отсутствии валидной температуры (`di->valid = false`) пишется NaN-guard (`0x7FC00000`).

**Совместимость:** читатели ASWF v4 продолжают работать (поле добавлено в хвост строки);
v5-читатели получают температуру покадрово.

### #FW-42 / #FW-43 / #FW-44 / #FW-46 / #FW-48: пакет прошивки firmware-v1.0.11

**Статус:** исправлены, прошивка `firmware-v1.0.11`.

- **#FW-42:** настраиваемый префикс имени файла применяется во всех экспортах (Спектр /
  Водопад / Мониторинг / n42 / CSV).
- **#FW-43:** повторная инициализация USB-спектрометра при «горячем» переподключении
  (hotplug) не срабатывала — исправлено.
- **#FW-44:** свежая плата без настроенного WiFi не выдавала диагностику в консоль —
  добавлен диагностический вывод при старте без сети.
- **#FW-46:** `sdkconfig` — смещение баланса USB-FIFO в сторону IN + hostname
  `atomspectra-gw`.
- **#FW-48:** экспериментальный «kick» линий DTR/RTS удалён (revert) — эффекта не давал.

### #MON-1: мониторинг перенесён из браузера на плату

**Статус:** исправлен, прошивка [`firmware-v1.0.6`](https://github.com/VibeEngineering-LLC/atomspectra-waterfall-esp32/releases/tag/firmware-v1.0.6).

Ранее запись «Мониторинга» (усреднённый CPS по интервалу) велась в браузере — при закрытой
или свёрнутой вкладке данные не накапливались (дыры), плюс дублировалась с записью водопада.

**Исправление:** сбор мониторинга перенесён на плату (board-side) — накопление идёт
независимо от состояния браузерной вкладки.

### #FW-39: артефакт склейки на границах сегментов водопада

**Статус:** исправлен, прошивка `firmware-v1.0.6`.

При сшивке сегментов `.aswf` в единый водопад на границах появлялись полосы /
неравномерность времени по краям.

**Причина:** в заголовке сегмента поле `row_stride` записывалось как `16406` вместо
фактического `16402` — рассинхрон шага строки при склейке (`main/web_waterfall.c`).

**Исправление:** `row_stride` в заголовке приведён к фактическому `16402`. Файлы, снятые
прежней прошивкой, чинятся пересчётом шага (утилита сверки на ПК).

### #UI-39 / #UI-40 / #UI-41: перекрестие водопада и быстрые тумблеры линий нуклидов

**Статус:** реализованы, прошивка `firmware-v1.0.6`.

- **#UI-39:** на основном экране водопада добавлено перекрестие с оверлеем — канал /
  энергия / отсчёты + номер строки и время «от сейчас» (`#N · −Mm Ss`, `live → LIVE`).
- **#UI-40 / #UI-41:** быстрая кнопка вкл/выкл линий нуклидов на страницах «Водопад» и
  «Спектр» (сам выбор набора нуклидов не сбрасывается, состояние переживает F5).

### #BRIDGE-3: гонка ответов `-inf`/`-cal` в bridge-режиме портила serial и температуру

**Статус:** исправлен, прошивка [`firmware-v1.0.5`](https://github.com/VibeEngineering-LLC/atomspectra-waterfall-esp32/releases/tag/firmware-v1.0.5).

В режиме TCP-моста (порт 8234) при одновременном опросе прибора командами `-inf` и `-cal`
ответы перемешивались: серийный номер вырождался в `FFFFFFFF`, температура читалась как `0`,
поля разных ответов слипались.

**Исправление:** разделение и сериализация ответных потоков `-inf` и `-cal` в bridge-пути.

**Проверка (#TEST-2 Block A, 2026-07-09):** 40 итераций интерливинга `-inf`/`-cal` через
TCP :8234 — 0 испорченных `serial` / `t1` / `version`.

### #BRIDGE-1 + #DATA-1: развязка USB-приёма и сквозной контроль целостности (формат v4)

**Статус:** реализованы, прошивка [`firmware-v1.0.4`](https://github.com/VibeEngineering-LLC/atomspectra-waterfall-esp32/releases/tag/firmware-v1.0.4).

- **#BRIDGE-1:** под нагрузкой TCP-моста приём от спектрометра развязан через внутреннее
  RX-кольцо (`data_cb → RX ring → usb_rxw`); добавлена метрика `rx_ring_drops` в статус-JSON.
- **#DATA-1 (a / b / c):** сквозной контроль целостности потока — per-row **CRC32** в строке
  `.aswf` + глобальный **seq** сегмента (NVS-персист, детект пропусков) + reconciliation-
  счётчик «прибор-total против записанного». PC-клиент верифицирует CRC/seq при сборке.

**Проверка (#TEST-2 Block A):** `rx_ring_drops` дельта = 0; per-row CRC 64/64; n42 ↔ aswf
192/192.

### #UI-38: временная ось X на «Мониторинге» + часовой пояс

**Статус:** реализован, прошивка `firmware-v1.0.4`.

График «Мониторинг» получил временную ось X (секунды / минуты / часы + реальное время);
часовой пояс задаётся на странице «Система».

### #UI-36 / #UI-37: масштаб спектра «как AtomSpectra» и кнопка проверки обновления

**Статус:** реализованы, прошивка `firmware-v1.0.4` (стендовая сборка v1.0.3).

- **#UI-36:** отображение спектра приведено к виду AtomSpectra — логарифмическая ось Y с
  авто-диапазоном, лёгкая заливка под линией.
- **#UI-37:** кнопка «Проверить обновление» подогнана по размеру текста и прижата вправо.

### #FW-23: n42-экспорт — `StartDateTime` 1970 на каждой строке при автозапуске после ребута

**Статус:** исправлен в [`79eff24`](https://github.com/VibeEngineering-LLC/atomspectra-waterfall-esp32/commit/79eff24), в `main`. Прошивка `firmware-v1.0.1`.

При автозапуске записи водопада после ребута/включения n42-экспорт показывал
`<StartDateTime>1970-01-01T...Z</StartDateTime>` на каждом сегменте — время старта не
синхронизировано с реальными часами.

**Причина:** порядок инициализации в `app_main()` — `usb_host_cdc_init()` (boot-автозапуск
водопада) вызывается **раньше** `init_sntp()`. `spectrogram_start()` латчит
`started_at = time(NULL)`, пока RTC ещё у epoch-0 (SNTP-ответ не пришёл) → `started_at`
фиксируется near-1970. Переподключение USB/WiFi значение не правит.

**Исправление:** добавлена SNTP-callback `spectrogram_time_synced()`
(`main/spectrogram.c:831`). При приходе первого SNTP-ответа, если запись активна и
`started_at < WF_SANE_EPOCH`, `started_at` пересчитывается назад по прошедшему времени:
`started_at = time(NULL) − elapsed`. Реальное начало записи восстанавливается ретроактивно,
не теряя уже записанные сегменты.

**Проверка (2026-07-08):** полевая выгрузка `waterfall (30).n42` с платы (прошивка
`firmware-v1.0.1`, залита прошивальщиком) — 60 сегментов, **ноль `1970`**, все
`2026-07-08`; boot-autostart сегмент датирован корректно (`12:24:51Z`), StartDateTime
монотонно возрастает. Баг устранён на железе.

### #WF-2: `/ws/waterfall` отдаёт 404 → бесконечный цикл reconnect

**Статус:** исправлен в [`6c81c37`](https://github.com/VibeEngineering-LLC/atomspectra-waterfall-esp32/commit/6c81c37), в `main`.

WebSocket-эндпоинт водопада `/ws/waterfall` периодически отдавал `404 Not Found`, браузер
уходил в цикл переподключения — стрим водопада не поднимался.

**Причина:** переполнен реестр URI-хендлеров ESP-IDF httpd. `CONFIG_HTTPD_MAX_URI_HANDLERS`
= 45, а суммарное число регистрируемых хендлеров (страницы, ассеты, REST API, сегментные
эндпоинты) выросло выше 45. Регистрация `/ws/waterfall` падала с
`ESP_ERR_HTTPD_HANDLERS_FULL` → сервер не знал маршрут → `404`.

**Исправление:** `CONFIG_HTTPD_MAX_URI_HANDLERS` поднят `45 → 60` в `sdkconfig.defaults` —
запас под текущие и будущие эндпоинты.

### #WF-1: Плата уходила в краш-луп при активной записи водопада

**Статус:** исправлен в [`33fb4a4`](https://github.com/VibeEngineering-LLC/atomspectra-waterfall-esp32/commit/33fb4a4) (+ [`2b36737`](https://github.com/VibeEngineering-LLC/atomspectra-waterfall-esp32/commit/2b36737)), в `main`.

Под нагрузкой активной записи водопада (запись в `.aswf`-сегменты + одновременный
USB-приём от детектора + WiFi-опрос) плата уходила в краш-луп — ребут в среднем каждые
~15 минут.

**Причина:** `CONFIG_SPI_FLASH_AUTO_SUSPEND` был включён на flash-чипе, не входящем в
whitelist ESP-IDF для этой опции — под конкурентной нагрузкой SPI flash уходил в
auto-suspend в неподходящий момент записи, что приводило к краху.

**Исправление:** `CONFIG_SPI_FLASH_AUTO_SUSPEND` отключён.

**Проверка (#STAB-2, 2026-07-04):** ретест 9.40 ч непрерывной записи водопада через
плату (не эмуляция) — **0 ребутов, 0 потерянных flash-сегментов** (`seg_dropped`) за
весь прогон, 52 чистых `SEG_ROLLOVER`. До фикса — ребут каждые ~15 мин под той же
нагрузкой. Полный отчёт с телеметрией и графиками: [`docs/stab2_report.md`](docs/stab2_report.md)
([веб-версия с графиками](docs/stab2_report.html)).

### BUG-AS-07: Сборка падает на чистом клоне — undefined `spectrogram_is_recording`

**Статус:** исправлен в [`1b21d61`](https://github.com/VibeEngineering-LLC/atomspectra-waterfall-esp32/commit/1b21d61)

GitHub Actions job `build` (ESP-IDF v5.4) падал на чистом клоне репозитория за ~2 минуты.
Локально сборка проходила — баг проявлялся только на свежем clone (CI, чужая машина).

**Причина:** в публичный репозиторий попал **незавершённый фрагмент** функции
реконнекта USB. В `main/usb_host_cdc.c` (после восстановления CDC-соединения с прибором)
был вызов `spectrogram_is_recording()` — чтобы при активной записи водопада возобновить
набор спектра командой `-sta`. Сама функция определена в файлах подсистемы записи
водопада, которые на момент того коммита **не были закоммичены** (фича в незавершённой
локальной разработке). На машине автора всё компилировалось, потому что определение
лежало в рабочей копии; на чистом клоне компилятор видел только вызов без объявления →
`implicit declaration of function 'spectrogram_is_recording'`. В ESP-IDF действует
`-Werror=all`, поэтому implicit declaration — не предупреждение, а жёсткая ошибка сборки.

**Исправление:** незавершённый хук возобновления записи убран из `usb_host_cdc.c` в
публичном репозитории — сборка снова замкнута только на закоммиченные символы. Сама
функция автоматического возобновления набора спектра при реконнекте остаётся в
неопубликованной локальной ветке и будет внесена единым целостным коммитом вместе со
всей подсистемой записи водопада на диск (а не отдельным «висящим» вызовом).

**Обновление:** обещанный единый коммит внесён — подсистема автономной записи
сегментами (`spectrogram.[ch]`, эндпоинты `/segments` `/segment`, хук реконнекта в
`usb_host_cdc.c`, авто-возобновление через `spectrogram_restore()`) опубликована
целостно в ветке `rec-11-autonomous-recording`. Сборка на чистом клоне снова
самодостаточна — символ `spectrogram_is_recording()` теперь определён в составе того же
коммита, что и его вызов.

**Проверка:** CI job `build` на коммите `1b21d61` — зелёный (до этого `b0b8856` и
`958ac19` падали с тем же `implicit declaration`). Профилактика на будущее: перед push
ветку проверять «как чистый клон» — собирать только из закоммиченных файлов, а не из
рабочей копии с незакоммиченными изменениями.

### BUG-AS-06: Запись стрима водопада на диск самопроизвольно отключается

**Статус:** исправлен (см. `web/waterfall.html`, сериализатор записи `dskEnqueue`)

При активной записи водопада в файл («💾 Стрим на диск», формат `.aswf` через
File System Access API браузера) запись через некоторое время самопроизвольно
прекращалась — индикатор записи гас, файл переставал расти, хотя поток WebSocket
продолжал поступать. Воспроизводилось тем надёжнее, чем чаще приходили строки
(меньший интервал водопада); при больших интервалах — реже, но всё равно
систематически.

**Причина:** входящие WS-строки записывались в `FileSystemWritableFileStream`
вызовом `dskWritable.write(...)` **без `await`**, а каждые 30 секунд таймер
`flushDiskHeader` перезаписывал заголовок файла (ещё несколько `write()`).
`FileSystemWritableFileStream.write()` асинхронный: попытка начать новую запись,
пока предыдущая ещё не завершилась, бросает исключение **синхронно** (поток
заблокирован, `The stream is locked`). На стыке «запись строки данных ↔ перезапись
заголовка» исключение возникало систематически (примерно раз в 30 секунд); оно
ловилось обработчиком ошибок записи, который вызывал `stopDiskStream()` — поэтому
запись «сама» отключалась.

**Исправление:** все записи в поток сериализованы через единую Promise-очередь
(`dskEnqueue`) — следующая запись стартует только после завершения предыдущей, ни
одного конкурентного `write()`. Позиции записи заданы явными байтовыми смещениями
(`{type:"write",position:…}`) вместо неявного курсора: заголовок (позиции 0 и 8) и
строки данных (позиция ≥ `8 + резерв заголовка`) больше не пересекаются.
`stopDiskStream()` дожидается опустошения очереди (`await dskQueue`) перед финальной
записью заголовка и `close()`; добавлен реентранси-гард `dskStopping`.

**Проверка:** сборка прошивки (ESP-IDF v5.4) без ошибок; `node --check` встроенного
JS — без синтаксических ошибок; правка зеркально продублирована в демо
(`demo/waterfall.html`). Функциональный тест на плате: запись водопада с интервалом
5 с (строки приходят чаще, чем срабатывает 30-секундный флаш заголовка) — запись
держится без самоотключения.

### BUG-AS-05: Web UI обрушивается при многократном Ctrl+F5 (HTTP 431)

**Статус:** исправлен (см. `sdkconfig.defaults`, ключ `CONFIG_HTTPD_MAX_REQ_HDR_LEN`)

При многократной жёсткой перезагрузке (Ctrl+F5) страницы водопада интерфейс мог
обрушиться — часть ассетов не загружалась, графики/стрим переставали обновляться.

**Причина:** при жёстком reload браузер не использует кэш и шлёт запрос с полным
набором HTTP-заголовков (`Authorization: Basic` от web-auth + `Sec-Fetch-*` +
`Accept*` + `Cache-Control: no-cache`). Суммарный блок заголовков превышал лимит
ESP-IDF httpd `CONFIG_HTTPD_MAX_REQ_HDR_LEN` (по умолчанию **512 Б**), и сервер
отвечал `431 Request Header Fields Too Large` вместо страницы. Прошивка при этом
оставалась полностью стабильной (heap/CPU без изменений, без перезагрузок) —
обрушивался только UI в браузере.

**Исправление:** в `sdkconfig.defaults` подняты лимиты httpd:
`CONFIG_HTTPD_MAX_REQ_HDR_LEN=1024` и `CONFIG_HTTPD_MAX_URI_LEN=1024` (512 → 1024 Б).

**Проверка:** стресс-тест Ctrl+F5 с захватом UART — после фикса ответы `431`
исчезли (было: на каждый F5 → стало: 0), без перезагрузок прошивки, WS-реконнекты
проходят. Методика и сырые логи до/после: [`tests/stress/`](tests/stress/README.md).

### BUG-AS-04: Калибровка отсутствует в XML после перезагрузки

**Статус:** исправлен в [`3ab490f`](https://github.com/VibeEngineering-LLC/atomspectra-esp32/commit/3ab490f)

После перезагрузки ESP32 калибровка могла отсутствовать в JSON API и XML-экспорте, несмотря на сохранённый `calib.bin`.

**Причина:** в `main.c` вызов `spectrum_load_calibration()` стоял **перед** `spectrum_restore_autosave()`. Autosave восстанавливал полную структуру `s_spectrum` через `fread()`, затирая калибровку, загруженную из `calib.bin`.

**Исправление:** порядок вызовов в `app_main()` изменён — сначала `spectrum_restore_autosave()`, затем `spectrum_load_calibration()`.

### BUG-AS-02: Курсор keV показывает неверную энергию

**Статус:** исправлен в [`a9dab5e`](https://github.com/VibeEngineering-LLC/atomspectra-esp32/commit/a9dab5e)

При наведении курсора на спектр значение keV не соответствовало реальной энергии канала.

**Причина:** в Web UI функция расчёта энергии неправильно применяла полиномиальные коэффициенты — порядок коэффициентов был инвертирован (BecqMoni UI показывает коэффициенты в нисходящем порядке: a=ch⁴, b=ch³, …, e=offset, а внутренний формат — восходящий: c₀=offset, c₁=ch¹, …).

**Исправление:** функция `channelToKeV()` в `index.html` пересчитывает по восходящему полиному `E(ch) = c₀ + c₁·ch + c₂·ch² + c₃·ch³ + c₄·ch⁴`.

### BUG-AS-01: Web UI — русские подписи и логарифмическая шкала

**Статус:** исправлен в [`a9dab5e`](https://github.com/VibeEngineering-LLC/atomspectra-esp32/commit/a9dab5e)

- Подписи осей и кнопок были на английском
- Логарифмическая шкала Y отрисовывалась некорректно (нулевые каналы давали -Infinity)
- Имя устройства отображалось как «Atom Spectra» вместо реального из прибора

**Исправление:** полная переработка Web UI — русские метки, защита `Math.log10()` от нулей, корректный device name.

### calibEditing: Поля калибровки перезаписываются во время редактирования

**Статус:** исправлен в [`7cf0beb`](https://github.com/VibeEngineering-LLC/atomspectra-esp32/commit/7cf0beb)

При ручном вводе калибровочных коэффициентов через Web UI цикл `update()` (1 раз в секунду) перезаписывал содержимое полей текущими значениями из API.

**Исправление:** добавлен флаг `calibEditing` — пока пользователь редактирует форму, автообновление полей калибровки приостанавливается.

---

## Ограничения

### Flash-память: износ при автосохранении

Автосохранение текущего спектра (`current.bin`, ~33 КБ) выполняется каждые 60 секунд. Это ~1440 записей в сутки.

| Тип flash-памяти | Ресурс | Оценка срока |
|---|---|---|
| Winbond (типовой, 100K циклов) | 100 000 стираний/сектор | **15–30 лет** |
| Дешёвый no-name (10K циклов) | 10 000 стираний/сектор | **1.5–5 лет** |

LittleFS использует copy-on-write и wear leveling по всему разделу (12.9 МБ), что распределяет нагрузку. При использовании оригинальных плат ESP32-S3-DevKitC-1 с Winbond flash проблем не ожидается.

**Возможные оптимизации** (не реализованы, не требуются при текущих сроках):
- Увеличить интервал автосохранения (например, 5 минут)
- Сохранять только при изменении спектра (delta-check)
- Хранить в PSRAM, писать на flash только при штатном выключении

### Один TCP-клиент

TCP-мост (порт 8234) поддерживает **одно** одновременное подключение. При попытке второго соединения оно будет отклонено. BecqMoni или AtomSpectra на ПК работают, пока не откроется второй экземпляр.

### ESP32 поддерживает только WiFi 2.4 GHz

Сети 5 GHz не поддерживаются аппаратно. Убедитесь, что роутер вещает на 2.4 GHz.

### Максимум каналов — 8192

Прибор Atom Spectra передаёт 8192 канала. Это аппаратное ограничение спектрометра, не прошивки.

### Автономная запись: кольцо keep-last при переполнении флеша не прошло длинный soak

**Контекст:** фича + фикс #WF-1 (`33fb4a4`) уже в `main`; ветка `rec-11-autonomous-recording`
как единое целое ещё не влита (лишние несвязанные коммиты сверху).

Автономная запись водопада сегментами (`seg_NNNNN.aswf`) и ротация сегментов при
достижении `WF_SEG_MAX_ROWS` (64 строки) **подтверждены** на плате: `seg_00000.aswf`
финализируется при заполнении и автоматически создаётся `seg_00001.aswf`.

Режим **keep-last ring** (при заполнении флеша затирается самый старый ещё не выгруженный
сегмент; счётчики `seg_count` / `seg_dropped`) реализован, но **не проверен длинным
soak-тестом** до фактического исчерпания раздела (≈763 строки, ≈5 суток при интервале
10 мин). До прохождения этого теста граница «флеш полон → перезапись старого» считается
непротестированной — этот конкретный сценарий (реальное исчерпание раздела) ещё не прогнан,
хотя сам код записи уже в `main`.

**Обновление 2026-07-04 (#STAB-2):** 9.40-часовой soak-тест записи через плату
подтвердил надёжность самой записи сегментов — **0 ребутов, 0 `seg_dropped`**, 52 чистых
`SEG_ROLLOVER` (см. фикс #WF-1 выше). Это не тот же тест, что soak до исчерпания
763-строчного раздела (кольцо keep-last по сегментам всё ещё не прогнано до реального
переполнения) — но попутно обнаружено **отдельное** ограничение экспорта: поле
`ring_capacity` в `/api/waterfall/status` фиксировано на **256 строк** (~4.25 ч при
кадансе ~60 с) — меньше, чем оценка ёмкости раздела (~763 строки) выше. При выгрузке
n42-экспорта запись длиннее ~4.25 ч оказывается обрезанной (см. #FW-19 ниже). Полный
разбор: [`docs/stab2_report.md`](docs/stab2_report.md) §6.

### #FW-19: экспорт n42 обрезан ёмкостью flash-кольца (256 строк ≈ 4.25 ч)

**Статус:** открыт (задача #32).

При выгрузке n42-экспорта записи, шедшей дольше ~4.25 ч, файл содержит только
**последние 256 строк** — независимо от того, сколько строк реально записано
(`total_rows`). Причина — `ring_capacity=256` (`/api/waterfall/status`): кольцо физически
хранит только последние 256 строк, более старые перезаписываются новыми до выгрузки.

Это **не регрессия #WF-1** — счётчик `seg_dropped` (потери на записи) остаётся 0, т.е.
сама запись данные не теряет; ограничение — в объёме хранения экспортируемого кольца, не
в надёжности записи. Событие `RING_OVERFLOW` не детектирует сам факт перезаписи (его
условие — лаг `total_rows − flash_rows ≥ ring_capacity`, а не факт wraparound).

**Обходной путь:** для записей длиннее ~4.25 ч — периодически забирать сегменты через
`/api/waterfall/segment` (см. раздел «Автономная запись сегментами» в
[`WATERFALL.md`](WATERFALL.md)), не дожидаясь конца записи для единого экспорта.
Подробности и телеметрия теста, в котором обнаружена проблема:
[`docs/stab2_report.md`](docs/stab2_report.md) §6.

### Pull-опрос (`wf_pull_client.py`, #REC-12): нет пина сегмента от кольца keep-last

Кольцо keep-last (`make_room()`, `main/spectrogram.c:304-321`) при нехватке места на Flash
удаляет самый старый ЗАВЕРШЁННЫЙ сегмент — кроме текущего открытого и **запиненного**
(`s_seg_pinned`). Пин ставит только push-выгрузка (#REC-11-A2, `wf_offload.c`); pull-путь
(`GET /api/waterfall/segment`, `main/web_waterfall.c:468`) сегмент во время скачивания
**не пинит**.

Если PC-клиент опрашивает РЕЖЕ, чем плата успевает заполнить Flash необработанными
сегментами — кольцо может стереть сегмент раньше, чем `wf_pull_client.py` успеет его
забрать. Следующий `GET .../segment?name=...` вернёт ошибку (`error:get:...` в логе
клиента), delete/ack не отправится, но сегмент физически уже удалён — данные потеряны
безвозвратно, дыра в едином `.aswf`. Это НЕ детектируется как «пауза платы» (`gap`,
`wf_pull_client.py:161-169` ловит только паузы записи, не съеденные кольцом сегменты).

**Обходной путь:** держать `--interval` клиента заметно короче времени, за которое
кольцо успевает съесть непрочитанный сегмент при текущей скорости записи. Автоматической
защиты (pin, как у push) для pull-скачивания пока нет — не реализовано.

---

## Совместимость

| Компонент | Версия |
|---|---|
| ESP-IDF | v5.1+ (протестировано на v5.4) |
| Целевой чип | ESP32-S3 (USB OTG Host обязателен) |
| Спектрометр | KB Radar «Atom Spectra» (FTDI FT232R, 600000 бод) |
| BecqMoni XML | FormatVersion 120920 |
| Web UI | Современные браузеры (Chrome, Firefox, Safari, Edge) |
