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
+24
View File
@@ -73,6 +73,8 @@ type
// Добавить/обновить спот (вызывается из потока кластера).
procedure Add(const S: TDXSpot);
// Убрать все споты позывного (SPOT_DELETE в TCI; регистр не важен).
procedure RemoveCall(const Call: string);
procedure Clear;
// Выбросить просроченное прямо сейчас. Нужен тому, кто следит за временем
// снаружи: сам стор чистится только на Add и снимках, а когда споты никто
@@ -364,6 +366,28 @@ begin
end;
end;
procedure TDXSpotStore.RemoveCall(const Call: string);
var i, j: Integer; Up: string;
begin
Up := UpperCase(Trim(Call));
if Up = '' then Exit;
FLock.Enter;
try
i := 0;
while i < FCount do
if UpperCase(FSpots[i].Call) = Up then
begin
for j := i to FCount - 2 do FSpots[j] := FSpots[j + 1];
Dec(FCount);
Inc(FVersion);
end
else
Inc(i);
finally
FLock.Leave;
end;
end;
function CompareSpotFreq(const A, B: TDXSpot): Integer;
begin
if A.FreqHz < B.FreqHz then Result := -1