mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
fix(tci): ревизия — потоки, валидация, арбитраж и синхронизация клиентов
Разбор семи проходов ревью ветки. Ниже — по сути, а не по списку. Потоки. Сетевые потоки больше не читают модель контроллера напрямую. Слайсы снимаются в потоке контроллера (RefreshSlices → FSliceSnap, на событиях rfSliceFreq/rfSliceState/rfDevice/…), железо — тоже (RefreshDev → TTCIDevSnap: имя платы, границы, число панов, HasTX). Копия TCtrlSlice из чужого потока портила счётчик ссылок managed-строк, а BackendCaps и BoardDisplayName смотрят в FNetwork, который UI освобождает на смене устройства. По той же причине ActiveTXFreqHz переведён на GetSliceView. Sync-методы читают живую таблицу: они уже в потоке контроллера. Жизненный цикл. Stop ждёт выхода клиентских потоков БЕЗ таймаута, прокачивая очередь Synchronize: выйти по таймауту нельзя — следом освобождаются и клиенты, и сам сервер. OnDisconnect зовётся и при остановке (иначе захваты параметров ушедших клиентов доживали до следующего запуска). Отправка переехала на поток самого клиента (recv с TCI_POLL_MS): общий поток задерживал всех на таймаут записи в один медленный сокет. WebUtils.SockSend шлёт с MSG_NOSIGNAL — SIGPIPE убивал headless-процесс. Транспорт. Слот протокола выдаётся только после Upgrade, а сокет до него живёт по таймауту handshake: восемь молчащих соединений закрывали дверь настоящим клиентам. Handshake с заголовком Origin получает 403 — авторизации в TCI нет, и без этого открытая вкладка браузера дотягивалась до TRX и VFO. Заголовки разбираются построчно, текстовые кадры проверяются на UTF-8, close длиной один байт отвергается, на close отвечаем close. Валидация. Все установки ходят через TCITryArg* — «vfo^0~0~abc» больше не превращается в честный ноль. Частота проверяется дважды: в потоке клиента по снимку и в SyncSetVfo/SyncSetCenter по живым границам (устройство успевают сменить между разбором и исполнением). Границы теперь из ОДНОГО источника (FreqLimits поверх VisibleFreqBounds) — тот же, что уходит в VFO_LIMITS; сами VFO_LIMITS переобъявляются при смене железа, и их кэш ведётся независимо от того, подключён ли кто-то. Слайс двигается только TuneSliceInBand, как у CAT: прямой SetSliceTarget уводил TX-слайс в DUC на чужой диапазон без антенн и фильтров. Параметры потоков сверяются со списками спецификации, а IQ_START и прочие запуски честно отвечают ошибкой вместо молчания. Синхронизация клиентов (§3.5). Появился захват параметра на 200 мс: два логгера больше не перетягивают частоту. Пачка инициализации уходит под FClientLock — изменение между строкой снимка и READY терялось навсегда. Глобальные величины (tune_drive, cw_macros_*, split_enable, mon_volume) рассылаются всем, а правки оператора приходят событиями: rfTXProfile, rfActiveVfo, rfMonVolume и новый rfCWSettings. Создание и удаление слайса рассылается по rfDevice (сравнение расстановки), у живого пана без слайсов канал A показывает центр — иначе клиент навсегда оставался с частотой удалённого слайса. Прочее. SliceFreqChanged переехал внутрь SetSliceTarget — один путь для мыши, CAT и TCI (перетаскивание флага мимо клиентов проходило молча). VOLUME и MON_VOLUME развели: SetVolume правит АКТИВНУЮ громкость, поэтому команда на DUP-передаче уезжала в монитор — добавлен адресный SetRxVolume. Настройки сохраняются только после успешного применения, при отказе поднимается прежний слушатель. Время спота — UTC. Подписки на измерители читаются и пишутся под локом клиента. Проверено стендом (сырой WS-клиент + живой TRadioController без железа): 73 проверки, включая изоляцию медленного клиента, остановку под Synchronize, арбитраж до и после 200 мс, отбраковку по живым границам и переобъявление VFO_LIMITS. На реальном железе и с реальным клиентом по-прежнему не гонялось. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -91,6 +91,19 @@ function TCIArgInt(const M: TTCIMessage; Idx, Def: Integer): Integer;
|
||||
function TCIArgFloat(const M: TTCIMessage; Idx: Integer; Def: Double): Double;
|
||||
function TCIArgBool(const M: TTCIMessage; Idx: Integer; Def: Boolean): Boolean;
|
||||
|
||||
{ Строгий разбор аргумента: False — аргумента нет или он не число/не Boolean.
|
||||
Всё, что ставит параметр радио, обязано ходить через эти функции: варианты с
|
||||
умолчанием превращали «vfo^0~0~abc;» в честный ноль и перестраивали приёмник
|
||||
на 0 Гц. Умолчания остаются только там, где значение необязательно. }
|
||||
function TCITryArgInt(const M: TTCIMessage; Idx: Integer; out V: Integer): Boolean;
|
||||
function TCITryArgFloat(const M: TTCIMessage; Idx: Integer; out V: Double): Boolean;
|
||||
function TCITryArgBool(const M: TTCIMessage; Idx: Integer; out V: Boolean): Boolean;
|
||||
|
||||
{ Наборы значений, оговорённые протоколом для параметров потоков (§4.3). }
|
||||
function TCIValidIQRate(V: Integer): Boolean; // 48/96/192/384 кГц
|
||||
function TCIValidAudioRate(V: Integer): Boolean; // 8/12/24/48 кГц
|
||||
function TCIValidSampleType(const S: string): Boolean; // int16/int24/int32/float32
|
||||
|
||||
{ ── Сборка ─────────────────────────────────────────────────────────────── }
|
||||
|
||||
function TCIBuild(const Name: string): string; overload;
|
||||
@@ -226,6 +239,57 @@ begin
|
||||
else Result := Def;
|
||||
end;
|
||||
|
||||
function TCITryArgFloat(const M: TTCIMessage; Idx: Integer; out V: Double): Boolean;
|
||||
var S: string;
|
||||
begin
|
||||
V := 0;
|
||||
S := Trim(TCIArg(M, Idx));
|
||||
if S = '' then Exit(False);
|
||||
Result := TryStrToFloat(StringReplace(S, ',', '.', [rfReplaceAll]), V,
|
||||
TCIFormatSettings);
|
||||
end;
|
||||
|
||||
function TCITryArgInt(const M: TTCIMessage; Idx: Integer; out V: Integer): Boolean;
|
||||
var D: Double;
|
||||
begin
|
||||
V := 0;
|
||||
Result := TCITryArgFloat(M, Idx, D);
|
||||
// Величины протокола целочисленные, но клиенты шлют и «100.0»; за пределами
|
||||
// Integer округлять нечего — это не значение, а мусор.
|
||||
if Result then
|
||||
begin
|
||||
Result := (D >= -2147483648.0) and (D <= 2147483647.0);
|
||||
if Result then V := Round(D);
|
||||
end;
|
||||
end;
|
||||
|
||||
function TCITryArgBool(const M: TTCIMessage; Idx: Integer; out V: Boolean): Boolean;
|
||||
var S: string;
|
||||
begin
|
||||
V := False;
|
||||
S := LowerCase(Trim(TCIArg(M, Idx)));
|
||||
if (S = 'true') or (S = '1') then begin V := True; Result := True; end
|
||||
else if (S = 'false') or (S = '0') then begin V := False; Result := True; end
|
||||
else Result := False;
|
||||
end;
|
||||
|
||||
function TCIValidIQRate(V: Integer): Boolean;
|
||||
begin
|
||||
Result := (V = 48000) or (V = 96000) or (V = 192000) or (V = 384000);
|
||||
end;
|
||||
|
||||
function TCIValidAudioRate(V: Integer): Boolean;
|
||||
begin
|
||||
Result := (V = 8000) or (V = 12000) or (V = 24000) or (V = 48000);
|
||||
end;
|
||||
|
||||
function TCIValidSampleType(const S: string): Boolean;
|
||||
var T: string;
|
||||
begin
|
||||
T := LowerCase(Trim(S));
|
||||
Result := (T = 'int16') or (T = 'int24') or (T = 'int32') or (T = 'float32');
|
||||
end;
|
||||
|
||||
{ ═══════════════════════════════════════════════════════════════════════════
|
||||
Сборка
|
||||
═══════════════════════════════════════════════════════════════════════════ }
|
||||
|
||||
Reference in New Issue
Block a user