mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-26 04:47:35 +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:
+146
-8
@@ -89,7 +89,9 @@ type
|
||||
rfDeviceList, // список discovered устройств обновился
|
||||
rfPanFreq, // центр доп. пана уехал (ретюн DDC извне)
|
||||
rfSliceFreq, // слайс перестроен извне (CAT); Id — FSliceFreqId
|
||||
rfSliceState // у слайса сменились мода/фильтр/АРУ/DSP/громкость; Id — FSliceFreqId
|
||||
rfSliceState, // у слайса сменились мода/фильтр/АРУ/DSP/громкость; Id — FSliceFreqId
|
||||
rfCWSettings, // правка телеграфа (скорость, задержка, pitch…)
|
||||
rfMonVolume // громкость self-monitor'а (TX), отдельно от rfVolume
|
||||
);
|
||||
|
||||
TRadioStateEvent = procedure(Sender: TObject; Field: TRadioField) of object;
|
||||
@@ -193,6 +195,29 @@ type
|
||||
DMR: TDMRSliceDecoder; // владеет; nil для не-DMR слайса
|
||||
end;
|
||||
|
||||
// POD-снимок слайса для фронтендов, которые читают модель из своих потоков
|
||||
// (TCI-сервер: команда исполняется в потоке клиента). Только скаляры: копия
|
||||
// TCtrlSlice тащит за собой managed-поля (DevName/InDevName) и объекты, а
|
||||
// копирование строки из чужого потока, пока UI-поток её же переписывает,
|
||||
// портит счётчик ссылок — это уже не рассинхрон, а порча кучи.
|
||||
TSliceView = record
|
||||
Id: Integer;
|
||||
PanId: Integer;
|
||||
TargetHz: Double;
|
||||
Mode: Integer;
|
||||
FilterLo: Integer;
|
||||
FilterHi: Integer;
|
||||
AGC: TWDSPAGCMode;
|
||||
Volume: Double;
|
||||
Muted: Boolean;
|
||||
FMSQOn: Boolean;
|
||||
FMSQLevel: Integer;
|
||||
NRMode: Integer;
|
||||
NBMode: Integer;
|
||||
SNB: Boolean;
|
||||
ANF: Boolean;
|
||||
end;
|
||||
|
||||
{ TRadioController }
|
||||
TRadioController = class
|
||||
private
|
||||
@@ -795,6 +820,10 @@ type
|
||||
procedure VolumeBy(Delta: Integer);
|
||||
// Громкость self-monitor'а на передаче отдельно от RX-громкости: слайдер
|
||||
// правит её только когда MonitoringTX, а CAT (ZZTM) — в любой момент.
|
||||
{ Адресная громкость ПРИЁМА: правит FVolume независимо от того, идёт ли
|
||||
передача. Слайдер (SetVolume) правит активную — на self-monitor это
|
||||
громкость монитора; внешнему клиенту (TCI VOLUME) такой контекст не нужен. }
|
||||
procedure SetRxVolume(V: Integer);
|
||||
procedure SetTXMonVolume(V: Integer);
|
||||
// True — сейчас звучит self-monitor даунлинка на передаче (TX + DUP + RX MUTE
|
||||
// off). В этом контексте SetVolume/слайдер правят FTxMonVolume, иначе FVolume.
|
||||
@@ -876,6 +905,14 @@ type
|
||||
function ActiveMicInDevName: string;
|
||||
function SliceCount: Integer;
|
||||
function GetSlice(Id: Integer; out S: TCtrlSlice): Boolean;
|
||||
// Снимок слайса для фронтендов, читающих модель из СВОИХ потоков (TCI).
|
||||
// См. TSliceView: копировать целиком TCtrlSlice оттуда нельзя.
|
||||
function GetSliceView(Id: Integer; out V: TSliceView): Boolean;
|
||||
// Слайсы пана по порядку (0-й = «канал A» пана): та же нумерация, что у
|
||||
// флагов. Нужны TCI, где приёмник — это пан, а канал — слайс на нём.
|
||||
function PanSliceCount(PanId: Integer): Integer;
|
||||
function PanSliceId(PanId, Index: Integer): Integer; // 0 = нет такого
|
||||
function SlicePanIndex(Id: Integer; out PanId, Index: Integer): Boolean;
|
||||
// Слот ↔ слайс. Слот 0..MAX_SLICES-1 = буква B..G и не зависит от того,
|
||||
// создан ли слайс сейчас: на слот вешаются настройки (CAT-порт, Auto TX).
|
||||
class function SliceSlotLetter(Slot: Integer): Char;
|
||||
@@ -2052,8 +2089,7 @@ begin
|
||||
PanId := FSlices[idx].PanId;
|
||||
if SliceFitsCapture(TargetHz, PanId) then
|
||||
begin
|
||||
SetSliceTarget(Id, TargetHz);
|
||||
SliceFreqChanged(Id);
|
||||
SetSliceTarget(Id, TargetHz); // он же шлёт SliceFreqChanged
|
||||
Exit(True);
|
||||
end;
|
||||
|
||||
@@ -2078,8 +2114,8 @@ begin
|
||||
if Dist > Half * 2 * 0.95 then Exit;
|
||||
NewCenter := (TargetHz + ActiveVfoHz) / 2;
|
||||
SetCenter(NewCenter); // несёт Changed(rfCenterFreq) + сдвиги слайсов
|
||||
SetSliceTarget(Id, TargetHz);
|
||||
SliceFreqChanged(Id); // rfCenterFreq флаги не перезаливает — нужен свой
|
||||
SetSliceTarget(Id, TargetHz); // SliceFreqChanged — внутри: rfCenterFreq
|
||||
// флаги не перезаливает, нужен свой
|
||||
Result := True;
|
||||
end;
|
||||
end;
|
||||
@@ -2513,10 +2549,11 @@ begin
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetSliceTarget(Id: Integer; TargetHz: Double);
|
||||
var idx: Integer;
|
||||
var idx: Integer; Moved: Boolean;
|
||||
begin
|
||||
idx := FindSliceIndex(Id);
|
||||
if idx < 0 then Exit;
|
||||
Moved := FSlices[idx].TargetHz <> TargetHz;
|
||||
FSlices[idx].TargetHz := TargetHz;
|
||||
if Assigned(FDSPEngine) then
|
||||
FDSPEngine.SetSliceShift(Id, TargetHz - PanCenterHz(FSlices[idx].PanId));
|
||||
@@ -2524,6 +2561,10 @@ begin
|
||||
// первый MOX, но в телеграфе ключ замыкает прошивка без всякого MOX: DUC
|
||||
// обязан стоять правильно ВСЕГДА, а не только на передаче.
|
||||
if FTxSliceId = Id then PushNetworkState;
|
||||
// Уведомление — здесь, а не у каждого вызывающего: слайс двигают мышью, CAT,
|
||||
// TCI и бэнд-логика, и каждый забывал сказать об этом остальным (TCI-клиенты
|
||||
// оставались на старой частоте после перетаскивания флага мышью).
|
||||
if Moved then SliceFreqChanged(Id);
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetSliceMode(Id, Mode: Integer);
|
||||
@@ -2921,6 +2962,85 @@ begin
|
||||
if Result then S := FSlices[idx];
|
||||
end;
|
||||
|
||||
function TRadioController.GetSliceView(Id: Integer; out V: TSliceView): Boolean;
|
||||
// Читается из чужих потоков (TCI), поэтому — поле за полем и только скаляры.
|
||||
// Active перечитывается после копирования: слайс могли удалить прямо во время
|
||||
// снятия снимка, и тогда честнее вернуть False, чем полуживую запись.
|
||||
var idx: Integer;
|
||||
begin
|
||||
FillChar(V, SizeOf(V), 0);
|
||||
Result := False;
|
||||
idx := FindSliceIndex(Id);
|
||||
if idx < 0 then Exit;
|
||||
V.Id := FSlices[idx].Id;
|
||||
V.PanId := FSlices[idx].PanId;
|
||||
V.TargetHz := FSlices[idx].TargetHz;
|
||||
V.Mode := FSlices[idx].Mode;
|
||||
V.FilterLo := FSlices[idx].FilterLo;
|
||||
V.FilterHi := FSlices[idx].FilterHi;
|
||||
V.AGC := FSlices[idx].AGC;
|
||||
V.Volume := FSlices[idx].Volume;
|
||||
V.Muted := FSlices[idx].Muted;
|
||||
V.FMSQOn := FSlices[idx].FMSQOn;
|
||||
V.FMSQLevel := FSlices[idx].FMSQLevel;
|
||||
V.NRMode := FSlices[idx].NRMode;
|
||||
V.NBMode := FSlices[idx].NBMode;
|
||||
V.SNB := FSlices[idx].SNB;
|
||||
V.ANF := FSlices[idx].ANF;
|
||||
Result := FSlices[idx].Active and (FSlices[idx].Id = Id);
|
||||
end;
|
||||
|
||||
function TRadioController.PanSliceCount(PanId: Integer): Integer;
|
||||
var i: Integer;
|
||||
begin
|
||||
Result := 0;
|
||||
for i := 0 to MAX_SLICES - 1 do
|
||||
if FSlices[i].Active and (FSlices[i].PanId = PanId) then Inc(Result);
|
||||
end;
|
||||
|
||||
function TRadioController.PanSliceId(PanId, Index: Integer): Integer;
|
||||
var i, Seen: Integer;
|
||||
begin
|
||||
Result := 0;
|
||||
if Index < 0 then Exit;
|
||||
Seen := 0;
|
||||
for i := 0 to MAX_SLICES - 1 do
|
||||
if FSlices[i].Active and (FSlices[i].PanId = PanId) then
|
||||
begin
|
||||
if Seen = Index then Exit(FSlices[i].Id);
|
||||
Inc(Seen);
|
||||
end;
|
||||
end;
|
||||
|
||||
function TRadioController.SlicePanIndex(Id: Integer; out PanId, Index: Integer): Boolean;
|
||||
var i, Seen, Pan: Integer;
|
||||
begin
|
||||
Result := False;
|
||||
PanId := 0; Index := 0;
|
||||
if Id <= 0 then Exit;
|
||||
Pan := -1;
|
||||
for i := 0 to MAX_SLICES - 1 do
|
||||
if FSlices[i].Active and (FSlices[i].Id = Id) then
|
||||
begin
|
||||
Pan := FSlices[i].PanId;
|
||||
Break;
|
||||
end;
|
||||
if Pan < 0 then Exit;
|
||||
|
||||
Seen := 0;
|
||||
for i := 0 to MAX_SLICES - 1 do
|
||||
if FSlices[i].Active and (FSlices[i].PanId = Pan) then
|
||||
begin
|
||||
if FSlices[i].Id = Id then
|
||||
begin
|
||||
PanId := Pan;
|
||||
Index := Seen;
|
||||
Exit(True);
|
||||
end;
|
||||
Inc(Seen);
|
||||
end;
|
||||
end;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Слоты слайсов: настройки (CAT-порт, Auto TX) висят на слоте/букве, а слайс
|
||||
// на слоте может появляться и исчезать.
|
||||
@@ -3216,11 +3336,13 @@ begin
|
||||
end;
|
||||
|
||||
function TRadioController.ActiveTXFreqHz: Double;
|
||||
var S: TCtrlSlice;
|
||||
// Снимок (GetSliceView), а не GetSlice: функцию зовут и внешние фронтенды из
|
||||
// своих потоков, а копия TCtrlSlice тащит managed-строки слайса.
|
||||
var S: TSliceView;
|
||||
begin
|
||||
// Мультислайс-TX: если выбран слайс-источник — передаём на его частоте
|
||||
// (слайсы не несут repeater-конфиг, FM-сдвиг не применяем).
|
||||
if (FTxSliceId > 0) and GetSlice(FTxSliceId, S) then
|
||||
if (FTxSliceId > 0) and GetSliceView(FTxSliceId, S) then
|
||||
Result := S.TargetHz
|
||||
else
|
||||
begin
|
||||
@@ -4722,9 +4844,20 @@ begin
|
||||
// иначе FVolume. Обе персистятся; слайдер редактирует активную.
|
||||
if MonitoringTX then FTxMonVolume := V else FVolume := V;
|
||||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetVolume(ActiveVolume / 100.0);
|
||||
// rfVolume — для слайдера и оверлея: они показывают ActiveVolume. А вот кто
|
||||
// именно изменился, слайдеру всё равно, зато не всё равно внешним клиентам:
|
||||
// у них громкость приёма и громкость монитора — РАЗНЫЕ величины.
|
||||
if MonitoringTX then Changed(rfMonVolume);
|
||||
Changed(rfVolume);
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetRxVolume(V: Integer);
|
||||
begin
|
||||
FVolume := EnsureRange(V, 0, 100);
|
||||
if MonitoringTX then Changed(rfVolume) // звучит монитор — трогать тракт нечем
|
||||
else ApplyActiveVolume; // он же несёт Changed(rfVolume)
|
||||
end;
|
||||
|
||||
procedure TRadioController.VolumeBy(Delta: Integer);
|
||||
begin SetVolume(ActiveVolume + Delta); end;
|
||||
|
||||
@@ -4734,6 +4867,7 @@ procedure TRadioController.SetTXMonVolume(V: Integer);
|
||||
begin
|
||||
FTxMonVolume := EnsureRange(V, 0, 100);
|
||||
if MonitoringTX then ApplyActiveVolume;
|
||||
Changed(rfMonVolume);
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetMute(On_: Boolean);
|
||||
@@ -6224,6 +6358,10 @@ begin
|
||||
FSettings.SaveCW(FDevMAC, FCWSettings);
|
||||
FSettings.Save;
|
||||
end;
|
||||
// Телеграф правят и оператор, и CAT, и TCI, а скорость с задержкой макросов —
|
||||
// величины общие для радио: об их смене обязаны узнать все фронтенды, а не
|
||||
// только тот, кто её заказал.
|
||||
Changed(rfCWSettings);
|
||||
end;
|
||||
|
||||
procedure TRadioController.SyncCWKeyer;
|
||||
|
||||
Reference in New Issue
Block a user