Восемь дефектов, найденных прогоном настоящего 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>