🇷🇺 Русская версия · 🇬🇧 English
Список известных багов, ограничений и исправленных проблем AtomSpectra ESP32 Gateway.
Статус: ограничение 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/усиления, индивидуальны для каждого
экземпляра детектора. Чужие значения (в т.ч. из примеров в документации) могут задать
неверное смещение/усиление и повредить прибор.Обходной путь / восстановление: обратиться к производителю прибора (KB Radar) — восстановление заводского профиля детектора выполняет производитель. Самостоятельно, «вслепую» записывать параметры настройки нельзя (см. предупреждение выше).
Рекомендация на будущее: шлюз умеет читать -inf (показывает поля в Web UI), но
не делает их резервную копию. Имеет смысл при настроенном приборе один раз сохранить два
ответа прибора как эталонный «слепок» DSP-конфигурации:
-inf — основные пороги, POT / POT2, таблицу термокомпенсации MAX
(Tco […]) и pile-up;-tc_pot? — таблицу термокомпенсации baseline (POT2 / V от
температуры); её в -inf нет (см. выше).Тогда повторный сброс можно будет зафиксировать и передать производителю (KB Radar) оба слепка как референс заводской настройки.
Статус: открыт
Серийный номер прибора (serial_number) остаётся пустым после подключения.
Причина: прибор в ответ на команду -inf возвращает менее 40 строк текста. Код ожидает серийный номер в строке 39 (process_info_response), но реальный ответ короче. Калибровка (строки 0–10) считывается корректно; серийный номер — нет.
Влияние: в Web UI, XML и CSV экспорте поле серийного номера пустое. Функциональность спектрометра не затронута.
Обходной путь: серийный номер можно задать вручную через Web UI (панель калибровки).
Статус: исправлен, прошивка 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 тика (было — все одинаково).
Статус: исправлены, прошивка firmware-v1.0.11.
max-width контейнера .wrap. max-width снята, сетки переведены на auto-fit —
интерфейс занимает всю ширину окна.Статус: реализовано, прошивка 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-читатели получают температуру покадрово.
Статус: исправлены, прошивка firmware-v1.0.11.
sdkconfig — смещение баланса USB-FIFO в сторону IN + hostname
atomspectra-gw.Статус: исправлен, прошивка firmware-v1.0.6.
Ранее запись «Мониторинга» (усреднённый CPS по интервалу) велась в браузере — при закрытой или свёрнутой вкладке данные не накапливались (дыры), плюс дублировалась с записью водопада.
Исправление: сбор мониторинга перенесён на плату (board-side) — накопление идёт независимо от состояния браузерной вкладки.
Статус: исправлен, прошивка firmware-v1.0.6.
При сшивке сегментов .aswf в единый водопад на границах появлялись полосы /
неравномерность времени по краям.
Причина: в заголовке сегмента поле row_stride записывалось как 16406 вместо
фактического 16402 — рассинхрон шага строки при склейке (main/web_waterfall.c).
Исправление: row_stride в заголовке приведён к фактическому 16402. Файлы, снятые
прежней прошивкой, чинятся пересчётом шага (утилита сверки на ПК).
Статус: реализованы, прошивка firmware-v1.0.6.
#N · −Mm Ss, live → LIVE).-inf/-cal в bridge-режиме портила serial и температуруСтатус: исправлен, прошивка 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.
Статус: реализованы, прошивка firmware-v1.0.4.
data_cb → RX ring → usb_rxw); добавлена метрика rx_ring_drops в статус-JSON..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.
Статус: реализован, прошивка firmware-v1.0.4.
График «Мониторинг» получил временную ось X (секунды / минуты / часы + реальное время); часовой пояс задаётся на странице «Система».
Статус: реализованы, прошивка firmware-v1.0.4 (стендовая сборка v1.0.3).
StartDateTime 1970 на каждой строке при автозапуске после ребутаСтатус: исправлен в 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
монотонно возрастает. Баг устранён на железе.
/ws/waterfall отдаёт 404 → бесконечный цикл reconnectСтатус: исправлен в 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 —
запас под текущие и будущие эндпоинты.
Статус: исправлен в 33fb4a4 (+ 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
(веб-версия с графиками).
spectrogram_is_recordingСтатус: исправлен в 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
ветку проверять «как чистый клон» — собирать только из закоммиченных файлов, а не из
рабочей копии с незакоммиченными изменениями.
Статус: исправлен (см. 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-секундный флаш заголовка) — запись
держится без самоотключения.
Статус: исправлен (см. 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/.
Статус: исправлен в 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().
Статус: исправлен в 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⁴.
Статус: исправлен в a9dab5e
Исправление: полная переработка Web UI — русские метки, защита Math.log10() от нулей, корректный device name.
Статус: исправлен в 7cf0beb
При ручном вводе калибровочных коэффициентов через Web UI цикл update() (1 раз в секунду) перезаписывал содержимое полей текущими значениями из API.
Исправление: добавлен флаг calibEditing — пока пользователь редактирует форму, автообновление полей калибровки приостанавливается.
Автосохранение текущего спектра (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 проблем не ожидается.
Возможные оптимизации (не реализованы, не требуются при текущих сроках):
TCP-мост (порт 8234) поддерживает одно одновременное подключение. При попытке второго соединения оно будет отклонено. BecqMoni или AtomSpectra на ПК работают, пока не откроется второй экземпляр.
Сети 5 GHz не поддерживаются аппаратно. Убедитесь, что роутер вещает на 2.4 GHz.
Прибор Atom Spectra передаёт 8192 канала. Это аппаратное ограничение спектрометра, не прошивки.
Контекст: фича + фикс #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 §6.
Статус: открыт (задача #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), не дожидаясь конца записи для единого экспорта.
Подробности и телеметрия теста, в котором обнаружена проблема:
docs/stab2_report.md §6.
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) |