AtomSpectra · Pull-стрим сегментов + шов на лету · 2026-07-04

REC-12 — Pull-стрим сегментов водопада
+ шов на лету в единый .aswf

Стрим стабилен — 11.00 ч #REC-13 (пин pull-пути) — не критично
11.00
Часов непрерывной записи
60
Сегментов склеено
660
Строк спектра · 0 потерь
10.32 МБ
Склеенный .aswf
§1

Цель

После #REC-11 (pull-эндпоинты /api/waterfall/segments + .../segment + .../segment/delete) плата умеет отдавать финализированные сегменты водопада по HTTP. Но каждый сегмент — отдельный .aswf с собственным заголовком, а длинная запись (> ring_capacity) не восстанавливается постфактум — кольцо keep-last перезаписывает старые строки (#FW-19).

#REC-12 замыкает pull-модель: PC-клиент периодически опрашивает плату, забирает новые сегменты и на лету склеивает их в один растущий .aswf. Тест — 11 ч непрерывной записи с Cs-137 662 кэВ в обычных условиях (комнатная температура, WiFi). Валидируется склейка без швов, отсутствие потерь, идемпотентность и открываемость результата штатным вьюером.

§2

Схема эксперимента

[AtomSpectra детектор] │ USB (внутри платы) ▼ [ESP32-S3 atomspectra-gw] [PC Windows 11] /api/waterfall/segments ── HTTP ──► wf_recorder_app.py (UI поверх wf_pull_client.py) /api/waterfall/segment │ /api/waterfall/segment/delete ├─ spectrogram_04-07-2026.aswf (единый файл, растёт) /api/status (t1/t2/t3) ├─ ...aswf.state.json (ingested по seg-name+size) └─ ...aswf.temps.csv (телеметрия температуры)

Плата пишет водопад в flash-кольцо, финализирует сегменты по возрасту и объёму. PC-клиент раз в --interval секунд опрашивает список сегментов, тянет новые (по state.json), приписывает только строки в конец растущего файла (заголовок сегмента отбрасывается, первый заголовок остаётся как шапка склеенного файла), fsync, и только затем — POST .../delete для этого сегмента на плате.

Сеть/файл/плата отвалились посреди шага — сегмент останется в списке платы, попадёт в следующий проход. Дубликаты режутся state.json по паре (name, size).

§3

Параметры теста

ПараметрЗначение
Прошивка платыmain после #WF-1 (CONFIG_SPI_FLASH_AUTO_SUSPEND выкл)
PC-клиент (ядро)scripts/wf_pull_client.py
PC-клиент (UI-сборщик)scripts/wf_recorder_app.py
Launcherscripts/wf_recorder.bat
Интервал опроса60 с
ИсточникCs-137 662 кэВ, комнатная температура
СоединениеWiFi (плата ↔ PC)

Запуск (двойной клик по wf_recorder.bat) или напрямую:

python scripts\wf_recorder_app.py --host http://atomspectra.local --interval 60

3.1 Готовый Windows exe (#PKG-1)

Для пользователей без Python — самодостаточный wf_recorder.exe (~12 МБ, tkinter + requests + wf_pull_client внутри):

cd scripts
python -m PyInstaller --onefile --windowed --name wf_recorder \
  --hidden-import wf_pull_client --paths . wf_recorder_app.py
# результат: scripts/dist/wf_recorder.exe

Логика внутри exe идентична .py-версии (тот же wf_pull_client.Stitcher, тот же state.json, тот же порядок «сшить → fsync → delete на плате»), включая fix _default_output_path() для frozen-запуска.

Asset обновлён 2026-07-05 (commit 85eadb7): исправлен старт exe — режим --windowed PyInstaller обнуляет sys.stdout/sys.stderr; вызов .reconfigure() в wf_recorder_app.py и wf_pull_client.py обёрнут guard-ом if sys.stdout is not None.
§4

Результаты

Общая длительность

СобытиеЗначение
Первый сегмент склеен2026-07-04 09:30:03 UTC
Последний сегмент склеен2026-07-04 20:30:05 UTC
Продолжительность (dur_sum)11.00 ч (39 601 с)
Строк спектра всего660 (по 60.00 с/строка)
Сегментов забрано60 (seg_00000 … seg_00059)
Размер каждого сегмента на плате184 350 байт (константно)

Разброс интервала строки (39 601 / 660 = 60.001 с) — на уровне единиц миллисекунд, точно попадает в номинал без дрейфа.

Склеенный .aswf

МетрикаЗначение
Файлreceived/spectrogram_04-07-2026.aswf (локально, не в git)
Размер10 818 864 байт (~10.32 МБ)
Строк (из шапки v2)660
Формула проверки60 × 184 350 − 59 × 4104 = 10 818 864 ✔

4104 байт — размер заголовка сегмента. Stitcher оставляет заголовок первого сегмента как шапку итогового файла и отбрасывает шапки 59 последующих. Совпадение до байта — прямое доказательство, что склейка не добавляет и не теряет ни одного байта данных.

Стабильность стрима

МетрикаЗначение
Провалов опроса (error:... в логе клиента)0
Сегментов, потерянных кольцом до забора0 (seg_dropped = 0)
Ребутов платы0
Пропусков строк на границах сегментов0 (визуально по 3D-водопаду)
Скачков центроида на стыкахнет (пик Cs-137 стабилен по всей высоте, 2D-карта)

Телеметрия температуры прибора (t1)

ПараметрЗначение
Замеров603 (по одному на проход опроса)
Диапазон25.5 … 27.0 °C
Разброс1.5 °C за 10.7 ч (комнатная эксплуатация)

t2, t3 = 0 (не задействованы этой сборкой прошивки) — норма.

§5

Артефакт: pull-путь без пина сегмента vs. 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) сегмент во время скачивания не пинит.

