mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
fix(tci): жизненный цикл, синхронизация слайсов и WebSocket по RFC
Разбор ревью ветки. Критичное — четыре отказа жизненного цикла и один пробел синхронизации. Use-after-free стора спотов: FTCIAdapter освобождается ДО FDXStore. Команда SPOT/SPOT_DELETE, пришедшая между их гибелью, обращалась к освобождённой памяти. Bind-адрес: TCIParseIPv4 стал строгим (out + Boolean, ровно четыре октета 0..255). Кривой адрес — отказ поднимать сокет, а не молчаливый INADDR_ANY: авторизации в TCI нет. В UI порт и адрес применяются по уходу фокуса и по Close, а не на каждую букву — набор «127.0.0.1» по дороге проходил через «127.0.0.» и открывал порт наружу. Остановка при висящем Synchronize: флаг Stopping (адаптер не начинает новых Invoke), прокачка CheckSynchronize в цикле ожидания Stop и запрет освобождать клиента, чей поток не вышел. Владение переделано: клиента освобождает только тик-поток (ReapClients), клиентский лишь помечает себя закрытым. Отправка больше не блокирует вызывающего: Send/Broadcast кладут строку в очередь клиента, в сокет пишет тик-поток вне общего лока, склеивая очередь в общие кадры. Медленный клиент морозил UI на таймаут отправки за каждое движение ручки VFO; теперь он просто вылетает. Слайсы: в контроллере появилось rfSliceState (нагрузка — FSliceFreqId), его шлют сами сеттеры слайса; SyncSetVfo зовёт SliceFreqChanged, как CAT. Адаптер разворачивает Id в пару (приёмник, канал) и рассылает состояние именно этого канала, а не канала 0 каждого пана. WebSocket по RFC 6455: маска обязательна, FIN/continuation собираются, RSV и незнакомые opcode рвут соединение, control-кадры ≤125 и только целиком, 64-битная длина не сворачивается в отрицательный Integer, пустой Sec-WebSocket-Key получает 400. Хвост пакета handshake больше не выбрасывается — первая команда не теряется. Клиент после исключения в разборе не остаётся висеть в массиве. Ещё: DSP и squelch доп. приёмников читаются и пишутся из TCtrlSlice (парные сеттеры сохраняли соседние поля значениями главного тракта); параметры потоков — в TTCIClient, они клиентские по спецификации; эхо под своим локом; ApplySettings возвращает результат, отказ старта виден оператору; инициализация объявляет только существующие каналы; SET_IN_FOCUS реализован через OnFocusRequest. Осознанно не сделано и записано в doc/TCI.md §3.1: AGC_GAIN для приёмников >0 (AGC-T один на тракт), цвет спота, KEYER, TX_FOOTSWITCH, арбитраж нескольких клиентов. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+76
-10
@@ -39,7 +39,30 @@ TCI-клиенты ──WebSocket──► TTCIServer ──► TTCIAdapter ─
|
||||
подключённым — как того требует §3.5 спецификации. Отвечающий на команду
|
||||
клиент дополнительно получает прямой ответ.
|
||||
|
||||
### 1.1 Настройки
|
||||
### 1.1 Три правила, на которых держится транспорт
|
||||
|
||||
Всё это не украшения, а лечение конкретных отказов — менять с оглядкой.
|
||||
|
||||
1. **Отправка никогда не блокирует вызывающего.** `Send`/`Broadcast` кладут
|
||||
строку в очередь клиента (микросекунды под его локом), в сокет пишет
|
||||
тик-поток (`FlushClients`, 20 мс, вне общего лока). Уведомления рождаются
|
||||
внутри `Changed()` контроллера, то есть в UI-потоке: писать оттуда прямо в
|
||||
сокет означало бы отдать интерфейс во власть самого медленного клиента
|
||||
(таймаут отправки × число клиентов на каждое движение ручки VFO).
|
||||
Переполнилась очередь (`TCI_OUT_MAX`) или не прошла запись — клиент
|
||||
выбрасывается, а не тормозит остальных.
|
||||
2. **Объект клиента освобождает только тик-поток** (`ReapClients`) и только
|
||||
после того, как клиентский поток честно вышел. Поэтому указатель, взятый
|
||||
кем угодно под `FClientLock`, гарантированно жив внутри лока.
|
||||
3. **`Stop` прокачивает очередь `Synchronize`.** Останавливает сервер поток
|
||||
контроллера (UI), а клиентский поток в этот момент может висеть как раз на
|
||||
`Invoke` в него же. Без прокачки это взаимный клин; по его таймауту сервер
|
||||
освобождал бы объекты из-под живых потоков. Дополнительно на время
|
||||
остановки взводится `Stopping`, и адаптер новых `Invoke` уже не начинает.
|
||||
Если поток всё же не вышел — объект НЕ освобождается: утечка на выходе
|
||||
дешевле обращения к освобождённой памяти.
|
||||
|
||||
### 1.2 Настройки
|
||||
|
||||
Секция `tci` в корне `settings.json`:
|
||||
|
||||
@@ -51,7 +74,19 @@ UI — вкладка **Advanced → TCI Server** (галка, порт, инт
|
||||
**нет авторизации**: открытый наружу порт означает полный доступ к трансиверу,
|
||||
поэтому умолчание слушает только петлю.
|
||||
|
||||
### 1.2 Маппинг модели
|
||||
Отсюда же два правила вокруг адреса:
|
||||
|
||||
- разбор `bind_addr` строгий (ровно четыре октета 0..255); всё непонятное —
|
||||
отказ поднимать сервер, а не молчаливый `0.0.0.0`. Пустая строка и явный
|
||||
`0.0.0.0` — единственные способы попросить «все интерфейсы»;
|
||||
- порт и адрес применяются по уходу фокуса из поля и по кнопке Close, а не на
|
||||
каждое нажатие клавиши: иначе набор `127.0.0.1` по дороге проходил бы через
|
||||
«`127.0.0.`» и сервер успевал перезапуститься на всех интерфейсах.
|
||||
|
||||
Отказ старта (порт занят, адрес не разобран) виден оператору: `ApplySettings`
|
||||
возвращает результат, MainForm показывает сообщение.
|
||||
|
||||
### 1.3 Маппинг модели
|
||||
|
||||
| TCI | EWSDR |
|
||||
|---|---|
|
||||
@@ -101,7 +136,7 @@ UI — вкладка **Advanced → TCI Server** (галка, порт, инт
|
||||
| `RX_MUTE`, `RX_VOLUME` | громкость/мьют слайса |
|
||||
| `MON_VOLUME`, `MON_ENABLE` | `SetTXMonVolume`, `SetRxMuteOnTx` |
|
||||
| `AGC_MODE` | `off`→Off, `fast`→Fast, `normal`→Medium |
|
||||
| `AGC_GAIN` | `SetAGCTop` (AGC-T) |
|
||||
| `AGC_GAIN` | `SetAGCTop` (AGC-T) — **только приёмник 0**, см. §3 |
|
||||
| `RX_NR_ENABLE`, `RX_NB_ENABLE`, `RX_ANF_ENABLE` | `SetNR/SetNB/SetANF`, для панов — `SetSliceDSP` |
|
||||
| `LOCK` | `SetVfoLock` |
|
||||
| `SQL_ENABLE`, `SQL_LEVEL` | FM-шумоподавитель; дБ (-140..0) ↔ порог 0..100 |
|
||||
@@ -111,10 +146,14 @@ UI — вкладка **Advanced → TCI Server** (галка, порт, инт
|
||||
### 2.3 Однонаправленное управление (§4.3)
|
||||
|
||||
`TX_ENABLE` (по `BackendCaps.HasTX` + `TXProhibited`), `CW_MACROS_SPEED_UP/DOWN`,
|
||||
`SET_IN_FOCUS` (поднимает окно программы через `OnFocusRequest` — адаптер до
|
||||
окна не дотягивается, действие ставит MainForm),
|
||||
`SPOT`, `SPOT_DELETE`, `SPOT_CLEAR` (в `TDXSpotStore`, спот виден на всех
|
||||
панадаптерах), `RX_SENSORS_ENABLE`, `TX_SENSORS_ENABLE` (период — на клиента),
|
||||
`IQ_SAMPLERATE`, `AUDIO_SAMPLERATE`, `AUDIO_STREAM_*`, `TX_STREAM_AUDIO_BUFFERING`
|
||||
(значения принимаются и подтверждаются; сами потоки — этап 2).
|
||||
(значения принимаются и подтверждаются; сами потоки — этап 2). Параметры
|
||||
потоков — настройки **клиента**, а не устройства: живут в `TTCIClient`, и один
|
||||
клиент не переопределяет их остальным.
|
||||
|
||||
### 2.4 Уведомления (§4.4, §4.5)
|
||||
|
||||
@@ -124,6 +163,16 @@ UI — вкладка **Advanced → TCI Server** (галка, порт, инт
|
||||
`CLICKED_ON_SPOT` (клик по подписи спота на любом панадаптере),
|
||||
`CALLSIGN_SEND` (после `CW_MSG`).
|
||||
|
||||
Отдельная история — **доп. приёмники**. У главного тракта на каждое поле есть
|
||||
своё `rfXxx`, а у слайсов не было ничего: правка слайса не доходила ни до UI,
|
||||
ни до остальных клиентов. Поэтому в контроллере появилось `rfSliceState`
|
||||
(полезная нагрузка — `FSliceFreqId`, как у `rfSliceFreq`), и его шлют сами
|
||||
сеттеры слайса: `SetSliceMode`, `SetSliceFilter`, `SetSliceAGCMode`,
|
||||
`SetSliceVolume`, `SetSliceMute`, `SetSliceDSP`, `SetSliceFMSquelch`.
|
||||
Адаптер разворачивает Id обратно в пару (приёмник, канал) и рассылает
|
||||
состояние именно этого канала. Частоту слайса, поставленную по TCI, тоже
|
||||
сопровождает `SliceFreqChanged` — как это делает CAT.
|
||||
|
||||
### 2.5 Телеграф (§3.2)
|
||||
|
||||
`CW_MACROS`, `CW_MSG`, `CW_MACROS_STOP`, `CW_TERMINAL`.
|
||||
@@ -153,7 +202,17 @@ UI — вкладка **Advanced → TCI Server** (галка, порт, инт
|
||||
| `RX_BALANCE` | баланса каналов у слайса нет |
|
||||
| `DIGL_OFFSET`, `DIGU_OFFSET` | смещения цифровых мод не реализованы |
|
||||
| `RX_CHANNEL_ENABLE` | канал B главного приёмника — это VFO B, он есть всегда; создание второго слайса на пане по TCI — этап 2 |
|
||||
| `SET_IN_FOCUS` | окно программы не поднимаем |
|
||||
|
||||
## 3.1 Ограничения, о которых честнее знать заранее
|
||||
|
||||
| Что | Как ведёт себя |
|
||||
|---|---|
|
||||
| `AGC_GAIN` у приёмника > 0 | AGC-T в ewsdr один на приёмный тракт, у слайса своего нет. Команда от имени доп. приёмника **игнорируется** (раньше молча правила главный), в ответ уходит текущее значение |
|
||||
| Цвет спота (`SPOT`, arg4 ARGB) | не читается: `TDXSpot` цвета не хранит, подписи красятся по моде/возрасту |
|
||||
| `KEYER`, `TX_FOOTSWITCH` | не реализованы: своего ключа-уведомления и опроса педали наружу у контроллера нет |
|
||||
| Арбитраж клиентов (§3.5, захват параметра ~200 мс) | нет. Команды разных клиентов идут подряд, последняя побеждает. С двумя активными логгерами возможна «перетяжка» частоты или моды |
|
||||
| Мода, фильтр, АРУ, шумодавы у канала B доп. пана | в TCI это свойства **приёмника**, а не канала: они относятся к каналу A. У канала B по протоколу есть только частота, IF и громкость |
|
||||
| Команды конфигурации потоков | подтверждаются как принятые, хотя самих потоков нет (этап 2). Клиент по ответу может решить, что функция доступна |
|
||||
|
||||
Отдельно: у `TX_SENSORS` второй аргумент — уровень микрофона; измерителя
|
||||
микрофона в EWSDR нет, шлём нижнюю границу шкалы (-60 дБм), чтобы клиент не
|
||||
@@ -185,7 +244,11 @@ UI — вкладка **Advanced → TCI Server** (галка, порт, инт
|
||||
5. **`LINEOUT_STREAM` + `LINE_OUT_RECORDER_*`** — запись в WAV/MP3.
|
||||
|
||||
Также в очереди: `RX_CHANNEL_ENABLE` как реальное создание/удаление второго
|
||||
слайса пана и `SET_IN_FOCUS`.
|
||||
слайса пана, `KEYER` и арбитраж нескольких клиентов (§3.5).
|
||||
|
||||
Приёмный буфер `TWsClient` — 4 КБ, и сейчас это жёсткий потолок: кадр крупнее
|
||||
рвёт соединение (команд такой длины у TCI нет). Под TX-аудио его придётся
|
||||
растить вместе с этапом 2.
|
||||
|
||||
---
|
||||
|
||||
@@ -193,10 +256,13 @@ UI — вкладка **Advanced → TCI Server** (галка, порт, инт
|
||||
|
||||
Стендом (WS-клиент на сыром сокете, без внешних библиотек):
|
||||
|
||||
- **Транспорт:** handshake, маска входящих фреймов, несколько команд в одном
|
||||
фрейме, регистронезависимость, экранированный текст, ping/pong, игнорирование
|
||||
бинарных фреймов, рассылка всем клиентам, чистая остановка сервера с живым
|
||||
клиентом (потоки дренируются, зависаний нет).
|
||||
- **Транспорт:** строгий разбор bind-адреса и отказ подниматься на кривом;
|
||||
первый кадр, приклеенный к пакету handshake; сборка фрагментированного
|
||||
сообщения; несколько команд в одном кадре; отказ от незамаскированных кадров
|
||||
с разрывом соединения; ping/pong; рассылка двум клиентам; чистая остановка с
|
||||
живыми клиентами; медленный клиент (5000 рассылок не блокируют вызывающего,
|
||||
клиент вылетает сам); остановка сервера в тот момент, когда команда клиента
|
||||
висит в `Synchronize` у потока контроллера.
|
||||
- **Сквозной прогон** с настоящим `TRadioController` (движки созданы, железо не
|
||||
подключено): пачка инициализации из 57 строк со всеми обязательными
|
||||
командами и `READY` в конце; `VFO`, `MODULATION` (`cw` на 7 МГц дал CWL),
|
||||
|
||||
Reference in New Issue
Block a user