.aswfПосле #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). Валидируется склейка без швов, отсутствие потерь, идемпотентность и открываемость
результата штатным вьюером.
Плата пишет водопад в flash-кольцо, финализирует сегменты по
возрасту и объёму. PC-клиент раз в --interval секунд опрашивает список
сегментов, тянет новые (по state.json), приписывает только строки
в конец растущего файла (заголовок сегмента отбрасывается, первый заголовок остаётся
как шапка склеенного файла), fsync, и только затем — POST .../delete
для этого сегмента на плате.
Сеть/файл/плата отвалились посреди шага — сегмент останется в списке платы, попадёт
в следующий проход. Дубликаты режутся state.json по паре (name, size).
| Параметр | Значение |
|---|---|
| Прошивка платы | main после #WF-1 (CONFIG_SPI_FLASH_AUTO_SUSPEND выкл) |
| PC-клиент (ядро) | scripts/wf_pull_client.py |
| PC-клиент (UI-сборщик) | scripts/wf_recorder_app.py |
| Launcher | scripts/wf_recorder.bat |
| Интервал опроса | 60 с |
| Источник | Cs-137 662 кэВ, комнатная температура |
| Соединение | WiFi (плата ↔ PC) |
Запуск (двойной клик по wf_recorder.bat) или напрямую:
python scripts\wf_recorder_app.py --host http://atomspectra.local --interval 60
Для пользователей без Python — самодостаточный wf_recorder.exe
(~12 МБ, tkinter + requests + wf_pull_client внутри):
wf-recorder-v0.1.0.received/spectrogram.aswf рядом с exe (создастся автоматически).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 (commit85eadb7): исправлен старт exe — режим--windowedPyInstaller обнуляетsys.stdout/sys.stderr; вызов.reconfigure()вwf_recorder_app.pyиwf_pull_client.pyобёрнут guard-омif sys.stdout is not None.
| Событие | Значение |
|---|---|
| Первый сегмент склеен | 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 (не задействованы этой сборкой прошивки) — норма.
Кольцо 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 ч) не требуется.
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 ч между первым и последним замером).
| Файл | Роль |
|---|---|
received/spectrogram_04-07-2026.aswf | Склеенный .aswf (11 ч), локально, ~10.3 МБ, gitignored |
received/…aswf.state.json | Ingested-state (name+size) |
received/…aswf.temps.csv | Телеметрия температуры (603 строки) |
scripts/wf_pull_client.py | PC-клиент (ядро) |
scripts/wf_recorder_app.py | PC-клиент (UI-сборщик) |
scripts/wf_recorder.bat | Launcher |
main/web_waterfall.c | Firmware pull-эндпоинты платы |
Артефакты записи (.aswf, .state.json, .temps.csv) —
локально, не публикуются (gitignored: *.aswf, *.n42; каталог
received/ игнорируется отдельно).
#REC-12 → закрыт. Pull-стрим доказан на железе: 11 ч непрерывной
записи, 60 сегментов, 660 строк — 0 потерь, 0 швов, 0 дубликатов, склеенный файл
байт-в-байт совпадает с ожидаемым размером (60 × сегмент − 59 × шапка).
Открывается штатным вьюером как единая запись.
Схема покрывает основной сценарий длинных записей, обходя ограничение #FW-19 (экспорт n42 обрезан кольцом на ~4.25 ч) — pull-клиент забирает сегменты до заполнения кольца, восстанавливать хвост постфактум не требуется.
ring_capacity
и/или документирование требования периодического pull. #REC-12 смягчает симптом, но
не устраняет причину.s_seg_pinned push-пути). На текущей длительности и типовых
интервалах не критично, документировано в KNOWN_ISSUES.md.Pull-стрим wf_recorder_app.py подтверждён: 11.00 ч непрерывной
записи, 60 сегментов склеены на лету в единый 10.32 МБ .aswf. 0 потерь,
0 швов, 0 ребутов, склеенный файл байт-в-байт совпадает с ожидаемым размером. Пик
Cs-137 662 кэВ стабилен по всей высоте 3D-водопада и 2D-карты. Схема закрывает основной
сценарий длинных записей, обходя ограничение #FW-19 без изменения прошивки.