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
+76
View File
@@ -92,6 +92,8 @@ type
TOnWebSettingsChange = procedure(Enabled: Boolean; Port: Integer;
const BindAddr, User, Pass: string; SpecPixels: Integer) of object;
TOnTCISettingsChange = procedure(Enabled: Boolean; Port: Integer;
const BindAddr: string) of object;
TOnCATChange = procedure(
const SerEnabled: array of Boolean;
const SerPort: array of string;
@@ -470,6 +472,9 @@ type
FEdWebUser: TFlatEdit;
FEdWebPass: TFlatEdit;
FCmbWebRes: TFlatComboBox;
FChkTCIEnabled: TFlatCheckBox;
FEdTCIPort: TFlatSpinEdit;
FEdTCIBind: TFlatEdit;
// ---- Close button ----
FBtnClose: TFlatButton;
@@ -483,6 +488,7 @@ type
FOnVHFCalChange: TOnVHFCalChange;
FOnADCChange: TOnADCChange;
FOnWebSettingsChange: TOnWebSettingsChange;
FOnTCISettingsChange: TOnTCISettingsChange;
FOnDXClusterChange: TOnDXClusterChange;
FOnDisplayChange: TOnDisplayParamChange;
FOnWaterfallChange: TOnWaterfallParamChange;
@@ -535,6 +541,7 @@ type
procedure BuildAdvancedTab;
procedure OnADCChkChange(Sender: TObject);
procedure OnWebAnyChange(Sender: TObject);
procedure OnTCIAnyChange(Sender: TObject);
procedure CollectAlexFromUI;
procedure FireAlexChange;
procedure OnAlexAnyChange(Sender: TObject);
@@ -720,6 +727,8 @@ type
procedure LoadDXClusterSettings(const D: TDXClusterSettings);
procedure LoadWebSettings(Enabled: Boolean; Port: Integer;
const BindAddr, User, Pass: string; SpecPixels: Integer);
procedure LoadTCISettings(Enabled: Boolean; Port: Integer;
const BindAddr: string);
property OnCATChange: TOnCATChange read FOnCATChange write FOnCATChange;
property OnSliceCATChange: TOnSliceCATChange read FOnSliceCATChange write FOnSliceCATChange;
@@ -756,6 +765,7 @@ type
property OnWfAGCNFChange: TOnWfAGCNFChange read FOnWfAGCNFChange write FOnWfAGCNFChange;
property OnADCChange: TOnADCChange read FOnADCChange write FOnADCChange;
property OnWebSettingsChange: TOnWebSettingsChange read FOnWebSettingsChange write FOnWebSettingsChange;
property OnTCISettingsChange: TOnTCISettingsChange read FOnTCISettingsChange write FOnTCISettingsChange;
property OnDXClusterChange: TOnDXClusterChange read FOnDXClusterChange write FOnDXClusterChange;
end;
@@ -3441,6 +3451,52 @@ begin
Lbl := MakeLbl(Grp, '1024 px ~160 KB/s, 4096 px ~650 KB/s per client',
GRP_PAD + LBL_W + 12, Y + ROW_H - 8, 320);
Lbl.Font.Size := 8;
// ── TCI Server ────────────────────────────────────────────────────────────
// Протокол Expert Electronics поверх WebSocket: логгеры, скиммеры, цифра.
Grp := MakeGroupPanel(FPageAdvanced, 'TCI Server', MARGIN, 564, SETTINGS_CARD_W, 190);
Y := R1;
Chk := TFlatCheckBox.Create(Self);
Chk.Parent := Grp;
Chk.Caption := 'Enabled';
Chk.SetBounds(DpiScale(GRP_PAD), DpiScale(Y), DpiScale(260), DpiScale(22));
Chk.Font.Size := 9;
Chk.Font.Color := CLR_TEXT;
Chk.Checked := False;
Chk.OnChange := OnTCIAnyChange;
FChkTCIEnabled := Chk;
Y := Y + ROW_H;
MakeLbl(Grp, 'Port:', GRP_PAD, Y + 4, LBL_W);
Spin := TFlatSpinEdit.Create(Self);
Spin.Parent := Grp;
Spin.SetBounds(DpiScale(GRP_PAD + LBL_W + 12), DpiScale(Y), DpiScale(90), DpiScale(BTN_H));
Spin.Color := CLR_INPUT;
Spin.Font.Color := CLR_INPUT_TEXT;
Spin.Font.Size := 9;
Spin.MinValue := 1;
Spin.MaxValue := 65535;
Spin.Value := 40001;
Spin.OnChange := OnTCIAnyChange;
FEdTCIPort := Spin;
Y := Y + ROW_H;
MakeLbl(Grp, 'Interface:', GRP_PAD, Y + 4, LBL_W);
Ed := TFlatEdit.Create(Self);
Ed.Parent := Grp;
Ed.SetBounds(DpiScale(GRP_PAD + LBL_W + 12), DpiScale(Y), DpiScale(ED_W), DpiScale(BTN_H));
Ed.Color := CLR_INPUT;
Ed.Font.Color := CLR_INPUT_TEXT;
Ed.Font.Size := 9;
Ed.Text := '127.0.0.1';
Ed.OnChange := OnTCIAnyChange;
FEdTCIBind := Ed;
// Авторизации в протоколе нет: открытый наружу порт = полный доступ к трансиверу.
Lbl := MakeLbl(Grp, 'No authentication in TCI — keep 127.0.0.1 unless the network is trusted',
GRP_PAD, Y + ROW_H, 460);
Lbl.Font.Size := 8;
end;
procedure TSettingsForm.BuildDXClusterTab;
@@ -3689,6 +3745,26 @@ begin
Res);
end;
procedure TSettingsForm.OnTCIAnyChange(Sender: TObject);
begin
if FLoading then Exit;
if not Assigned(FOnTCISettingsChange) then Exit;
FOnTCISettingsChange(FChkTCIEnabled.Checked, FEdTCIPort.Value, FEdTCIBind.Text);
end;
procedure TSettingsForm.LoadTCISettings(Enabled: Boolean; Port: Integer;
const BindAddr: string);
begin
FLoading := True;
try
FChkTCIEnabled.Checked := Enabled;
FEdTCIPort.Value := EnsureRange(Port, 1, 65535);
FEdTCIBind.Text := BindAddr;
finally
FLoading := False;
end;
end;
procedure TSettingsForm.LoadADCSettings(Dither, Random: Boolean);
begin
FLoading := True;