mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
Разбор семи проходов ревью ветки. Ниже — по сути, а не по списку. Потоки. Сетевые потоки больше не читают модель контроллера напрямую. Слайсы снимаются в потоке контроллера (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>
2500 lines
99 KiB
ObjectPascal
2500 lines
99 KiB
ObjectPascal
unit TCIAdapter;
|
||
|
||
{
|
||
TCIAdapter.pas — мост TCI ↔ TRadioController.
|
||
|
||
Роль та же, что у TCATAdapter в CAT-подсистеме: равноправный клиент
|
||
контроллера, никаких обращений к MainForm.
|
||
|
||
TCI-клиенты ──WS──► TTCIServer ──► TTCIAdapter ──► TRadioController
|
||
логгер/скиммер транспорт команды/ ядро
|
||
уведомления
|
||
|
||
Потоки:
|
||
• Геттеры читают поля контроллера напрямую из потока клиента (атомарное
|
||
чтение, как в CAT).
|
||
• Сеттеры пишут параметр в scratch-поля под FLock и зовут
|
||
if CanInvoke then FController.Invoke(SyncXxx) — исполнение в потоке контроллера.
|
||
• Уведомления наружу идут из OnState (поток контроллера) рассылкой всем
|
||
клиентам: сервер TCI обязан синхронизировать всех подключённых (§3.5).
|
||
|
||
Маппинг модели (выбран при проектировании, см. doc/TCI.md):
|
||
приёмник TCI = панадаптер ewsdr (0 = главный тракт, 1.. = доп. DDC-паны)
|
||
канал A/B = VFO A/B у приёмника 0; у панов 1.. — первый и второй слайс
|
||
|
||
Чего в ewsdr нет (RIT/XIT, BIN/ANC/APF/DSE/NF, параметры NB, смещения
|
||
DIGL/DIGU): значения принимаются, хранятся здесь и отражаются клиентам —
|
||
так синхронизация между несколькими клиентами остаётся честной, а поведение
|
||
радио не выдумывается. Всё такое помечено «эхо» и перечислено в doc/TCI.md.
|
||
|
||
Потоки IQ/аудио (§3.4) — следующий этап: команды управления потоками
|
||
принимаются и подтверждаются, но сами потоки не идут.
|
||
}
|
||
|
||
{$IFDEF FPC}
|
||
{$MODE Delphi}
|
||
{$LONGSTRINGS ON}
|
||
{$ENDIF}
|
||
|
||
interface
|
||
|
||
uses
|
||
Classes, SysUtils, DateUtils, Math, SyncObjs,
|
||
RadioController, RadioBackend, WDSPEngine, Settings,
|
||
DXSpotStore, TCIProtocol, TCIServer;
|
||
|
||
const
|
||
TCI_MAX_RX = MAX_PANS; // приёмник TCI = панадаптер
|
||
// Захват параметра клиентом (§3.5): пока владелец его крутит, остальные
|
||
// могут только слушать. Без этого два логгера перетягивают частоту друг у
|
||
// друга бесконечно.
|
||
TCI_HOLD_MS = 200;
|
||
TCI_HOLD_SLOTS = 64;
|
||
|
||
type
|
||
{ Параметры TCI, которым в ewsdr нет соответствия: храним и отражаем. }
|
||
TTCIRxEcho = record
|
||
RitOn, XitOn: Boolean;
|
||
RitHz, XitHz: Integer;
|
||
BinOn, ANCOn: Boolean;
|
||
APFOn, DSEOn: Boolean;
|
||
NFOn: Boolean;
|
||
NBThreshold: Integer; // 1..100
|
||
NBDuration: Integer; // 1..300
|
||
ChannelBOn: Boolean;
|
||
BalanceDb: array[0..TCI_CHANNELS-1] of Integer;
|
||
end;
|
||
|
||
{ Снимок «железной» части контроллера: всё, ради чего иначе пришлось бы
|
||
трогать FNetwork из потока клиента. Backend живёт в UI-потоке и на смене
|
||
устройства освобождается — чтение его Caps и имени платы (а это ещё и
|
||
строки) из чужого потока даёт обращение к освобождённой памяти. }
|
||
TTCIDevSnap = record
|
||
DevName: string; // копия, принадлежит адаптеру
|
||
LimLoHz: Double; // границы настройки (VFO_LIMITS и проверка команд)
|
||
LimHiHz: Double;
|
||
MaxPans: Integer;
|
||
SampleRate: Integer;
|
||
HasTX: Boolean;
|
||
end;
|
||
|
||
{ Запись снимка таблицы слайсов: то, что сетевым потокам разрешено читать. }
|
||
TTCISliceSnap = record
|
||
Used: Boolean;
|
||
V: TSliceView;
|
||
end;
|
||
|
||
{ Захваченный параметр (§3.5): кто им сейчас управляет и когда трогал.
|
||
Owner = nil — изменение пришло не от клиента (оператор, CAT, бэнд-логика). }
|
||
TTCIHold = record
|
||
Key: string;
|
||
Owner: TTCIClient;
|
||
At: QWord;
|
||
end;
|
||
|
||
TTCIAdapter = class
|
||
private
|
||
FController: TRadioController;
|
||
FSpots: TDXSpotStore; // может быть nil (демон/тесты)
|
||
FServer: TTCIServer;
|
||
FLock: TCriticalSection;
|
||
FCfg: TTCISettings; // применённая конфигурация (для отката)
|
||
|
||
// ── Эхо-состояние (пишут потоки клиентов ⇒ только под FEchoLock) ──
|
||
FEchoLock: TCriticalSection;
|
||
FEcho: array[0..TCI_MAX_RX-1] of TTCIRxEcho;
|
||
FDiglOffset: Integer;
|
||
FDiguOffset: Integer;
|
||
FCwTerminal: Boolean;
|
||
|
||
// ── Снимок железа: пишет поток контроллера, читают сетевые ──
|
||
FDevLock: TCriticalSection;
|
||
FDevSnap: TTCIDevSnap;
|
||
|
||
// ── Снимок слайсов: пишет поток контроллера, читают сетевые ──
|
||
FSliceLock: TCriticalSection;
|
||
FSliceSnap: array[0..MAX_SLICES-1] of TTCISliceSnap;
|
||
|
||
// ── Захват параметров клиентами (§3.5) ──
|
||
FHoldLock: TCriticalSection;
|
||
FHolds: array[0..TCI_HOLD_SLOTS-1] of TTCIHold;
|
||
FHoldCount: Integer;
|
||
|
||
// ── Кэш для подавления повторов в уведомлениях ──
|
||
FLastLimLo: Double; // последние разосланные VFO_LIMITS (поток контроллера)
|
||
FLastLimHi: Double;
|
||
FLastTxFreq: Double;
|
||
FLastTxEnable: Boolean;
|
||
FAppFocus: Boolean; // последнее, что сказал UI (для пачки состояния)
|
||
FOnFocusRequest: TThreadMethod;
|
||
|
||
// ── scratch для маршалинга в поток контроллера ──
|
||
FsFreq: Double;
|
||
FsInt: Integer;
|
||
FsInt2: Integer;
|
||
FsInt3: Integer;
|
||
FsBool: Boolean;
|
||
FsStr: string;
|
||
|
||
// ── Sync-методы (поток контроллера) ──
|
||
procedure SyncSetVfo;
|
||
procedure SyncSetCenter;
|
||
procedure SyncSetMode;
|
||
procedure SyncSetFilter;
|
||
procedure SyncSetTRX;
|
||
procedure SyncSetTune;
|
||
procedure SyncSetDrive;
|
||
procedure SyncSetTuneDrive;
|
||
procedure SyncSetSplit;
|
||
procedure SyncSetVolume;
|
||
procedure SyncSetMute;
|
||
procedure SyncSetRxMute;
|
||
procedure SyncSetRxVolume;
|
||
procedure SyncSetMonVolume;
|
||
procedure SyncSetMonEnable;
|
||
procedure SyncSetAGCMode;
|
||
procedure SyncSetAGCTop;
|
||
procedure SyncSetNR;
|
||
procedure SyncSetNB;
|
||
procedure SyncSetANF;
|
||
procedure SyncSetLock;
|
||
procedure SyncSetSql;
|
||
procedure SyncSetSqlLevel;
|
||
procedure SyncSetRun;
|
||
procedure SyncSetCWSpeed;
|
||
procedure SyncSetCWDelay;
|
||
procedure SyncCWSend;
|
||
procedure SyncCWStop;
|
||
procedure SyncFocus;
|
||
|
||
// ── Помощники модели ──
|
||
function CanInvoke: Boolean;
|
||
function RxCount: Integer;
|
||
function ValidRx(Rx: Integer): Boolean;
|
||
function RxActive(Rx: Integer): Boolean;
|
||
procedure CalcFreqLimits(out LoHz, HiHz: Double); // живые (поток контроллера)
|
||
procedure FreqLimits(out LoHz, HiHz: Double); // из снимка (любой поток)
|
||
function FreqSaneLive(Hz: Double): Boolean; // проверка перед установкой
|
||
procedure PushVfoLimits; // разослать VFO_LIMITS, если границы уехали
|
||
function FreqSane(Hz: Double): Boolean;
|
||
procedure RefreshDev; // снимок железа (поток контроллера)
|
||
function DevSnap: TTCIDevSnap;
|
||
procedure RefreshSlices; // снимок таблицы слайсов (поток контроллера)
|
||
function SliceMapSig: string; // «кто где стоит»: для детекта появления/ухода
|
||
procedure PushChannelMap; // каналы приёмников появились/исчезли
|
||
function SnapSlice(Rx, Ch: Integer; out V: TSliceView): Boolean;
|
||
function SliceIdOf(Rx, Ch: Integer): Integer; // из снимка
|
||
function SliceIdLive(Rx, Ch: Integer): Integer; // из живой таблицы (Sync*)
|
||
function RxSlice(Rx: Integer; out S: TSliceView): Boolean;
|
||
function SliceRxCh(Id: Integer; out Rx, Ch: Integer): Boolean;
|
||
{ Захват параметра (§3.5). True — параметр наш (свободен, наш или отпущен
|
||
по таймауту), захват продлевается. False — им сейчас управляет другой. }
|
||
function Claim(const Key: string; Owner: TTCIClient): Boolean;
|
||
function HoldKey(const Name: string; Rx, Ch: Integer): string;
|
||
procedure DropHolds(Owner: TTCIClient);
|
||
function ChanFreq(Rx, Ch: Integer): Double;
|
||
function ChanCount(Rx: Integer): Integer;
|
||
function RxCenterHz(Rx: Integer): Double;
|
||
function RxMode(Rx: Integer): Integer;
|
||
procedure RxFilter(Rx: Integer; out Lo, Hi: Integer);
|
||
function RxMuted(Rx: Integer): Boolean;
|
||
function RxVolumeDb(Rx, Ch: Integer): Double;
|
||
function RxAGCUi(Rx: Integer): Integer;
|
||
function RxNROn(Rx: Integer): Boolean;
|
||
function RxNBOn(Rx: Integer): Boolean;
|
||
function RxANFOn(Rx: Integer): Boolean;
|
||
function RxSqlOn(Rx: Integer): Boolean;
|
||
function RxSqlLevel(Rx: Integer): Integer;
|
||
function RxSMeterDbm(Rx, Ch: Integer): Double;
|
||
function TxEnabled: Boolean;
|
||
|
||
// ── Формирование строк состояния ──
|
||
function StrVfo(Rx, Ch: Integer): string;
|
||
function StrIf(Rx, Ch: Integer): string;
|
||
function StrDds(Rx: Integer): string;
|
||
function StrModulation(Rx: Integer): string;
|
||
function StrFilterBand(Rx: Integer): string;
|
||
function StrTrx: string;
|
||
function StrTune: string;
|
||
function StrDrive: string;
|
||
function StrVolume: string;
|
||
function StrMute: string;
|
||
function StrAGCMode(Rx: Integer): string;
|
||
function StrAGCGain(Rx: Integer): string;
|
||
function StrLock(Rx: Integer): string;
|
||
function StrSqlEnable(Rx: Integer): string;
|
||
function StrSqlLevel(Rx: Integer): string;
|
||
function StrTxEnable(Rx: Integer): string;
|
||
function StrRxVolume(Rx, Ch: Integer): string;
|
||
function StrNR(Rx: Integer): string;
|
||
function StrNB(Rx: Integer): string;
|
||
function StrANF(Rx: Integer): string;
|
||
{ Полный набор строк по приёмнику — рассылка после правки слайса. }
|
||
procedure BroadcastRxState(Rx, Ch: Integer);
|
||
|
||
procedure SendInit(Client: TTCIClient);
|
||
procedure SendState(Client: TTCIClient);
|
||
procedure Reply(Client: TTCIClient; const S: string);
|
||
|
||
// ── События сервера/контроллера ──
|
||
procedure HandleCommand(Client: TTCIClient; const Cmd: string);
|
||
procedure DispatchCommand(Client: TTCIClient; const M: TTCIMessage);
|
||
procedure HandleConnect(Client: TTCIClient);
|
||
procedure HandleDisconnect(Client: TTCIClient);
|
||
procedure HandleTick;
|
||
procedure PushSensors(Client: TTCIClient);
|
||
procedure OnState(Sender: TObject; Field: TRadioField);
|
||
|
||
// ── Отдельные команды (чтобы HandleCommand не превратился в простыню) ──
|
||
procedure CmdFreq(Client: TTCIClient; const M: TTCIMessage; IsIF: Boolean);
|
||
procedure CmdCWMacros(const M: TTCIMessage; IsMsg: Boolean);
|
||
procedure CmdSpot(const M: TTCIMessage);
|
||
public
|
||
constructor Create(AController: TRadioController; ASpots: TDXSpotStore = nil);
|
||
destructor Destroy; override;
|
||
|
||
{ Настройки TCI: включение/порт/адрес. Зовётся при старте и из настроек.
|
||
False — включить просили, а порт не открылся (занят/нет прав/кривой
|
||
адрес): вызывающий обязан сказать это оператору, иначе тот останется с
|
||
галкой «включено» и мёртвым сервером. }
|
||
function ApplySettings(const T: TTCISettings): Boolean;
|
||
|
||
function Active: Boolean;
|
||
function ClientCount: Integer;
|
||
|
||
{ Уведомление о клике по споту на панораме (§4.4) — зовёт UI. }
|
||
procedure NotifySpotClicked(const Call: string; FreqHz: Double;
|
||
Rx: Integer = 0; Ch: Integer = 0);
|
||
{ Статус фокуса главного окна (APP_FOCUS) — зовёт UI. }
|
||
procedure NotifyAppFocus(InFocus: Boolean);
|
||
|
||
property Server: TTCIServer read FServer;
|
||
{ SET_IN_FOCUS (§4.3): клиент просит поднять окно программы. Ставит UI;
|
||
вызывается в потоке контроллера. nil — команда игнорируется. }
|
||
property OnFocusRequest: TThreadMethod read FOnFocusRequest write FOnFocusRequest;
|
||
end;
|
||
|
||
implementation
|
||
|
||
{ ═══════════════════════════════════════════════════════════════════════════
|
||
Жизненный цикл
|
||
═══════════════════════════════════════════════════════════════════════════ }
|
||
|
||
constructor TTCIAdapter.Create(AController: TRadioController; ASpots: TDXSpotStore);
|
||
var i, c: Integer;
|
||
begin
|
||
inherited Create;
|
||
FController := AController;
|
||
FSpots := ASpots;
|
||
FLock := TCriticalSection.Create;
|
||
FEchoLock := TCriticalSection.Create;
|
||
|
||
for i := 0 to TCI_MAX_RX - 1 do
|
||
begin
|
||
FillChar(FEcho[i], SizeOf(FEcho[i]), 0);
|
||
FEcho[i].NBThreshold := 50;
|
||
FEcho[i].NBDuration := 25;
|
||
for c := 0 to TCI_CHANNELS - 1 do FEcho[i].BalanceDb[c] := 0;
|
||
end;
|
||
FLastLimLo := 0;
|
||
FLastLimHi := 0;
|
||
FLastTxFreq := 0;
|
||
FLastTxEnable := True;
|
||
FAppFocus := True;
|
||
FDevLock := TCriticalSection.Create;
|
||
FSliceLock := TCriticalSection.Create;
|
||
FillChar(FSliceSnap, SizeOf(FSliceSnap), 0);
|
||
FHoldLock := TCriticalSection.Create;
|
||
FHoldCount := 0;
|
||
|
||
FServer := TTCIServer.Create;
|
||
FServer.OnCommand := HandleCommand;
|
||
FServer.OnConnect := HandleConnect;
|
||
FServer.OnDisconnect := HandleDisconnect;
|
||
FServer.OnTick := HandleTick;
|
||
|
||
RefreshDev;
|
||
// Первый снимок — прямо здесь: конструктор идёт в потоке контроллера, а
|
||
// слайсы могли быть восстановлены ещё до появления адаптера (события об их
|
||
// создании мы уже не увидим).
|
||
RefreshSlices;
|
||
|
||
// Многоадресная подписка: OnStateChanged занят MainForm.
|
||
FController.AddStateListener(OnState);
|
||
end;
|
||
|
||
destructor TTCIAdapter.Destroy;
|
||
begin
|
||
// Сначала отписка: контроллер живёт дольше адаптера, и Changed() после
|
||
// нашей смерти позвал бы метод освобождённого объекта.
|
||
if FController <> nil then FController.RemoveStateListener(OnState);
|
||
if FServer <> nil then
|
||
begin
|
||
FServer.Stop;
|
||
FreeAndNil(FServer);
|
||
end;
|
||
FLock.Free;
|
||
FEchoLock.Free;
|
||
FDevLock.Free;
|
||
FSliceLock.Free;
|
||
FHoldLock.Free;
|
||
inherited;
|
||
end;
|
||
|
||
function TTCIAdapter.ApplySettings(const T: TTCISettings): Boolean;
|
||
// Порядок важен: сначала проверяем новую конфигурацию, и только потом гасим
|
||
// работающий сервер. Иначе опечатка в адресе или занятый порт оставляли
|
||
// оператора вообще без TCI — старый слушатель уже закрыт, новый не открылся.
|
||
// Не удалось поднять новый — возвращаемся на прежний.
|
||
var
|
||
NewPort, OldPort: Word;
|
||
begin
|
||
RefreshDev; // зовут из потока контроллера — заодно освежаем снимки
|
||
RefreshSlices;
|
||
NewPort := Word(EnsureRange(T.Port, 1, 65535));
|
||
// Адрес разбирается ДО открытия сокета: невалидный означает отказ, а не
|
||
// «слушаем все интерфейсы» — авторизации в TCI нет (см. TCIParseIPv4).
|
||
if T.Enabled and not TTCIServer.ValidSettings(NewPort, T.BindAddr) then
|
||
Exit(False);
|
||
|
||
OldPort := FServer.Port;
|
||
FServer.Stop;
|
||
if not T.Enabled then
|
||
begin
|
||
FCfg := T;
|
||
Exit(True);
|
||
end;
|
||
|
||
Result := FServer.Configure(NewPort, T.BindAddr) and FServer.Start;
|
||
if Result then
|
||
begin
|
||
FCfg := T;
|
||
Exit;
|
||
end;
|
||
// Порт занят (или отобран правами) — поднимаем то, что работало.
|
||
if FCfg.Enabled and TTCIServer.ValidSettings(OldPort, FCfg.BindAddr) then
|
||
if FServer.Configure(OldPort, FCfg.BindAddr) then FServer.Start;
|
||
end;
|
||
|
||
function TTCIAdapter.CanInvoke: Boolean;
|
||
// Пока сервер останавливается, новых вызовов в поток контроллера не начинаем:
|
||
// останавливает нас как раз он (UI), и Synchronize из потока клиента в него
|
||
// уже не вернётся.
|
||
begin
|
||
Result := (FController <> nil) and
|
||
((FServer = nil) or not FServer.Stopping);
|
||
end;
|
||
|
||
function TTCIAdapter.Active: Boolean;
|
||
begin
|
||
Result := (FServer <> nil) and FServer.Running;
|
||
end;
|
||
|
||
function TTCIAdapter.ClientCount: Integer;
|
||
begin
|
||
if FServer = nil then Result := 0 else Result := FServer.ClientCount;
|
||
end;
|
||
|
||
{ ═══════════════════════════════════════════════════════════════════════════
|
||
Модель: приёмник = панадаптер, канал = VFO A/B (пан 0) или слайс (паны 1..)
|
||
═══════════════════════════════════════════════════════════════════════════ }
|
||
|
||
function TTCIAdapter.RxCount: Integer;
|
||
begin
|
||
Result := DevSnap.MaxPans;
|
||
if Result < 1 then Result := 1;
|
||
if Result > TCI_MAX_RX then Result := TCI_MAX_RX;
|
||
end;
|
||
|
||
function TTCIAdapter.ValidRx(Rx: Integer): Boolean;
|
||
begin
|
||
Result := (Rx >= 0) and (Rx < RxCount);
|
||
end;
|
||
|
||
function TTCIAdapter.RxActive(Rx: Integer): Boolean;
|
||
// Приёмник существует физически. TRX_COUNT в протоколе объявляется один раз и
|
||
// равен потолку железа, но пан из этого потолка может быть ещё не создан —
|
||
// врать про его частоту нельзя (см. SendState).
|
||
begin
|
||
if Rx = 0 then Result := True
|
||
else Result := ValidRx(Rx) and FController.PanDDCActive(Rx);
|
||
end;
|
||
|
||
procedure TTCIAdapter.CalcFreqLimits(out LoHz, HiHz: Double);
|
||
// Живые границы настройки. Только поток контроллера: VisibleFreqBounds смотрит
|
||
// в Caps бэкенда. XVTR → диапазон слота; устройства нет — тот же запасной
|
||
// диапазон, что уходит в VFO_LIMITS. «Нет устройства = можно всё» недопустимо:
|
||
// SetCenter/SetPanDDCFreq ничего не клампят и отдадут число прямо в backend.
|
||
begin
|
||
FController.VisibleFreqBounds(LoHz, HiHz);
|
||
if (not FController.FDevConnected) or (HiHz <= LoHz) then
|
||
begin
|
||
LoHz := 10000;
|
||
HiHz := 30000000;
|
||
end;
|
||
if LoHz < 1 then LoHz := 1; // нулевой частоты не бывает ни у кого
|
||
end;
|
||
|
||
function TTCIAdapter.FreqSaneLive(Hz: Double): Boolean;
|
||
// Та же проверка, что FreqSane, но по ЖИВЫМ границам и в потоке контроллера.
|
||
// Нужна отдельно: команду разбирает поток клиента, а исполняется она позже —
|
||
// за это время оператор успевает сменить устройство (Pluto → HPSDR), и снимок,
|
||
// по которому частоту пропустили, уже не описывает то радио, куда она уедет.
|
||
var LoHz, HiHz: Double;
|
||
begin
|
||
Result := False;
|
||
if IsNan(Hz) or IsInfinite(Hz) or (Hz <= 0) then Exit;
|
||
CalcFreqLimits(LoHz, HiHz);
|
||
Result := (Hz >= LoHz) and (Hz <= HiHz);
|
||
end;
|
||
|
||
procedure TTCIAdapter.RefreshDev;
|
||
// Снимок железа. Только поток контроллера: здесь дёргаются BackendCaps и
|
||
// BoardDisplayName, а они смотрят в FNetwork, который UI освобождает на смене
|
||
// устройства. Сетевые потоки читают уже готовую копию (DevSnap).
|
||
var
|
||
D: TTCIDevSnap;
|
||
Caps: TBackendCaps;
|
||
begin
|
||
Caps := FController.BackendCaps;
|
||
D.DevName := FController.BoardDisplayName;
|
||
D.MaxPans := Caps.MaxPans;
|
||
D.HasTX := Caps.HasTX;
|
||
D.SampleRate := FController.FSampleRate;
|
||
CalcFreqLimits(D.LimLoHz, D.LimHiHz);
|
||
|
||
FDevLock.Enter;
|
||
try
|
||
FDevSnap := D; // строка присваивается ТОЛЬКО здесь и только под локом
|
||
finally
|
||
FDevLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
function TTCIAdapter.DevSnap: TTCIDevSnap;
|
||
begin
|
||
FDevLock.Enter;
|
||
try Result := FDevSnap; finally FDevLock.Leave; end;
|
||
end;
|
||
|
||
procedure TTCIAdapter.FreqLimits(out LoHz, HiHz: Double);
|
||
// ЕДИНСТВЕННЫЙ источник границ настройки: и для VFO_LIMITS в пачке
|
||
// инициализации, и для проверки частоты в командах. Раньше их было два, и они
|
||
// расходились: клиенту объявлялись пределы АЦП, а команда под трансвертером
|
||
// принимала вообще любое число.
|
||
var D: TTCIDevSnap;
|
||
begin
|
||
D := DevSnap;
|
||
LoHz := D.LimLoHz;
|
||
HiHz := D.LimHiHz;
|
||
end;
|
||
|
||
procedure TTCIAdapter.PushVfoLimits;
|
||
// Границы настройки не вечны: подключилось устройство, включился или выключился
|
||
// трансвертер — и объявленные при подключении VFO_LIMITS начинают врать. Клиент
|
||
// узнаёт об этом только от нас: перезапросить их в протоколе нечем.
|
||
// Дедуп — по последнему известному значению: rfDevice приходит и на создание
|
||
// слайса, и на обновление списка устройств.
|
||
// ВАЖНО: кэш ведём всегда, даже когда клиентов нет. Иначе так: клиент видел
|
||
// пределы устройства → все отключились → радио отвалилось (кэш бы не заметил)
|
||
// → новый клиент получил запасные 10 кГц…30 МГц → радио вернулось → новые
|
||
// пределы совпали с давним кэшем, рассылка подавилась, и клиент навсегда остался
|
||
// с запасными.
|
||
var Lo, Hi: Double;
|
||
begin
|
||
FreqLimits(Lo, Hi);
|
||
if (Lo = FLastLimLo) and (Hi = FLastLimHi) then Exit;
|
||
FLastLimLo := Lo;
|
||
FLastLimHi := Hi;
|
||
if (FServer = nil) or (FServer.ClientCount = 0) then Exit;
|
||
FServer.Broadcast(TCIBuild('vfo_limits',
|
||
[TCIIntStr(Round(Lo)), TCIIntStr(Round(Hi))]));
|
||
end;
|
||
|
||
function TTCIAdapter.FreqSane(Hz: Double): Boolean;
|
||
// Годится ли частота к установке. Отрицательная и нулевая — точно нет: именно
|
||
// такие получались из неразобранных аргументов.
|
||
var LoHz, HiHz: Double;
|
||
begin
|
||
Result := False;
|
||
if IsNan(Hz) or IsInfinite(Hz) or (Hz <= 0) then Exit;
|
||
FreqLimits(LoHz, HiHz);
|
||
Result := (Hz >= LoHz) and (Hz <= HiHz);
|
||
end;
|
||
|
||
procedure TTCIAdapter.RefreshSlices;
|
||
// Снимок таблицы слайсов. Зовётся ТОЛЬКО в потоке контроллера (OnState и
|
||
// Invoke при подключении клиента) — там писателей нет, и снимок каждой записи
|
||
// получается согласованным. Сетевые потоки читают уже его: прямое чтение
|
||
// FSlices из них давало смесь старых и новых полей одного слайса (частота
|
||
// новая, мода ещё старая), даже когда managed-строк это не касалось.
|
||
var
|
||
i, Id: Integer;
|
||
Tmp: array[0..MAX_SLICES-1] of TTCISliceSnap;
|
||
V: TSliceView;
|
||
begin
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
begin
|
||
Tmp[i].Used := False;
|
||
FillChar(Tmp[i].V, SizeOf(Tmp[i].V), 0);
|
||
Id := FController.SliceIdBySlot(i); // порядок слотов = порядок каналов
|
||
if (Id > 0) and FController.GetSliceView(Id, V) then
|
||
begin
|
||
Tmp[i].Used := True;
|
||
Tmp[i].V := V;
|
||
end;
|
||
end;
|
||
FSliceLock.Enter;
|
||
try
|
||
for i := 0 to MAX_SLICES - 1 do FSliceSnap[i] := Tmp[i];
|
||
finally
|
||
FSliceLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
function TTCIAdapter.SliceMapSig: string;
|
||
// Слепок расстановки: id и пан каждого слайса по порядку слотов. Меняется и
|
||
// когда слайс создали или удалили, и когда из-за удаления первого второй стал
|
||
// каналом A.
|
||
var i: Integer;
|
||
begin
|
||
Result := '';
|
||
FSliceLock.Enter;
|
||
try
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSliceSnap[i].Used then
|
||
Result := Result + IntToStr(FSliceSnap[i].V.Id) + ':' +
|
||
IntToStr(FSliceSnap[i].V.PanId) + ';';
|
||
finally
|
||
FSliceLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
procedure TTCIAdapter.PushChannelMap;
|
||
// Каналы доп. приёмников появились, исчезли или перенумеровались. Об исчезнувшем
|
||
// канале протокол сказать почти ничего не даёт: команды «приёмника больше нет» в
|
||
// TCI 2.0 нет вовсе, а про канал B есть только RX_CHANNEL_ENABLE. Поэтому шлём
|
||
// полную картину каждого живого приёмника — клиент перезаливает её целиком.
|
||
var Rx, Ch, N: Integer;
|
||
begin
|
||
if (FServer = nil) or (FServer.ClientCount = 0) then Exit;
|
||
for Rx := 1 to RxCount - 1 do
|
||
begin
|
||
if not RxActive(Rx) then Continue;
|
||
N := ChanCount(Rx);
|
||
for Ch := 0 to N - 1 do BroadcastRxState(Rx, Ch);
|
||
FServer.Broadcast(TCIBuild('rx_channel_enable',
|
||
[TCIIntStr(Rx), '1', TCIBoolStr(N > 1)]));
|
||
end;
|
||
end;
|
||
|
||
function TTCIAdapter.SnapSlice(Rx, Ch: Integer; out V: TSliceView): Boolean;
|
||
// Слайс канала Ch приёмника Rx из снимка. Для Rx=0 слайса нет: главный тракт
|
||
// живёт в полях контроллера.
|
||
var i, Seen: Integer;
|
||
begin
|
||
Result := False;
|
||
FillChar(V, SizeOf(V), 0);
|
||
if (Rx <= 0) or (Ch < 0) then Exit;
|
||
Seen := 0;
|
||
FSliceLock.Enter;
|
||
try
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSliceSnap[i].Used and (FSliceSnap[i].V.PanId = Rx) then
|
||
begin
|
||
if Seen = Ch then
|
||
begin
|
||
V := FSliceSnap[i].V;
|
||
Exit(True);
|
||
end;
|
||
Inc(Seen);
|
||
end;
|
||
finally
|
||
FSliceLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
function TTCIAdapter.SliceIdOf(Rx, Ch: Integer): Integer;
|
||
// Слайсы пана Rx по порядку в таблице: 0-й = канал A, 1-й = канал B.
|
||
var V: TSliceView;
|
||
begin
|
||
if SnapSlice(Rx, Ch, V) then Result := V.Id else Result := 0;
|
||
end;
|
||
|
||
function TTCIAdapter.SliceIdLive(Rx, Ch: Integer): Integer;
|
||
// То же, но из живой таблицы. Только для Sync-методов: они идут в потоке
|
||
// контроллера, где чтение безопасно, а снимок мог бы отстать на такт — команда
|
||
// на установку обязана попасть в тот слайс, который есть сейчас.
|
||
begin
|
||
if Rx <= 0 then Result := 0
|
||
else Result := FController.PanSliceId(Rx, Ch);
|
||
end;
|
||
|
||
function TTCIAdapter.RxSlice(Rx: Integer; out S: TSliceView): Boolean;
|
||
// Слайс, представляющий приёмник Rx как целое (канал A).
|
||
begin
|
||
Result := SnapSlice(Rx, 0, S);
|
||
end;
|
||
|
||
function TTCIAdapter.SliceRxCh(Id: Integer; out Rx, Ch: Integer): Boolean;
|
||
// Обратное отображение слайс → (приёмник, канал). Нужно уведомлениям: контроллер
|
||
// сообщает об изменении Id, а клиенту адресуются номера TCI.
|
||
var i, Seen, Pan: Integer;
|
||
begin
|
||
Result := False;
|
||
Rx := 0; Ch := 0;
|
||
if Id <= 0 then Exit;
|
||
Pan := -1;
|
||
FSliceLock.Enter;
|
||
try
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSliceSnap[i].Used and (FSliceSnap[i].V.Id = Id) then
|
||
begin
|
||
Pan := FSliceSnap[i].V.PanId;
|
||
Break;
|
||
end;
|
||
// Пан 0 в модели TCI — это VFO A/B главного приёмника, а не его слайсы:
|
||
// слайсу на главном пане в протоколе места нет.
|
||
if (Pan <= 0) or not ValidRx(Pan) then Exit;
|
||
Seen := 0;
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSliceSnap[i].Used and (FSliceSnap[i].V.PanId = Pan) then
|
||
begin
|
||
if FSliceSnap[i].V.Id = Id then
|
||
begin
|
||
Rx := Pan;
|
||
Ch := Seen;
|
||
Exit(Seen < TCI_CHANNELS);
|
||
end;
|
||
Inc(Seen);
|
||
end;
|
||
finally
|
||
FSliceLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
function TTCIAdapter.HoldKey(const Name: string; Rx, Ch: Integer): string;
|
||
// Ключ захвата: параметр + адресат. VFO и IF — одно и то же значение с разных
|
||
// сторон, поэтому у них общее имя.
|
||
begin
|
||
Result := Name + '/' + IntToStr(Rx) + '/' + IntToStr(Ch);
|
||
end;
|
||
|
||
function TTCIAdapter.Claim(const Key: string; Owner: TTCIClient): Boolean;
|
||
// §3.5: захвативший параметр держит его 200 мс после последнего изменения.
|
||
// Owner = nil — изменение от оператора/CAT: оно тоже захватывает параметр, но
|
||
// НЕ отбирает его у клиента, который прямо сейчас им управляет.
|
||
var
|
||
i, Free_: Integer;
|
||
Now_: QWord;
|
||
begin
|
||
Now_ := GetTickCount64;
|
||
FHoldLock.Enter;
|
||
try
|
||
Free_ := -1;
|
||
for i := 0 to FHoldCount - 1 do
|
||
begin
|
||
if FHolds[i].Key = Key then
|
||
begin
|
||
if (FHolds[i].Owner <> Owner) and (Now_ - FHolds[i].At < TCI_HOLD_MS) then
|
||
Exit(False);
|
||
FHolds[i].Owner := Owner;
|
||
FHolds[i].At := Now_;
|
||
Exit(True);
|
||
end;
|
||
// Заодно подбираем протухшую запись под переиспользование.
|
||
if (Free_ < 0) and (Now_ - FHolds[i].At >= TCI_HOLD_MS) then Free_ := i;
|
||
end;
|
||
if Free_ < 0 then
|
||
begin
|
||
if FHoldCount >= TCI_HOLD_SLOTS then Exit(True); // мест нет — не мешаем
|
||
Free_ := FHoldCount;
|
||
Inc(FHoldCount);
|
||
end;
|
||
FHolds[Free_].Key := Key;
|
||
FHolds[Free_].Owner := Owner;
|
||
FHolds[Free_].At := Now_;
|
||
Result := True;
|
||
finally
|
||
FHoldLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
procedure TTCIAdapter.DropHolds(Owner: TTCIClient);
|
||
// Клиент отключился: его захваты снимаем сразу. Указатели мы только сравниваем
|
||
// (разыменовывать нечего), но освободившийся адрес мог бы достаться новому
|
||
// клиенту — и тот получил бы чужие захваты в наследство.
|
||
var i: Integer;
|
||
begin
|
||
if Owner = nil then Exit;
|
||
FHoldLock.Enter;
|
||
try
|
||
for i := 0 to FHoldCount - 1 do
|
||
if FHolds[i].Owner = Owner then
|
||
begin
|
||
FHolds[i].Key := '';
|
||
FHolds[i].Owner := nil;
|
||
FHolds[i].At := 0;
|
||
end;
|
||
finally
|
||
FHoldLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
function TTCIAdapter.ChanCount(Rx: Integer): Integer;
|
||
var i: Integer;
|
||
begin
|
||
if Rx = 0 then begin Result := 2; Exit; end; // VFO A/B всегда есть
|
||
Result := 0;
|
||
FSliceLock.Enter;
|
||
try
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSliceSnap[i].Used and (FSliceSnap[i].V.PanId = Rx) then Inc(Result);
|
||
finally
|
||
FSliceLock.Leave;
|
||
end;
|
||
if Result > TCI_CHANNELS then Result := TCI_CHANNELS;
|
||
// Канал A в TCI выключить нечем: он есть у приёмника всегда. Пока пан жив, но
|
||
// слайсов на нём не осталось, показываем канал A на центре пана — иначе после
|
||
// удаления последнего слайса клиент навсегда остался бы с его частотой.
|
||
if (Result = 0) and RxActive(Rx) then Result := 1;
|
||
end;
|
||
|
||
function TTCIAdapter.ChanFreq(Rx, Ch: Integer): Double;
|
||
var S: TSliceView;
|
||
begin
|
||
Result := 0;
|
||
if Rx = 0 then
|
||
begin
|
||
if Ch = 1 then Result := FController.FVfoB else Result := FController.FVfoA;
|
||
Exit;
|
||
end;
|
||
if SnapSlice(Rx, Ch, S) then Exit(S.TargetHz);
|
||
// Слайса нет: у живого пана канал A стоит на его центре (см. ChanCount).
|
||
if (Ch = 0) and RxActive(Rx) then Result := RxCenterHz(Rx);
|
||
end;
|
||
|
||
function TTCIAdapter.RxCenterHz(Rx: Integer): Double;
|
||
begin
|
||
if Rx = 0 then Result := FController.FCenterFreq
|
||
else Result := FController.PanDDCFreq(Rx);
|
||
end;
|
||
|
||
function TTCIAdapter.RxMode(Rx: Integer): Integer;
|
||
var S: TSliceView;
|
||
begin
|
||
Result := FController.FMode;
|
||
if Rx = 0 then Exit;
|
||
if SnapSlice(Rx, 0, S) then Result := S.Mode;
|
||
end;
|
||
|
||
procedure TTCIAdapter.RxFilter(Rx: Integer; out Lo, Hi: Integer);
|
||
var S: TSliceView;
|
||
begin
|
||
Lo := FController.FilterLo;
|
||
Hi := FController.FilterHi;
|
||
if Rx = 0 then Exit;
|
||
if SnapSlice(Rx, 0, S) then
|
||
begin
|
||
Lo := S.FilterLo;
|
||
Hi := S.FilterHi;
|
||
end;
|
||
end;
|
||
|
||
function TTCIAdapter.RxMuted(Rx: Integer): Boolean;
|
||
var S: TSliceView;
|
||
begin
|
||
if Rx = 0 then begin Result := FController.FMuted; Exit; end;
|
||
Result := SnapSlice(Rx, 0, S) and S.Muted;
|
||
end;
|
||
|
||
function TTCIAdapter.RxVolumeDb(Rx, Ch: Integer): Double;
|
||
// Громкость в TCI — величина канальная: у доп. пана второй слайс звучит своей.
|
||
var S: TSliceView;
|
||
begin
|
||
if Rx = 0 then begin Result := TCIVolumeToDb(FController.FVolume); Exit; end;
|
||
Result := TCI_VOL_MIN_DB;
|
||
if SnapSlice(Rx, Ch, S) then Result := TCIVolumeToDb(Round(S.Volume * 100));
|
||
end;
|
||
|
||
function TTCIAdapter.RxAGCUi(Rx: Integer): Integer;
|
||
var S: TSliceView;
|
||
begin
|
||
Result := FController.FAGCMode;
|
||
if Rx = 0 then Exit;
|
||
if SnapSlice(Rx, 0, S) then Result := TRadioController.AGCModeToUI(S.AGC);
|
||
end;
|
||
|
||
// DSP и шумоподавитель: у доп. приёмника своё состояние (зеркало в TCtrlSlice).
|
||
// Читать тут глобальные поля главного тракта нельзя — клиент увидел бы чужие
|
||
// переключатели и, что хуже, записал бы их обратно.
|
||
function TTCIAdapter.RxNROn(Rx: Integer): Boolean;
|
||
var S: TSliceView;
|
||
begin
|
||
if Rx = 0 then Result := FController.FNRMode > 0
|
||
else Result := RxSlice(Rx, S) and (S.NRMode > 0);
|
||
end;
|
||
|
||
function TTCIAdapter.RxNBOn(Rx: Integer): Boolean;
|
||
var S: TSliceView;
|
||
begin
|
||
if Rx = 0 then Result := FController.FNBMode > 0
|
||
else Result := RxSlice(Rx, S) and (S.NBMode > 0);
|
||
end;
|
||
|
||
function TTCIAdapter.RxANFOn(Rx: Integer): Boolean;
|
||
var S: TSliceView;
|
||
begin
|
||
if Rx = 0 then Result := FController.FANF
|
||
else Result := RxSlice(Rx, S) and S.ANF;
|
||
end;
|
||
|
||
function TTCIAdapter.RxSqlOn(Rx: Integer): Boolean;
|
||
var S: TSliceView;
|
||
begin
|
||
if Rx = 0 then Result := FController.FFMSQOn
|
||
else Result := RxSlice(Rx, S) and S.FMSQOn;
|
||
end;
|
||
|
||
function TTCIAdapter.RxSqlLevel(Rx: Integer): Integer;
|
||
var S: TSliceView;
|
||
begin
|
||
if Rx = 0 then begin Result := FController.FFMSQLevel; Exit; end;
|
||
if RxSlice(Rx, S) then Result := S.FMSQLevel else Result := 0;
|
||
end;
|
||
|
||
function TTCIAdapter.RxSMeterDbm(Rx, Ch: Integer): Double;
|
||
var Id: Integer;
|
||
begin
|
||
if Rx = 0 then
|
||
begin
|
||
Result := FController.ReadSMeterDBm;
|
||
Exit;
|
||
end;
|
||
Result := -140;
|
||
Id := SliceIdOf(Rx, Ch);
|
||
if Id > 0 then Result := FController.SliceSMeter(Id);
|
||
end;
|
||
|
||
function TTCIAdapter.TxEnabled: Boolean;
|
||
begin
|
||
Result := DevSnap.HasTX and (not FController.TXProhibited);
|
||
end;
|
||
|
||
{ ═══════════════════════════════════════════════════════════════════════════
|
||
Формирование строк состояния
|
||
═══════════════════════════════════════════════════════════════════════════ }
|
||
|
||
function TTCIAdapter.StrVfo(Rx, Ch: Integer): string;
|
||
begin
|
||
Result := TCIBuild('vfo', [TCIIntStr(Rx), TCIIntStr(Ch),
|
||
TCIIntStr(Round(ChanFreq(Rx, Ch)))]);
|
||
end;
|
||
|
||
function TTCIAdapter.StrIf(Rx, Ch: Integer): string;
|
||
begin
|
||
Result := TCIBuild('if', [TCIIntStr(Rx), TCIIntStr(Ch),
|
||
TCIIntStr(Round(ChanFreq(Rx, Ch) - RxCenterHz(Rx)))]);
|
||
end;
|
||
|
||
function TTCIAdapter.StrDds(Rx: Integer): string;
|
||
begin
|
||
Result := TCIBuild('dds', [TCIIntStr(Rx), TCIIntStr(Round(RxCenterHz(Rx)))]);
|
||
end;
|
||
|
||
function TTCIAdapter.StrModulation(Rx: Integer): string;
|
||
begin
|
||
Result := TCIBuild('modulation', [TCIIntStr(Rx), TCIModeName(RxMode(Rx))]);
|
||
end;
|
||
|
||
function TTCIAdapter.StrFilterBand(Rx: Integer): string;
|
||
var Lo, Hi: Integer;
|
||
begin
|
||
RxFilter(Rx, Lo, Hi);
|
||
Result := TCIBuild('rx_filter_band', [TCIIntStr(Rx), TCIIntStr(Lo), TCIIntStr(Hi)]);
|
||
end;
|
||
|
||
function TTCIAdapter.StrTrx: string;
|
||
begin
|
||
Result := TCIBuild('trx', ['0', TCIBoolStr(FController.FTransmitting)]);
|
||
end;
|
||
|
||
function TTCIAdapter.StrTune: string;
|
||
begin
|
||
Result := TCIBuild('tune', ['0', TCIBoolStr(FController.FTuning)]);
|
||
end;
|
||
|
||
function TTCIAdapter.StrDrive: string;
|
||
begin
|
||
Result := TCIBuild('drive', ['0', TCIIntStr(FController.FDrivePercent)]);
|
||
end;
|
||
|
||
function TTCIAdapter.StrVolume: string;
|
||
begin
|
||
Result := TCIBuild('volume', [TCIIntStr(Round(TCIVolumeToDb(FController.FVolume)))]);
|
||
end;
|
||
|
||
function TTCIAdapter.StrMute: string;
|
||
begin
|
||
Result := TCIBuild('mute', [TCIBoolStr(FController.FMuted)]);
|
||
end;
|
||
|
||
function TTCIAdapter.StrAGCMode(Rx: Integer): string;
|
||
var Ui: Integer; Name: string;
|
||
begin
|
||
Ui := RxAGCUi(Rx);
|
||
// UI: 0=Fast 1=Medium 2=Slow 3=Long 4=Off; TCI знает normal/fast/off.
|
||
if Ui = 4 then Name := 'off'
|
||
else if Ui = 0 then Name := 'fast'
|
||
else Name := 'normal';
|
||
Result := TCIBuild('agc_mode', [TCIIntStr(Rx), Name]);
|
||
end;
|
||
|
||
function TTCIAdapter.StrAGCGain(Rx: Integer): string;
|
||
begin
|
||
Result := TCIBuild('agc_gain', [TCIIntStr(Rx), TCIIntStr(FController.FAGCTop)]);
|
||
end;
|
||
|
||
function TTCIAdapter.StrLock(Rx: Integer): string;
|
||
begin
|
||
Result := TCIBuild('lock', [TCIIntStr(Rx), TCIBoolStr(FController.FVfoLock)]);
|
||
end;
|
||
|
||
function TTCIAdapter.StrSqlEnable(Rx: Integer): string;
|
||
begin
|
||
Result := TCIBuild('sql_enable', [TCIIntStr(Rx), TCIBoolStr(RxSqlOn(Rx))]);
|
||
end;
|
||
|
||
function TTCIAdapter.StrSqlLevel(Rx: Integer): string;
|
||
begin
|
||
Result := TCIBuild('sql_level', [TCIIntStr(Rx),
|
||
TCIIntStr(Round(TCILevelToSql(RxSqlLevel(Rx))))]);
|
||
end;
|
||
|
||
function TTCIAdapter.StrTxEnable(Rx: Integer): string;
|
||
begin
|
||
Result := TCIBuild('tx_enable', [TCIIntStr(Rx), TCIBoolStr(TxEnabled)]);
|
||
end;
|
||
|
||
function TTCIAdapter.StrRxVolume(Rx, Ch: Integer): string;
|
||
begin
|
||
Result := TCIBuild('rx_volume', [TCIIntStr(Rx), TCIIntStr(Ch),
|
||
TCIIntStr(Round(RxVolumeDb(Rx, Ch)))]);
|
||
end;
|
||
|
||
function TTCIAdapter.StrNR(Rx: Integer): string;
|
||
begin
|
||
Result := TCIBuild('rx_nr_enable', [TCIIntStr(Rx), TCIBoolStr(RxNROn(Rx))]);
|
||
end;
|
||
|
||
function TTCIAdapter.StrNB(Rx: Integer): string;
|
||
begin
|
||
Result := TCIBuild('rx_nb_enable', [TCIIntStr(Rx), TCIBoolStr(RxNBOn(Rx))]);
|
||
end;
|
||
|
||
function TTCIAdapter.StrANF(Rx: Integer): string;
|
||
begin
|
||
Result := TCIBuild('rx_anf_enable', [TCIIntStr(Rx), TCIBoolStr(RxANFOn(Rx))]);
|
||
end;
|
||
|
||
procedure TTCIAdapter.BroadcastRxState(Rx, Ch: Integer);
|
||
// Слайс перенастроили (кто угодно: TCI, CAT, оператор мышью) — синхронизируем
|
||
// всех клиентов. Канал B доп. пана в протоколе несёт только частоту и
|
||
// громкость: вид связи, фильтр, АРУ и шумодавы в TCI — свойства приёмника
|
||
// целиком, и относятся к каналу A.
|
||
begin
|
||
if (FServer = nil) or (FServer.ClientCount = 0) then Exit;
|
||
FServer.Broadcast(StrVfo(Rx, Ch));
|
||
FServer.Broadcast(StrIf(Rx, Ch));
|
||
FServer.Broadcast(StrRxVolume(Rx, Ch));
|
||
if Ch <> 0 then Exit;
|
||
FServer.Broadcast(StrModulation(Rx));
|
||
FServer.Broadcast(StrFilterBand(Rx));
|
||
FServer.Broadcast(StrAGCMode(Rx));
|
||
FServer.Broadcast(TCIBuild('rx_mute', [TCIIntStr(Rx), TCIBoolStr(RxMuted(Rx))]));
|
||
FServer.Broadcast(StrNR(Rx));
|
||
FServer.Broadcast(StrNB(Rx));
|
||
FServer.Broadcast(StrANF(Rx));
|
||
FServer.Broadcast(StrSqlEnable(Rx));
|
||
FServer.Broadcast(StrSqlLevel(Rx));
|
||
end;
|
||
|
||
{ ═══════════════════════════════════════════════════════════════════════════
|
||
Подключение клиента: инициализация + текущее состояние
|
||
═══════════════════════════════════════════════════════════════════════════ }
|
||
|
||
procedure TTCIAdapter.Reply(Client: TTCIClient; const S: string);
|
||
begin
|
||
if (Client <> nil) and (S <> '') then Client.Send(S);
|
||
end;
|
||
|
||
procedure TTCIAdapter.SendInit(Client: TTCIClient);
|
||
// Исполняется в потоке клиента, поэтому всё «железное» берётся из снимка
|
||
// (DevSnap), а не из контроллера: BackendCaps и BoardDisplayName смотрят в
|
||
// FNetwork, который UI освобождает на смене устройства.
|
||
var
|
||
D: TTCIDevSnap;
|
||
Half: Integer;
|
||
DevName: string;
|
||
begin
|
||
D := DevSnap;
|
||
|
||
Half := D.SampleRate div 2;
|
||
if Half <= 0 then Half := 48000;
|
||
|
||
DevName := Trim(D.DevName);
|
||
if DevName = '' then DevName := TCI_APP_NAME;
|
||
|
||
Reply(Client, TCIBuild('protocol', [TCI_APP_NAME, TCI_VERSION]));
|
||
Reply(Client, TCIBuild('device', [DevName]));
|
||
Reply(Client, TCIBuild('receive_only', [TCIBoolStr(not D.HasTX)]));
|
||
Reply(Client, TCIBuild('trx_count', [TCIIntStr(RxCount)]));
|
||
Reply(Client, TCIBuild('channel_count', [TCIIntStr(TCI_CHANNELS)]));
|
||
// Границы — из того же снимка, что и проверка частоты в командах (§2.1).
|
||
Reply(Client, TCIBuild('vfo_limits',
|
||
[TCIIntStr(Round(D.LimLoHz)), TCIIntStr(Round(D.LimHiHz))]));
|
||
Reply(Client, TCIBuild('if_limits', [TCIIntStr(-Half), TCIIntStr(Half)]));
|
||
Reply(Client, TCIBuild('modulations_list', [TCI_MODULATIONS]));
|
||
end;
|
||
|
||
procedure TTCIAdapter.SendState(Client: TTCIClient);
|
||
var
|
||
Rx, Ch, Chans: Integer;
|
||
E: TTCIRxEcho;
|
||
begin
|
||
for Rx := 0 to RxCount - 1 do
|
||
begin
|
||
// Приёмника ещё нет (пан не создан): TRX_COUNT объявляет потолок железа, а
|
||
// не число живых панов, и раньше на такой номер уходили dds/vfo/if с нулём
|
||
// — клиент принимал ноль за настоящую частоту. Молчим до появления пана:
|
||
// он придёт с rfPanFreq/rfSliceState.
|
||
if not RxActive(Rx) then Continue;
|
||
|
||
FEchoLock.Enter;
|
||
try E := FEcho[Rx]; finally FEchoLock.Leave; end;
|
||
|
||
Reply(Client, StrDds(Rx));
|
||
// Только реально существующие каналы: объявив пану второй канал, которого
|
||
// нет, мы отдали бы клиенту vfo:rx,1,0 — и он принял бы ноль за частоту.
|
||
// У пана без слайсов каналов нет вовсе — тогда только dds.
|
||
Chans := ChanCount(Rx);
|
||
for Ch := 0 to Chans - 1 do
|
||
begin
|
||
Reply(Client, StrVfo(Rx, Ch));
|
||
Reply(Client, StrIf(Rx, Ch));
|
||
Reply(Client, StrRxVolume(Rx, Ch));
|
||
Reply(Client, TCIBuild('rx_balance', [TCIIntStr(Rx), TCIIntStr(Ch),
|
||
TCIIntStr(E.BalanceDb[Ch])]));
|
||
// VFO_LOCK — уведомление поканальное (§4.5): без него клиент, вошедший
|
||
// на запертой ручке, о запрете не знает.
|
||
Reply(Client, TCIBuild('vfo_lock', [TCIIntStr(Rx), TCIIntStr(Ch),
|
||
TCIBoolStr(FController.FVfoLock)]));
|
||
end;
|
||
Reply(Client, TCIBuild('rx_channel_enable',
|
||
[TCIIntStr(Rx), '1', TCIBoolStr(Chans > 1)]));
|
||
Reply(Client, StrModulation(Rx));
|
||
Reply(Client, StrFilterBand(Rx));
|
||
Reply(Client, StrAGCMode(Rx));
|
||
Reply(Client, StrAGCGain(Rx));
|
||
Reply(Client, TCIBuild('rx_mute', [TCIIntStr(Rx), TCIBoolStr(RxMuted(Rx))]));
|
||
Reply(Client, StrNB(Rx));
|
||
Reply(Client, TCIBuild('rx_nb_param', [TCIIntStr(Rx),
|
||
TCIIntStr(E.NBThreshold), TCIIntStr(E.NBDuration)]));
|
||
Reply(Client, StrNR(Rx));
|
||
Reply(Client, StrANF(Rx));
|
||
Reply(Client, TCIBuild('rx_bin_enable', [TCIIntStr(Rx), TCIBoolStr(E.BinOn)]));
|
||
Reply(Client, TCIBuild('rx_anc_enable', [TCIIntStr(Rx), TCIBoolStr(E.ANCOn)]));
|
||
Reply(Client, TCIBuild('rx_apf_enable', [TCIIntStr(Rx), TCIBoolStr(E.APFOn)]));
|
||
Reply(Client, TCIBuild('rx_dse_enable', [TCIIntStr(Rx), TCIBoolStr(E.DSEOn)]));
|
||
Reply(Client, TCIBuild('rx_nf_enable', [TCIIntStr(Rx), TCIBoolStr(E.NFOn)]));
|
||
Reply(Client, StrLock(Rx));
|
||
Reply(Client, StrSqlEnable(Rx));
|
||
Reply(Client, StrSqlLevel(Rx));
|
||
Reply(Client, TCIBuild('rit_enable', [TCIIntStr(Rx), TCIBoolStr(E.RitOn)]));
|
||
Reply(Client, TCIBuild('rit_offset', [TCIIntStr(Rx), TCIIntStr(E.RitHz)]));
|
||
Reply(Client, TCIBuild('xit_enable', [TCIIntStr(Rx), TCIBoolStr(E.XitOn)]));
|
||
Reply(Client, TCIBuild('xit_offset', [TCIIntStr(Rx), TCIIntStr(E.XitHz)]));
|
||
Reply(Client, StrTxEnable(Rx));
|
||
end;
|
||
|
||
Reply(Client, TCIBuild('split_enable', ['0', TCIBoolStr(FController.FSplitTxB)]));
|
||
Reply(Client, StrTrx);
|
||
Reply(Client, StrTune);
|
||
Reply(Client, StrDrive);
|
||
Reply(Client, TCIBuild('tune_drive', ['0', TCIIntStr(FController.FTXSettings.TUNLevel)]));
|
||
Reply(Client, StrVolume);
|
||
Reply(Client, StrMute);
|
||
Reply(Client, TCIBuild('mon_volume', [TCIIntStr(Round(TCIVolumeToDb(FController.FTxMonVolume)))]));
|
||
Reply(Client, TCIBuild('mon_enable', [TCIBoolStr(not FController.FRxMuteOnTx)]));
|
||
Reply(Client, TCIBuild('cw_macros_speed', [TCIIntStr(FController.FCWSettings.Speed)]));
|
||
Reply(Client, TCIBuild('cw_macros_delay', [TCIIntStr(FController.FCWSettings.RFDelayMS)]));
|
||
FEchoLock.Enter;
|
||
try
|
||
Reply(Client, TCIBuild('digl_offset', [TCIIntStr(FDiglOffset)]));
|
||
Reply(Client, TCIBuild('digu_offset', [TCIIntStr(FDiguOffset)]));
|
||
finally
|
||
FEchoLock.Leave;
|
||
end;
|
||
Reply(Client, TCIBuild('iq_samplerate', [TCIIntStr(Client.IQRate)]));
|
||
Reply(Client, TCIBuild('audio_samplerate', [TCIIntStr(Client.AudioRate)]));
|
||
// Частота передачи и фокус окна — состояние, а не только событие: на
|
||
// стабильном радио TX_FREQUENCY не придёт ещё очень долго (уведомление шлётся
|
||
// по изменению), а логгеру она нужна сразу — особенно в split и на TX-слайсе.
|
||
Reply(Client, TCIBuild('tx_frequency',
|
||
[TCIIntStr(Round(FController.ActiveTXFreqHz))]));
|
||
Reply(Client, TCIBuild('app_focus', [TCIBoolStr(FAppFocus)]));
|
||
if FController.FRunning then Reply(Client, TCIBuild('start'))
|
||
else Reply(Client, TCIBuild('stop'));
|
||
end;
|
||
|
||
procedure TTCIAdapter.HandleConnect(Client: TTCIClient);
|
||
// Зовётся сервером под FClientLock: рассылка ждёт, пока пачка не уложена в
|
||
// очередь целиком, поэтому изменение, случившееся посреди дампа, приходит
|
||
// ПОСЛЕ него, а не теряется. Отсюда запрет: никаких Invoke в поток контроллера
|
||
// — он сам может стоять на этом локе внутри Broadcast.
|
||
begin
|
||
// Не отдать READY молча нельзя: клиент, ждущий его, повиснет навсегда, —
|
||
// поэтому дамп состояния под защитой, а READY уходит в любом случае.
|
||
try
|
||
SendInit(Client);
|
||
SendState(Client);
|
||
except
|
||
on E: Exception do
|
||
Reply(Client, TCIBuild('tci_error', ['init', TCIEscape(E.Message)]));
|
||
end;
|
||
Reply(Client, TCIBuild('ready'));
|
||
Client.Ready := True;
|
||
end;
|
||
|
||
procedure TTCIAdapter.HandleDisconnect(Client: TTCIClient);
|
||
// Клиент ушёл — снимаем его захваты параметров, иначе следующий, кому достанется
|
||
// тот же адрес объекта, унаследовал бы чужие права (§3.5).
|
||
begin
|
||
DropHolds(Client);
|
||
end;
|
||
|
||
{ ═══════════════════════════════════════════════════════════════════════════
|
||
Sync-методы: исполняются в потоке контроллера (через Invoke)
|
||
═══════════════════════════════════════════════════════════════════════════ }
|
||
|
||
procedure TTCIAdapter.SyncSetVfo;
|
||
var Id: Integer;
|
||
begin
|
||
// Окончательная проверка — здесь: см. FreqSaneLive.
|
||
if not FreqSaneLive(FsFreq) then Exit;
|
||
if FsInt = 0 then
|
||
begin
|
||
if FsInt2 = 1 then FController.SetVfoB(FsFreq) else FController.SetVfoA(FsFreq);
|
||
Exit;
|
||
end;
|
||
Id := SliceIdLive(FsInt, FsInt2);
|
||
// Тот же путь, что у CAT-порта слайса: внутри включённого диапазона слайс
|
||
// ходит свободно, за захваченную полосу окно DDC переедет само, за границы
|
||
// диапазона команда отбрасывается. Прямой SetSliceTarget (как было) уводил
|
||
// слайс куда угодно, а для TX-слайса эта частота идёт прямо в DUC — то есть
|
||
// в эфир на чужом диапазоне, без переключения антенн и фильтров.
|
||
// SliceFreqChanged шлёт сам SetSliceTarget — и для нас, и для UI, и для CAT.
|
||
if Id > 0 then FController.TuneSliceInBand(Id, FsFreq);
|
||
end;
|
||
|
||
procedure TTCIAdapter.SyncSetCenter;
|
||
begin
|
||
// Окончательная проверка — здесь: см. FreqSaneLive. Ни SetCenter, ни
|
||
// SetPanDDCFreq границ не клампят, число уходит прямо в backend.
|
||
if not FreqSaneLive(FsFreq) then Exit;
|
||
if FsInt = 0 then FController.SetCenter(FsFreq)
|
||
else FController.SetPanDDCFreq(FsInt, FsFreq);
|
||
end;
|
||
|
||
procedure TTCIAdapter.SyncSetMode;
|
||
var Id: Integer;
|
||
begin
|
||
if FsInt = 0 then begin FController.SetMode(FsInt2); Exit; end;
|
||
Id := SliceIdLive(FsInt, 0);
|
||
if Id > 0 then FController.SetSliceMode(Id, FsInt2);
|
||
end;
|
||
|
||
procedure TTCIAdapter.SyncSetFilter;
|
||
var Id: Integer;
|
||
begin
|
||
if FsInt = 0 then begin FController.SetFilterEdges(FsInt2, FsInt3); Exit; end;
|
||
Id := SliceIdLive(FsInt, 0);
|
||
if Id > 0 then FController.SetSliceFilter(Id, FsInt2, FsInt3);
|
||
end;
|
||
|
||
procedure TTCIAdapter.SyncSetTRX;
|
||
begin
|
||
FController.SetMOX(FsBool);
|
||
end;
|
||
|
||
procedure TTCIAdapter.SyncSetTune;
|
||
begin
|
||
FController.SetTune(FsBool);
|
||
end;
|
||
|
||
procedure TTCIAdapter.SyncSetDrive;
|
||
begin
|
||
FController.SetDrive(FsInt);
|
||
end;
|
||
|
||
procedure TTCIAdapter.SyncSetTuneDrive;
|
||
var T: TTXSettings;
|
||
begin
|
||
T := FController.FTXSettings;
|
||
T.TUNLevel := FsInt;
|
||
FController.SetTXSettings(T);
|
||
end;
|
||
|
||
procedure TTCIAdapter.SyncSetSplit;
|
||
begin
|
||
FController.SetSplit(FsBool);
|
||
end;
|
||
|
||
procedure TTCIAdapter.SyncSetVolume;
|
||
// Адресно: SetVolume правит АКТИВНУЮ громкость, и на передаче с самоконтролем
|
||
// команда VOLUME уехала бы в громкость монитора, а в ответ клиент получал бы
|
||
// нетронутый FVolume. У TCI это разные команды — VOLUME и MON_VOLUME.
|
||
begin
|
||
FController.SetRxVolume(FsInt);
|
||
end;
|
||
|
||
procedure TTCIAdapter.SyncSetMute;
|
||
begin
|
||
FController.SetMute(FsBool);
|
||
end;
|
||
|
||
procedure TTCIAdapter.SyncSetRxMute;
|
||
var Id: Integer;
|
||
begin
|
||
if FsInt = 0 then begin FController.SetMute(FsBool); Exit; end;
|
||
Id := SliceIdLive(FsInt, 0);
|
||
if Id > 0 then FController.SetSliceMute(Id, FsBool);
|
||
end;
|
||
|
||
procedure TTCIAdapter.SyncSetRxVolume;
|
||
var Id: Integer;
|
||
begin
|
||
// Тоже адресно (см. SyncSetVolume): RX_VOLUME — про приём, не про монитор.
|
||
if FsInt = 0 then begin FController.SetRxVolume(FsInt2); Exit; end;
|
||
Id := SliceIdLive(FsInt, FsInt3); // FsInt3 — канал: у слайсов громкость своя
|
||
if Id > 0 then FController.SetSliceVolume(Id, FsInt2 / 100.0);
|
||
end;
|
||
|
||
procedure TTCIAdapter.SyncSetMonVolume;
|
||
begin
|
||
FController.SetTXMonVolume(FsInt);
|
||
end;
|
||
|
||
procedure TTCIAdapter.SyncSetMonEnable;
|
||
begin
|
||
// Самоконтроль на передаче = RX-аудио НЕ глушится (кнопка RX MUTE).
|
||
FController.SetRxMuteOnTx(not FsBool);
|
||
end;
|
||
|
||
procedure TTCIAdapter.SyncSetAGCMode;
|
||
var Id: Integer;
|
||
begin
|
||
if FsInt = 0 then begin FController.SetAGCMode(FsInt2); Exit; end;
|
||
Id := SliceIdLive(FsInt, 0);
|
||
if Id > 0 then
|
||
FController.SetSliceAGCMode(Id, TRadioController.AGCModeFromUI(FsInt2));
|
||
end;
|
||
|
||
procedure TTCIAdapter.SyncSetAGCTop;
|
||
begin
|
||
FController.SetAGCTop(FsInt);
|
||
end;
|
||
|
||
// DSP слайса ставится одной командой на все четыре блока, поэтому остальные три
|
||
// берём из САМОГО слайса. Подставлять сюда поля главного тракта (как было)
|
||
// значило бы: включил клиент NR на доп. приёмнике — и заодно переписал ему NB,
|
||
// SNB и ANF значениями главного.
|
||
procedure TTCIAdapter.SyncSetNR;
|
||
var Id: Integer; S: TSliceView;
|
||
begin
|
||
if FsInt = 0 then
|
||
begin
|
||
if FsBool then FController.SetNR(1) else FController.SetNR(0);
|
||
Exit;
|
||
end;
|
||
Id := SliceIdLive(FsInt, 0);
|
||
if (Id > 0) and FController.GetSliceView(Id, S) then
|
||
FController.SetSliceDSP(Id, Ord(FsBool), S.NBMode, S.SNB, S.ANF);
|
||
end;
|
||
|
||
procedure TTCIAdapter.SyncSetNB;
|
||
var Id: Integer; S: TSliceView;
|
||
begin
|
||
if FsInt = 0 then
|
||
begin
|
||
if FsBool then FController.SetNB(1) else FController.SetNB(0);
|
||
Exit;
|
||
end;
|
||
Id := SliceIdLive(FsInt, 0);
|
||
if (Id > 0) and FController.GetSliceView(Id, S) then
|
||
FController.SetSliceDSP(Id, S.NRMode, Ord(FsBool), S.SNB, S.ANF);
|
||
end;
|
||
|
||
procedure TTCIAdapter.SyncSetANF;
|
||
var Id: Integer; S: TSliceView;
|
||
begin
|
||
if FsInt = 0 then begin FController.SetANF(FsBool); Exit; end;
|
||
Id := SliceIdLive(FsInt, 0);
|
||
if (Id > 0) and FController.GetSliceView(Id, S) then
|
||
FController.SetSliceDSP(Id, S.NRMode, S.NBMode, S.SNB, FsBool);
|
||
end;
|
||
|
||
procedure TTCIAdapter.SyncSetLock;
|
||
begin
|
||
FController.SetVfoLock(FsBool);
|
||
end;
|
||
|
||
// Squelch слайса — тоже парный сеттер: второй параметр берём из слайса, а не
|
||
// из главного тракта (иначе правка порога сбрасывала бы включение, и наоборот).
|
||
procedure TTCIAdapter.SyncSetSql;
|
||
var Id: Integer; S: TSliceView;
|
||
begin
|
||
if FsInt = 0 then begin FController.SetFMSquelch(FsBool); Exit; end;
|
||
Id := SliceIdLive(FsInt, 0);
|
||
if (Id > 0) and FController.GetSliceView(Id, S) then
|
||
FController.SetSliceFMSquelch(Id, FsBool, S.FMSQLevel);
|
||
end;
|
||
|
||
procedure TTCIAdapter.SyncSetSqlLevel;
|
||
var Id: Integer; S: TSliceView;
|
||
begin
|
||
if FsInt = 0 then begin FController.SetFMSquelchLevel(FsInt2); Exit; end;
|
||
Id := SliceIdLive(FsInt, 0);
|
||
if (Id > 0) and FController.GetSliceView(Id, S) then
|
||
FController.SetSliceFMSquelch(Id, S.FMSQOn, FsInt2);
|
||
end;
|
||
|
||
procedure TTCIAdapter.SyncSetRun;
|
||
begin
|
||
FController.SetRun(FsBool);
|
||
end;
|
||
|
||
procedure TTCIAdapter.SyncSetCWSpeed;
|
||
var C: TCWSettings;
|
||
begin
|
||
C := FController.CWSettings;
|
||
C.Speed := FsInt;
|
||
FController.SetCWSettings(C);
|
||
end;
|
||
|
||
procedure TTCIAdapter.SyncSetCWDelay;
|
||
var C: TCWSettings;
|
||
begin
|
||
C := FController.CWSettings;
|
||
C.RFDelayMS := FsInt;
|
||
FController.SetCWSettings(C);
|
||
end;
|
||
|
||
procedure TTCIAdapter.SyncCWSend;
|
||
begin
|
||
FController.CWXSend(FsStr);
|
||
end;
|
||
|
||
procedure TTCIAdapter.SyncCWStop;
|
||
begin
|
||
FController.CWXAbort;
|
||
end;
|
||
|
||
procedure TTCIAdapter.SyncFocus;
|
||
// SET_IN_FOCUS: поднять окно программы. Само окно адаптеру недоступно (он
|
||
// равноправный клиент контроллера) — действие ставит UI через OnFocusRequest.
|
||
begin
|
||
if Assigned(FOnFocusRequest) then FOnFocusRequest;
|
||
end;
|
||
|
||
{ ═══════════════════════════════════════════════════════════════════════════
|
||
Разбор команд клиента
|
||
═══════════════════════════════════════════════════════════════════════════ }
|
||
|
||
procedure TTCIAdapter.CmdFreq(Client: TTCIClient; const M: TTCIMessage; IsIF: Boolean);
|
||
// VFO:rx,ch[,hz] и IF:rx,ch[,hz] — разница только в системе отсчёта.
|
||
var
|
||
Rx, Ch: Integer;
|
||
Hz: Double;
|
||
begin
|
||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||
if not TCITryArgInt(M, 1, Ch) then Ch := 0;
|
||
if not ValidRx(Rx) then Exit;
|
||
if (Ch < 0) or (Ch >= TCI_CHANNELS) then Exit;
|
||
|
||
// Частота ставится, только если её удалось разобрать И она годная: «vfo:0,0,abc»
|
||
// раньше превращалось в честный ноль и уводило приёмник на 0 Гц.
|
||
if (M.ArgCount >= 3) and TCITryArgFloat(M, 2, Hz) then
|
||
begin
|
||
if IsIF then Hz := RxCenterHz(Rx) + Hz;
|
||
if FreqSane(Hz) and Claim(HoldKey('VFO', Rx, Ch), Client) then
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
FsInt := Rx; FsInt2 := Ch; FsFreq := Hz;
|
||
if CanInvoke then FController.Invoke(SyncSetVfo);
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
if IsIF then Reply(Client, StrIf(Rx, Ch)) else Reply(Client, StrVfo(Rx, Ch));
|
||
end;
|
||
|
||
procedure TTCIAdapter.CmdCWMacros(const M: TTCIMessage; IsMsg: Boolean);
|
||
// CW_MACROS:trx,текст; CW_MSG:trx,префикс,позывной,суффикс;
|
||
// Разметку TCI приводим к тому, что понимает передатчик текста ewsdr:
|
||
// |ABBR| — слитная передача (у нас такой команды нет) → скобки снимаем;
|
||
// < / > — шаг скорости ±5 wpm (передатчик работает на одной скорости) → снимаем;
|
||
// CALL$N — повтор позывного N раз.
|
||
var
|
||
Text, Prefix, Call, Suffix, Rep: string;
|
||
P, N, i: Integer;
|
||
begin
|
||
if IsMsg then
|
||
begin
|
||
// cw_msg:arg1; — доотправка позывного по ходу передачи. Наш передатчик
|
||
// текста уже отданное не редактирует, поэтому такую форму игнорируем.
|
||
if M.ArgCount < 4 then Exit;
|
||
Prefix := TCIUnescape(TCIArg(M, 1));
|
||
Call := TCIUnescape(TCIArg(M, 2));
|
||
Suffix := TCIUnescape(TCIArg(M, 3));
|
||
if Prefix = '_' then Prefix := '';
|
||
if Suffix = '_' then Suffix := '';
|
||
|
||
P := Pos('$', Call);
|
||
if P > 0 then
|
||
begin
|
||
Rep := Copy(Call, P + 1, MaxInt);
|
||
Call := Copy(Call, 1, P - 1);
|
||
N := StrToIntDef(Trim(Rep), 1);
|
||
if N < 1 then N := 1;
|
||
if N > 5 then N := 5;
|
||
Text := '';
|
||
for i := 1 to N do
|
||
begin
|
||
if Text <> '' then Text := Text + ' ';
|
||
Text := Text + Call;
|
||
end;
|
||
Call := Text;
|
||
end;
|
||
|
||
Text := Trim(Prefix + ' ' + Call + ' ' + Suffix);
|
||
end
|
||
else
|
||
begin
|
||
if M.ArgCount < 2 then Exit;
|
||
Text := TCIUnescape(TCIArg(M, 1));
|
||
end;
|
||
|
||
Text := StringReplace(Text, '|', '', [rfReplaceAll]);
|
||
Text := StringReplace(Text, '<', '', [rfReplaceAll]);
|
||
Text := StringReplace(Text, '>', '', [rfReplaceAll]);
|
||
Text := Trim(Text);
|
||
if Text = '' then Exit;
|
||
|
||
FLock.Enter;
|
||
try
|
||
FsStr := Text;
|
||
if CanInvoke then FController.Invoke(SyncCWSend);
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
|
||
// Позывной ушёл в эфир целиком — подтверждаем финальный вариант (§3.2.2).
|
||
if IsMsg and (Call <> '') then
|
||
FServer.Broadcast(TCIBuild('callsign_send', [Call]));
|
||
end;
|
||
|
||
procedure TTCIAdapter.CmdSpot(const M: TTCIMessage);
|
||
// SPOT:позывной,мода,частота,цвет ARGB,текст;
|
||
var
|
||
S: TDXSpot;
|
||
begin
|
||
if FSpots = nil then Exit;
|
||
if M.ArgCount < 3 then Exit;
|
||
|
||
S.Call := TCIUnescape(TCIArg(M, 0));
|
||
S.FreqHz := TCIArgFloat(M, 2, 0);
|
||
S.Comment := TCIUnescape(TCIArg(M, 4));
|
||
S.Spotter := 'TCI';
|
||
// Поле называется TimeUTC и рисуется рядом со спотами кластера, которые
|
||
// приходят в UTC: местное время сдвигало бы подпись на часовой пояс.
|
||
// Stamp — наоборот, местное: по нему считается возраст спота (TTL).
|
||
S.TimeUTC := FormatDateTime('hhnn', LocalTimeToUniversal(Now));
|
||
S.Stamp := Now;
|
||
S.ModeGuessed := False;
|
||
S.Mode := DXModeFromComment(TCIArg(M, 1));
|
||
if S.Mode = dxmUnknown then S.Mode := DXModeFromComment(S.Comment);
|
||
if S.Mode = dxmUnknown then
|
||
begin
|
||
S.Mode := DXModeFromFreq(S.FreqHz);
|
||
S.ModeGuessed := S.Mode <> dxmUnknown;
|
||
end;
|
||
if (S.Call = '') or (S.FreqHz <= 0) then Exit;
|
||
FSpots.Add(S); // стор потокобезопасен, маршалинг не нужен
|
||
end;
|
||
|
||
procedure TTCIAdapter.HandleCommand(Client: TTCIClient; const Cmd: string);
|
||
// Внешняя оболочка: разбор + «одна команда не роняет соединение». Команда
|
||
// приходит из сети, а исполняется в потоке контроллера, где может рвануть что
|
||
// угодно (не поднятые движки, чужие сеттеры) — клиент за это платить не должен.
|
||
var
|
||
M: TTCIMessage;
|
||
begin
|
||
if not TCIParse(Cmd, M) then Exit;
|
||
try
|
||
DispatchCommand(Client, M);
|
||
except
|
||
on E: Exception do
|
||
Client.Send(TCIBuild('tci_error', [TCIEscape(LowerCase(M.Name)),
|
||
TCIEscape(E.Message)]));
|
||
end;
|
||
end;
|
||
|
||
procedure TTCIAdapter.DispatchCommand(Client: TTCIClient; const M: TTCIMessage);
|
||
var
|
||
Rx, Ch, V: Integer;
|
||
Lo, Hi: Integer;
|
||
B: Boolean;
|
||
D: Double;
|
||
Name: string;
|
||
begin
|
||
|
||
// ── Управление устройством ──
|
||
if M.Name = 'START' then
|
||
begin
|
||
FLock.Enter;
|
||
try FsBool := True; if CanInvoke then FController.Invoke(SyncSetRun); finally FLock.Leave; end;
|
||
Exit;
|
||
end;
|
||
if M.Name = 'STOP' then
|
||
begin
|
||
FLock.Enter;
|
||
try FsBool := False; if CanInvoke then FController.Invoke(SyncSetRun); finally FLock.Leave; end;
|
||
Exit;
|
||
end;
|
||
|
||
// ── Частоты ──
|
||
if M.Name = 'VFO' then begin CmdFreq(Client, M, False); Exit; end;
|
||
if M.Name = 'IF' then begin CmdFreq(Client, M, True); Exit; end;
|
||
|
||
if M.Name = 'DDS' then
|
||
begin
|
||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||
if not ValidRx(Rx) then Exit;
|
||
if (M.ArgCount >= 2) and TCITryArgFloat(M, 1, D) and FreqSane(D) and
|
||
Claim(HoldKey('DDS', Rx, 0), Client) then
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
FsInt := Rx; FsFreq := D;
|
||
if CanInvoke then FController.Invoke(SyncSetCenter);
|
||
finally FLock.Leave; end;
|
||
end;
|
||
Reply(Client, StrDds(Rx));
|
||
Exit;
|
||
end;
|
||
|
||
// ── Вид связи и фильтр ──
|
||
if M.Name = 'MODULATION' then
|
||
begin
|
||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||
if not ValidRx(Rx) then Exit;
|
||
if M.ArgCount >= 2 then
|
||
begin
|
||
V := TCIModeIndex(TCIArg(M, 1), RxMode(Rx), ChanFreq(Rx, 0));
|
||
if (V >= 0) and Claim(HoldKey('MOD', Rx, 0), Client) then
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
FsInt := Rx; FsInt2 := V;
|
||
if CanInvoke then FController.Invoke(SyncSetMode);
|
||
finally FLock.Leave; end;
|
||
end;
|
||
end;
|
||
Reply(Client, StrModulation(Rx));
|
||
Exit;
|
||
end;
|
||
|
||
if M.Name = 'RX_FILTER_BAND' then
|
||
begin
|
||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||
if not ValidRx(Rx) then Exit;
|
||
// Обе кромки обязаны разобраться, и нижняя обязана быть ниже верхней:
|
||
// «rx_filter_band:0,x,y» иначе схлопывал фильтр в 0..0 и приёмник глох.
|
||
if (M.ArgCount >= 3) and TCITryArgInt(M, 1, Lo) and TCITryArgInt(M, 2, Hi) and
|
||
(Lo < Hi) and Claim(HoldKey('FILT', Rx, 0), Client) then
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
FsInt := Rx;
|
||
FsInt2 := Lo;
|
||
FsInt3 := Hi;
|
||
if CanInvoke then FController.Invoke(SyncSetFilter);
|
||
finally FLock.Leave; end;
|
||
end;
|
||
Reply(Client, StrFilterBand(Rx));
|
||
Exit;
|
||
end;
|
||
|
||
// ── Передача ──
|
||
if M.Name = 'TRX' then
|
||
begin
|
||
// arg3 (источник сигнала: tci/mic1/…) игнорируем: аудио по TCI ещё нет,
|
||
// модуляция берётся из выбранного в программе входа.
|
||
if TCITryArgBool(M, 1, B) and Claim(HoldKey('TRX', 0, 0), Client) then
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
FsBool := B;
|
||
if CanInvoke then FController.Invoke(SyncSetTRX);
|
||
finally FLock.Leave; end;
|
||
end;
|
||
Reply(Client, StrTrx);
|
||
Exit;
|
||
end;
|
||
|
||
if M.Name = 'TUNE' then
|
||
begin
|
||
if TCITryArgBool(M, 1, B) and Claim(HoldKey('TUNE', 0, 0), Client) then
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
FsBool := B;
|
||
if CanInvoke then FController.Invoke(SyncSetTune);
|
||
finally FLock.Leave; end;
|
||
end;
|
||
Reply(Client, StrTune);
|
||
Exit;
|
||
end;
|
||
|
||
if M.Name = 'DRIVE' then
|
||
begin
|
||
if TCITryArgInt(M, 1, V) and Claim(HoldKey('DRIVE', 0, 0), Client) then
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
FsInt := EnsureRange(V, 0, 100);
|
||
if CanInvoke then FController.Invoke(SyncSetDrive);
|
||
finally FLock.Leave; end;
|
||
end;
|
||
Reply(Client, StrDrive);
|
||
Exit;
|
||
end;
|
||
|
||
if M.Name = 'TUNE_DRIVE' then
|
||
begin
|
||
if TCITryArgInt(M, 1, V) and Claim(HoldKey('TUNEDRIVE', 0, 0), Client) then
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
FsInt := EnsureRange(V, 0, 100);
|
||
if CanInvoke then FController.Invoke(SyncSetTuneDrive);
|
||
finally FLock.Leave; end;
|
||
end;
|
||
// Уровень TUN — величина общая для радио: отвечать одному автору значило бы
|
||
// оставить остальных клиентов со старым числом (Broadcast включает автора).
|
||
FServer.Broadcast(TCIBuild('tune_drive', ['0',
|
||
TCIIntStr(FController.FTXSettings.TUNLevel)]));
|
||
Exit;
|
||
end;
|
||
|
||
if M.Name = 'SPLIT_ENABLE' then
|
||
begin
|
||
if TCITryArgBool(M, 1, B) and Claim(HoldKey('SPLIT', 0, 0), Client) then
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
FsBool := B;
|
||
if CanInvoke then FController.Invoke(SyncSetSplit);
|
||
finally FLock.Leave; end;
|
||
end;
|
||
// Split — свойство радио, а не клиента: остальным о нём узнать больше
|
||
// неоткуда (rfActiveVfo несёт его же, но только когда значение сменилось).
|
||
FServer.Broadcast(TCIBuild('split_enable',
|
||
['0', TCIBoolStr(FController.FSplitTxB)]));
|
||
Exit;
|
||
end;
|
||
|
||
// ── Громкость ──
|
||
if M.Name = 'VOLUME' then
|
||
begin
|
||
if TCITryArgFloat(M, 0, D) and Claim(HoldKey('VOL', 0, 0), Client) then
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
FsInt := TCIDbToVolume(D);
|
||
if CanInvoke then FController.Invoke(SyncSetVolume);
|
||
finally FLock.Leave; end;
|
||
end;
|
||
Reply(Client, StrVolume);
|
||
Exit;
|
||
end;
|
||
|
||
if M.Name = 'MUTE' then
|
||
begin
|
||
if TCITryArgBool(M, 0, B) and Claim(HoldKey('MUTE', 0, 0), Client) then
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
FsBool := B;
|
||
if CanInvoke then FController.Invoke(SyncSetMute);
|
||
finally FLock.Leave; end;
|
||
end;
|
||
Reply(Client, StrMute);
|
||
Exit;
|
||
end;
|
||
|
||
if M.Name = 'RX_MUTE' then
|
||
begin
|
||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||
if not ValidRx(Rx) then Exit;
|
||
if TCITryArgBool(M, 1, B) and Claim(HoldKey('RXMUTE', Rx, 0), Client) then
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
FsInt := Rx; FsBool := B;
|
||
if CanInvoke then FController.Invoke(SyncSetRxMute);
|
||
finally FLock.Leave; end;
|
||
end;
|
||
Reply(Client, TCIBuild('rx_mute', [TCIIntStr(Rx), TCIBoolStr(RxMuted(Rx))]));
|
||
Exit;
|
||
end;
|
||
|
||
if M.Name = 'RX_VOLUME' then
|
||
begin
|
||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||
if not TCITryArgInt(M, 1, Ch) then Ch := 0;
|
||
Ch := EnsureRange(Ch, 0, TCI_CHANNELS - 1);
|
||
if not ValidRx(Rx) then Exit;
|
||
if (M.ArgCount >= 3) and TCITryArgFloat(M, 2, D) and
|
||
Claim(HoldKey('RXVOL', Rx, Ch), Client) then
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
FsInt := Rx;
|
||
FsInt2 := TCIDbToVolume(D);
|
||
FsInt3 := Ch;
|
||
if CanInvoke then FController.Invoke(SyncSetRxVolume);
|
||
finally FLock.Leave; end;
|
||
end;
|
||
Reply(Client, StrRxVolume(Rx, Ch));
|
||
Exit;
|
||
end;
|
||
|
||
if M.Name = 'RX_BALANCE' then
|
||
begin
|
||
// Баланса каналов у нас нет — эхо, чтобы клиенты не расходились.
|
||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||
if not TCITryArgInt(M, 1, Ch) then Ch := 0;
|
||
Ch := EnsureRange(Ch, 0, TCI_CHANNELS - 1);
|
||
if not ValidRx(Rx) then Exit;
|
||
FEchoLock.Enter;
|
||
try
|
||
if (M.ArgCount >= 3) and TCITryArgInt(M, 2, V) then
|
||
FEcho[Rx].BalanceDb[Ch] := EnsureRange(V, -40, 40);
|
||
V := FEcho[Rx].BalanceDb[Ch];
|
||
finally
|
||
FEchoLock.Leave;
|
||
end;
|
||
FServer.Broadcast(TCIBuild('rx_balance', [TCIIntStr(Rx), TCIIntStr(Ch),
|
||
TCIIntStr(V)]));
|
||
Exit;
|
||
end;
|
||
|
||
if M.Name = 'MON_VOLUME' then
|
||
begin
|
||
if TCITryArgFloat(M, 0, D) and Claim(HoldKey('MONVOL', 0, 0), Client) then
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
FsInt := TCIDbToVolume(D);
|
||
if CanInvoke then FController.Invoke(SyncSetMonVolume);
|
||
finally FLock.Leave; end;
|
||
end;
|
||
Reply(Client, TCIBuild('mon_volume',
|
||
[TCIIntStr(Round(TCIVolumeToDb(FController.FTxMonVolume)))]));
|
||
Exit;
|
||
end;
|
||
|
||
if M.Name = 'MON_ENABLE' then
|
||
begin
|
||
if TCITryArgBool(M, 0, B) and Claim(HoldKey('MONEN', 0, 0), Client) then
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
FsBool := B;
|
||
if CanInvoke then FController.Invoke(SyncSetMonEnable);
|
||
finally FLock.Leave; end;
|
||
end;
|
||
Reply(Client, TCIBuild('mon_enable', [TCIBoolStr(not FController.FRxMuteOnTx)]));
|
||
Exit;
|
||
end;
|
||
|
||
// ── АРУ ──
|
||
if M.Name = 'AGC_MODE' then
|
||
begin
|
||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||
if not ValidRx(Rx) then Exit;
|
||
if M.ArgCount >= 2 then
|
||
begin
|
||
Name := LowerCase(TCIArg(M, 1));
|
||
if Name = 'off' then V := 4
|
||
else if Name = 'fast' then V := 0
|
||
else if Name = 'normal' then V := 1
|
||
else V := -1;
|
||
if (V >= 0) and Claim(HoldKey('AGC', Rx, 0), Client) then
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
FsInt := Rx; FsInt2 := V;
|
||
if CanInvoke then FController.Invoke(SyncSetAGCMode);
|
||
finally FLock.Leave; end;
|
||
end;
|
||
end;
|
||
Reply(Client, StrAGCMode(Rx));
|
||
Exit;
|
||
end;
|
||
|
||
if M.Name = 'AGC_GAIN' then
|
||
begin
|
||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||
if not ValidRx(Rx) then Exit;
|
||
// AGC-T (порог АРУ) в ewsdr один на приёмный тракт: у слайса своего нет.
|
||
// Правка от имени доп. приёмника трогала бы главный — не делаем этого,
|
||
// просто отвечаем текущим значением (ограничение, см. doc/TCI.md).
|
||
if (Rx = 0) and TCITryArgInt(M, 1, V) and Claim(HoldKey('AGCT', 0, 0), Client) then
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
FsInt := EnsureRange(V, TCI_AGC_MIN_DB, TCI_AGC_MAX_DB);
|
||
if CanInvoke then FController.Invoke(SyncSetAGCTop);
|
||
finally FLock.Leave; end;
|
||
end;
|
||
Reply(Client, StrAGCGain(Rx));
|
||
Exit;
|
||
end;
|
||
|
||
// ── Шумоподавление ──
|
||
if (M.Name = 'RX_NR_ENABLE') or (M.Name = 'RX_NB_ENABLE') or
|
||
(M.Name = 'RX_ANF_ENABLE') then
|
||
begin
|
||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||
if not ValidRx(Rx) then Exit;
|
||
if TCITryArgBool(M, 1, B) and Claim(HoldKey(M.Name, Rx, 0), Client) then
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
FsInt := Rx; FsBool := B;
|
||
if M.Name = 'RX_NR_ENABLE' then if CanInvoke then FController.Invoke(SyncSetNR)
|
||
else if M.Name = 'RX_NB_ENABLE' then if CanInvoke then FController.Invoke(SyncSetNB)
|
||
else if CanInvoke then FController.Invoke(SyncSetANF);
|
||
finally FLock.Leave; end;
|
||
end;
|
||
if M.Name = 'RX_NR_ENABLE' then Reply(Client, StrNR(Rx))
|
||
else if M.Name = 'RX_NB_ENABLE' then Reply(Client, StrNB(Rx))
|
||
else Reply(Client, StrANF(Rx));
|
||
Exit;
|
||
end;
|
||
|
||
if M.Name = 'RX_NB_PARAM' then
|
||
begin
|
||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||
if not ValidRx(Rx) then Exit;
|
||
FEchoLock.Enter;
|
||
try
|
||
if M.ArgCount >= 3 then
|
||
begin
|
||
if TCITryArgInt(M, 1, V) then FEcho[Rx].NBThreshold := EnsureRange(V, 1, 100);
|
||
if TCITryArgInt(M, 2, V) then FEcho[Rx].NBDuration := EnsureRange(V, 1, 300);
|
||
end;
|
||
V := FEcho[Rx].NBThreshold;
|
||
Ch := FEcho[Rx].NBDuration;
|
||
finally
|
||
FEchoLock.Leave;
|
||
end;
|
||
FServer.Broadcast(TCIBuild('rx_nb_param',
|
||
[TCIIntStr(Rx), TCIIntStr(V), TCIIntStr(Ch)]));
|
||
Exit;
|
||
end;
|
||
|
||
// ── Эхо-переключатели обработки (в ewsdr соответствующих трактов нет) ──
|
||
if (M.Name = 'RX_BIN_ENABLE') or (M.Name = 'RX_ANC_ENABLE') or
|
||
(M.Name = 'RX_APF_ENABLE') or (M.Name = 'RX_DSE_ENABLE') or
|
||
(M.Name = 'RX_NF_ENABLE') then
|
||
begin
|
||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||
if not ValidRx(Rx) then Exit;
|
||
FEchoLock.Enter;
|
||
try
|
||
if TCITryArgBool(M, 1, B) then
|
||
begin
|
||
if M.Name = 'RX_BIN_ENABLE' then FEcho[Rx].BinOn := B
|
||
else if M.Name = 'RX_ANC_ENABLE' then FEcho[Rx].ANCOn := B
|
||
else if M.Name = 'RX_APF_ENABLE' then FEcho[Rx].APFOn := B
|
||
else if M.Name = 'RX_DSE_ENABLE' then FEcho[Rx].DSEOn := B
|
||
else FEcho[Rx].NFOn := B;
|
||
end;
|
||
if M.Name = 'RX_BIN_ENABLE' then B := FEcho[Rx].BinOn
|
||
else if M.Name = 'RX_ANC_ENABLE' then B := FEcho[Rx].ANCOn
|
||
else if M.Name = 'RX_APF_ENABLE' then B := FEcho[Rx].APFOn
|
||
else if M.Name = 'RX_DSE_ENABLE' then B := FEcho[Rx].DSEOn
|
||
else B := FEcho[Rx].NFOn;
|
||
finally
|
||
FEchoLock.Leave;
|
||
end;
|
||
FServer.Broadcast(TCIBuild(LowerCase(M.Name), [TCIIntStr(Rx), TCIBoolStr(B)]));
|
||
Exit;
|
||
end;
|
||
|
||
// ── Блокировка, шумоподавитель ──
|
||
if M.Name = 'LOCK' then
|
||
begin
|
||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||
if not ValidRx(Rx) then Exit;
|
||
if TCITryArgBool(M, 1, B) and Claim(HoldKey('LOCK', 0, 0), Client) then
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
FsBool := B;
|
||
if CanInvoke then FController.Invoke(SyncSetLock);
|
||
finally FLock.Leave; end;
|
||
end;
|
||
Reply(Client, StrLock(Rx));
|
||
Exit;
|
||
end;
|
||
|
||
if M.Name = 'SQL_ENABLE' then
|
||
begin
|
||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||
if not ValidRx(Rx) then Exit;
|
||
if TCITryArgBool(M, 1, B) and Claim(HoldKey('SQL', Rx, 0), Client) then
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
FsInt := Rx; FsBool := B;
|
||
if CanInvoke then FController.Invoke(SyncSetSql);
|
||
finally FLock.Leave; end;
|
||
end;
|
||
Reply(Client, StrSqlEnable(Rx));
|
||
Exit;
|
||
end;
|
||
|
||
if M.Name = 'SQL_LEVEL' then
|
||
begin
|
||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||
if not ValidRx(Rx) then Exit;
|
||
if TCITryArgFloat(M, 1, D) and Claim(HoldKey('SQLLEV', Rx, 0), Client) then
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
FsInt := Rx;
|
||
FsInt2 := TCISqlToLevel(D);
|
||
if CanInvoke then FController.Invoke(SyncSetSqlLevel);
|
||
finally FLock.Leave; end;
|
||
end;
|
||
Reply(Client, StrSqlLevel(Rx));
|
||
Exit;
|
||
end;
|
||
|
||
// ── Расстройка: своего RIT/XIT в ewsdr нет — храним и отражаем ──
|
||
if (M.Name = 'RIT_ENABLE') or (M.Name = 'XIT_ENABLE') then
|
||
begin
|
||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||
if not ValidRx(Rx) then Exit;
|
||
FEchoLock.Enter;
|
||
try
|
||
if TCITryArgBool(M, 1, B) then
|
||
begin
|
||
if M.Name = 'RIT_ENABLE' then FEcho[Rx].RitOn := B
|
||
else FEcho[Rx].XitOn := B;
|
||
end;
|
||
if M.Name = 'RIT_ENABLE' then B := FEcho[Rx].RitOn else B := FEcho[Rx].XitOn;
|
||
finally
|
||
FEchoLock.Leave;
|
||
end;
|
||
FServer.Broadcast(TCIBuild(LowerCase(M.Name), [TCIIntStr(Rx), TCIBoolStr(B)]));
|
||
Exit;
|
||
end;
|
||
|
||
if (M.Name = 'RIT_OFFSET') or (M.Name = 'XIT_OFFSET') then
|
||
begin
|
||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||
if not ValidRx(Rx) then Exit;
|
||
FEchoLock.Enter;
|
||
try
|
||
if TCITryArgInt(M, 1, V) then
|
||
begin
|
||
V := EnsureRange(V, -50000, 50000);
|
||
if M.Name = 'RIT_OFFSET' then FEcho[Rx].RitHz := V
|
||
else FEcho[Rx].XitHz := V;
|
||
end;
|
||
if M.Name = 'RIT_OFFSET' then V := FEcho[Rx].RitHz else V := FEcho[Rx].XitHz;
|
||
finally
|
||
FEchoLock.Leave;
|
||
end;
|
||
FServer.Broadcast(TCIBuild(LowerCase(M.Name), [TCIIntStr(Rx), TCIIntStr(V)]));
|
||
Exit;
|
||
end;
|
||
|
||
if M.Name = 'RX_CHANNEL_ENABLE' then
|
||
begin
|
||
// Канал B у главного приёмника — это VFO B, он есть всегда; у доп. панов
|
||
// вторым каналом был бы второй слайс (создание слайсов по TCI — этап 2).
|
||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||
if not TCITryArgInt(M, 1, Ch) then Ch := 1;
|
||
if not ValidRx(Rx) then Exit;
|
||
if TCITryArgBool(M, 2, B) then
|
||
begin
|
||
FEchoLock.Enter;
|
||
try FEcho[Rx].ChannelBOn := B; finally FEchoLock.Leave; end;
|
||
end;
|
||
B := ChanCount(Rx) > 1;
|
||
FServer.Broadcast(TCIBuild('rx_channel_enable',
|
||
[TCIIntStr(Rx), TCIIntStr(Ch), TCIBoolStr(B)]));
|
||
Exit;
|
||
end;
|
||
|
||
// ── Смещения цифровых видов (эхо) ──
|
||
if (M.Name = 'DIGL_OFFSET') or (M.Name = 'DIGU_OFFSET') then
|
||
begin
|
||
FEchoLock.Enter;
|
||
try
|
||
if TCITryArgInt(M, 0, V) then
|
||
begin
|
||
V := EnsureRange(V, 0, 4000);
|
||
if M.Name = 'DIGL_OFFSET' then FDiglOffset := V else FDiguOffset := V;
|
||
end;
|
||
if M.Name = 'DIGL_OFFSET' then V := FDiglOffset else V := FDiguOffset;
|
||
finally
|
||
FEchoLock.Leave;
|
||
end;
|
||
FServer.Broadcast(TCIBuild(LowerCase(M.Name), [TCIIntStr(V)]));
|
||
Exit;
|
||
end;
|
||
|
||
// ── Телеграф ──
|
||
if (M.Name = 'CW_MACROS_SPEED') or (M.Name = 'CW_KEYER_SPEED') then
|
||
begin
|
||
if TCITryArgInt(M, 0, V) and Claim(HoldKey('CWSPEED', 0, 0), Client) then
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
FsInt := EnsureRange(V, 5, 60);
|
||
if CanInvoke then FController.Invoke(SyncSetCWSpeed);
|
||
finally FLock.Leave; end;
|
||
end;
|
||
FServer.Broadcast(TCIBuild('cw_macros_speed',
|
||
[TCIIntStr(FController.FCWSettings.Speed)]));
|
||
Exit;
|
||
end;
|
||
|
||
if (M.Name = 'CW_MACROS_SPEED_UP') or (M.Name = 'CW_MACROS_SPEED_DOWN') then
|
||
begin
|
||
if not TCITryArgInt(M, 0, V) then Exit;
|
||
if M.Name = 'CW_MACROS_SPEED_DOWN' then V := -V;
|
||
if Claim(HoldKey('CWSPEED', 0, 0), Client) then
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
FsInt := EnsureRange(FController.FCWSettings.Speed + V, 5, 60);
|
||
if CanInvoke then FController.Invoke(SyncSetCWSpeed);
|
||
finally FLock.Leave; end;
|
||
end;
|
||
FServer.Broadcast(TCIBuild('cw_macros_speed', [TCIIntStr(FController.FCWSettings.Speed)]));
|
||
Exit;
|
||
end;
|
||
|
||
if M.Name = 'CW_MACROS_DELAY' then
|
||
begin
|
||
if TCITryArgInt(M, 0, V) and Claim(HoldKey('CWDELAY', 0, 0), Client) then
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
FsInt := EnsureRange(V, 0, 1000);
|
||
if CanInvoke then FController.Invoke(SyncSetCWDelay);
|
||
finally FLock.Leave; end;
|
||
end;
|
||
FServer.Broadcast(TCIBuild('cw_macros_delay',
|
||
[TCIIntStr(FController.FCWSettings.RFDelayMS)]));
|
||
Exit;
|
||
end;
|
||
|
||
if M.Name = 'CW_MACROS' then begin CmdCWMacros(M, False); Exit; end;
|
||
if M.Name = 'CW_MSG' then begin CmdCWMacros(M, True); Exit; end;
|
||
if M.Name = 'CW_MACROS_STOP' then
|
||
begin
|
||
FLock.Enter;
|
||
try if CanInvoke then FController.Invoke(SyncCWStop); finally FLock.Leave; end;
|
||
Exit;
|
||
end;
|
||
if M.Name = 'CW_TERMINAL' then
|
||
begin
|
||
FEchoLock.Enter;
|
||
try
|
||
if TCITryArgBool(M, 0, B) then FCwTerminal := B;
|
||
B := FCwTerminal;
|
||
finally
|
||
FEchoLock.Leave;
|
||
end;
|
||
FServer.Broadcast(TCIBuild('cw_terminal', [TCIBoolStr(B)]));
|
||
Exit;
|
||
end;
|
||
|
||
// ── Споты ──
|
||
if M.Name = 'SPOT' then begin CmdSpot(M); Exit; end;
|
||
if M.Name = 'SPOT_DELETE' then
|
||
begin
|
||
if FSpots <> nil then FSpots.RemoveCall(TCIUnescape(TCIArg(M, 0)));
|
||
Exit;
|
||
end;
|
||
if M.Name = 'SPOT_CLEAR' then
|
||
begin
|
||
if FSpots <> nil then FSpots.Clear;
|
||
Exit;
|
||
end;
|
||
|
||
// ── Измерители ──
|
||
// Период ставится ДО включения: иначе тик-поток успел бы отправить первую
|
||
// пачку со старым периодом.
|
||
if M.Name = 'RX_SENSORS_ENABLE' then
|
||
begin
|
||
if not TCITryArgBool(M, 0, B) then Exit;
|
||
if TCITryArgInt(M, 1, V) then
|
||
Client.SetRxSensorsMs(EnsureRange(V, TCI_SENSOR_MIN_MS, TCI_SENSOR_MAX_MS));
|
||
Client.SetRxSensors(B);
|
||
Exit;
|
||
end;
|
||
if M.Name = 'TX_SENSORS_ENABLE' then
|
||
begin
|
||
if not TCITryArgBool(M, 0, B) then Exit;
|
||
if TCITryArgInt(M, 1, V) then
|
||
Client.SetTxSensorsMs(EnsureRange(V, TCI_SENSOR_MIN_MS, TCI_SENSOR_MAX_MS));
|
||
Client.SetTxSensors(B);
|
||
Exit;
|
||
end;
|
||
|
||
// Поднять окно программы (§4.3). Через UI: адаптер до окна не дотягивается.
|
||
if M.Name = 'SET_IN_FOCUS' then
|
||
begin
|
||
FLock.Enter;
|
||
try if CanInvoke then FController.Invoke(SyncFocus); finally FLock.Leave; end;
|
||
Exit;
|
||
end;
|
||
|
||
// ── Параметры потоков ──
|
||
// Это настройки КЛИЕНТА (§4.3), а не устройства: два логгера вправе просить
|
||
// разную частоту дискретизации. Поэтому живут в его объекте, а не в адаптере
|
||
// — иначе один клиент перенастраивал бы будущие потоки всем остальным.
|
||
// Сами потоки — этап 2, значения только принимаются и подтверждаются.
|
||
if M.Name = 'IQ_SAMPLERATE' then
|
||
begin
|
||
// Набор частот оговорён протоколом; чужое значение отвергаем, а в ответе
|
||
// отдаём действующее — клиент увидит, что его не приняли.
|
||
if TCITryArgInt(M, 0, V) and TCIValidIQRate(V) then Client.IQRate := V;
|
||
Reply(Client, TCIBuild('iq_samplerate', [TCIIntStr(Client.IQRate)]));
|
||
Exit;
|
||
end;
|
||
if M.Name = 'AUDIO_SAMPLERATE' then
|
||
begin
|
||
if TCITryArgInt(M, 0, V) and TCIValidAudioRate(V) then Client.AudioRate := V;
|
||
Reply(Client, TCIBuild('audio_samplerate', [TCIIntStr(Client.AudioRate)]));
|
||
Exit;
|
||
end;
|
||
if M.Name = 'AUDIO_STREAM_SAMPLES' then
|
||
begin
|
||
if TCITryArgInt(M, 0, V) then
|
||
Client.AudioSamples := EnsureRange(V, 100, 2048);
|
||
Exit;
|
||
end;
|
||
if M.Name = 'AUDIO_STREAM_CHANNELS' then
|
||
begin
|
||
if TCITryArgInt(M, 0, V) then
|
||
Client.AudioChannels := EnsureRange(V, 1, 2);
|
||
Exit;
|
||
end;
|
||
if M.Name = 'AUDIO_STREAM_SAMPLE_TYPE' then
|
||
begin
|
||
Name := LowerCase(Trim(TCIArg(M, 0)));
|
||
if TCIValidSampleType(Name) then Client.AudioSampleType := Name;
|
||
Exit;
|
||
end;
|
||
if M.Name = 'TX_STREAM_AUDIO_BUFFERING' then
|
||
begin
|
||
if TCITryArgInt(M, 0, V) then
|
||
Client.TxBuffering := EnsureRange(V, 50, 500);
|
||
Exit;
|
||
end;
|
||
|
||
// Запуск потоков (§3.4) — этап 2. Молчать нельзя: клиент решил бы, что поток
|
||
// пошёл, и ждал бы данных бесконечно. Отвечаем ошибкой на конкретную команду.
|
||
if (M.Name = 'IQ_START') or (M.Name = 'IQ_STOP') or
|
||
(M.Name = 'AUDIO_START') or (M.Name = 'AUDIO_STOP') or
|
||
(M.Name = 'LINE_OUT_START') or (M.Name = 'LINE_OUT_STOP') or
|
||
(M.Name = 'LINE_OUT_RECORDER_START') or
|
||
(M.Name = 'LINE_OUT_RECORDER_SAVE') or
|
||
(M.Name = 'LINE_OUT_RECORDER_BREAK') then
|
||
begin
|
||
Reply(Client, TCIBuild('tci_error', [LowerCase(M.Name),
|
||
'binary streams are not implemented']));
|
||
Exit;
|
||
end;
|
||
|
||
// Всё прочее незнакомое протокол разрешает игнорировать (§3.1).
|
||
end;
|
||
|
||
{ ═══════════════════════════════════════════════════════════════════════════
|
||
Измерители (тик сервера, 20 мс)
|
||
═══════════════════════════════════════════════════════════════════════════ }
|
||
|
||
procedure TTCIAdapter.PushSensors(Client: TTCIClient);
|
||
var
|
||
Now_: QWord;
|
||
Rx, Ch: Integer;
|
||
Snap: TRadioSnapshot;
|
||
begin
|
||
if not Client.Ready then Exit;
|
||
Now_ := GetTickCount64;
|
||
|
||
// Набор {включено, период, время последней отправки} проверяется и обновляется
|
||
// одним шагом под локом клиента: пишет его поток, читает этот.
|
||
if Client.DueRxSensors(Now_) then
|
||
begin
|
||
for Rx := 0 to RxCount - 1 do
|
||
begin
|
||
if not RxActive(Rx) then Continue; // несуществующий пан молчит, а не «-140»
|
||
for Ch := 0 to ChanCount(Rx) - 1 do
|
||
Client.Send(TCIBuild('rx_channel_sensors',
|
||
[TCIIntStr(Rx), TCIIntStr(Ch), TCIFloatStr(RxSMeterDbm(Rx, Ch), 1)]));
|
||
// Устаревшая форма — её ещё ждут старые клиенты.
|
||
Client.Send(TCIBuild('rx_sensors',
|
||
[TCIIntStr(Rx), TCIFloatStr(RxSMeterDbm(Rx, 0), 1)]));
|
||
end;
|
||
end;
|
||
|
||
if Client.DueTxSensors(Now_) then
|
||
begin
|
||
Snap := FController.GetSnapshot;
|
||
// arg2 — уровень микрофона: измерителя микрофона в ewsdr нет, отдаём
|
||
// нижнюю границу шкалы, чтобы клиент не рисовал случайные значения.
|
||
Client.Send(TCIBuild('tx_sensors',
|
||
['0', '-60.0', TCIFloatStr(Snap.FwdW, 1), TCIFloatStr(Snap.FwdW, 1),
|
||
TCIFloatStr(Snap.SWR, 2)]));
|
||
end;
|
||
end;
|
||
|
||
procedure TTCIAdapter.HandleTick;
|
||
begin
|
||
// Тик крутится в своём потоке сервера: исключение здесь остановило бы
|
||
// измерители у ВСЕХ клиентов до перезапуска сервера.
|
||
try
|
||
FServer.EnumClients(PushSensors);
|
||
except
|
||
// молча: следующий тик через 20 мс попробует снова
|
||
end;
|
||
end;
|
||
|
||
{ ═══════════════════════════════════════════════════════════════════════════
|
||
Уведомления: изменение состояния контроллера → всем клиентам
|
||
═══════════════════════════════════════════════════════════════════════════ }
|
||
|
||
procedure TTCIAdapter.OnState(Sender: TObject; Field: TRadioField);
|
||
var
|
||
Rx, Ch: Integer;
|
||
TxHz: Double;
|
||
MapChanged: Boolean;
|
||
Sig: string;
|
||
begin
|
||
// Снимок слайсов обновляем ДО всего остального и НЕЗАВИСИМО от того, есть ли
|
||
// клиенты: подключившийся читает уже готовый снимок, а не заставляет поток
|
||
// контроллера сниматься по требованию (Invoke из потока клиента ждал бы UI).
|
||
// Все правки слайсов приходят сюда: сеттеры шлют rfSliceFreq/rfSliceState,
|
||
// создание и удаление — rfDevice.
|
||
// Снимок железа — первым: от него зависят и границы частоты, и всё, что
|
||
// сетевые потоки читают о плате.
|
||
if Field in [rfDevice, rfConnected, rfDeviceList, rfXvtr, rfBand, rfSampleRate] then
|
||
RefreshDev;
|
||
|
||
MapChanged := False;
|
||
if Field in [rfSliceFreq, rfSliceState, rfDevice, rfPanFreq, rfSampleRate,
|
||
rfBand, rfXvtr, rfCenterFreq] then
|
||
begin
|
||
Sig := SliceMapSig;
|
||
RefreshSlices;
|
||
MapChanged := Sig <> SliceMapSig;
|
||
end;
|
||
|
||
// Границы настройки — тоже до гейта по клиентам: их кэш обязан пережить
|
||
// время, когда не подключён никто (см. PushVfoLimits).
|
||
if Field in [rfDevice, rfXvtr, rfBand] then PushVfoLimits;
|
||
|
||
if (FServer = nil) or (FServer.ClientCount = 0) then Exit;
|
||
|
||
// §3.5: инициатор изменения захватывает параметр на 200 мс. Здесь инициатор —
|
||
// не клиент (оператор, CAT, бэнд-логика), поэтому владелец nil. Если параметр
|
||
// прямо сейчас крутит клиент, Claim его не отберёт — в том числе когда это
|
||
// изменение и есть эхо его собственной команды.
|
||
case Field of
|
||
rfVfoA: Claim(HoldKey('VFO', 0, 0), nil);
|
||
rfVfoB: Claim(HoldKey('VFO', 0, 1), nil);
|
||
rfMode: Claim(HoldKey('MOD', 0, 0), nil);
|
||
rfFilter,
|
||
rfFilterBW: Claim(HoldKey('FILT', 0, 0), nil);
|
||
rfAGCMode: Claim(HoldKey('AGC', 0, 0), nil);
|
||
rfAGCTop: Claim(HoldKey('AGCT', 0, 0), nil);
|
||
rfVolume: Claim(HoldKey('VOL', 0, 0), nil);
|
||
rfMute: Claim(HoldKey('MUTE', 0, 0), nil);
|
||
rfDrive: Claim(HoldKey('DRIVE', 0, 0), nil);
|
||
rfTransmitting: Claim(HoldKey('TRX', 0, 0), nil);
|
||
rfTuning: Claim(HoldKey('TUNE', 0, 0), nil);
|
||
rfVfoLock: Claim(HoldKey('LOCK', 0, 0), nil);
|
||
rfCenterFreq: Claim(HoldKey('DDS', 0, 0), nil);
|
||
rfSliceFreq:
|
||
if SliceRxCh(FController.FSliceFreqId, Rx, Ch) then
|
||
Claim(HoldKey('VFO', Rx, Ch), nil);
|
||
end;
|
||
|
||
case Field of
|
||
rfVfoA:
|
||
begin
|
||
FServer.Broadcast(StrVfo(0, 0));
|
||
FServer.Broadcast(StrIf(0, 0));
|
||
end;
|
||
rfVfoB:
|
||
begin
|
||
FServer.Broadcast(StrVfo(0, 1));
|
||
FServer.Broadcast(StrIf(0, 1));
|
||
end;
|
||
rfMode:
|
||
begin
|
||
FServer.Broadcast(StrModulation(0));
|
||
FServer.Broadcast(StrFilterBand(0));
|
||
end;
|
||
rfFilter, rfFilterBW: FServer.Broadcast(StrFilterBand(0));
|
||
rfAGCMode: FServer.Broadcast(StrAGCMode(0));
|
||
rfAGCTop: FServer.Broadcast(StrAGCGain(0));
|
||
rfVolume: FServer.Broadcast(StrVolume);
|
||
rfMute: FServer.Broadcast(StrMute);
|
||
rfDrive: FServer.Broadcast(StrDrive);
|
||
rfTransmitting: FServer.Broadcast(StrTrx);
|
||
rfTuning: FServer.Broadcast(StrTune);
|
||
rfVfoLock:
|
||
begin
|
||
FServer.Broadcast(StrLock(0));
|
||
for Ch := 0 to TCI_CHANNELS - 1 do
|
||
FServer.Broadcast(TCIBuild('vfo_lock',
|
||
['0', TCIIntStr(Ch), TCIBoolStr(FController.FVfoLock)]));
|
||
end;
|
||
rfNR: FServer.Broadcast(TCIBuild('rx_nr_enable', ['0', TCIBoolStr(FController.FNRMode > 0)]));
|
||
rfNB: FServer.Broadcast(TCIBuild('rx_nb_enable', ['0', TCIBoolStr(FController.FNBMode > 0)]));
|
||
rfANF: FServer.Broadcast(TCIBuild('rx_anf_enable', ['0', TCIBoolStr(FController.FANF)]));
|
||
rfFMSQ: FServer.Broadcast(StrSqlEnable(0));
|
||
rfFMSQLevel: FServer.Broadcast(StrSqlLevel(0));
|
||
rfRxMuteOnTx: FServer.Broadcast(TCIBuild('mon_enable',
|
||
[TCIBoolStr(not FController.FRxMuteOnTx)]));
|
||
rfCenterFreq:
|
||
begin
|
||
FServer.Broadcast(StrDds(0));
|
||
FServer.Broadcast(StrIf(0, 0));
|
||
FServer.Broadcast(StrIf(0, 1));
|
||
end;
|
||
rfSampleRate:
|
||
FServer.Broadcast(TCIBuild('if_limits',
|
||
[TCIIntStr(-(FController.FSampleRate div 2)),
|
||
TCIIntStr(FController.FSampleRate div 2)]));
|
||
rfRunning:
|
||
if FController.FRunning then FServer.Broadcast(TCIBuild('start'))
|
||
else FServer.Broadcast(TCIBuild('stop'));
|
||
rfPanFreq:
|
||
// Центр пана уехал: у его каналов изменилась и IF (она отсчитывается
|
||
// от центра), хотя абсолютная частота могла остаться прежней.
|
||
for Rx := 1 to RxCount - 1 do
|
||
if FController.PanDDCActive(Rx) then
|
||
begin
|
||
FServer.Broadcast(StrDds(Rx));
|
||
for Ch := 0 to ChanCount(Rx) - 1 do FServer.Broadcast(StrIf(Rx, Ch));
|
||
end;
|
||
rfSliceFreq, rfSliceState:
|
||
// Кто именно изменился — в FSliceFreqId: рассылать состояние канала 0
|
||
// всех панов (как было) значило бы врать про второй слайс.
|
||
if SliceRxCh(FController.FSliceFreqId, Rx, Ch) then
|
||
BroadcastRxState(Rx, Ch);
|
||
rfBand, rfXvtr:
|
||
begin
|
||
FServer.Broadcast(StrTxEnable(0));
|
||
FServer.Broadcast(StrVfo(0, 0));
|
||
end;
|
||
rfActiveVfo:
|
||
// Сюда же приходит смена split (SetSplit шлёт rfActiveVfo): без этого
|
||
// переключение TX-VFO из окна программы мимо клиентов проходило молча.
|
||
FServer.Broadcast(TCIBuild('split_enable',
|
||
['0', TCIBoolStr(FController.FSplitTxB)]));
|
||
rfMonVolume:
|
||
// Громкость самоконтроля — отдельная величина: раньше её правка уезжала
|
||
// клиентам как обычный volume, то есть враньём.
|
||
FServer.Broadcast(TCIBuild('mon_volume',
|
||
[TCIIntStr(Round(TCIVolumeToDb(FController.FTxMonVolume)))]));
|
||
rfTXProfile:
|
||
// Уровень TUN живёт в TX-настройках: сменил его оператор или профиль —
|
||
// клиентам об этом больше узнать неоткуда.
|
||
FServer.Broadcast(TCIBuild('tune_drive', ['0',
|
||
TCIIntStr(FController.FTXSettings.TUNLevel)]));
|
||
rfCWSettings:
|
||
begin
|
||
FServer.Broadcast(TCIBuild('cw_macros_speed',
|
||
[TCIIntStr(FController.FCWSettings.Speed)]));
|
||
FServer.Broadcast(TCIBuild('cw_macros_delay',
|
||
[TCIIntStr(FController.FCWSettings.RFDelayMS)]));
|
||
end;
|
||
end;
|
||
|
||
// Слайс создали или удалили (rfDevice) — у приёмника изменился набор каналов,
|
||
// и клиент, подключённый до этого, о новом канале не узнает никак.
|
||
if MapChanged then PushChannelMap;
|
||
|
||
// Частота передачи — отдельным уведомлением, но только когда она реально
|
||
// изменилась: поле дёргается на каждый шаг ручки.
|
||
if Field in [rfVfoA, rfVfoB, rfActiveVfo, rfBand, rfXvtr, rfTransmitting] then
|
||
begin
|
||
TxHz := FController.ActiveTXFreqHz;
|
||
if Abs(TxHz - FLastTxFreq) >= 1 then
|
||
begin
|
||
FLastTxFreq := TxHz;
|
||
FServer.Broadcast(TCIBuild('tx_frequency', [TCIIntStr(Round(TxHz))]));
|
||
end;
|
||
if TxEnabled <> FLastTxEnable then
|
||
begin
|
||
FLastTxEnable := TxEnabled;
|
||
FServer.Broadcast(StrTxEnable(0));
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
{ ═══════════════════════════════════════════════════════════════════════════
|
||
Уведомления, которые инициирует UI
|
||
═══════════════════════════════════════════════════════════════════════════ }
|
||
|
||
procedure TTCIAdapter.NotifySpotClicked(const Call: string; FreqHz: Double;
|
||
Rx, Ch: Integer);
|
||
begin
|
||
if (FServer = nil) or (FServer.ClientCount = 0) then Exit;
|
||
FServer.Broadcast(TCIBuild('rx_clicked_on_spot',
|
||
[TCIIntStr(Rx), TCIIntStr(Ch), TCIEscape(Call), TCIIntStr(Round(FreqHz))]));
|
||
// Устаревшая форма — её ещё слушают старые клиенты.
|
||
FServer.Broadcast(TCIBuild('clicked_on_spot',
|
||
[TCIEscape(Call), TCIIntStr(Round(FreqHz))]));
|
||
end;
|
||
|
||
procedure TTCIAdapter.NotifyAppFocus(InFocus: Boolean);
|
||
begin
|
||
// Значение помним всегда: подключившемуся клиенту фокус уходит в пачке
|
||
// состояния, а не только по следующей активации окна.
|
||
FAppFocus := InFocus;
|
||
if (FServer = nil) or (FServer.ClientCount = 0) then Exit;
|
||
FServer.Broadcast(TCIBuild('app_focus', [TCIBoolStr(InFocus)]));
|
||
end;
|
||
|
||
end.
|