mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
fix(tci): дефекты живого прогона — length аудио, маршруты тапов, MOX, рекордер, EOF сокета
Восемь дефектов, найденных прогоном настоящего TCI-клиента (три приёмника: NFM, DIGU, FMRAW) и его отчётом. 1. UI доп. панорам не перерисовывался: rfSliceState рассылался, но ветки в MainForm.OnControllerState не было (частоту несёт отдельный rfSliceFreq). 2. Stream.length у аудио — сэмплы НА КАНАЛ (§4.3), у IQ — вещественные отсчёты (§3.4: комплексных = length/channels). Было ×каналы везде, у стерео получалось вдвое больше. Развилка в TCIFillHeader + разбор TX-аудио в HandleBinary. 3+4. Дыры в маршрутах аудио движка: demod-тап звался только для DMR/FMRAW (у DIGU не было RX_AUDIO), а пост-громкостный — только для нецифровых (у FMRAW не было LINEOUT). Плюс мьют слайса больше не убивает RX_AUDIO: движку сообщают SetAudioTapsActive. 5. Клиент, поставивший TRX, уходил — MOX оставался. FTrxOwner + StopTxOf; TCIMicRequested снимается и по окончании любой передачи. 6. Гонка снятия IQ-тапа: SetIQTap(nil) возвращался раньше, чем DSP-поток выходил из вызова. FIQTapLock (порядок FSliceLock → FIQTapLock). 7. Рекордер был кольцом «последние N секунд», а §4.3 говорит про МАКСИМАЛЬНОЕ время записи с удалением по истечении. Переделан в линейный буфер с окном по часам от START. 8. TCIServer.HandleClient считал recv = 0 таймаутом: ноль — это EOF, errno при нём не трогается и несёт EAGAIN от прошлого истёкшего TCI_POLL_MS. Обычный TCP-разрыв без close-кадра не освобождал слот до остановки сервера, и после нескольких аварийных отключений новые клиенты упирались в TCI_MAX_CLIENTS. Теперь R = 0 рвёт связь безусловно, errno спрашивается только при R < 0. Попутно: MainForm.RecreateDSPEngine (смена sample rate до START) терял внутренние колбэки контроллера — введён AttachEngineCallbacks. Стенд test/tci заведён в репозиторий (run.sh, 126/126 зелёных, включая сквозной прогон через живой WDSP), доп. проверки на оба пути отключения клиента. doc/TCI.md приведена в соответствие: правило про recv = 0 в §1.1, единицы Stream.length, линейный буфер рекордера. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+49
-22
@@ -23,7 +23,7 @@ TCI-клиенты ──WebSocket──► TTCIServer ──► TTCIAdapter ─
|
||||
| `TCIProtocol.pas` (~380 строк) | Чистый слой протокола: разбор `имя:арг1,арг2;`, сборка строк, экранирование `^ ~ *`, словарь видов связи, пересчёт громкости/порога в дБ. Зависит только от RTL + `RadioModes`. |
|
||||
| `TCIServer.pas` (~1250 строк) | WebSocket-сервер: accept-поток, поток на клиента, HTTP-Upgrade, разбор фреймов, рассылка, тик 20 мс. Сокеты и фреймы переиспользованы из веб-подсистемы (`WebUtils`, `WsClient`). |
|
||||
| `TCIAdapter.pas` (~2600 строк) | Мост к `TRadioController`: реализация команд, пачка инициализации, уведомления об изменениях состояния, измерители, захват параметров (§3.5), подключение потоков к тапам аудио/IQ. |
|
||||
| `TCIStreams.pas` (~600 строк) | Бинарные потоки (§3.4): дециматор/интерполятор, нарезка блоков с заголовком, кольцо записи линейного выхода и писатель WAV. Зависит только от RTL + `TCIProtocol`/`TCIServer`. |
|
||||
| `TCIStreams.pas` (~600 строк) | Бинарные потоки (§3.4): дециматор/интерполятор, нарезка блоков с заголовком, буфер записи линейного выхода и писатель WAV. Зависит только от RTL + `TCIProtocol`/`TCIServer`. |
|
||||
|
||||
Принципы те же, что у CAT (см. `doc/CAT_STATUS.md`):
|
||||
|
||||
@@ -98,13 +98,23 @@ TCI-клиенты ──WebSocket──► TTCIServer ──► TTCIAdapter ─
|
||||
уложиться в `TCI_HANDSHAKE_MS` (5 с), иначе закрывается по таймауту сокета;
|
||||
восемь слотов `TCI_MAX_CLIENTS` считаются только среди поднявшихся. Раньше
|
||||
восемь молчащих TCP-соединений навсегда закрывали дверь настоящим клиентам.
|
||||
5. **Браузерные клиенты не пускаются.** Handshake с заголовком `Origin`
|
||||
5. **`recv` = 0 — это конец связи, а не таймаут.** Приёмный цикл клиента ждёт
|
||||
не дольше `TCI_POLL_MS`, поэтому «ошибка» `EAGAIN` для него — норма, но
|
||||
спрашивать `errno` разрешено **только при отрицательном** результате: ноль
|
||||
означает EOF, `errno` при нём не трогается и вполне может нести `EAGAIN` от
|
||||
прошлого истёкшего опроса. Пока проверка была общей (`R <= 0`), обычный
|
||||
TCP-разрыв без close-кадра — упавший клиент, выдернутый кабель — выглядел
|
||||
как таймаут: поток крутился впустую, слот не освобождался до остановки
|
||||
сервера, и после нескольких аварийных отключений новые клиенты упирались в
|
||||
`TCI_MAX_CLIENTS`. Места в буфере при этом всегда хватает (кадр крупнее
|
||||
буфера рвётся раньше), так что иного смысла у нуля нет.
|
||||
6. **Браузерные клиенты не пускаются.** Handshake с заголовком `Origin`
|
||||
получает 403. Origin шлёт только браузер, а авторизации в TCI нет: без этой
|
||||
проверки любая открытая вкладка дотягивалась бы по `ws://127.0.0.1:40001`
|
||||
до `TRX`, `TUNE` и `VFO`. Своей web-странице нужен явный прокси, а не дыра
|
||||
по умолчанию.
|
||||
|
||||
6. **Блоки потоков нарезает и раскладывает DSP-поток.** Тап зовётся прямо из
|
||||
7. **Блоки потоков нарезает и раскладывает DSP-поток.** Тап зовётся прямо из
|
||||
потока WDSP, поэтому в `TCIStreams` нет ни одного ожидания: блок уходит в
|
||||
кольцо клиента (микросекунды под его локом), а в сокет его пишет, как и
|
||||
команды, поток самого клиента. Порядок локов везде один: `FSliceLock` →
|
||||
@@ -420,13 +430,17 @@ TCI-клиент отобрал бы звук у динамика и у брау
|
||||
в заголовке мы не будем ни в каком случае. Смена rate устройства или DDC пана
|
||||
на ходу пересобирает прореживание (`SetSourceRate`).
|
||||
|
||||
**Размер блока.** `AUDIO_STREAM_SAMPLES` — это сэмплы **на канал**, а в
|
||||
`Stream.length` уходит, как велит §3.4, количество вещественных отсчётов
|
||||
(`samples × channels`). Сходится и с умолчаниями ExpertSDR3 (2048 при 48 кГц
|
||||
даёт ~43 мс), и с потолком `data[16384]`: 2048 × 2 канала × float32 = ровно
|
||||
16384 байта. Смена любого параметра потока на ходу **перезапускает** уже идущие
|
||||
потоки этого клиента: блок с новой частотой посреди старого потока клиенты
|
||||
разбирают как мусор.
|
||||
**Размер блока.** `AUDIO_STREAM_SAMPLES` — это сэмплы **на канал**, и у
|
||||
аудиопотоков ровно это число уходит в `Stream.length`: §4.3 говорит про arg1
|
||||
«количество сэмплов, указываемое в поле Stream.length», а умолчание 2048 при
|
||||
48 кГц сходится с потолком `data[16384]` только так — 2048 × 2 канала ×
|
||||
float32 = ровно 16384 байта, то есть `length` считает сэмплы, а не отсчёты.
|
||||
У **IQ** единица другая: §3.4 определяет число комплексных отсчётов как
|
||||
`length / channels`, поэтому там в `length` уходит `samples × channels`.
|
||||
Развилку держит `TCIFillHeader` (по типу потока), обратную — разбор
|
||||
`TX_AUDIO_STREAM` в `HandleBinary`. Смена любого параметра потока на ходу
|
||||
**перезапускает** уже идущие потоки этого клиента: блок с новой частотой
|
||||
посреди старого потока клиенты разбирают как мусор.
|
||||
|
||||
**Передача (§3.4, §4.2).** `TRX:0,true,tci` берёт модуляцию из аудиопотока
|
||||
клиента — но только если у него запущен `AUDIO_START` (буквально по документу:
|
||||
@@ -440,9 +454,19 @@ web-клиента: явная просьба сильнее умолчания
|
||||
отбрасывается молча: отвечать ошибкой на каждый чужой блок значит захлебнуться.
|
||||
|
||||
**Запись линейного выхода.** Рекордер один на приёмник (а не на клиента):
|
||||
пишет он то, что слышно в аппарате. Кольцо на запрошенное время (потолок 300 с)
|
||||
пишет он то, что слышно в аппарате. Буфер на запрошенное время (потолок 300 с)
|
||||
в int16 48 кГц стерео — это ровно то, что уйдёт в WAV, и вдвое меньше памяти,
|
||||
чем float32. `SAVE` завершает запись и отдаёт кольцо отдельному потоку-писателю:
|
||||
чем float32. ★Буфер **линейный, не кольцевой**: §4.3 называет arg2
|
||||
максимальным временем записи и прямо говорит, что по его истечении запись
|
||||
удаляется, а чтобы сохранить файл, `SAVE` нужно прислать внутри интервала.
|
||||
Поэтому `START` открывает окно длиной arg2, пишем от его начала, а по концу
|
||||
окна данные выбрасываются вместе с памятью (`DropData` из `Feed` и из `Take`).
|
||||
Срок считается **по часам** от `START`, а не по накопленным сэмплам: линейный
|
||||
выход может молчать (мьют, стоящий приёмник), а время записи всё равно идёт.
|
||||
Кольцо «последние N секунд» вело себя иначе в обе стороны — начало записи
|
||||
затирало само себя, а `SAVE` через час после `START` отдавал файл, которого у
|
||||
ExpertSDR3 давно бы не было.
|
||||
`SAVE` завершает запись и отдаёт буфер отдельному потоку-писателю:
|
||||
файл бывает в десятки мегабайт, а команда пришла в потоке клиента, который в
|
||||
это время не читает свой сокет. MP3 не поддержан — кодера в проекте нет,
|
||||
и на `.mp3` уходит честный `tci_error`.
|
||||
@@ -563,8 +587,10 @@ web-клиента: явная просьба сильнее умолчания
|
||||
HTTP-заголовков.
|
||||
- **Транспорт:** 101 на заголовки с табуляцией и без пробела после двоеточия;
|
||||
отказ 403 при `Origin`; ровно восемь поднявшихся клиентов и 503 девятому;
|
||||
освобождение слотов после отключения; молчащий сокет уходит по таймауту
|
||||
handshake; ответный close-кадр; `Stop` с живым клиентом.
|
||||
освобождение слотов после отключения — **обоими** путями, и штатным
|
||||
close-кадром, и обычным TCP-разрывом без него (см. правило 5 в §1.1);
|
||||
молчащий сокет уходит по таймауту handshake; ответный close-кадр;
|
||||
`Stop` с живым клиентом.
|
||||
- **Адаптер:** `vfo:0,0,abc`, `vfo:0,0,-1`, `dds:0,broken`,
|
||||
`rx_filter_band:0,x,y` не меняют ничего, а годная частота проходит; захват
|
||||
параметра (второй клиент не перебивает первого раньше 200 мс и перебивает
|
||||
@@ -593,8 +619,8 @@ web-клиента: явная просьба сильнее умолчания
|
||||
|
||||
### Стенд этапа 2 (бинарные потоки) — 112 проверок, все зелёные
|
||||
|
||||
Отдельная программа (`tcitest.pas` в scratchpad, собирается тем же fpc без
|
||||
внешних библиотек) проверяет потоки на четырёх уровнях:
|
||||
Отдельная программа (`test/tci/tcitest.pas`, прогон — `test/tci/run.sh`,
|
||||
внешних библиотек не требует) проверяет потоки на четырёх уровнях:
|
||||
|
||||
- **Формат и математика:** коэффициенты прореживания (в том числе «просят выше,
|
||||
чем есть» и «нацело не делится»), умолчания размера блока, поля заголовка,
|
||||
@@ -612,9 +638,10 @@ web-клиента: явная просьба сильнее умолчания
|
||||
стерео float32 с верным заголовком и длиной; IQ 384→48 кГц уходит только
|
||||
целым блоком (полблока не отправляется); моно int16; пересчёт после смены
|
||||
rate источника; переполнение кольца теряет старые блоки, но клиент жив.
|
||||
- **Рекордер и WAV:** кольцо ограничено запрошенным временем, после заворота
|
||||
первым идёт самый старый отсчёт, `Take` опустошает, файл получает верные
|
||||
RIFF/fmt/data и длину.
|
||||
- **Рекордер и WAV:** буфер ограничен запрошенным временем и пишется с начала
|
||||
окна (переполнение отбрасывает новое, а не затирает старое), до срока запись
|
||||
есть, после срока её нет и истёкший рекордер не оживает, `Take` завершает
|
||||
запись, файл получает верные RIFF/fmt/data и длину.
|
||||
- **Команды на живом сервере** (настоящий `TRadioController`, WS-клиент на
|
||||
сыром сокете): отказ на несуществующий приёмник и на нечисловой аргумент,
|
||||
отказ на старт потока с незапущенного пана, подтверждение и отбраковка
|
||||
@@ -651,9 +678,9 @@ send просто возвращает EPIPE, и клиент выбрасыва
|
||||
TCI в его граф пока не заведён — юниты LCL-free, подключается одной строкой в
|
||||
`ewsdrd.lpr`, как web).
|
||||
|
||||
★Пробная сборка стенда: `-Mobjfpc` обязателен. С `-Mdelphi` в командной строке
|
||||
вложенные комментарии выключаются, и `{$MODE Delphi}` внутри шапки `WebUtils.pas`
|
||||
закрывает комментарий раньше времени — компиляция падает на «illegal character».
|
||||
★Про сборку стенда — `test/tci/README.md`: там записаны обе грабли (обязательный
|
||||
`-Mobjfpc` и отдельный каталог `.ppu` с абсолютным путём, иначе линковка тянет
|
||||
`PlatformUtils` из GUI-сборки, собранный с LCL).
|
||||
|
||||
**На реальном железе и с реальным клиентом (Log4OM/N1MM/WSJT-X/CW Skimmer) не
|
||||
проверялось — ни команды, ни потоки.**
|
||||
|
||||
Reference in New Issue
Block a user