# TCI в EWSDR — статус реализации Ветка разработки: `feature/tci-protocol`. Дата последнего обновления: 2026-08-17. Эталон протокола — «Протокол TCI, версия 2.0» Expert Electronics (`doc/TCI Protocol_RU.pdf`, 12 января 2024). EWSDR выступает **сервером** (как ExpertSDR3): порт слушаем мы, клиенты — логгеры, скиммеры, программы цифровых видов, внешние усилители и коммутаторы. --- ## 1. Архитектура ``` TCI-клиенты ──WebSocket──► TTCIServer ──► TTCIAdapter ──► TRadioController логгер/скиммер/цифра транспорт команды + ядро уведомления ``` | Файл | Назначение | |---|---| | `TCIProtocol.pas` (~380 строк) | Чистый слой протокола: разбор `имя:арг1,арг2;`, сборка строк, экранирование `^ ~ *`, словарь видов связи, пересчёт громкости/порога в дБ. Зависит только от RTL + `RadioModes`. | | `TCIServer.pas` (~660 строк) | WebSocket-сервер: accept-поток, поток на клиента, HTTP-Upgrade, разбор фреймов, рассылка, тик 20 мс. Сокеты и фреймы переиспользованы из веб-подсистемы (`WebUtils`, `WsClient`). | | `TCIAdapter.pas` (~1100 строк) | Мост к `TRadioController`: реализация команд, пачка инициализации, уведомления об изменениях состояния, измерители. | Принципы те же, что у CAT (см. `doc/CAT_STATUS.md`): - **TCI — равноправный клиент контроллера.** Никаких обращений к `MainForm`; единственное исключение — стор спотов `TDXSpotStore`, который передаётся адаптеру ссылкой (команды `SPOT`/`SPOT_DELETE`/`SPOT_CLEAR` кладут споты в ту же базу, что и DX-кластер). - **Потоки.** Геттеры читают поля контроллера напрямую из потока клиента; сеттеры пишут параметр в scratch-поля под `FLock` и зовут `FController.Invoke(SyncXxx)` — исполнение в потоке контроллера (GUI = `TThread.Synchronize`). - **Синхронизация клиентов.** Изменение любого поля контроллера приходит в `OnState` (multicast-подписка `AddStateListener`) и рассылается всем подключённым — как того требует §3.5 спецификации. Отвечающий на команду клиент дополнительно получает прямой ответ. ### 1.1 Настройки Секция `tci` в корне `settings.json`: ```json "tci": { "enabled": false, "port": 40001, "bind_addr": "127.0.0.1" } ``` UI — вкладка **Advanced → TCI Server** (галка, порт, интерфейс). В протоколе **нет авторизации**: открытый наружу порт означает полный доступ к трансиверу, поэтому умолчание слушает только петлю. ### 1.2 Маппинг модели | TCI | EWSDR | |---|---| | приёмник (`TRX_COUNT`) | панадаптер: 0 = главный тракт, 1.. = доп. DDC-паны. Число = `BackendCaps.MaxPans` | | канал A/B (`CHANNEL_COUNT` = 2) | у приёмника 0 — VFO A / VFO B; у панов 1.. — первый и второй слайс пана | | `DDS` | центр панадаптера (`SetCenter` / `SetPanDDCFreq`) | | `IF` | смещение канала от центра панорамы | | `VFO` | абсолютная частота канала | | передатчик (`arg1` у TRX/TUNE/DRIVE) | всегда один — главный тракт | --- ## 2. Что реализовано ### 2.1 Инициализация (§4.1) `PROTOCOL`, `DEVICE`, `RECEIVE_ONLY`, `TRX_COUNT`, `CHANNEL_COUNT`, `VFO_LIMITS`, `IF_LIMITS`, `MODULATIONS_LIST`, `READY`. `READY` шлётся **после** полного дампа состояния: клиент, дождавшийся его, уже знает всё. Границы частот берутся из `BackendCaps`; пока устройство не подключено — 10 кГц…30 МГц. `IF_LIMITS` = ±sample rate/2, пересылается при смене частоты дискретизации. Список видов связи: `am,sam,dsb,lsb,usb,cw,nfm,wfm,digl,digu,dmr,fmraw`. `dmr`/`fmraw` — наше расширение (протокол расширяемый, §1.4). `CWL`/`CWU` схлопываются в `cw`; при установке `cw` боковая сохраняется, если уже телеграф, иначе выбирается по частоте (ниже 10 МГц — CWL). ### 2.2 Двунаправленное управление (§4.2) Полностью проведено в контроллер: | Команда | Куда легло | |---|---| | `START` / `STOP` | `SetRun` | | `DDS` | `SetCenter` / `SetPanDDCFreq` | | `IF`, `VFO` | `SetVfoA/B`, `SetSliceTarget` | | `MODULATION` | `SetMode` / `SetSliceMode` | | `TRX` | `SetMOX` | | `TUNE` | `SetTune` | | `DRIVE` | `SetDrive` | | `TUNE_DRIVE` | `FTXSettings.TUNLevel` через `SetTXSettings` | | `SPLIT_ENABLE` | `SetSplit` | | `RX_FILTER_BAND` | `SetFilterEdges` / `SetSliceFilter` | | `VOLUME`, `MUTE` | `SetVolume`, `SetMute` (дБ ↔ 0..100) | | `RX_MUTE`, `RX_VOLUME` | громкость/мьют слайса | | `MON_VOLUME`, `MON_ENABLE` | `SetTXMonVolume`, `SetRxMuteOnTx` | | `AGC_MODE` | `off`→Off, `fast`→Fast, `normal`→Medium | | `AGC_GAIN` | `SetAGCTop` (AGC-T) | | `RX_NR_ENABLE`, `RX_NB_ENABLE`, `RX_ANF_ENABLE` | `SetNR/SetNB/SetANF`, для панов — `SetSliceDSP` | | `LOCK` | `SetVfoLock` | | `SQL_ENABLE`, `SQL_LEVEL` | FM-шумоподавитель; дБ (-140..0) ↔ порог 0..100 | | `CW_MACROS_SPEED`, `CW_KEYER_SPEED` | `TCWSettings.Speed` | | `CW_MACROS_DELAY` | `TCWSettings.RFDelayMS` | ### 2.3 Однонаправленное управление (§4.3) `TX_ENABLE` (по `BackendCaps.HasTX` + `TXProhibited`), `CW_MACROS_SPEED_UP/DOWN`, `SPOT`, `SPOT_DELETE`, `SPOT_CLEAR` (в `TDXSpotStore`, спот виден на всех панадаптерах), `RX_SENSORS_ENABLE`, `TX_SENSORS_ENABLE` (период — на клиента), `IQ_SAMPLERATE`, `AUDIO_SAMPLERATE`, `AUDIO_STREAM_*`, `TX_STREAM_AUDIO_BUFFERING` (значения принимаются и подтверждаются; сами потоки — этап 2). ### 2.4 Уведомления (§4.4, §4.5) `RX_CHANNEL_SENSORS` + устаревшая `RX_SENSORS` (S-метр в дБм, период 30…1000 мс на клиента), `TX_SENSORS`, `TX_FREQUENCY`, `VFO_LOCK`, `APP_FOCUS` (активация/деактивация главного окна), `RX_CLICKED_ON_SPOT` + устаревшая `CLICKED_ON_SPOT` (клик по подписи спота на любом панадаптере), `CALLSIGN_SEND` (после `CW_MSG`). ### 2.5 Телеграф (§3.2) `CW_MACROS`, `CW_MSG`, `CW_MACROS_STOP`, `CW_TERMINAL`. Текст приводится к тому, что понимает передатчик текста ewsdr (`CWXSend`): экранирование `^ ~ *` снимается, `CALL$N` разворачивается в N повторов позывного, префикс/суффикс `_` считаются пустыми. **Не поддержано:** шаг скорости внутри текста (`<` / `>`) и слитная передача аббревиатур (`|SK|`) — эти символы просто снимаются, потому что `TCWSender` работает на одной скорости и не знает прос-знаков. Доотправка позывного (`cw_msg:arg1;`) игнорируется: уже отданный в очередь текст не редактируется. --- ## 3. Осознанные заглушки («эхо») Значения принимаются, хранятся в адаптере и рассылаются клиентам — так синхронизация между несколькими клиентами остаётся честной, но на радио они не влияют, потому что соответствующего тракта в EWSDR нет: | Команда | Причина | |---|---| | `RIT_ENABLE`, `RIT_OFFSET`, `XIT_ENABLE`, `XIT_OFFSET` | расстройки RX/TX в контроллере нет вообще | | `RX_BIN_ENABLE` | псевдостерео не реализовано | | `RX_ANC_ENABLE`, `RX_APF_ENABLE`, `RX_DSE_ENABLE`, `RX_NF_ENABLE` | таких блоков в тракте нет | | `RX_NB_PARAM` | параметры NB в WDSP наружу не выведены | | `RX_BALANCE` | баланса каналов у слайса нет | | `DIGL_OFFSET`, `DIGU_OFFSET` | смещения цифровых мод не реализованы | | `RX_CHANNEL_ENABLE` | канал B главного приёмника — это VFO B, он есть всегда; создание второго слайса на пане по TCI — этап 2 | | `SET_IN_FOCUS` | окно программы не поднимаем | Отдельно: у `TX_SENSORS` второй аргумент — уровень микрофона; измерителя микрофона в EWSDR нет, шлём нижнюю границу шкалы (-60 дБм), чтобы клиент не рисовал случайные значения. Третий аргумент (RMS) и четвёртый (пик) отдаём одинаковыми — в телеметрии платы одно значение forward power. У `TRX` третий аргумент (источник сигнала `tci`/`mic1`/…) игнорируется: аудио по TCI ещё нет, модуляция берётся из выбранного в программе входа. --- ## 4. Этап 2 — бинарные потоки (§3.4) Не реализовано ничего из потоков; команды управления ими принимаются, но данные не идут. Что нужно сделать: 1. **Заголовок блока** уже описан — `TTCIStreamHeader` в `TCIProtocol.pas` (16 × uint32 + сэмплы). 2. **`RX_AUDIO_STREAM`** — нужен multicast-тап RX-аудио в контроллере. Сейчас есть только `OnAudioConsume` — одиночный перехват, которым владеет веб-адаптер (он же глушит локальный звук). Для TCI нужен именно тап «послушать, не забирая», по образцу `AddStateListener`. 3. **`TX_AUDIO_STREAM` + `TX_CHRONO`** — приём бинарных фреймов от клиента (сейчас `TCIServer` их отбрасывает; приёмный буфер `TWsClient` — 4 КБ, под 16 КБ блоков его придётся растить) и подача в TX-тракт наравне с веб-микрофоном (`PushMicSamples`). 4. **`IQ_STREAM`** — тап сырого IQ в `TWDSPEngine.PushIQItemToDSP` (как у декодера маяка), с децимацией до `IQ_SAMPLERATE` (48/96/192/384 кГц). 5. **`LINEOUT_STREAM` + `LINE_OUT_RECORDER_*`** — запись в WAV/MP3. Также в очереди: `RX_CHANNEL_ENABLE` как реальное создание/удаление второго слайса пана и `SET_IN_FOCUS`. --- ## 5. Проверено Стендом (WS-клиент на сыром сокете, без внешних библиотек): - **Транспорт:** handshake, маска входящих фреймов, несколько команд в одном фрейме, регистронезависимость, экранированный текст, ping/pong, игнорирование бинарных фреймов, рассылка всем клиентам, чистая остановка сервера с живым клиентом (потоки дренируются, зависаний нет). - **Сквозной прогон** с настоящим `TRadioController` (движки созданы, железо не подключено): пачка инициализации из 57 строк со всеми обязательными командами и `READY` в конце; `VFO`, `MODULATION` (`cw` на 7 МГц дал CWL), `RX_FILTER_BAND`, `DRIVE`, `VOLUME`, `MUTE`, `AGC_GAIN`, `LOCK`, `CW_MACROS_SPEED`, `SPOT`, `RIT_*` — состояние контроллера после прогона совпало с посланным; чтение (`vfo:0,1;`) отвечает; `RX_SENSORS_ENABLE:true,100` даёт поток `rx_channel_sensors` с заданным периодом. - **Устойчивость:** исключение в обработке команды не рвёт соединение — клиенту уходит `tci_error:<команда>,<сообщение>;`, остальные команды продолжают работать (проверено на контроллере без движков, где `SetMode`/`SetCWSettings` падают с AV на неинициализированном `FNetwork`). Сборка: `lazbuild -B --ws=qt6 ewsdr.lpr` и `./build-ewsdrd.sh` (демон собирается, TCI в его граф пока не заведён — юниты LCL-free, подключается одной строкой в `ewsdrd.lpr`, как web). **На реальном железе и с реальным клиентом (Log4OM/N1MM/WSJT-X/CW Skimmer) не проверялось.** Ответ на команду-установку клиент получает дважды: прямым ответом и рассылкой из `OnState`. Это осознанно — дубли идемпотентны, а рассылка нужна для тех случаев, когда значение поменял не клиент, а оператор.