feat(tci): TCI 2.0 — EWSDR как сервер (команды и уведомления)

Протокол Expert Electronics поверх WebSocket, порт 40001. EWSDR слушает,
клиенты — логгеры, скиммеры, цифровые программы.

- TCIProtocol.pas — чистый слой протокола: разбор/сборка `имя:арг;`,
  экранирование `^ ~ *`, словарь видов связи, пересчёт громкости в дБ.
- TCIServer.pas — WS-сервер поверх WebUtils/WsClient: accept-поток,
  поток на клиента, HTTP-Upgrade, фреймы, рассылка, тик 20 мс.
- TCIAdapter.pas — мост к TRadioController по схеме CAT: геттеры читают
  поля напрямую, сеттеры через Invoke, уведомления через AddStateListener.
  Маппинг: приёмник TCI = панадаптер, канал A/B = VFO A/B (пан 0) либо
  первый/второй слайс (паны 1..).

Попутно: TRadioController.RemoveStateListener (адаптер умирает раньше
контроллера) и TDXSpotStore.RemoveCall (spot_delete).

Настройки — секция "tci" в settings.json (умолчание: выключено,
127.0.0.1, так как авторизации в протоколе нет) и вкладка
Advanced → TCI Server.

Бинарные потоки (IQ/аудио) — этап 2. Статус, таблица команд и список
осознанных эхо-заглушек — в doc/TCI.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 16:22:49 +03:00
co-authored by Claude Opus 5
parent 92c01fa2c6
commit 82f0e4f771
10 changed files with 3208 additions and 0 deletions
+45
View File
@@ -35,6 +35,7 @@ uses
Settings,
WebServer, WebAdapter,
CATAdapter,
TCIAdapter,
SpectrumView, SpectrumViewOpengl,
PanZoomBar, PanafallPanel, PanDisplayPopup, PureSignalPopup, FlatPopupMenu,
WidebandView,
@@ -246,6 +247,8 @@ type
FWebSpecPixels: Integer; // потолок точек кадра спектра/водопада для web
// --- CAT ---
FCATAdapter: TCATAdapter; // адаптер CAT поверх контроллера (движок+транспорты+контекст)
FTCIAdapter: TTCIAdapter; // адаптер TCI (WebSocket-сервер Expert Electronics)
FTCICfg: TTCISettings; // текущие настройки TCI (UI/persist)
FCATLastGlobal: TGlobalSettings; // текущие CAT-настройки (UI/persist; ApplySettings → адаптер)
// Виджеты построены (конец FormCreate). До этого рендер состояния запрещён:
// адаптеры (CAT/web), создаваемые по ходу FormCreate, могут дёрнуть
@@ -835,6 +838,12 @@ type
procedure ApplyADCSettings(Dither, Random: Boolean);
procedure ApplyWebSettings(Enabled: Boolean; Port: Integer;
const BindAddr, User, Pass: string; SpecPixels: Integer);
procedure ApplyTCISettings(Enabled: Boolean; Port: Integer;
const BindAddr: string);
// Фокус главного окна → APP_FOCUS клиентам TCI (логгеры так решают, кому
// отдавать клавиатуру).
procedure TCIFormActivate(Sender: TObject);
procedure TCIFormDeactivate(Sender: TObject);
procedure ApplyGridParams(RefLevel, Range, GridStep: Double);
// Пушит активные grid-параметры (RX или TX в зависимости от FTransmitting)
// в FSpecView и сбрасывает кэш сетки. Вызывается при смене RX↔TX и при
@@ -1447,6 +1456,7 @@ begin
FreeAndNil(FWebAdapter); // адаптер не владеет сервером — освобождаем после Stop
FWebServer.Free;
FreeAndNil(FCATAdapter); // его Destroy останавливает+освобождает CAT движок/транспорты
FreeAndNil(FTCIAdapter); // Destroy останавливает TCI-сервер и его потоки
// Ядро освобождаем последним — его Destroy закрывает и освобождает движки
// (FreeEngines) и FSettings.
FreeAndNil(FController);
@@ -2705,6 +2715,14 @@ begin
// DX-кластер: база спотов + сетевой поток + оверлей подписей.
InitDXCluster;
// TCI-сервер — равноправный фронтенд рядом с CAT и web. Создаём после
// InitDXCluster: споты от TCI-клиентов кладутся в тот же стор.
FController.FSettings.LoadTCISettings(FTCICfg);
FTCIAdapter := TTCIAdapter.Create(FController, FDXStore);
FTCIAdapter.ApplySettings(FTCICfg);
OnActivate := TCIFormActivate;
OnDeactivate := TCIFormDeactivate;
// S-метр — внутри PanelToolbar, справа
PanelSMeterRight := TPanel.Create(Self);
PanelSMeterRight.Parent := PanelToolbar;
@@ -5633,6 +5651,9 @@ begin
FDXSpotOverlay.SpotAtPixel(X, Y, DXSpot) then
begin
OnDXTuneSpot(DXSpot.FreqHz, DXSpot.Mode);
// Клик по споту — уведомление TCI-клиентам (логгер открывает QSO-карточку).
if FTCIAdapter <> nil then
FTCIAdapter.NotifySpotClicked(DXSpot.Call, DXSpot.FreqHz, 0, FController.FActiveVfo);
Exit;
end;
@@ -6548,6 +6569,26 @@ begin
FController.FSettings.SaveWebSettings(W);
end;
procedure TMainForm.ApplyTCISettings(Enabled: Boolean; Port: Integer;
const BindAddr: string);
begin
FTCICfg.Enabled := Enabled;
FTCICfg.Port := Port;
FTCICfg.BindAddr := BindAddr;
FController.FSettings.SaveTCISettings(FTCICfg);
if FTCIAdapter <> nil then FTCIAdapter.ApplySettings(FTCICfg);
end;
procedure TMainForm.TCIFormActivate(Sender: TObject);
begin
if FTCIAdapter <> nil then FTCIAdapter.NotifyAppFocus(True);
end;
procedure TMainForm.TCIFormDeactivate(Sender: TObject);
begin
if FTCIAdapter <> nil then FTCIAdapter.NotifyAppFocus(False);
end;
procedure TMainForm.OnSampleRateHidePanel;
begin
FPanelHidden := not FPanelHidden;
@@ -7937,6 +7978,8 @@ begin
P.DXOverlay.SpotAtPixel(X, Y, DXSpot) then
begin
DXTuneSpotOnPan(P, DXSpot.FreqHz, DXSpot.Mode);
if FTCIAdapter <> nil then
FTCIAdapter.NotifySpotClicked(DXSpot.Call, DXSpot.FreqHz, P.PanId, 0);
Exit;
end;
W := TControl(Sender).Width;
@@ -9634,6 +9677,7 @@ begin
SF.OnWfAGCNFChange := ApplyWfAGCNF;
SF.OnADCChange := ApplyADCSettings;
SF.OnWebSettingsChange := ApplyWebSettings;
SF.OnTCISettingsChange := ApplyTCISettings;
SF.OnDXClusterChange := OnDXClusterSettingsChange;
end;
SF := TSettingsForm(FSettingsForm);
@@ -9689,6 +9733,7 @@ begin
SF.LoadADCSettings(FController.FDitherEnabled, FController.FRandomEnabled);
SF.LoadWebSettings(FWebEnabled, FWebPort, FWebBindAddr, FWebUser, FWebPass,
FWebSpecPixels);
SF.LoadTCISettings(FTCICfg.Enabled, FTCICfg.Port, FTCICfg.BindAddr);
SF.LoadDXClusterSettings(FDXCfg);
SF.LoadFPS(FDisplayFPS);
SF.LoadLightTheme(FLightTheme);