При интервале опроса 60 с и типовой скорости записи (сегмент ~10-11 мин) запас многократный — за час плата добавляет ~5-6 сегментов, клиент делает 60 проходов. На данном тесте кольцо ни разу не приблизилось к границе (seg_dropped = 0, все 60 сегментов забраны).

Но при аварийно длинном интервале клиента или очень быстрой сегментации кольцо может съесть непрочитанный сегмент. Это зафиксировано как известное ограничение в KNOWN_ISSUES.md — обходной путь: держать --interval заметно короче времени съедания кольцом непрочитанного сегмента при текущей скорости записи. Автоматической защиты (аналог s_seg_pinned для pull-скачивания) пока нет — на текущей длительности теста (11 ч) не требуется.

§6

Артефакт: started_at vs. dur — разные источники времени

started_at в шапке склеенного файла = time(NULL) платы на момент открытия первого сегмента (SNTP-синк через pool.ntp.org). Абсолютная метка — только если у платы был интернет на старте сегмента. Полное описание — в WATERFALL.md.

dur каждой строки — с живых часов прибора (total_time_sec), от интернета не зависит. Реляционные интервалы между строками честны всегда — и на этом тесте подтверждено: 660 строк × 60.001 с = 39 601 с, ровно совпадает с независимой оценкой по unix_ts из temps.csv (10.75 ч между первым и последним замером).

§7

Артефакты (файлы)

ФайлРоль
received/spectrogram_04-07-2026.aswfСклеенный .aswf (11 ч), локально, ~10.3 МБ, gitignored
received/…aswf.state.jsonIngested-state (name+size)
received/…aswf.temps.csvТелеметрия температуры (603 строки)
scripts/wf_pull_client.pyPC-клиент (ядро)
scripts/wf_recorder_app.pyPC-клиент (UI-сборщик)
scripts/wf_recorder.batLauncher
main/web_waterfall.cFirmware pull-эндпоинты платы

Артефакты записи (.aswf, .state.json, .temps.csv) — локально, не публикуются (gitignored: *.aswf, *.n42; каталог received/ игнорируется отдельно).

§8

Импликации

#REC-12 → закрыт. Pull-стрим доказан на железе: 11 ч непрерывной записи, 60 сегментов, 660 строк — 0 потерь, 0 швов, 0 дубликатов, склеенный файл байт-в-байт совпадает с ожидаемым размером (60 × сегмент − 59 × шапка). Открывается штатным вьюером как единая запись.

Схема покрывает основной сценарий длинных записей, обходя ограничение #FW-19 (экспорт n42 обрезан кольцом на ~4.25 ч) — pull-клиент забирает сегменты до заполнения кольца, восстанавливать хвост постфактум не требуется.

Незакрытые связанные

§9

Итог

Pull-стрим wf_recorder_app.py подтверждён: 11.00 ч непрерывной записи, 60 сегментов склеены на лету в единый 10.32 МБ .aswf. 0 потерь, 0 швов, 0 ребутов, склеенный файл байт-в-байт совпадает с ожидаемым размером. Пик Cs-137 662 кэВ стабилен по всей высоте 3D-водопада и 2D-карты. Схема закрывает основной сценарий длинных записей, обходя ограничение #FW-19 без изменения прошивки.