mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
feat(tci): приёмник = слот слайса; TRX/TUNE адресуют передатчик; стенд «как MSHV»
Модель приёмников переделана: приёмник TCI — это СЛОТ СЛАЙСА, а не панадаптер.
rx0 — главный тракт (каналы A/B = VFO A/B), rx N — слайс слота N−1, то есть
буквы B..G с флага, на каком бы пане он ни стоял. Панорама стала свойством
приёмника: от неё берутся DDS (у слайса только чтение) и поток IQ.
Почему: у Pluto панорама ровно одна (MaxPans = 1), и второй приёмник там
существует ТОЛЬКО как слайс главного пана — при нумерации по панам он был
недоступен вовсе, а TRX_COUNT навсегда равнялся единице. С другого конца —
клиенты: у MSHV в настройках всего «TCI Client rx1/rx2», то есть приёмники 0 и
1, третьего номера ввести некуда. Со слотами правило «первый созданный слайс =
приёмник 1» держится на любом железе: на openHPSDR слайс второго пана и на
Pluto слайс главного одинаково занимают слот B. Номер совпадает с буквой на
экране и с портом слайс-CAT. Цена: панорама без слайсов из TCI пропала, а
второй слайс пана перестал быть «каналом B» и стал своим приёмником — у канала
B в протоколе только частота, IF и громкость, у приёмника же всё.
TRX/TUNE раньше игнорировали arg1 (номер передатчика) целиком: клиент доп.
приёмника уводил в эфир слайс ОПЕРАТОРА — чужая частота, а с кросс-бандовым
мультислайс-TX и чужой диапазон, с чужими антенной и фильтрами; trx:9,true жал
PTT. Теперь номер разбирается и проверяется, приёмник N > 0 идёт через
RequestSliceTx (та же дверь, что у CAT-порта слайса: «в эфире только один» и
Auto TX), у контроллера появился параметр Tune для TUN тем же путём. Чужую
передачу не трогаем вовсе — ни источник модуляции, ни тон: SetMOX(True) поверх
идущей передачи не выходит рано, а заново выбирает микрофон. Хозяином эфира
клиент становится, только если передача началась именно от его команды, и
решает это Sync-метод в потоке контроллера (снимок «шла ли передача», взятый в
потоке клиента, врал: между разбором и исполнением влезает PTT оператора).
Разбор исходников MSHV (он фильтрует ВСЕ строки и бинарные блоки по номеру
приёмника) дал ещё три правки:
* ответ на TRX/TUNE адресуется номером АВТОРА, состояние в нём — «в эфире
именно твой слайс»; в рассылку идёт номер реально передающего;
* tx_enable рассылается каждому живому приёмнику со своим номером и входит в
картину нового приёмника — без этого у MSHV молча мёртвая PTT
(set_ptt начинается с `if (!tci_tx_enable) return;`);
* про несуществующий приёмник молчим целиком (LiveRx), в том числе на чтение:
ответ «vfo:1,0,0» MSHV принимал бы за конец инициализации.
Попутно, вне TCI: SendDUCSpecificFromSettings трогала FNetwork без Assigned, а
зовут её по любому PTT/TUN (SetMOX → SyncCWKeyer → она) — до подключения
устройства это была Access violation, у TCI её глотал обработчик команды.
Стенды: новый test/tci/mshv_sim.py — точная копия логики клиента MSHV, отвечает
на вопрос «почему он не подключается» одной строкой (на живом приложении
воспроизвёл ошибку инициализации для rx2 до правки). tcitest — 158/158, в
сквозном прогоне добавлено создание слайса на главном пане: он становится
приёмником 1, отвечает на vfo:1,0, слушается командой, отдаёт аудио с
receiver = 1 и замолкает после удаления.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+22
-12
@@ -958,10 +958,13 @@ type
|
|||||||
function SliceSlotOutDev(Slot: Integer): string;
|
function SliceSlotOutDev(Slot: Integer): string;
|
||||||
function SliceSlotInDev(Slot: Integer): string;
|
function SliceSlotInDev(Slot: Integer): string;
|
||||||
procedure SetSliceSlotAudio(Slot: Integer; const OutDev, InDev: string);
|
procedure SetSliceSlotAudio(Slot: Integer; const OutDev, InDev: string);
|
||||||
// Заявка на передачу ОТ ИМЕНИ слайса (CAT-порт слайса). Единственная точка,
|
// Заявка на передачу ОТ ИМЕНИ слайса (CAT-порт слайса, TCI с номером
|
||||||
// где внешняя PTT может сменить TX-источник, поэтому здесь же — защита
|
// приёмника). Единственная точка, где внешняя PTT может сменить TX-источник,
|
||||||
// «в эфире одновременно только один слайс». Выполнять в потоке контроллера.
|
// поэтому здесь же — защита «в эфире одновременно только один слайс».
|
||||||
procedure RequestSliceTx(SliceId: Integer; On_: Boolean);
|
// Tune = True — то же самое, но несущей настройки (TUN) вместо модуляции.
|
||||||
|
// Выполнять в потоке контроллера.
|
||||||
|
procedure RequestSliceTx(SliceId: Integer; On_: Boolean;
|
||||||
|
Tune: Boolean = False);
|
||||||
// Диапазон захваченной полосы пана (0 = главный), куда можно ставить слайс.
|
// Диапазон захваченной полосы пана (0 = главный), куда можно ставить слайс.
|
||||||
procedure CapturedSpan(out LoHz, HiHz: Double; PanId: Integer = 0);
|
procedure CapturedSpan(out LoHz, HiHz: Double; PanId: Integer = 0);
|
||||||
function SliceFitsCapture(TargetHz: Double; PanId: Integer = 0): Boolean;
|
function SliceFitsCapture(TargetHz: Double; PanId: Integer = 0): Boolean;
|
||||||
@@ -3313,11 +3316,13 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TRadioController.RequestSliceTx(SliceId: Integer; On_: Boolean);
|
procedure TRadioController.RequestSliceTx(SliceId: Integer; On_: Boolean;
|
||||||
// PTT, пришедшая по CAT-порту слайса. Аналог «Auto Switch TX Slice» в SmartSDR
|
Tune: Boolean = False);
|
||||||
// CAT: с Auto TX слайс сам становится TX-источником, без него — передаёт,
|
// PTT, пришедшая по CAT-порту слайса или по TCI с номером приёмника. Аналог
|
||||||
// только если оператор уже выбрал его источником (бейдж TX на флаге).
|
// «Auto Switch TX Slice» в SmartSDR CAT: с Auto TX слайс сам становится
|
||||||
// Выполняется в потоке контроллера (CAT маршалит через Invoke), поэтому
|
// TX-источником, без него — передаёт, только если оператор уже выбрал его
|
||||||
|
// источником (бейдж TX на флаге).
|
||||||
|
// Выполняется в потоке контроллера (CAT и TCI маршалят через Invoke), поэтому
|
||||||
// проверка «уже в эфире» и смена источника атомарны относительно GUI.
|
// проверка «уже в эфире» и смена источника атомарны относительно GUI.
|
||||||
var idx: Integer;
|
var idx: Integer;
|
||||||
begin
|
begin
|
||||||
@@ -3347,7 +3352,7 @@ begin
|
|||||||
SetTxSlice(SliceId);
|
SetTxSlice(SliceId);
|
||||||
if FTxSliceId <> SliceId then Exit; // SetTxSlice отказал (DMR/нет слайса)
|
if FTxSliceId <> SliceId then Exit; // SetTxSlice отказал (DMR/нет слайса)
|
||||||
end;
|
end;
|
||||||
SetMOX(True);
|
if Tune then SetTune(True) else SetMOX(True);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// Снимок кадра для web-зеркала: потолок точек задаёт настройка web.spec_pixels
|
// Снимок кадра для web-зеркала: потолок точек задаёт настройка web.spec_pixels
|
||||||
@@ -5836,7 +5841,12 @@ var
|
|||||||
RateKsps: Word;
|
RateKsps: Word;
|
||||||
HangMs: Word;
|
HangMs: Word;
|
||||||
begin
|
begin
|
||||||
if not (FNetwork.Connected and FNetwork.Running) then Exit;
|
// Assigned обязателен: сюда приходят и по PTT/TUN, а нажать их можно и до
|
||||||
|
// того, как устройство выбрано, — тогда FNetwork ещё nil (то же и с движком
|
||||||
|
// ниже). Раньше на этом месте была Access violation, и разбирать её приходил
|
||||||
|
// уже кто-то другой: SetMOX зовёт SyncCWKeyer, а тот — эту процедуру.
|
||||||
|
if not (Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running) then Exit;
|
||||||
|
if not (FWDSPReady and Assigned(FDSPEngine)) then Exit;
|
||||||
FillChar(DUCPkt, SizeOf(DUCPkt), 0);
|
FillChar(DUCPkt, SizeOf(DUCPkt), 0);
|
||||||
// DUC0 rate в ksps берём из WDSP-движка (фиксированный TX rate, не RX).
|
// DUC0 rate в ksps берём из WDSP-движка (фиксированный TX rate, не RX).
|
||||||
RateKsps := FDSPEngine.TXSampleRate div 1000;
|
RateKsps := FDSPEngine.TXSampleRate div 1000;
|
||||||
@@ -5938,7 +5948,7 @@ begin
|
|||||||
FDriveLevel := CalcDriveByte;
|
FDriveLevel := CalcDriveByte;
|
||||||
if FWDSPReady then
|
if FWDSPReady then
|
||||||
FDSPEngine.SetDriveLevel(EnsureRange(FTXSettings.TUNLevel, 0, 100) / 100.0);
|
FDSPEngine.SetDriveLevel(EnsureRange(FTXSettings.TUNLevel, 0, 100) / 100.0);
|
||||||
if FNetwork.Connected and FNetwork.Running then
|
if Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
|
||||||
begin
|
begin
|
||||||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), TXTuneFreqHz, FDriveLevel,
|
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), TXTuneFreqHz, FDriveLevel,
|
||||||
FTransmitting, True, True, FTuning, FTwoTone);
|
FTransmitting, True, True, FTuning, FTwoTone);
|
||||||
|
|||||||
+345
-157
@@ -47,7 +47,16 @@ uses
|
|||||||
DXSpotStore, TCIProtocol, TCIServer, TCIStreams;
|
DXSpotStore, TCIProtocol, TCIServer, TCIStreams;
|
||||||
|
|
||||||
const
|
const
|
||||||
TCI_MAX_RX = MAX_PANS; // приёмник TCI = панадаптер
|
// ★Приёмник TCI: 0 — главный тракт (каналы A/B = VFO A/B), N — слайс СЛОТА
|
||||||
|
// N−1, то есть буквы B..G с флага, независимо от того, на каком он пане.
|
||||||
|
// Панадаптером приёмник быть перестал: у Pluto пан ровно один (MaxPans=1),
|
||||||
|
// и «второй приёмник» там существует только как слайс главного пана — при
|
||||||
|
// нумерации по панам он был бы недоступен вовсе. А клиенты умеют мало
|
||||||
|
// номеров: у MSHV в настройках всего rx1/rx2, то есть приёмники 0 и 1.
|
||||||
|
// Со слотами правило «первый добавленный слайс = приёмник 1» держится на
|
||||||
|
// любом железе: на openHPSDR слайс второго пана и на Pluto слайс главного
|
||||||
|
// одинаково занимают слот B. Пан стал свойством слайса (его центр и IQ).
|
||||||
|
TCI_MAX_RX = 1 + MAX_SLICES;
|
||||||
// Аудио на выходе движка всегда 48 кГц (TWDSPEngine.Create), от него и
|
// Аудио на выходе движка всегда 48 кГц (TWDSPEngine.Create), от него и
|
||||||
// считаются все прореживания и пересчёты потоков.
|
// считаются все прореживания и пересчёты потоков.
|
||||||
TCI_AUDIO_ENGINE_RATE = 48000;
|
TCI_AUDIO_ENGINE_RATE = 48000;
|
||||||
@@ -170,6 +179,8 @@ type
|
|||||||
FsInt3: Integer;
|
FsInt3: Integer;
|
||||||
FsBool: Boolean;
|
FsBool: Boolean;
|
||||||
FsBool2: Boolean;
|
FsBool2: Boolean;
|
||||||
|
FsBool3: Boolean;
|
||||||
|
FsRes: Boolean; // ОБРАТНО из Sync-метода (под тем же FLock)
|
||||||
FsStr: string;
|
FsStr: string;
|
||||||
|
|
||||||
// ── Sync-методы (поток контроллера) ──
|
// ── Sync-методы (поток контроллера) ──
|
||||||
@@ -179,7 +190,6 @@ type
|
|||||||
procedure SyncSetFilter;
|
procedure SyncSetFilter;
|
||||||
procedure SyncSetTRX;
|
procedure SyncSetTRX;
|
||||||
procedure SyncStopTRX;
|
procedure SyncStopTRX;
|
||||||
procedure SyncSetTune;
|
|
||||||
procedure SyncSetDrive;
|
procedure SyncSetDrive;
|
||||||
procedure SyncSetTuneDrive;
|
procedure SyncSetTuneDrive;
|
||||||
procedure SyncSetSplit;
|
procedure SyncSetSplit;
|
||||||
@@ -235,6 +245,9 @@ type
|
|||||||
function RxCount: Integer;
|
function RxCount: Integer;
|
||||||
function ValidRx(Rx: Integer): Boolean;
|
function ValidRx(Rx: Integer): Boolean;
|
||||||
function RxActive(Rx: Integer): Boolean;
|
function RxActive(Rx: Integer): Boolean;
|
||||||
|
function LiveRx(Rx: Integer): Boolean; // номер в потолке И приёмник существует
|
||||||
|
function RxSlot(Rx: Integer): Integer; // слот слайса; −1 = главный тракт
|
||||||
|
function RxPanId(Rx: Integer): Integer; // пан приёмника; −1 = приёмника нет
|
||||||
procedure CalcFreqLimits(out LoHz, HiHz: Double); // живые (поток контроллера)
|
procedure CalcFreqLimits(out LoHz, HiHz: Double); // живые (поток контроллера)
|
||||||
procedure FreqLimits(out LoHz, HiHz: Double); // из снимка (любой поток)
|
procedure FreqLimits(out LoHz, HiHz: Double); // из снимка (любой поток)
|
||||||
function FreqSaneLive(Hz: Double): Boolean; // проверка перед установкой
|
function FreqSaneLive(Hz: Double): Boolean; // проверка перед установкой
|
||||||
@@ -250,6 +263,7 @@ type
|
|||||||
function SliceIdLive(Rx, Ch: Integer): Integer; // из живой таблицы (Sync*)
|
function SliceIdLive(Rx, Ch: Integer): Integer; // из живой таблицы (Sync*)
|
||||||
function RxSlice(Rx: Integer; out S: TSliceView): Boolean;
|
function RxSlice(Rx: Integer; out S: TSliceView): Boolean;
|
||||||
function SliceRxCh(Id: Integer; out Rx, Ch: Integer): Boolean;
|
function SliceRxCh(Id: Integer; out Rx, Ch: Integer): Boolean;
|
||||||
|
function TxRx: Integer; // приёмник, чей слайс сейчас источник передачи
|
||||||
{ Захват параметра (§3.5). True — параметр наш (свободен, наш или отпущен
|
{ Захват параметра (§3.5). True — параметр наш (свободен, наш или отпущен
|
||||||
по таймауту), захват продлевается. False — им сейчас управляет другой. }
|
по таймауту), захват продлевается. False — им сейчас управляет другой. }
|
||||||
function Claim(const Key: string; Owner: TTCIClient): Boolean;
|
function Claim(const Key: string; Owner: TTCIClient): Boolean;
|
||||||
@@ -277,8 +291,8 @@ type
|
|||||||
function StrDds(Rx: Integer): string;
|
function StrDds(Rx: Integer): string;
|
||||||
function StrModulation(Rx: Integer): string;
|
function StrModulation(Rx: Integer): string;
|
||||||
function StrFilterBand(Rx: Integer): string;
|
function StrFilterBand(Rx: Integer): string;
|
||||||
function StrTrx: string;
|
function StrTrx(Rx: Integer): string;
|
||||||
function StrTune: string;
|
function StrTune(Rx: Integer): string;
|
||||||
function StrDrive: string;
|
function StrDrive: string;
|
||||||
function StrVolume: string;
|
function StrVolume: string;
|
||||||
function StrMute: string;
|
function StrMute: string;
|
||||||
@@ -294,6 +308,7 @@ type
|
|||||||
function StrANF(Rx: Integer): string;
|
function StrANF(Rx: Integer): string;
|
||||||
{ Полный набор строк по приёмнику — рассылка после правки слайса. }
|
{ Полный набор строк по приёмнику — рассылка после правки слайса. }
|
||||||
procedure BroadcastRxState(Rx, Ch: Integer);
|
procedure BroadcastRxState(Rx, Ch: Integer);
|
||||||
|
procedure BroadcastTxEnable; // всем живым приёмникам, каждому со своим номером
|
||||||
|
|
||||||
procedure SendInit(Client: TTCIClient);
|
procedure SendInit(Client: TTCIClient);
|
||||||
procedure SendState(Client: TTCIClient);
|
procedure SendState(Client: TTCIClient);
|
||||||
@@ -480,14 +495,15 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
{ ═══════════════════════════════════════════════════════════════════════════
|
{ ═══════════════════════════════════════════════════════════════════════════
|
||||||
Модель: приёмник = панадаптер, канал = VFO A/B (пан 0) или слайс (паны 1..)
|
Модель: приёмник 0 = главный тракт (каналы = VFO A/B), приёмник N = слайс
|
||||||
|
слота N−1 (буквы B..G). См. комментарий у TCI_MAX_RX.
|
||||||
═══════════════════════════════════════════════════════════════════════════ }
|
═══════════════════════════════════════════════════════════════════════════ }
|
||||||
|
|
||||||
function TTCIAdapter.RxCount: Integer;
|
function TTCIAdapter.RxCount: Integer;
|
||||||
|
// Потолок, а не число живых: TRX_COUNT протокол объявляет один раз, при
|
||||||
|
// подключении (§4.1). Про приёмник, которого сейчас нет, просто молчим.
|
||||||
begin
|
begin
|
||||||
Result := DevSnap.MaxPans;
|
Result := TCI_MAX_RX;
|
||||||
if Result < 1 then Result := 1;
|
|
||||||
if Result > TCI_MAX_RX then Result := TCI_MAX_RX;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TTCIAdapter.ValidRx(Rx: Integer): Boolean;
|
function TTCIAdapter.ValidRx(Rx: Integer): Boolean;
|
||||||
@@ -495,13 +511,34 @@ begin
|
|||||||
Result := (Rx >= 0) and (Rx < RxCount);
|
Result := (Rx >= 0) and (Rx < RxCount);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TTCIAdapter.RxActive(Rx: Integer): Boolean;
|
function TTCIAdapter.LiveRx(Rx: Integer): Boolean;
|
||||||
// Приёмник существует физически. TRX_COUNT в протоколе объявляется один раз и
|
// Адресат команды. Про приёмник, которого нет (слот пуст), молчим целиком:
|
||||||
// равен потолку железа, но пан из этого потолка может быть ещё не создан —
|
// ответ с нулями клиент принял бы за настоящее состояние.
|
||||||
// врать про его частоту нельзя (см. SendState).
|
|
||||||
begin
|
begin
|
||||||
if Rx = 0 then Result := True
|
Result := ValidRx(Rx) and RxActive(Rx);
|
||||||
else Result := ValidRx(Rx) and FController.PanDDCActive(Rx);
|
end;
|
||||||
|
|
||||||
|
function TTCIAdapter.RxSlot(Rx: Integer): Integer;
|
||||||
|
// Слот слайса, стоящего за приёмником Rx. −1 — это главный тракт (rx0) либо
|
||||||
|
// номер вне потолка.
|
||||||
|
begin
|
||||||
|
if (Rx <= 0) or (Rx >= TCI_MAX_RX) then Result := -1 else Result := Rx - 1;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TTCIAdapter.RxActive(Rx: Integer): Boolean;
|
||||||
|
// Приёмник существует. Для слайса это значит «слот занят»: удалили слайс —
|
||||||
|
// приёмник исчез, и врать про его частоту нельзя (см. SendState).
|
||||||
|
var Slot: Integer;
|
||||||
|
begin
|
||||||
|
if Rx = 0 then Exit(True);
|
||||||
|
Slot := RxSlot(Rx);
|
||||||
|
if Slot < 0 then Exit(False);
|
||||||
|
FSliceLock.Enter;
|
||||||
|
try
|
||||||
|
Result := FSliceSnap[Slot].Used;
|
||||||
|
finally
|
||||||
|
FSliceLock.Leave;
|
||||||
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TTCIAdapter.CalcFreqLimits(out LoHz, HiHz: Double);
|
procedure TTCIAdapter.CalcFreqLimits(out LoHz, HiHz: Double);
|
||||||
@@ -670,40 +707,51 @@ begin
|
|||||||
begin
|
begin
|
||||||
if not RxActive(Rx) then Continue;
|
if not RxActive(Rx) then Continue;
|
||||||
N := ChanCount(Rx);
|
N := ChanCount(Rx);
|
||||||
|
FServer.Broadcast(StrDds(Rx));
|
||||||
for Ch := 0 to N - 1 do BroadcastRxState(Rx, Ch);
|
for Ch := 0 to N - 1 do BroadcastRxState(Rx, Ch);
|
||||||
FServer.Broadcast(TCIBuild('rx_channel_enable',
|
FServer.Broadcast(TCIBuild('rx_channel_enable',
|
||||||
[TCIIntStr(Rx), '1', TCIBoolStr(N > 1)]));
|
[TCIIntStr(Rx), '1', TCIBoolStr(N > 1)]));
|
||||||
|
// ★С адресом приёмника — иначе клиент его не увидит. Клиенты фильтруют
|
||||||
|
// входящие по arg1 (см. StrTrx), а MSHV на этом ещё и завязывает передачу:
|
||||||
|
// `set_ptt` начинается с `if (!tci_tx_enable) return;`. Слайс, созданный
|
||||||
|
// ПОСЛЕ подключения клиента, без этой строки получал бы молча мёртвую PTT.
|
||||||
|
FServer.Broadcast(StrTxEnable(Rx));
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TTCIAdapter.SnapSlice(Rx, Ch: Integer; out V: TSliceView): Boolean;
|
function TTCIAdapter.SnapSlice(Rx, Ch: Integer; out V: TSliceView): Boolean;
|
||||||
// Слайс канала Ch приёмника Rx из снимка. Для Rx=0 слайса нет: главный тракт
|
// Слайс приёмника Rx из снимка. У приёмника-слайса канал один — A: второго
|
||||||
// живёт в полях контроллера.
|
// «саб-приёмника» внутри слайса не бывает. Для Rx=0 слайса нет вовсе: главный
|
||||||
var i, Seen: Integer;
|
// тракт живёт в полях контроллера.
|
||||||
|
var Slot: Integer;
|
||||||
begin
|
begin
|
||||||
Result := False;
|
Result := False;
|
||||||
FillChar(V, SizeOf(V), 0);
|
FillChar(V, SizeOf(V), 0);
|
||||||
if (Rx <= 0) or (Ch < 0) then Exit;
|
Slot := RxSlot(Rx);
|
||||||
Seen := 0;
|
if (Slot < 0) or (Ch <> 0) then Exit;
|
||||||
FSliceLock.Enter;
|
FSliceLock.Enter;
|
||||||
try
|
try
|
||||||
for i := 0 to MAX_SLICES - 1 do
|
if FSliceSnap[Slot].Used then
|
||||||
if FSliceSnap[i].Used and (FSliceSnap[i].V.PanId = Rx) then
|
|
||||||
begin
|
begin
|
||||||
if Seen = Ch then
|
V := FSliceSnap[Slot].V;
|
||||||
begin
|
Result := True;
|
||||||
V := FSliceSnap[i].V;
|
|
||||||
Exit(True);
|
|
||||||
end;
|
|
||||||
Inc(Seen);
|
|
||||||
end;
|
end;
|
||||||
finally
|
finally
|
||||||
FSliceLock.Leave;
|
FSliceLock.Leave;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function TTCIAdapter.RxPanId(Rx: Integer): Integer;
|
||||||
|
// На каком пане живёт приёмник: у главного тракта это пан 0, у слайса — тот,
|
||||||
|
// где он стоит. Отсюда берутся его центр (DDS) и поток IQ.
|
||||||
|
var V: TSliceView;
|
||||||
|
begin
|
||||||
|
if Rx = 0 then Exit(0);
|
||||||
|
if SnapSlice(Rx, 0, V) then Result := V.PanId else Result := -1;
|
||||||
|
end;
|
||||||
|
|
||||||
function TTCIAdapter.SliceIdOf(Rx, Ch: Integer): Integer;
|
function TTCIAdapter.SliceIdOf(Rx, Ch: Integer): Integer;
|
||||||
// Слайсы пана Rx по порядку в таблице: 0-й = канал A, 1-й = канал B.
|
// Слайс приёмника Rx (канал A) из снимка.
|
||||||
var V: TSliceView;
|
var V: TSliceView;
|
||||||
begin
|
begin
|
||||||
if SnapSlice(Rx, Ch, V) then Result := V.Id else Result := 0;
|
if SnapSlice(Rx, Ch, V) then Result := V.Id else Result := 0;
|
||||||
@@ -713,9 +761,12 @@ function TTCIAdapter.SliceIdLive(Rx, Ch: Integer): Integer;
|
|||||||
// То же, но из живой таблицы. Только для Sync-методов: они идут в потоке
|
// То же, но из живой таблицы. Только для Sync-методов: они идут в потоке
|
||||||
// контроллера, где чтение безопасно, а снимок мог бы отстать на такт — команда
|
// контроллера, где чтение безопасно, а снимок мог бы отстать на такт — команда
|
||||||
// на установку обязана попасть в тот слайс, который есть сейчас.
|
// на установку обязана попасть в тот слайс, который есть сейчас.
|
||||||
|
var Slot: Integer;
|
||||||
begin
|
begin
|
||||||
if Rx <= 0 then Result := 0
|
Result := 0;
|
||||||
else Result := FController.PanSliceId(Rx, Ch);
|
Slot := RxSlot(Rx);
|
||||||
|
if (Slot < 0) or (Ch <> 0) then Exit;
|
||||||
|
Result := FController.SliceIdBySlot(Slot);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TTCIAdapter.RxSlice(Rx: Integer; out S: TSliceView): Boolean;
|
function TTCIAdapter.RxSlice(Rx: Integer; out S: TSliceView): Boolean;
|
||||||
@@ -726,35 +777,21 @@ end;
|
|||||||
|
|
||||||
function TTCIAdapter.SliceRxCh(Id: Integer; out Rx, Ch: Integer): Boolean;
|
function TTCIAdapter.SliceRxCh(Id: Integer; out Rx, Ch: Integer): Boolean;
|
||||||
// Обратное отображение слайс → (приёмник, канал). Нужно уведомлениям: контроллер
|
// Обратное отображение слайс → (приёмник, канал). Нужно уведомлениям: контроллер
|
||||||
// сообщает об изменении Id, а клиенту адресуются номера TCI.
|
// сообщает об изменении Id, а клиенту адресуются номера TCI. Слот слайса и есть
|
||||||
var i, Seen, Pan: Integer;
|
// его номер приёмника — искать по панам больше нечего.
|
||||||
|
var i: Integer;
|
||||||
begin
|
begin
|
||||||
Result := False;
|
Result := False;
|
||||||
Rx := 0; Ch := 0;
|
Rx := 0; Ch := 0;
|
||||||
if Id <= 0 then Exit;
|
if Id <= 0 then Exit;
|
||||||
Pan := -1;
|
|
||||||
FSliceLock.Enter;
|
FSliceLock.Enter;
|
||||||
try
|
try
|
||||||
for i := 0 to MAX_SLICES - 1 do
|
for i := 0 to MAX_SLICES - 1 do
|
||||||
if FSliceSnap[i].Used and (FSliceSnap[i].V.Id = Id) then
|
if FSliceSnap[i].Used and (FSliceSnap[i].V.Id = Id) then
|
||||||
begin
|
begin
|
||||||
Pan := FSliceSnap[i].V.PanId;
|
Rx := i + 1;
|
||||||
Break;
|
Ch := 0;
|
||||||
end;
|
Exit(True);
|
||||||
// Пан 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;
|
end;
|
||||||
finally
|
finally
|
||||||
FSliceLock.Leave;
|
FSliceLock.Leave;
|
||||||
@@ -830,22 +867,11 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
function TTCIAdapter.ChanCount(Rx: Integer): Integer;
|
function TTCIAdapter.ChanCount(Rx: Integer): Integer;
|
||||||
var i: Integer;
|
// У главного тракта два канала (VFO A/B), у приёмника-слайса — один: канал B
|
||||||
|
// в протоколе это второй саб-приёмник, а внутри слайса такого нет.
|
||||||
begin
|
begin
|
||||||
if Rx = 0 then begin Result := 2; Exit; end; // VFO A/B всегда есть
|
if Rx = 0 then Exit(2);
|
||||||
Result := 0;
|
if RxActive(Rx) then Result := 1 else 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;
|
end;
|
||||||
|
|
||||||
function TTCIAdapter.ChanFreq(Rx, Ch: Integer): Double;
|
function TTCIAdapter.ChanFreq(Rx, Ch: Integer): Double;
|
||||||
@@ -857,15 +883,21 @@ begin
|
|||||||
if Ch = 1 then Result := FController.FVfoB else Result := FController.FVfoA;
|
if Ch = 1 then Result := FController.FVfoB else Result := FController.FVfoA;
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
if SnapSlice(Rx, Ch, S) then Exit(S.TargetHz);
|
// Слайса нет — приёмника нет: частоту не выдумываем (SendState про такой
|
||||||
// Слайса нет: у живого пана канал A стоит на его центре (см. ChanCount).
|
// номер молчит целиком).
|
||||||
if (Ch = 0) and RxActive(Rx) then Result := RxCenterHz(Rx);
|
if SnapSlice(Rx, Ch, S) then Result := S.TargetHz;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TTCIAdapter.RxCenterHz(Rx: Integer): Double;
|
function TTCIAdapter.RxCenterHz(Rx: Integer): Double;
|
||||||
|
// DDS приёмника — центр панорамы, на которой он живёт. У слайса это центр его
|
||||||
|
// пана: для главного пана — FCenterFreq, для дополнительного — его DDC.
|
||||||
|
var Pan: Integer;
|
||||||
begin
|
begin
|
||||||
if Rx = 0 then Result := FController.FCenterFreq
|
Result := 0;
|
||||||
else Result := FController.PanDDCFreq(Rx);
|
Pan := RxPanId(Rx);
|
||||||
|
if Pan < 0 then Exit;
|
||||||
|
if Pan = 0 then Result := FController.FCenterFreq
|
||||||
|
else Result := FController.PanDDCFreq(Pan);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TTCIAdapter.RxMode(Rx: Integer): Integer;
|
function TTCIAdapter.RxMode(Rx: Integer): Integer;
|
||||||
@@ -1002,14 +1034,36 @@ begin
|
|||||||
Result := TCIBuild('rx_filter_band', [TCIIntStr(Rx), TCIIntStr(Lo), TCIIntStr(Hi)]);
|
Result := TCIBuild('rx_filter_band', [TCIIntStr(Rx), TCIIntStr(Lo), TCIIntStr(Hi)]);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TTCIAdapter.StrTrx: string;
|
function TTCIAdapter.TxRx: Integer;
|
||||||
|
// Номер приёмника, чей слайс СЕЙЧАС источник передачи (0 = главный VFO).
|
||||||
|
// Ответы и уведомления TRX/TUNE обязаны называть именно его: клиент на
|
||||||
|
// приёмнике 1, получивший «trx:0,true», решил бы, что его команда не прошла, а
|
||||||
|
// клиент на приёмнике 0 — что в эфире он, хотя передаёт чужой слайс.
|
||||||
|
var
|
||||||
|
Rx, Ch: Integer;
|
||||||
begin
|
begin
|
||||||
Result := TCIBuild('trx', ['0', TCIBoolStr(FController.FTransmitting)]);
|
Result := 0;
|
||||||
|
if FController = nil then Exit;
|
||||||
|
if (FController.TxSliceId > 0) and SliceRxCh(FController.TxSliceId, Rx, Ch) then
|
||||||
|
Result := Rx;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TTCIAdapter.StrTune: string;
|
function TTCIAdapter.StrTrx(Rx: Integer): string;
|
||||||
|
// Состояние передатчика ДЛЯ ПРИЁМНИКА Rx: «true» значит, что в эфире именно
|
||||||
|
// его слайс. Клиенты фильтруют входящие строки по номеру приёмника (MSHV,
|
||||||
|
// например, отбрасывает всё, что адресовано не ему), поэтому автору команды
|
||||||
|
// отвечаем ЕГО номером: `trx:1,false` — это «твоя заявка не прошла», а
|
||||||
|
// `trx:0,false` он бы просто не увидел. В рассылку уходит номер того, чей
|
||||||
|
// слайс сейчас источник передачи (`TxRx`).
|
||||||
begin
|
begin
|
||||||
Result := TCIBuild('tune', ['0', TCIBoolStr(FController.FTuning)]);
|
Result := TCIBuild('trx', [TCIIntStr(Rx),
|
||||||
|
TCIBoolStr(FController.FTransmitting and (TxRx = Rx))]);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TTCIAdapter.StrTune(Rx: Integer): string;
|
||||||
|
begin
|
||||||
|
Result := TCIBuild('tune', [TCIIntStr(Rx),
|
||||||
|
TCIBoolStr(FController.FTuning and (TxRx = Rx))]);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TTCIAdapter.StrDrive: string;
|
function TTCIAdapter.StrDrive: string;
|
||||||
@@ -1107,6 +1161,18 @@ begin
|
|||||||
FServer.Broadcast(StrSqlLevel(Rx));
|
FServer.Broadcast(StrSqlLevel(Rx));
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TTCIAdapter.BroadcastTxEnable;
|
||||||
|
// TX_ENABLE — величина всего радио, но адресуется приёмником, и клиент читает
|
||||||
|
// только строки со СВОИМ номером. Одной строки `tx_enable:0,…` мало: клиент на
|
||||||
|
// приёмнике 1 её отбрасывает, а у MSHV на этом флаге висит вся передача
|
||||||
|
// (`set_ptt`: `if (!tci_tx_enable) return;`).
|
||||||
|
var Rx: Integer;
|
||||||
|
begin
|
||||||
|
if (FServer = nil) or (FServer.ClientCount = 0) then Exit;
|
||||||
|
for Rx := 0 to RxCount - 1 do
|
||||||
|
if RxActive(Rx) then FServer.Broadcast(StrTxEnable(Rx));
|
||||||
|
end;
|
||||||
|
|
||||||
{ ═══════════════════════════════════════════════════════════════════════════
|
{ ═══════════════════════════════════════════════════════════════════════════
|
||||||
Подключение клиента: инициализация + текущее состояние
|
Подключение клиента: инициализация + текущее состояние
|
||||||
═══════════════════════════════════════════════════════════════════════════ }
|
═══════════════════════════════════════════════════════════════════════════ }
|
||||||
@@ -1206,8 +1272,9 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
Reply(Client, TCIBuild('split_enable', ['0', TCIBoolStr(FController.FSplitTxB)]));
|
Reply(Client, TCIBuild('split_enable', ['0', TCIBoolStr(FController.FSplitTxB)]));
|
||||||
Reply(Client, StrTrx);
|
// В пачке инициализации называем того, чей слайс сейчас источник передачи.
|
||||||
Reply(Client, StrTune);
|
Reply(Client, StrTrx(TxRx));
|
||||||
|
Reply(Client, StrTune(TxRx));
|
||||||
Reply(Client, StrDrive);
|
Reply(Client, StrDrive);
|
||||||
Reply(Client, TCIBuild('tune_drive', ['0', TCIIntStr(FController.FTXSettings.TUNLevel)]));
|
Reply(Client, TCIBuild('tune_drive', ['0', TCIIntStr(FController.FTXSettings.TUNLevel)]));
|
||||||
Reply(Client, StrVolume);
|
Reply(Client, StrVolume);
|
||||||
@@ -1295,11 +1362,15 @@ end;
|
|||||||
|
|
||||||
procedure TTCIAdapter.SyncSetCenter;
|
procedure TTCIAdapter.SyncSetCenter;
|
||||||
begin
|
begin
|
||||||
// Окончательная проверка — здесь: см. FreqSaneLive. Ни SetCenter, ни
|
// Окончательная проверка — здесь: см. FreqSaneLive. SetCenter границ не
|
||||||
// SetPanDDCFreq границ не клампят, число уходит прямо в backend.
|
// клампит, число уходит прямо в backend.
|
||||||
if not FreqSaneLive(FsFreq) then Exit;
|
if not FreqSaneLive(FsFreq) then Exit;
|
||||||
if FsInt = 0 then FController.SetCenter(FsFreq)
|
// Двигать можно только центр главной панорамы — она и есть приёмник 0.
|
||||||
else FController.SetPanDDCFreq(FsInt, FsFreq);
|
// У приёмника-слайса DDS только читается: панорама под ним общая, и увести
|
||||||
|
// её по просьбе одного клиента значит утащить за собой всех соседей по пану
|
||||||
|
// и картинку оператора. Слайсу двигаться незачем — за окном DDC следит
|
||||||
|
// TuneSliceInBand (см. SyncSetVfo).
|
||||||
|
if FsInt = 0 then FController.SetCenter(FsFreq);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TTCIAdapter.SyncSetMode;
|
procedure TTCIAdapter.SyncSetMode;
|
||||||
@@ -1319,12 +1390,71 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TTCIAdapter.SyncSetTRX;
|
procedure TTCIAdapter.SyncSetTRX;
|
||||||
|
// Поток контроллера. FsInt — номер приёмника из arg1, FsBool3 — это TUNE.
|
||||||
|
// Наружу отдаёт FsRes: «эфир подняла ИМЕННО ЭТА команда» — по нему в потоке
|
||||||
|
// клиента назначается хозяин передачи.
|
||||||
|
//
|
||||||
|
// ★Все решения принимаются здесь, а не в потоке клиента. Между разбором
|
||||||
|
// команды и её исполнением оператор успевает нажать PTT, и снимок «шла ли
|
||||||
|
// передача», снятый заранее, соврал бы — а по нему назначается хозяин эфира,
|
||||||
|
// то есть право снять чужую передачу.
|
||||||
|
var
|
||||||
|
Id: Integer;
|
||||||
|
WasTx, Started: Boolean;
|
||||||
begin
|
begin
|
||||||
|
FsRes := False;
|
||||||
|
WasTx := FController.FTransmitting or FController.FTuning;
|
||||||
|
|
||||||
// Источник модуляции ставим ДО SetMOX — именно он его и читает при выборе
|
// Источник модуляции ставим ДО SetMOX — именно он его и читает при выборе
|
||||||
// микрофона. Выключение передачи флаг снимает всегда: следующий раз оператор
|
// микрофона. Выключение передачи флаг снимает всегда: следующий раз оператор
|
||||||
// может нажать PTT сам, и тогда в эфир должен идти его микрофон.
|
// может нажать PTT сам, и тогда в эфир должен идти его микрофон.
|
||||||
FController.TCIMicRequested := FsBool and FsBool2;
|
// ★Ставим его ТОЛЬКО когда эфир поднимаем мы сами. SetMOX(True) поверх уже
|
||||||
|
// идущей передачи не выходит рано — он заново выбирает микрофон, и
|
||||||
|
// «trx:0,true,tci» посреди передачи оператора молча уводил модуляцию в ринг
|
||||||
|
// TCI. Хозяином эфира клиент при этом не становился, так что его уход
|
||||||
|
// оставлял в эфире несущую с тишиной. У TUNE источника нет вовсе (несущую
|
||||||
|
// даёт генератор тона), и трогать чужой выбор микрофона он не вправе.
|
||||||
|
if not FsBool3 then
|
||||||
|
begin
|
||||||
|
if not FsBool then FController.TCIMicRequested := False
|
||||||
|
else if not WasTx then FController.TCIMicRequested := FsBool2;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// Приёмник 0 — это главный VFO, то есть «радио целиком»: ведём себя как
|
||||||
|
// обычный CAT-порт (в эфир уходит выбранный оператором TX-источник). Приёмник
|
||||||
|
// N > 0 адресует слайс канала A своего пана — заявка идёт через контроллер,
|
||||||
|
// который один имеет право сменить TX-источник.
|
||||||
|
Id := SliceIdLive(FsInt, 0);
|
||||||
|
if FsBool and WasTx then
|
||||||
|
begin
|
||||||
|
// Передатчик занят — чужую передачу не перехватываем и поверх неё ничего не
|
||||||
|
// включаем: то же правило, что внутри RequestSliceTx, только оно обязано
|
||||||
|
// работать и для приёмника 0. Иначе «tune:0,true» подмешивал настроечный
|
||||||
|
// тон в чужую передачу, а хозяином TUN не становился никто — и после ухода
|
||||||
|
// клиента тон оставался в эфире. Своя же передача и так идёт: повторная
|
||||||
|
// команда — no-op.
|
||||||
|
end
|
||||||
|
else if FsInt > 0 then
|
||||||
|
begin
|
||||||
|
// Пан жив, но слайса на канале A может не быть (пан без слайсов показывает
|
||||||
|
// центр) — тогда просьба просто не исполняется. Подменять её главным VFO
|
||||||
|
// нельзя: это чужая частота, а то и чужой диапазон.
|
||||||
|
if Id > 0 then FController.RequestSliceTx(Id, FsBool, FsBool3);
|
||||||
|
end
|
||||||
|
else if FsBool3 then
|
||||||
|
FController.SetTune(FsBool)
|
||||||
|
else
|
||||||
FController.SetMOX(FsBool);
|
FController.SetMOX(FsBool);
|
||||||
|
|
||||||
|
Started := FsBool and (not WasTx) and
|
||||||
|
(FController.FTransmitting or FController.FTuning);
|
||||||
|
FsRes := Started;
|
||||||
|
// Заявку могли отклонить (нет Auto TX у слайса, DMR, запрет передачи на
|
||||||
|
// диапазоне) — тогда снимаем и источник модуляции: иначе следующая PTT
|
||||||
|
// оператора ушла бы в эфир с микрофоном ушедшего клиента вместо его
|
||||||
|
// собственного.
|
||||||
|
if FsBool and (not FsBool3) and (not WasTx) and (not Started) then
|
||||||
|
FController.TCIMicRequested := False;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TTCIAdapter.SyncStopTRX;
|
procedure TTCIAdapter.SyncStopTRX;
|
||||||
@@ -1332,9 +1462,14 @@ procedure TTCIAdapter.SyncStopTRX;
|
|||||||
// решение принято там, где известно, кто её начал, а здесь остаётся только
|
// решение принято там, где известно, кто её начал, а здесь остаётся только
|
||||||
// проверить, что она вообще идёт (оператор мог отпустить PTT сам).
|
// проверить, что она вообще идёт (оператор мог отпустить PTT сам).
|
||||||
begin
|
begin
|
||||||
if (FController = nil) or not FController.FTransmitting then Exit;
|
if FController = nil then Exit;
|
||||||
|
if not (FController.FTransmitting or FController.FTuning) then Exit;
|
||||||
FController.TCIMicRequested := False;
|
FController.TCIMicRequested := False;
|
||||||
FController.SetMOX(False);
|
// TUN снимаем именно SetTune: он гасит и тон, и PTT, и возвращает drive.
|
||||||
|
// SetMOX(False) поверх включённого TUN оставил бы взведённым генератор тона,
|
||||||
|
// а FTuning — поднятым, то есть аппарат в состоянии, которого на экране нет.
|
||||||
|
if FController.FTuning then FController.SetTune(False)
|
||||||
|
else FController.SetMOX(False);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TTCIAdapter.SyncTaps;
|
procedure TTCIAdapter.SyncTaps;
|
||||||
@@ -1352,11 +1487,6 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TTCIAdapter.SyncSetTune;
|
|
||||||
begin
|
|
||||||
FController.SetTune(FsBool);
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TTCIAdapter.SyncSetDrive;
|
procedure TTCIAdapter.SyncSetDrive;
|
||||||
begin
|
begin
|
||||||
FController.SetDrive(FsInt);
|
FController.SetDrive(FsInt);
|
||||||
@@ -1546,6 +1676,11 @@ begin
|
|||||||
if not TCITryArgInt(M, 1, Ch) then Ch := 0;
|
if not TCITryArgInt(M, 1, Ch) then Ch := 0;
|
||||||
if not ValidRx(Rx) then Exit;
|
if not ValidRx(Rx) then Exit;
|
||||||
if (Ch < 0) or (Ch >= TCI_CHANNELS) then Exit;
|
if (Ch < 0) or (Ch >= TCI_CHANNELS) then Exit;
|
||||||
|
// ★Про приёмник, которого нет (слайс не создан или уже удалён), молчим
|
||||||
|
// целиком — и на чтение тоже. Ответ «vfo:1,0,0» клиент принял бы за
|
||||||
|
// настоящую частоту, а MSHV именно этим ответом завершает инициализацию
|
||||||
|
// (network.cpp, Network::initAll) — то есть подключился бы к пустоте.
|
||||||
|
if (Ch >= ChanCount(Rx)) or not RxActive(Rx) then Exit;
|
||||||
|
|
||||||
// Частота ставится, только если её удалось разобрать И она годная: «vfo:0,0,abc»
|
// Частота ставится, только если её удалось разобрать И она годная: «vfo:0,0,abc»
|
||||||
// раньше превращалось в честный ноль и уводило приёмник на 0 Гц.
|
// раньше превращалось в честный ноль и уводило приёмник на 0 Гц.
|
||||||
@@ -1682,7 +1817,7 @@ procedure TTCIAdapter.DispatchCommand(Client: TTCIClient; const M: TTCIMessage);
|
|||||||
var
|
var
|
||||||
Rx, Ch, V: Integer;
|
Rx, Ch, V: Integer;
|
||||||
Lo, Hi: Integer;
|
Lo, Hi: Integer;
|
||||||
B, FromTCI: Boolean;
|
B, FromTCI, Tune, Started: Boolean;
|
||||||
D: Double;
|
D: Double;
|
||||||
Name: string;
|
Name: string;
|
||||||
begin
|
begin
|
||||||
@@ -1708,8 +1843,14 @@ begin
|
|||||||
if M.Name = 'DDS' then
|
if M.Name = 'DDS' then
|
||||||
begin
|
begin
|
||||||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||||||
if not ValidRx(Rx) then Exit;
|
if not LiveRx(Rx) then Exit;
|
||||||
if (M.ArgCount >= 2) and TCITryArgFloat(M, 1, D) and FreqSane(D) and
|
// Двигать центр можно только у приёмника 0 — у главной панорамы. У
|
||||||
|
// приёмника-слайса DDS читается (центр его пана), но не пишется: панорама
|
||||||
|
// под ним общая, и увести её по просьбе одного клиента значит утащить
|
||||||
|
// соседей по пану и картинку оператора. Ответ уходит всегда — с текущим
|
||||||
|
// значением, как и у AGC_GAIN доп. приёмника (§3.1).
|
||||||
|
if (Rx = 0) and
|
||||||
|
(M.ArgCount >= 2) and TCITryArgFloat(M, 1, D) and FreqSane(D) and
|
||||||
Claim(HoldKey('DDS', Rx, 0), Client) then
|
Claim(HoldKey('DDS', Rx, 0), Client) then
|
||||||
begin
|
begin
|
||||||
FLock.Enter;
|
FLock.Enter;
|
||||||
@@ -1726,7 +1867,7 @@ begin
|
|||||||
if M.Name = 'MODULATION' then
|
if M.Name = 'MODULATION' then
|
||||||
begin
|
begin
|
||||||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||||||
if not ValidRx(Rx) then Exit;
|
if not LiveRx(Rx) then Exit;
|
||||||
if M.ArgCount >= 2 then
|
if M.ArgCount >= 2 then
|
||||||
begin
|
begin
|
||||||
V := TCIModeIndex(TCIArg(M, 1), RxMode(Rx), ChanFreq(Rx, 0));
|
V := TCIModeIndex(TCIArg(M, 1), RxMode(Rx), ChanFreq(Rx, 0));
|
||||||
@@ -1746,7 +1887,7 @@ begin
|
|||||||
if M.Name = 'RX_FILTER_BAND' then
|
if M.Name = 'RX_FILTER_BAND' then
|
||||||
begin
|
begin
|
||||||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||||||
if not ValidRx(Rx) then Exit;
|
if not LiveRx(Rx) then Exit;
|
||||||
// Обе кромки обязаны разобраться, и нижняя обязана быть ниже верхней:
|
// Обе кромки обязаны разобраться, и нижняя обязана быть ниже верхней:
|
||||||
// «rx_filter_band:0,x,y» иначе схлопывал фильтр в 0..0 и приёмник глох.
|
// «rx_filter_band:0,x,y» иначе схлопывал фильтр в 0..0 и приёмник глох.
|
||||||
if (M.ArgCount >= 3) and TCITryArgInt(M, 1, Lo) and TCITryArgInt(M, 2, Hi) and
|
if (M.ArgCount >= 3) and TCITryArgInt(M, 1, Lo) and TCITryArgInt(M, 2, Hi) and
|
||||||
@@ -1765,51 +1906,75 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
// ── Передача ──
|
// ── Передача ──
|
||||||
if M.Name = 'TRX' then
|
// ★arg1 у TRX/TUNE — НОМЕР ПЕРЕДАТЧИКА, и он не декорация: клиент, работающий
|
||||||
|
// на приёмнике N, просит эфир своему слайсу, а не тому, который оператор
|
||||||
|
// выбрал мышкой. Игнорировать его значит передать на чужой частоте, а с
|
||||||
|
// кросс-бандовым мультислайс-TX — и на чужом диапазоне, с чужими антенной и
|
||||||
|
// фильтрами. Поэтому номер разбирается и проверяется, как везде, а исполнение
|
||||||
|
// для N > 0 идёт через RequestSliceTx — ту же дверь, что у CAT-порта слайса
|
||||||
|
// (там же и «в эфире только один», и уважение к флагу Auto TX).
|
||||||
|
if (M.Name = 'TRX') or (M.Name = 'TUNE') then
|
||||||
begin
|
begin
|
||||||
// arg3 — источник сигнала. Наш только 'tci': модуляция берётся из
|
if not TCITryArgInt(M, 0, Rx) or not ValidRx(Rx) then
|
||||||
// аудиопотока этого клиента. Остальные значения (mic1/mic2/micpc/ecoder2)
|
begin
|
||||||
// называют физические входы ExpertSDR3, которых у нас нет, — они значат
|
Reply(Client, TCIBuild('tci_error', [LowerCase(M.Name), 'bad receiver']));
|
||||||
// «микрофон, выбранный в программе», то есть ровно то, что и без arg3.
|
Exit;
|
||||||
// Требование «включен аудиопоток по TCI» (§4.2) проверяем буквально:
|
end;
|
||||||
// без AUDIO_START модулировать нечем, и молча оставить оператора с
|
// Номер объявлен (TRX_COUNT = потолок железа), но пана под ним может не
|
||||||
// тишиной в эфире хуже, чем передавать с его микрофона.
|
// быть вовсе. Свести такую просьбу к главному VFO — то же самое, что
|
||||||
|
// передать на чужой частоте, только молча.
|
||||||
|
if (Rx > 0) and not RxActive(Rx) then
|
||||||
|
begin
|
||||||
|
Reply(Client, TCIBuild('tci_error',
|
||||||
|
[LowerCase(M.Name), 'receiver is not running']));
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
// Ключ захвата (§3.5) — общий на радио, а не на приёмник: передатчик один,
|
||||||
|
// и два клиента, дёргающие эфир с разных приёмников, спорят именно за него.
|
||||||
if TCITryArgBool(M, 1, B) and Claim(HoldKey('TRX', 0, 0), Client) then
|
if TCITryArgBool(M, 1, B) and Claim(HoldKey('TRX', 0, 0), Client) then
|
||||||
begin
|
begin
|
||||||
|
// arg3 — источник сигнала (только у TRX). Наш только 'tci': модуляция
|
||||||
|
// берётся из аудиопотока этого клиента. Остальные значения
|
||||||
|
// (mic1/mic2/micpc/ecoder2) называют физические входы ExpertSDR3, которых
|
||||||
|
// у нас нет, — они значат «микрофон, выбранный в программе», то есть ровно
|
||||||
|
// то, что и без arg3. Требование «включен аудиопоток по TCI» (§4.2)
|
||||||
|
// проверяем буквально: без AUDIO_START модулировать нечем, и молча
|
||||||
|
// оставить оператора с тишиной в эфире хуже, чем передавать с его
|
||||||
|
// микрофона.
|
||||||
|
Tune := M.Name = 'TUNE';
|
||||||
Name := LowerCase(Trim(TCIArg(M, 2)));
|
Name := LowerCase(Trim(TCIArg(M, 2)));
|
||||||
FromTCI := B and (Name = 'tci') and HasAudioStream(Client);
|
FromTCI := B and (not Tune) and (Name = 'tci') and HasAudioStream(Client);
|
||||||
FTxLock.Enter;
|
FTxLock.Enter;
|
||||||
try
|
try
|
||||||
if FromTCI then FTxClient := Client
|
if FromTCI then FTxClient := Client
|
||||||
else if FTxClient = Client then FTxClient := nil;
|
else if (not Tune) and (FTxClient = Client) then FTxClient := nil;
|
||||||
// Кто поставил трансивер в эфир: его уход обязан эфир и снять
|
|
||||||
// (см. StopTxOf). Источник модуляции тут ни при чём — несущую без
|
|
||||||
// хозяина оставлять нельзя в любом случае.
|
|
||||||
if B then FTrxOwner := Client
|
|
||||||
else if FTrxOwner = Client then FTrxOwner := nil;
|
|
||||||
finally FTxLock.Leave; end;
|
finally FTxLock.Leave; end;
|
||||||
|
Started := False;
|
||||||
FLock.Enter;
|
FLock.Enter;
|
||||||
try
|
try
|
||||||
|
FsInt := Rx;
|
||||||
FsBool := B;
|
FsBool := B;
|
||||||
FsBool2 := FromTCI;
|
FsBool2 := FromTCI;
|
||||||
|
FsBool3 := Tune;
|
||||||
|
FsRes := False;
|
||||||
if CanInvoke then FController.Invoke(SyncSetTRX);
|
if CanInvoke then FController.Invoke(SyncSetTRX);
|
||||||
|
Started := FsRes; // ответ Sync-метода — под тем же локом, что и вопрос
|
||||||
finally FLock.Leave; end;
|
finally FLock.Leave; end;
|
||||||
end;
|
// Кто поставил трансивер в эфир: его уход обязан эфир и снять (см.
|
||||||
Reply(Client, StrTrx);
|
// StopTxOf). Источник модуляции тут ни при чём — несущую без хозяина
|
||||||
Exit;
|
// оставлять нельзя в любом случае. ★Хозяином записываемся, только если
|
||||||
end;
|
// эфир и правда начался ИМЕННО от этой команды — а знает об этом только
|
||||||
|
// сам Sync-метод: он один видит состояние передатчика до и после, на
|
||||||
if M.Name = 'TUNE' then
|
// потоке контроллера и без окна, в которое влезает PTT оператора.
|
||||||
begin
|
FTxLock.Enter;
|
||||||
if TCITryArgBool(M, 1, B) and Claim(HoldKey('TUNE', 0, 0), Client) then
|
|
||||||
begin
|
|
||||||
FLock.Enter;
|
|
||||||
try
|
try
|
||||||
FsBool := B;
|
if Started then FTrxOwner := Client
|
||||||
if CanInvoke then FController.Invoke(SyncSetTune);
|
else if (not B) and (FTrxOwner = Client) then FTrxOwner := nil;
|
||||||
finally FLock.Leave; end;
|
finally FTxLock.Leave; end;
|
||||||
end;
|
end;
|
||||||
Reply(Client, StrTune);
|
// ★Автору отвечаем ЕГО номером приёмника (см. StrTrx): клиент фильтрует
|
||||||
|
// входящие по arg1, и отказ, названный чужим номером, до него не дойдёт.
|
||||||
|
if M.Name = 'TUNE' then Reply(Client, StrTune(Rx)) else Reply(Client, StrTrx(Rx));
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -1893,7 +2058,7 @@ begin
|
|||||||
if M.Name = 'RX_MUTE' then
|
if M.Name = 'RX_MUTE' then
|
||||||
begin
|
begin
|
||||||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||||||
if not ValidRx(Rx) then Exit;
|
if not LiveRx(Rx) then Exit;
|
||||||
if TCITryArgBool(M, 1, B) and Claim(HoldKey('RXMUTE', Rx, 0), Client) then
|
if TCITryArgBool(M, 1, B) and Claim(HoldKey('RXMUTE', Rx, 0), Client) then
|
||||||
begin
|
begin
|
||||||
FLock.Enter;
|
FLock.Enter;
|
||||||
@@ -1911,7 +2076,7 @@ begin
|
|||||||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||||||
if not TCITryArgInt(M, 1, Ch) then Ch := 0;
|
if not TCITryArgInt(M, 1, Ch) then Ch := 0;
|
||||||
Ch := EnsureRange(Ch, 0, TCI_CHANNELS - 1);
|
Ch := EnsureRange(Ch, 0, TCI_CHANNELS - 1);
|
||||||
if not ValidRx(Rx) then Exit;
|
if not LiveRx(Rx) then Exit;
|
||||||
if (M.ArgCount >= 3) and TCITryArgFloat(M, 2, D) and
|
if (M.ArgCount >= 3) and TCITryArgFloat(M, 2, D) and
|
||||||
Claim(HoldKey('RXVOL', Rx, Ch), Client) then
|
Claim(HoldKey('RXVOL', Rx, Ch), Client) then
|
||||||
begin
|
begin
|
||||||
@@ -1933,7 +2098,7 @@ begin
|
|||||||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||||||
if not TCITryArgInt(M, 1, Ch) then Ch := 0;
|
if not TCITryArgInt(M, 1, Ch) then Ch := 0;
|
||||||
Ch := EnsureRange(Ch, 0, TCI_CHANNELS - 1);
|
Ch := EnsureRange(Ch, 0, TCI_CHANNELS - 1);
|
||||||
if not ValidRx(Rx) then Exit;
|
if not LiveRx(Rx) then Exit;
|
||||||
FEchoLock.Enter;
|
FEchoLock.Enter;
|
||||||
try
|
try
|
||||||
if (M.ArgCount >= 3) and TCITryArgInt(M, 2, V) then
|
if (M.ArgCount >= 3) and TCITryArgInt(M, 2, V) then
|
||||||
@@ -1980,7 +2145,7 @@ begin
|
|||||||
if M.Name = 'AGC_MODE' then
|
if M.Name = 'AGC_MODE' then
|
||||||
begin
|
begin
|
||||||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||||||
if not ValidRx(Rx) then Exit;
|
if not LiveRx(Rx) then Exit;
|
||||||
if M.ArgCount >= 2 then
|
if M.ArgCount >= 2 then
|
||||||
begin
|
begin
|
||||||
Name := LowerCase(TCIArg(M, 1));
|
Name := LowerCase(TCIArg(M, 1));
|
||||||
@@ -2004,7 +2169,7 @@ begin
|
|||||||
if M.Name = 'AGC_GAIN' then
|
if M.Name = 'AGC_GAIN' then
|
||||||
begin
|
begin
|
||||||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||||||
if not ValidRx(Rx) then Exit;
|
if not LiveRx(Rx) then Exit;
|
||||||
// AGC-T (порог АРУ) в ewsdr один на приёмный тракт: у слайса своего нет.
|
// AGC-T (порог АРУ) в ewsdr один на приёмный тракт: у слайса своего нет.
|
||||||
// Правка от имени доп. приёмника трогала бы главный — не делаем этого,
|
// Правка от имени доп. приёмника трогала бы главный — не делаем этого,
|
||||||
// просто отвечаем текущим значением (ограничение, см. doc/TCI.md).
|
// просто отвечаем текущим значением (ограничение, см. doc/TCI.md).
|
||||||
@@ -2025,7 +2190,7 @@ begin
|
|||||||
(M.Name = 'RX_ANF_ENABLE') then
|
(M.Name = 'RX_ANF_ENABLE') then
|
||||||
begin
|
begin
|
||||||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||||||
if not ValidRx(Rx) then Exit;
|
if not LiveRx(Rx) then Exit;
|
||||||
if TCITryArgBool(M, 1, B) and Claim(HoldKey(M.Name, Rx, 0), Client) then
|
if TCITryArgBool(M, 1, B) and Claim(HoldKey(M.Name, Rx, 0), Client) then
|
||||||
begin
|
begin
|
||||||
FLock.Enter;
|
FLock.Enter;
|
||||||
@@ -2045,7 +2210,7 @@ begin
|
|||||||
if M.Name = 'RX_NB_PARAM' then
|
if M.Name = 'RX_NB_PARAM' then
|
||||||
begin
|
begin
|
||||||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||||||
if not ValidRx(Rx) then Exit;
|
if not LiveRx(Rx) then Exit;
|
||||||
FEchoLock.Enter;
|
FEchoLock.Enter;
|
||||||
try
|
try
|
||||||
if M.ArgCount >= 3 then
|
if M.ArgCount >= 3 then
|
||||||
@@ -2069,7 +2234,7 @@ begin
|
|||||||
(M.Name = 'RX_NF_ENABLE') then
|
(M.Name = 'RX_NF_ENABLE') then
|
||||||
begin
|
begin
|
||||||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||||||
if not ValidRx(Rx) then Exit;
|
if not LiveRx(Rx) then Exit;
|
||||||
FEchoLock.Enter;
|
FEchoLock.Enter;
|
||||||
try
|
try
|
||||||
if TCITryArgBool(M, 1, B) then
|
if TCITryArgBool(M, 1, B) then
|
||||||
@@ -2096,7 +2261,7 @@ begin
|
|||||||
if M.Name = 'LOCK' then
|
if M.Name = 'LOCK' then
|
||||||
begin
|
begin
|
||||||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||||||
if not ValidRx(Rx) then Exit;
|
if not LiveRx(Rx) then Exit;
|
||||||
if TCITryArgBool(M, 1, B) and Claim(HoldKey('LOCK', 0, 0), Client) then
|
if TCITryArgBool(M, 1, B) and Claim(HoldKey('LOCK', 0, 0), Client) then
|
||||||
begin
|
begin
|
||||||
FLock.Enter;
|
FLock.Enter;
|
||||||
@@ -2112,7 +2277,7 @@ begin
|
|||||||
if M.Name = 'SQL_ENABLE' then
|
if M.Name = 'SQL_ENABLE' then
|
||||||
begin
|
begin
|
||||||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||||||
if not ValidRx(Rx) then Exit;
|
if not LiveRx(Rx) then Exit;
|
||||||
if TCITryArgBool(M, 1, B) and Claim(HoldKey('SQL', Rx, 0), Client) then
|
if TCITryArgBool(M, 1, B) and Claim(HoldKey('SQL', Rx, 0), Client) then
|
||||||
begin
|
begin
|
||||||
FLock.Enter;
|
FLock.Enter;
|
||||||
@@ -2128,7 +2293,7 @@ begin
|
|||||||
if M.Name = 'SQL_LEVEL' then
|
if M.Name = 'SQL_LEVEL' then
|
||||||
begin
|
begin
|
||||||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||||||
if not ValidRx(Rx) then Exit;
|
if not LiveRx(Rx) then Exit;
|
||||||
if TCITryArgFloat(M, 1, D) and Claim(HoldKey('SQLLEV', Rx, 0), Client) then
|
if TCITryArgFloat(M, 1, D) and Claim(HoldKey('SQLLEV', Rx, 0), Client) then
|
||||||
begin
|
begin
|
||||||
FLock.Enter;
|
FLock.Enter;
|
||||||
@@ -2146,7 +2311,7 @@ begin
|
|||||||
if (M.Name = 'RIT_ENABLE') or (M.Name = 'XIT_ENABLE') then
|
if (M.Name = 'RIT_ENABLE') or (M.Name = 'XIT_ENABLE') then
|
||||||
begin
|
begin
|
||||||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||||||
if not ValidRx(Rx) then Exit;
|
if not LiveRx(Rx) then Exit;
|
||||||
FEchoLock.Enter;
|
FEchoLock.Enter;
|
||||||
try
|
try
|
||||||
if TCITryArgBool(M, 1, B) then
|
if TCITryArgBool(M, 1, B) then
|
||||||
@@ -2165,7 +2330,7 @@ begin
|
|||||||
if (M.Name = 'RIT_OFFSET') or (M.Name = 'XIT_OFFSET') then
|
if (M.Name = 'RIT_OFFSET') or (M.Name = 'XIT_OFFSET') then
|
||||||
begin
|
begin
|
||||||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||||||
if not ValidRx(Rx) then Exit;
|
if not LiveRx(Rx) then Exit;
|
||||||
FEchoLock.Enter;
|
FEchoLock.Enter;
|
||||||
try
|
try
|
||||||
if TCITryArgInt(M, 1, V) then
|
if TCITryArgInt(M, 1, V) then
|
||||||
@@ -2188,7 +2353,7 @@ begin
|
|||||||
// вторым каналом был бы второй слайс (создание слайсов по TCI — этап 2).
|
// вторым каналом был бы второй слайс (создание слайсов по TCI — этап 2).
|
||||||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||||||
if not TCITryArgInt(M, 1, Ch) then Ch := 1;
|
if not TCITryArgInt(M, 1, Ch) then Ch := 1;
|
||||||
if not ValidRx(Rx) then Exit;
|
if not LiveRx(Rx) then Exit;
|
||||||
if TCITryArgBool(M, 2, B) then
|
if TCITryArgBool(M, 2, B) then
|
||||||
begin
|
begin
|
||||||
FEchoLock.Enter;
|
FEchoLock.Enter;
|
||||||
@@ -2533,7 +2698,9 @@ begin
|
|||||||
// держать при этом FStreamLock значило бы связать два лока без нужды.
|
// держать при этом FStreamLock значило бы связать два лока без нужды.
|
||||||
if K = tstIQ then
|
if K = tstIQ then
|
||||||
begin
|
begin
|
||||||
SrcRate := FController.IQTapRateHz(Rx);
|
// ★Частота IQ — свойство ПАНОРАМЫ, а приёмник теперь слайс: спрашиваем
|
||||||
|
// рейт того пана, на котором он стоит.
|
||||||
|
SrcRate := FController.IQTapRateHz(RxPanId(Rx));
|
||||||
WantRate := C.IQRate;
|
WantRate := C.IQRate;
|
||||||
Chans := 2; // IQ комплексный по определению
|
Chans := 2; // IQ комплексный по определению
|
||||||
ST := tsyFloat32; // формат IQ в TCI не настраивается
|
ST := tsyFloat32; // формат IQ в TCI не настраивается
|
||||||
@@ -2695,19 +2862,26 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
function TTCIAdapter.StreamRxOf(PanId, SliceId: Integer): Integer;
|
function TTCIAdapter.StreamRxOf(PanId, SliceId: Integer): Integer;
|
||||||
// Какому приёмнику TCI принадлежит это аудио. Главный тракт — приёмник 0.
|
// Какому приёмнику TCI принадлежит это аудио. Главный тракт (SliceId = 0 на
|
||||||
// У доп. пана в потоках участвует только канал A (первый слайс): «аудиопоток
|
// пане 0) — приёмник 0, любой слайс — приёмник своего слота, на каком бы пане
|
||||||
// приёмника» в протоколе один на приёмник, второго канала у него нет.
|
// он ни стоял. Зовётся из DSP-потока и берёт FSliceLock — ДО FStreamLock
|
||||||
// Зовётся из DSP-потока и берёт FSliceLock — ДО FStreamLock (порядок!).
|
// (порядок!).
|
||||||
|
var i: Integer;
|
||||||
begin
|
begin
|
||||||
Result := -1;
|
Result := -1;
|
||||||
if PanId <= 0 then
|
if SliceId <= 0 then
|
||||||
begin
|
begin
|
||||||
if SliceId = 0 then Result := 0; // слайсы главного пана в модель не входят
|
if PanId = 0 then Result := 0;
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
if not ValidRx(PanId) then Exit;
|
FSliceLock.Enter;
|
||||||
if SliceIdOf(PanId, 0) = SliceId then Result := PanId;
|
try
|
||||||
|
for i := 0 to MAX_SLICES - 1 do
|
||||||
|
if FSliceSnap[i].Used and (FSliceSnap[i].V.Id = SliceId) then
|
||||||
|
Exit(i + 1);
|
||||||
|
finally
|
||||||
|
FSliceLock.Leave;
|
||||||
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TTCIAdapter.OnAudioTap(Kind: TRadioAudioKind; PanId, SliceId: Integer;
|
procedure TTCIAdapter.OnAudioTap(Kind: TRadioAudioKind; PanId, SliceId: Integer;
|
||||||
@@ -2742,14 +2916,23 @@ procedure TTCIAdapter.OnIQTap(PanId: Integer; PI_, PQ_: PDouble;
|
|||||||
N, RateHz: Integer);
|
N, RateHz: Integer);
|
||||||
// DSP-поток, один вызов на накопленный блок. У пана этот вызов идёт под
|
// DSP-поток, один вызов на накопленный блок. У пана этот вызов идёт под
|
||||||
// FSliceLock движка, поэтому здесь тем более нельзя ждать.
|
// FSliceLock движка, поэтому здесь тем более нельзя ждать.
|
||||||
var i: Integer;
|
//
|
||||||
|
// IQ — величина ПАНОРАМЫ, а приёмник теперь слайс: поток получают все
|
||||||
|
// приёмники, чьи слайсы стоят на этом пане (и приёмник 0, если пан главный).
|
||||||
|
// Карту «приёмник → пан» строим ДО FStreamLock: порядок локов один на весь
|
||||||
|
// адаптер — FSliceLock, потом FStreamLock.
|
||||||
|
var
|
||||||
|
i: Integer;
|
||||||
|
Mine: array[0..TCI_MAX_RX-1] of Boolean;
|
||||||
begin
|
begin
|
||||||
if (N <= 0) or (RateHz <= 0) then Exit;
|
if (N <= 0) or (RateHz <= 0) then Exit;
|
||||||
if (PanId < 0) or (PanId >= TCI_MAX_RX) then Exit;
|
if (PanId < 0) or (PanId >= MAX_PANS) then Exit;
|
||||||
|
for i := 0 to TCI_MAX_RX - 1 do Mine[i] := RxPanId(i) = PanId;
|
||||||
FStreamLock.Enter;
|
FStreamLock.Enter;
|
||||||
try
|
try
|
||||||
for i := 0 to High(FStreams) do
|
for i := 0 to High(FStreams) do
|
||||||
if (FStreams[i].Kind = tstIQ) and (FStreams[i].Rx = PanId) then
|
if (FStreams[i].Kind = tstIQ) and (FStreams[i].Rx >= 0) and
|
||||||
|
(FStreams[i].Rx < TCI_MAX_RX) and Mine[FStreams[i].Rx] then
|
||||||
begin
|
begin
|
||||||
// Rate устройства могли сменить уже после START (смена sample rate,
|
// Rate устройства могли сменить уже после START (смена sample rate,
|
||||||
// другой rate DDC пана): пересчитываем прореживание на месте, иначе
|
// другой rate DDC пана): пересчитываем прореживание на месте, иначе
|
||||||
@@ -3181,8 +3364,11 @@ begin
|
|||||||
rfVolume: Claim(HoldKey('VOL', 0, 0), nil);
|
rfVolume: Claim(HoldKey('VOL', 0, 0), nil);
|
||||||
rfMute: Claim(HoldKey('MUTE', 0, 0), nil);
|
rfMute: Claim(HoldKey('MUTE', 0, 0), nil);
|
||||||
rfDrive: Claim(HoldKey('DRIVE', 0, 0), nil);
|
rfDrive: Claim(HoldKey('DRIVE', 0, 0), nil);
|
||||||
rfTransmitting: Claim(HoldKey('TRX', 0, 0), nil);
|
// Передатчик один — и ключ захвата у него один на всех, кто его дёргает:
|
||||||
rfTuning: Claim(HoldKey('TUNE', 0, 0), nil);
|
// и TRX, и TUNE, и оператор. Отдельный ключ 'TUNE' делал защиту дырявой —
|
||||||
|
// TUN поверх чужого MOX не спорил с ним ни за что.
|
||||||
|
rfTransmitting,
|
||||||
|
rfTuning: Claim(HoldKey('TRX', 0, 0), nil);
|
||||||
rfVfoLock: Claim(HoldKey('LOCK', 0, 0), nil);
|
rfVfoLock: Claim(HoldKey('LOCK', 0, 0), nil);
|
||||||
rfCenterFreq: Claim(HoldKey('DDS', 0, 0), nil);
|
rfCenterFreq: Claim(HoldKey('DDS', 0, 0), nil);
|
||||||
rfSliceFreq:
|
rfSliceFreq:
|
||||||
@@ -3212,8 +3398,8 @@ begin
|
|||||||
rfVolume: FServer.Broadcast(StrVolume);
|
rfVolume: FServer.Broadcast(StrVolume);
|
||||||
rfMute: FServer.Broadcast(StrMute);
|
rfMute: FServer.Broadcast(StrMute);
|
||||||
rfDrive: FServer.Broadcast(StrDrive);
|
rfDrive: FServer.Broadcast(StrDrive);
|
||||||
rfTransmitting: FServer.Broadcast(StrTrx);
|
rfTransmitting: FServer.Broadcast(StrTrx(TxRx));
|
||||||
rfTuning: FServer.Broadcast(StrTune);
|
rfTuning: FServer.Broadcast(StrTune(TxRx));
|
||||||
rfVfoLock:
|
rfVfoLock:
|
||||||
begin
|
begin
|
||||||
FServer.Broadcast(StrLock(0));
|
FServer.Broadcast(StrLock(0));
|
||||||
@@ -3253,10 +3439,12 @@ begin
|
|||||||
if FController.FRunning then FServer.Broadcast(TCIBuild('start'))
|
if FController.FRunning then FServer.Broadcast(TCIBuild('start'))
|
||||||
else FServer.Broadcast(TCIBuild('stop'));
|
else FServer.Broadcast(TCIBuild('stop'));
|
||||||
rfPanFreq:
|
rfPanFreq:
|
||||||
// Центр пана уехал: у его каналов изменилась и IF (она отсчитывается
|
// Центр пана уехал: у приёмников, которые на нём стоят, изменилась IF
|
||||||
// от центра), хотя абсолютная частота могла остаться прежней.
|
// (она отсчитывается от центра), хотя абсолютная частота могла остаться
|
||||||
|
// прежней. Какие это приёмники — знает RxPanId: слайсы одного пана
|
||||||
|
// раскиданы по слотам, подряд они не лежат.
|
||||||
for Rx := 1 to RxCount - 1 do
|
for Rx := 1 to RxCount - 1 do
|
||||||
if FController.PanDDCActive(Rx) then
|
if RxPanId(Rx) > 0 then
|
||||||
begin
|
begin
|
||||||
FServer.Broadcast(StrDds(Rx));
|
FServer.Broadcast(StrDds(Rx));
|
||||||
for Ch := 0 to ChanCount(Rx) - 1 do FServer.Broadcast(StrIf(Rx, Ch));
|
for Ch := 0 to ChanCount(Rx) - 1 do FServer.Broadcast(StrIf(Rx, Ch));
|
||||||
@@ -3268,7 +3456,7 @@ begin
|
|||||||
BroadcastRxState(Rx, Ch);
|
BroadcastRxState(Rx, Ch);
|
||||||
rfBand, rfXvtr:
|
rfBand, rfXvtr:
|
||||||
begin
|
begin
|
||||||
FServer.Broadcast(StrTxEnable(0));
|
BroadcastTxEnable;
|
||||||
FServer.Broadcast(StrVfo(0, 0));
|
FServer.Broadcast(StrVfo(0, 0));
|
||||||
end;
|
end;
|
||||||
rfActiveVfo:
|
rfActiveVfo:
|
||||||
@@ -3312,7 +3500,7 @@ begin
|
|||||||
if TxEnabled <> FLastTxEnable then
|
if TxEnabled <> FLastTxEnable then
|
||||||
begin
|
begin
|
||||||
FLastTxEnable := TxEnabled;
|
FLastTxEnable := TxEnabled;
|
||||||
FServer.Broadcast(StrTxEnable(0));
|
BroadcastTxEnable;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|||||||
+204
-35
@@ -165,12 +165,37 @@ UI — вкладка **CAT → TCI Server**, справа от «TCP CAT Server
|
|||||||
|
|
||||||
| TCI | EWSDR |
|
| TCI | EWSDR |
|
||||||
|---|---|
|
|---|---|
|
||||||
| приёмник (`TRX_COUNT`) | панадаптер: 0 = главный тракт, 1.. = доп. DDC-паны. Число = `BackendCaps.MaxPans` |
|
| приёмник (`TRX_COUNT` = 7) | 0 — главный тракт; N — **слайс слота N−1**, то есть буквы B..G с флага, на каком бы пане он ни стоял. Потолок = 1 + `MAX_SLICES` |
|
||||||
| канал A/B (`CHANNEL_COUNT` = 2) | у приёмника 0 — VFO A / VFO B; у панов 1.. — первый и второй слайс пана |
|
| канал A/B (`CHANNEL_COUNT` = 2) | у приёмника 0 — VFO A / VFO B; у приёмника-слайса канал один (A): второго саб-приёмника внутри слайса не бывает |
|
||||||
| `DDS` | центр панадаптера (`SetCenter` / `SetPanDDCFreq`) |
|
| панорама | не адресуется: стала свойством приёмника — от неё берутся `DDS` и поток IQ |
|
||||||
| `IF` | смещение канала от центра панорамы |
|
| `DDS` | центр панорамы, на которой стоит приёмник. Пишется только у приёмника 0 (`SetCenter`); у слайса читается |
|
||||||
| `VFO` | абсолютная частота канала |
|
| `IF` | смещение канала от центра его панорамы |
|
||||||
| передатчик (`arg1` у TRX/TUNE/DRIVE) | всегда один — главный тракт |
|
| `VFO` | абсолютная частота канала (у слайса — `TuneSliceInBand`) |
|
||||||
|
| передатчик (`arg1` у TRX/TUNE) | передатчик один, но `arg1` называет, ЧЕЙ слайс должен в него попасть: 0 — главный VFO (как обычный CAT-порт), N — слайс слота N−1 через `RequestSliceTx`. См. §2.7 |
|
||||||
|
| `arg1` у `DRIVE`, `TUNE_DRIVE` | мощность одна на радио — номер не адресует ничего |
|
||||||
|
|
||||||
|
★**Почему приёмник — слайс, а не панорама.** Сперва приёмником был панадаптер,
|
||||||
|
как в ExpertSDR3, где приёмник = DDC. У нас это разошлось с жизнью сразу на
|
||||||
|
двух концах. У Pluto панорама ровно одна (`BackendCaps.MaxPans = 1`), и второй
|
||||||
|
приёмник существует там **только** как слайс главного пана — при нумерации по
|
||||||
|
панам он был бы недоступен вообще, а `TRX_COUNT` навсегда равнялся единице. С
|
||||||
|
другого конца — клиенты: у MSHV в настройках всего две модели, «TCI Client rx1»
|
||||||
|
и «rx2», то есть приёмники 0 и 1, и никакого третьего номера ввести некуда
|
||||||
|
(`hvrigcontrol.cpp`: `netServPort[3]`/`[4]`, `network.cpp`: `tci_trx`). Значит
|
||||||
|
«первый добавленный слайс» обязан быть приёмником **1** на любом железе.
|
||||||
|
|
||||||
|
Слот слайса даёт ровно это. Слоты глобальные и стабильные, у каждого своя
|
||||||
|
буква на флаге (`SliceIdBySlot`, `SliceSlotLetter`), и по ним же раздаются
|
||||||
|
порты слайс-CAT — то есть номер приёмника TCI совпадает с буквой, которую
|
||||||
|
оператор видит на экране: B → 1, C → 2, … Первый созданный слайс занимает слот
|
||||||
|
B независимо от того, где он живёт: на openHPSDR это может быть слайс второго
|
||||||
|
пана, на Pluto — слайс главного, и в обоих случаях он приёмник 1.
|
||||||
|
|
||||||
|
Цена: панорама без единого слайса из TCI пропадает (слушать там нечего, но и
|
||||||
|
её IQ клиенту недоступен), а второй слайс панорамы перестал быть «каналом B» и
|
||||||
|
стал самостоятельным приёмником — что скорее выигрыш: у канала B в протоколе
|
||||||
|
есть только частота, IF и громкость, а у приёмника — мода, фильтр, АРУ,
|
||||||
|
шумодавы, squelch, свои потоки и `TRX`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -209,19 +234,20 @@ UI — вкладка **CAT → TCI Server**, справа от «TCP CAT Server
|
|||||||
— особенно важно при split и TX-слайсе), текущий `APP_FOCUS` и `VFO_LOCK` на
|
— особенно важно при split и TX-слайсе), текущий `APP_FOCUS` и `VFO_LOCK` на
|
||||||
каждый канал.
|
каждый канал.
|
||||||
|
|
||||||
**У живого пана канал A есть всегда.** Выключить канал A в TCI нечем — он
|
**Приёмник живёт ровно столько, сколько его слайс.** Удалили слайс — приёмник
|
||||||
существует по определению, поэтому пан без слайсов показывает канал A на своём
|
исчез вместе с ним: ни частоты, ни состояния, ни потоков. Создали снова — он
|
||||||
центре (`DDS`), а не хранит частоту удалённого слайса. Команды на такой канал
|
займёт первый свободный слот, то есть, как правило, тот же номер (так же
|
||||||
игнорируются: слайса под ним нет. Как только слайс появится, канал станет
|
устроены и порты слайс-CAT, привязанные к слоту).
|
||||||
настоящим.
|
|
||||||
|
|
||||||
**Приёмники, которых ещё нет, молчат.** `TRX_COUNT` объявляет потолок железа
|
**Приёмники, которых ещё нет, молчат.** `TRX_COUNT` объявляет потолок (1 + 6
|
||||||
(`BackendCaps.MaxPans`) один раз и навсегда, а пан из этого потолка может быть
|
слотов слайсов) один раз и навсегда, а слот из этого потолка может быть пуст.
|
||||||
не создан. Для несуществующего пана не шлётся ничего (раньше уходили
|
Про пустой слот не шлётся ничего — ни `vfo`, ни `dds`, ни показания
|
||||||
`dds`/`vfo`/`if` с нулём, и клиент принимал ноль за настоящую частоту);
|
измерителей, и команды к нему тоже игнорируются целиком (`LiveRx`). Раньше
|
||||||
состояние приходит, когда пан появится — с `rfPanFreq`/`rfSliceState`. У
|
уходили `dds`/`vfo`/`if` с нулём, и клиент принимал ноль за настоящую частоту;
|
||||||
существующего пана без слайсов есть только `DDS`. Показания измерителей для
|
для MSHV это вообще фатально — именно ответом на `vfo:<rx>,0;` он завершает
|
||||||
таких приёмников тоже не отправляются.
|
инициализацию, то есть подключился бы к пустоте. Состояние появляется в момент
|
||||||
|
создания слайса: `PushChannelMap` рассылает картину нового приёмника целиком,
|
||||||
|
включая `tx_enable` с его номером.
|
||||||
|
|
||||||
Список видов связи: `am,sam,dsb,lsb,usb,cw,nfm,wfm,digl,digu,dmr,fmraw`.
|
Список видов связи: `am,sam,dsb,lsb,usb,cw,nfm,wfm,digl,digu,dmr,fmraw`.
|
||||||
`dmr`/`fmraw` — наше расширение (протокол расширяемый, §1.4). `CWL`/`CWU`
|
`dmr`/`fmraw` — наше расширение (протокол расширяемый, §1.4). `CWL`/`CWU`
|
||||||
@@ -264,8 +290,8 @@ UI — вкладка **CAT → TCI Server**, справа от «TCP CAT Server
|
|||||||
| `DDS` | `SetCenter` / `SetPanDDCFreq` |
|
| `DDS` | `SetCenter` / `SetPanDDCFreq` |
|
||||||
| `IF`, `VFO` | `SetVfoA/B`, `TuneSliceInBand` |
|
| `IF`, `VFO` | `SetVfoA/B`, `TuneSliceInBand` |
|
||||||
| `MODULATION` | `SetMode` / `SetSliceMode` |
|
| `MODULATION` | `SetMode` / `SetSliceMode` |
|
||||||
| `TRX` | `SetMOX` |
|
| `TRX` | `SetMOX` (приёмник 0) / `RequestSliceTx` (приёмник N) |
|
||||||
| `TUNE` | `SetTune` |
|
| `TUNE` | `SetTune` (приёмник 0) / `RequestSliceTx(…, Tune)` (приёмник N) |
|
||||||
| `DRIVE` | `SetDrive` |
|
| `DRIVE` | `SetDrive` |
|
||||||
| `TUNE_DRIVE` | `FTXSettings.TUNLevel` через `SetTXSettings` |
|
| `TUNE_DRIVE` | `FTXSettings.TUNLevel` через `SetTXSettings` |
|
||||||
| `SPLIT_ENABLE` | `SetSplit` |
|
| `SPLIT_ENABLE` | `SetSplit` |
|
||||||
@@ -494,6 +520,71 @@ ExpertSDR3 давно бы не было.
|
|||||||
скорости и не знает прос-знаков. Доотправка позывного (`cw_msg:arg1;`)
|
скорости и не знает прос-знаков. Доотправка позывного (`cw_msg:arg1;`)
|
||||||
игнорируется: уже отданный в очередь текст не редактируется.
|
игнорируется: уже отданный в очередь текст не редактируется.
|
||||||
|
|
||||||
|
### 2.7 Кто именно уходит в эфир (`TRX`, `TUNE`)
|
||||||
|
|
||||||
|
★`arg1` у этих двух команд — **номер передатчика**, и он не декорация. Клиент
|
||||||
|
живёт на своём приёмнике: скиммер на пане 2, цифра на пане 1, логгер на
|
||||||
|
главном. Нажимая передачу, он просит эфир **своему** слайсу — а не тому,
|
||||||
|
который оператор выбрал мышкой минуту назад. Пока номер игнорировался,
|
||||||
|
`trx:2,true` уводил в эфир чужой слайс: другая частота, а с кросс-бандовым
|
||||||
|
мультислайс-TX — и другой диапазон, то есть чужие антенна и фильтры. Клиент об
|
||||||
|
этом не узнавал никак (ответ всегда говорил `trx:0,…`), а оператор видел
|
||||||
|
передачу там, где её никто не просил.
|
||||||
|
|
||||||
|
Как устроено теперь:
|
||||||
|
|
||||||
|
- номер разбирается и проверяется, как у всех остальных команд:
|
||||||
|
не число или вне `TRX_COUNT` — `tci_error … bad receiver`; номер в потолке,
|
||||||
|
но пана под ним нет — `receiver is not running`. Свести такую просьбу к
|
||||||
|
главному VFO нельзя: молча передать не на той частоте хуже, чем отказать;
|
||||||
|
- **приёмник 0** — это «радио целиком», то есть главный VFO. Ведём себя как
|
||||||
|
обычный CAT-порт: в эфир уходит выбранный оператором TX-источник;
|
||||||
|
- **приёмник N > 0** адресует слайс слота N−1, и заявка идёт через
|
||||||
|
`TRadioController.RequestSliceTx` — ту же дверь, что у CAT-порта слайса
|
||||||
|
(см. `doc/CAT_SLICES.md`). Оттуда бесплатно достаются оба правила: «в эфире
|
||||||
|
одновременно только один» (чужую передачу не перехватываем) и уважение к
|
||||||
|
флагу **Auto TX** (без него слайс сам себя источником не назначает);
|
||||||
|
- `TUNE` ходит той же дорогой — `RequestSliceTx(…, Tune := True)`: несущая
|
||||||
|
настройки обязана вставать на тот же слайс, что и модуляция;
|
||||||
|
- **ответ автору называет ЕГО номер приёмника**, а состояние в нём — «в эфире
|
||||||
|
именно твой слайс» (`FTransmitting and (TxRx = Rx)`). Так надо потому, что
|
||||||
|
клиенты фильтруют входящие строки по `arg1`: MSHV, например, отбрасывает
|
||||||
|
всё, что адресовано не его приёмнику (`network.cpp`:
|
||||||
|
`if (ls2.at(0)!=tci_trx) continue;`), — и отказ, названный чужим номером, до
|
||||||
|
него бы просто не дошёл. В **рассылку** уходит номер того, чей слайс сейчас
|
||||||
|
источник передачи (`TxRx` по `TxSliceId`): клиент на приёмнике 1 по ней
|
||||||
|
видит, что в эфире не он. Смена TX-источника оператором доходит до клиентов
|
||||||
|
сама: `SetTxSlice` заканчивается `Changed(rfTransmitting)`;
|
||||||
|
- ключ захвата (§3.5) — **один на передатчик**, `TRX/0/0`: за него спорят и
|
||||||
|
`TRX`, и `TUNE`, и изменения от оператора (`rfTransmitting`, `rfTuning`).
|
||||||
|
Отдельный ключ `TUNE` делал защиту дырявой — TUN поверх чужого MOX не спорил
|
||||||
|
ни с кем;
|
||||||
|
- **чужую передачу не трогаем вовсе.** Пока передатчик занят (оператор, другой
|
||||||
|
клиент, аппаратная PTT), `TRX:…,true` и `TUNE:…,true` не делают ничего — то же
|
||||||
|
правило, что внутри `RequestSliceTx`, только теперь оно работает и для
|
||||||
|
приёмника 0. Дырок было две: `SetMOX(True)` **не выходит рано** на уже идущей
|
||||||
|
передаче — он заново выбирает источник модуляции, и `trx:0,true,tci` посреди
|
||||||
|
передачи оператора уводил её в ринг TCI (а уход клиента оставлял в эфире
|
||||||
|
несущую с тишиной: хозяином он при этом не становился, снимать было некому);
|
||||||
|
`tune:0,true` таким же образом подмешивал настроечный тон в чужую передачу и
|
||||||
|
оставался в эфире после ухода;
|
||||||
|
- хозяином эфира (`FTrxOwner`, чей уход эфир снимает) клиент записывается,
|
||||||
|
только если передача началась **именно от его команды**. Знает об этом лишь
|
||||||
|
сам Sync-метод — он один видит состояние передатчика до и после, на потоке
|
||||||
|
контроллера. Снимок «шла ли передача», взятый в потоке клиента до `Invoke`,
|
||||||
|
врал бы: между разбором команды и её исполнением оператор успевает нажать
|
||||||
|
PTT, и хозяином стал бы не тот. Поэтому решение принимается внутри, а наружу
|
||||||
|
возвращается один флаг (`FsRes`, под тем же `FLock`, что и аргументы);
|
||||||
|
- по той же причине отказ откатывает `TCIMicRequested`: иначе следующая PTT
|
||||||
|
оператора ушла бы в эфир с микрофоном ушедшего клиента.
|
||||||
|
|
||||||
|
Попутно на этой дороге нашлась Access violation, не связанная с TCI:
|
||||||
|
`SendDUCSpecificFromSettings` трогала `FNetwork` без `Assigned`, а зовут её по
|
||||||
|
любому PTT/TUN (`SetMOX` → `SyncCWKeyer` → она), в том числе **до подключения
|
||||||
|
устройства**. В GUI падение маскировалось, у TCI ошибку глотал обработчик
|
||||||
|
команды — и «передача» жила только в ответе клиенту. Теперь там обычная
|
||||||
|
проверка, как в остальных двенадцати местах контроллера.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Осознанные заглушки («эхо»)
|
## 3. Осознанные заглушки («эхо»)
|
||||||
@@ -512,24 +603,78 @@ ExpertSDR3 давно бы не было.
|
|||||||
| `DIGL_OFFSET`, `DIGU_OFFSET` | смещения цифровых мод не реализованы |
|
| `DIGL_OFFSET`, `DIGU_OFFSET` | смещения цифровых мод не реализованы |
|
||||||
| `RX_CHANNEL_ENABLE` | канал B главного приёмника — это VFO B, он есть всегда; создание второго слайса на пане по TCI не делаем (см. §4) |
|
| `RX_CHANNEL_ENABLE` | канал B главного приёмника — это VFO B, он есть всегда; создание второго слайса на пане по TCI не делаем (см. §4) |
|
||||||
|
|
||||||
|
**Почему именно заглушки, а не «сделать по-быстрому».** Правило одно: *TCI не
|
||||||
|
заводит в радио состояние, которого оператор не видит на экране и не может
|
||||||
|
снять*. Клиент подключается и отключается когда ему вздумается, а оставленный
|
||||||
|
им перекос остаётся в аппарате навсегда — искать его будет человек, у которого
|
||||||
|
нет ни кнопки, ни индикации. Поэтому настоящими сделаны ровно те параметры, у
|
||||||
|
которых в интерфейсе есть орган управления (`NR`/`NB`/`ANF`, АРУ, фильтр,
|
||||||
|
громкость, squelch, скорость CW), а остальные приняты, сохранены и разосланы
|
||||||
|
ради честной синхронизации клиентов между собой — и только.
|
||||||
|
|
||||||
|
Дальше — построчно, потому что причины у них разные:
|
||||||
|
|
||||||
|
- **`RIT`/`XIT`.** Расстройка — не кнопка, а сквозной сдвиг: `VFO` → DDC/DUC,
|
||||||
|
панорама, флаг слайса, CAT, каналы памяти. Сделанная **только** ради TCI, она
|
||||||
|
сразу разошлась бы с тем, что показывает экран и что отвечает CAT-логгеру, —
|
||||||
|
и это худший вид ошибки в аппарате: слышно одно, написано другое. Заводить
|
||||||
|
надо целиком, как фичу радио, тогда TCI получит её даром.
|
||||||
|
- **`RX_BIN_ENABLE`, `RX_BALANCE`.** Единственные два, где тракт-то есть:
|
||||||
|
`SetRXAPanelBinaural` и `SetRXAPanelPan` в биндингах WDSP лежат готовые. Но в
|
||||||
|
EWSDR нет ни органа управления, ни поля в профиле, ни персиста, — включённое
|
||||||
|
клиентом псевдостерео или уехавший баланс оператор не найдёт чем вернуть.
|
||||||
|
Порядок обратный: сперва UI, потом TCI.
|
||||||
|
- **`RX_ANC_ENABLE`, `RX_APF_ENABLE`, `RX_DSE_ENABLE`, `RX_NF_ENABLE`.** Этих
|
||||||
|
узлов в тракте нет физически. У нас `NR`/`NR2` (EMNR), `NB`/`NB2` (ANB/SNBA)
|
||||||
|
и `ANF` — они в TCI настоящие. `ANC` и `DSE` — блоки собственной обработки
|
||||||
|
Expert, не WDSP; `APF` и `NF` были бы новыми узлами в цепочке.
|
||||||
|
- **`RX_NB_PARAM`.** Пороги шумодава у нас — подобранные константы
|
||||||
|
`FILTER_NB_*`, одинаковые для всех каналов и не выведенные даже в интерфейс.
|
||||||
|
Клиент, покрутивший их по TCI, оставил бы шумодав расстроенным насовсем.
|
||||||
|
- **`DIGL_OFFSET`, `DIGU_OFFSET`.** `DIGU`/`DIGL` у нас — обычные SSB-моды со
|
||||||
|
своим фильтром, отдельного смещения в тракте нет. Введённое ради одной
|
||||||
|
команды, оно опять развело бы частоту на панораме с той, что слышно.
|
||||||
|
|
||||||
## 3.1 Ограничения, о которых честнее знать заранее
|
## 3.1 Ограничения, о которых честнее знать заранее
|
||||||
|
|
||||||
| Что | Как ведёт себя |
|
| Что | Как ведёт себя |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `AGC_GAIN` у приёмника > 0 | AGC-T в ewsdr один на приёмный тракт, у слайса своего нет. Команда от имени доп. приёмника **игнорируется** (раньше молча правила главный), в ответ уходит текущее значение |
|
| `AGC_GAIN` у приёмника > 0 | AGC-T в ewsdr один на приёмный тракт, у слайса своего нет. Команда от имени доп. приёмника **игнорируется** (раньше молча правила главный), в ответ уходит текущее значение |
|
||||||
|
| `DDS` у приёмника-слайса | только читается (центр его панорамы): панорама под слайсом общая, и увести её по просьбе одного клиента значит утащить соседей по пану и картинку оператора. Слайсу двигаться незачем — за окном DDC следит `TuneSliceInBand` |
|
||||||
|
| Панорама без слайсов | в TCI не видна вовсе: приёмник = слайс. Слушать там нечего, но и IQ такой панорамы клиенту недоступен |
|
||||||
| Цвет спота (`SPOT`, arg4 ARGB) | не читается: `TDXSpot` цвета не хранит, подписи красятся по моде/возрасту |
|
| Цвет спота (`SPOT`, arg4 ARGB) | не читается: `TDXSpot` цвета не хранит, подписи красятся по моде/возрасту |
|
||||||
| `KEYER`, `TX_FOOTSWITCH` | не реализованы: своего ключа-уведомления и опроса педали наружу у контроллера нет |
|
| `KEYER`, `TX_FOOTSWITCH` | не реализованы, причины разные — см. ниже под таблицей |
|
||||||
| Мода, фильтр, АРУ, шумодавы у канала B доп. пана | в TCI это свойства **приёмника**, а не канала: они относятся к каналу A. У канала B по протоколу есть только частота, IF и громкость |
|
| Канал B | есть только у приёмника 0 (VFO B). У приёмника-слайса канал один: слайс — это и есть «приёмник» целиком, со своими модой, фильтром, АРУ, шумодавами и потоками |
|
||||||
| Захват параметра (§3.5) | реализован для того, что клиенты действительно перетягивают (частота, DDS, мода, фильтр, TRX/TUNE/DRIVE, split, громкости, АРУ, шумодавы, squelch, скорость CW). Эхо-параметры (RIT/XIT, BIN/ANC/…) не захватываются: на радио они не влияют |
|
| Захват параметра (§3.5) | реализован для того, что клиенты действительно перетягивают (частота, DDS, мода, фильтр, TRX/TUNE/DRIVE, split, громкости, АРУ, шумодавы, squelch, скорость CW). Эхо-параметры (RIT/XIT, BIN/ANC/…) не захватываются: на радио они не влияют |
|
||||||
| Браузерные клиенты | отвергаются по `Origin` (403), см. §1.1. Web-интерфейсу ewsdr TCI не нужен — у него свой канал |
|
| Браузерные клиенты | отвергаются по `Origin` (403), см. §1.1. Web-интерфейсу ewsdr TCI не нужен — у него свой канал |
|
||||||
| `TRX_COUNT` | равен `BackendCaps.MaxPans`, а не числу живых панов: протокол объявляет его один раз. Про несуществующий приёмник просто ничего не шлётся (§2.1) |
|
| `TRX_COUNT` | равен потолку 1 + `MAX_SLICES` = 7, а не числу живых приёмников: протокол объявляет его один раз. Про несуществующий приёмник просто ничего не шлётся (§2.1) |
|
||||||
| Потоки на передаче | RX-аудио и линейный выход на TX замолкают — движок не зовёт аудио-колбэки, пока идёт передача (кроме дуплекса с самоконтролем). То же и с IQ: RX-пакеты на TX дропаются на входе DSP. Это поведение приёмного тракта, а не потоков |
|
| Потоки на передаче | RX-аудио и линейный выход на TX замолкают — движок не зовёт аудио-колбэки, пока идёт передача (кроме дуплекса с самоконтролем). То же и с IQ: RX-пакеты на TX дропаются на входе DSP. Это поведение приёмного тракта, а не потоков |
|
||||||
| `MUTE` и линейный выход | глушит и поток: движок под мьютом не зовёт `OnAudio`. Аудиопоток приёмника (`AUDIO_START`) мьют не трогает — он снимается до громкости |
|
| `MUTE` и линейный выход | глушит и поток: движок под мьютом не зовёт `OnAudio`. Аудиопоток приёмника (`AUDIO_START`) мьют не трогает — он снимается до громкости |
|
||||||
| DIGL/DIGU, 2 канала | по §3.4 в цифровых модах два канала должны нести комплексный сигнал; у нас это обычное стерео с выхода WDSP. Комплексный вывод демодулятора наружу не выведен |
|
| DIGL/DIGU, 2 канала | по §3.4 в цифровых модах два канала должны нести комплексный сигнал; у нас это обычное стерео с выхода WDSP — оба тапа стоят там, где сигнал уже вещественный: `rakDemod` сразу за демодулятором, `rakLineOut` за громкостью. Комплексный выход потребовал бы отдельной ветки панели RXA и третьего маршрута тапа, а цифровые клиенты берут `IQ_START` — он честно комплексный |
|
||||||
| Поток канала B доп. пана | не бывает: в протоколе аудиопоток один на приёмник, и мы отдаём канал A (первый слайс) |
|
| Поток канала B | не бывает: в протоколе аудиопоток один на приёмник, и он всегда про канал A |
|
||||||
| Формат IQ | всегда float32, два канала — `AUDIO_STREAM_SAMPLE_TYPE` относится к аудио (§4.3), а ExpertSDR3 IQ иначе и не шлёт |
|
| Формат IQ | всегда float32, два канала — `AUDIO_STREAM_SAMPLE_TYPE` относится к аудио (§4.3), а ExpertSDR3 IQ иначе и не шлёт |
|
||||||
| `IQ_SAMPLERATE` 384 кГц на Pluto 576/960 кГц | нацело не делится, поэтому уходит 192 кГц (см. §2.5). Клиент обязан читать частоту из заголовка блока, а не считать её равной запрошенной |
|
| `IQ_SAMPLERATE` 384 кГц на Pluto 576/960 кГц | нацело не делится, поэтому уходит 192 кГц (см. §2.5). Клиент обязан читать частоту из заголовка блока, а не считать её равной запрошенной |
|
||||||
| MP3 у рекордера | не поддержан (кодера в проекте нет): `LINE_OUT_RECORDER_SAVE` с `.mp3` отвечает `tci_error` |
|
| MP3 у рекордера | не поддержан: кодера в проекте нет, а тащить внешний (lame) ради рекордера — это новая зависимость и её лицензия в сборке, которых у ewsdr сейчас нигде нет. WAV пишется без потерь и открывается всем; на `.mp3` уходит честный `tci_error`, а не молчаливый WAV с чужим расширением |
|
||||||
|
|
||||||
|
**`KEYER` и `TX_FOOTSWITCH` — почему их нет.** Команды противоположные по
|
||||||
|
направлению, и мешать их в один пункт «не сделано» неправильно.
|
||||||
|
|
||||||
|
`KEYER` шлёт **клиент**: это его «клоподав», прокинутый к нам, и третий
|
||||||
|
аргумент — длительность предыдущего знака — существует ровно затем, чтобы
|
||||||
|
телеграфное ядро воспроизвело тайминг точно. У нас несущую в CW даёт либо
|
||||||
|
прошивка (бит CWX / аппаратный кейер), либо программный кейер для Pluto, и оба
|
||||||
|
берут фронты **как пришли**. Прокинуть в них фронты из WebSocket значит
|
||||||
|
отправить в эфир сетевой джиттер, а `arg3` подставить некуда: команды «сыграй
|
||||||
|
знак длиной 142 мс» у openHPSDR нет. Точка входа готова (`CWXKeyEvent`), но
|
||||||
|
делать это надо вместе с очередью знаков по `arg3` — и после того, как весь
|
||||||
|
передающий тракт TCI пройдёт живого клиента (он ещё не проверялся).
|
||||||
|
|
||||||
|
`TX_FOOTSWITCH` шлёт **сервер**, и это просто не выведено: флаг у контроллера
|
||||||
|
уже есть — `HPS_PTT` из HP-статуса ловится по фронту (`FHPpHWPTT` →
|
||||||
|
`FHWPTTActive`), — но события наружу нет. Плюс две оговорки, из-за которых
|
||||||
|
уведомление было бы полуправдой: бит не отличает педаль от тангенты микрофона,
|
||||||
|
а у Pluto такой линии нет вовсе. Работы тут — новый `rf`-эвент контроллера и
|
||||||
|
строчка в адаптере.
|
||||||
|
|
||||||
Отдельно: у `TX_SENSORS` второй аргумент — уровень микрофона; измерителя
|
Отдельно: у `TX_SENSORS` второй аргумент — уровень микрофона; измерителя
|
||||||
микрофона в EWSDR нет, шлём нижнюю границу шкалы (-60 дБм), чтобы клиент не
|
микрофона в EWSDR нет, шлём нижнюю границу шкалы (-60 дБм), чтобы клиент не
|
||||||
@@ -548,13 +693,24 @@ ExpertSDR3 давно бы не было.
|
|||||||
Бинарные потоки (§3.4) реализованы целиком — см. §2.5. Открыто:
|
Бинарные потоки (§3.4) реализованы целиком — см. §2.5. Открыто:
|
||||||
|
|
||||||
1. **`RX_CHANNEL_ENABLE` как реальное создание/удаление второго слайса пана.**
|
1. **`RX_CHANNEL_ENABLE` как реальное создание/удаление второго слайса пана.**
|
||||||
Сейчас это эхо: канал B главного приёмника — VFO B, он есть всегда, а
|
Сейчас это эхо, и причин две. Первая техническая: слайс **заводит MainForm**
|
||||||
заводить слайс по команде клиента значит отдать ему управление раскладкой
|
(`AddSliceAtFreq` + `CreateSliceFlag` — флаг, раскладка, персист), у
|
||||||
панорамы оператора.
|
контроллера есть только `RemoveSlice`. Адаптер TCI, как и CAT, до MainForm
|
||||||
2. **`KEYER`** — своего события «ключ нажат» у контроллера нет.
|
не дотягивается и не имеет права: он обязан собираться в демоне, где LCL
|
||||||
|
нет вовсе. Вторая по существу: канал B у ExpertSDR3 — второй приёмник внутри
|
||||||
|
того же DDC, а у нас слайс — полноценная сущность со своим флагом, звуком,
|
||||||
|
входом и CAT-портом. Клиент, «выключивший канал B», снёс бы оператору
|
||||||
|
рабочий слайс вместе с его модой, фильтром и портом. Правильный порядок:
|
||||||
|
сперва завести создание слайса в контроллер (от этого выиграет и демон), и
|
||||||
|
только потом привязать к нему команду.
|
||||||
|
2. **`KEYER`** — см. разбор под таблицей §3.1: нужна очередь знаков по `arg3`,
|
||||||
|
а не проброс сетевых фронтов в ключ.
|
||||||
3. **TCI в демоне.** Юниты LCL-free (стенд собирает и гоняет их вместе с
|
3. **TCI в демоне.** Юниты LCL-free (стенд собирает и гоняет их вместе с
|
||||||
`TRadioController` без единого виджета), подключается одной строкой в
|
`TRadioController` без единого виджета), подключается одной строкой в
|
||||||
`ewsdrd.lpr`, как web.
|
`ewsdrd.lpr`, как web. Не сделано намеренно и в этом порядке: в headless
|
||||||
|
сперва попадает то, что уже прошло живого клиента в GUI, где отказ виден
|
||||||
|
глазами. Цена ошибки в демоне выше — грабля с `SIGPIPE` (см. конец §5)
|
||||||
|
убивала именно `ewsdrd`, целиком и молча.
|
||||||
4. **Проверка на железе и с настоящим клиентом** — главное, см. конец §5.
|
4. **Проверка на железе и с настоящим клиентом** — главное, см. конец §5.
|
||||||
|
|
||||||
## 5. Проверено
|
## 5. Проверено
|
||||||
@@ -617,7 +773,7 @@ ExpertSDR3 давно бы не было.
|
|||||||
движков и сети валится с AV — клиент получает `tci_error`, соединение живо) и
|
движков и сети валится с AV — клиент получает `tci_error`, соединение живо) и
|
||||||
неразрывность пачки инициализации под крутящейся ручкой.
|
неразрывность пачки инициализации под крутящейся ручкой.
|
||||||
|
|
||||||
### Стенд этапа 2 (бинарные потоки) — 112 проверок, все зелёные
|
### Стенд этапа 2 (бинарные потоки) — 158 проверок, все зелёные
|
||||||
|
|
||||||
Отдельная программа (`test/tci/tcitest.pas`, прогон — `test/tci/run.sh`,
|
Отдельная программа (`test/tci/tcitest.pas`, прогон — `test/tci/run.sh`,
|
||||||
внешних библиотек не требует) проверяет потоки на четырёх уровнях:
|
внешних библиотек не требует) проверяет потоки на четырёх уровнях:
|
||||||
@@ -646,12 +802,25 @@ ExpertSDR3 давно бы не было.
|
|||||||
сыром сокете): отказ на несуществующий приёмник и на нечисловой аргумент,
|
сыром сокете): отказ на несуществующий приёмник и на нечисловой аргумент,
|
||||||
отказ на старт потока с незапущенного пана, подтверждение и отбраковка
|
отказ на старт потока с незапущенного пана, подтверждение и отбраковка
|
||||||
параметров, `SAVE` без записи и `SAVE` в `.mp3` отвечают ошибкой,
|
параметров, `SAVE` без записи и `SAVE` в `.mp3` отвечают ошибкой,
|
||||||
`TRX:0,true,tci` без аудиопотока модуляцию не берёт, а с потоком берёт и
|
`TRX:0,true,tci` без аудиопотока модуляцию не берёт, а с потоком берёт,
|
||||||
снимает её по `TRX:0,false` и по уходу клиента; чужой бинарный блок не рвёт
|
реально поднимает передачу и снимает её по `TRX:0,false` и по уходу клиента;
|
||||||
соединение; без передачи маркеров `TX_CHRONO` нет. Отдельно — согласование
|
чужой бинарный блок не рвёт соединение; без передачи маркеров `TX_CHRONO`
|
||||||
|
нет. Номер передатчика (§2.7): `trx:9`, `trx:abc` и `tune:9` отвечают ошибкой
|
||||||
|
и **не поднимают эфир**, приёмник без живого пана — тоже; ответ называет
|
||||||
|
передающий приёмник; `TUNE` через TCI включается и гасится, не оставляя за
|
||||||
|
собой ни тона, ни поднятой PTT. Чужая передача: поверх MOX оператора `TRX`
|
||||||
|
не уводит микрофон и не снимает передачу, `TUNE` не включает тон, а уход
|
||||||
|
такого клиента передачу оператора не гасит. Конкурирующий TCI-клиент
|
||||||
|
не получает `TX_CHRONO` чужой передачи, а его уход не снимает эфир
|
||||||
|
первого клиента. Отдельно — согласование
|
||||||
частот: на источнике 192 кГц просьба «384» подтверждается как 192, смена
|
частот: на источнике 192 кГц просьба «384» подтверждается как 192, смена
|
||||||
rate устройства сама переобъявляет и `iq_samplerate`, и `if_limits`, а на
|
rate устройства сама переобъявляет и `iq_samplerate`, и `if_limits`, а на
|
||||||
576 кГц (Pluto) та же просьба даёт законные 192 кГц.
|
576 кГц (Pluto) та же просьба даёт законные 192 кГц.
|
||||||
|
- **Слайс как приёмник** (часть E, живой движок): созданный на ГЛАВНОМ пане
|
||||||
|
слайс становится приёмником 1, отвечает на `vfo:1,0;` своей частотой (это и
|
||||||
|
есть вся инициализация MSHV), слушается командой `vfo:1,0,<Гц>`, отдаёт своё
|
||||||
|
аудио блоками с `receiver = 1`, а после удаления слайса приёмник 1 замолкает
|
||||||
|
целиком — вместо прежнего `vfo:1,0,0`.
|
||||||
- **Сквозной прогон через живой WDSP:** синтетический 24-битный IQ подаётся
|
- **Сквозной прогон через живой WDSP:** синтетический 24-битный IQ подаётся
|
||||||
в движок, а клиент по WebSocket получает блоки RX-аудио 12 кГц и IQ 48 кГц
|
в движок, а клиент по WebSocket получает блоки RX-аудио 12 кГц и IQ 48 кГц
|
||||||
с верными заголовками; после `AUDIO_STOP`/`IQ_STOP` блоки прекращаются.
|
с верными заголовками; после `AUDIO_STOP`/`IQ_STOP` блоки прекращаются.
|
||||||
|
|||||||
@@ -45,3 +45,28 @@ python3 -u test/tci/live_rx_test.py --visible --commands-only --dwell 1.5
|
|||||||
line-out для всех обнаруженных приёмников. Он никогда не посылает
|
line-out для всех обнаруженных приёмников. Он никогда не посылает
|
||||||
`START`, `STOP`, установку `TRX`/`TUNE`, TX-аудио и CW-текст. Если радио
|
`START`, `STOP`, установку `TRX`/`TUNE`, TX-аудио и CW-текст. Если радио
|
||||||
уже на передаче, прогон аварийно прекращается.
|
уже на передаче, прогон аварийно прекращается.
|
||||||
|
|
||||||
|
## Стенд «как MSHV»
|
||||||
|
|
||||||
|
```sh
|
||||||
|
test/tci/mshv_sim.py --rx 0 # в MSHV это «TCI Client rx1»
|
||||||
|
test/tci/mshv_sim.py --rx 1 --audio # «TCI Client rx2»
|
||||||
|
```
|
||||||
|
|
||||||
|
`mshv_sim.py` повторяет логику TCI-клиента MSHV по его исходникам — включая обе
|
||||||
|
особенности, из-за которых он ведёт себя не так, как наш стенд:
|
||||||
|
|
||||||
|
* **фильтр по номеру приёмника** — `if (ls2.at(0)!=tci_trx) continue;`: строка,
|
||||||
|
адресованная не его приёмнику, для MSHV не существует (и то же самое для
|
||||||
|
бинарных блоков, по полю `receiver` заголовка);
|
||||||
|
* **инициализация — один запрос и один ответ**: он шлёт `vfo:<rx>,0;` раз в
|
||||||
|
500 мс, максимум пять раз, и ждёт `vfo:<rx>,0,<частота>`. Не дождался —
|
||||||
|
«Error To Initialize TCI Server». Других причин у этой ошибки нет.
|
||||||
|
|
||||||
|
Поэтому стенд отвечает на вопрос «почему MSHV не подключается» одной строкой, а
|
||||||
|
заодно ловит то, что иначе проявляется молча: без `tx_enable` со **своим**
|
||||||
|
номером приёмника у MSHV не работает PTT (`set_ptt` начинается с
|
||||||
|
`if (!tci_tx_enable) return;`).
|
||||||
|
|
||||||
|
По умолчанию стенд безопасен на приём: ни `TRX`, ни `TUNE`, ни `START`/`STOP`,
|
||||||
|
ни TX-аудио он не шлёт. Передачу включает явный `--tx`.
|
||||||
|
|||||||
@@ -339,6 +339,21 @@ class LiveTest:
|
|||||||
f"rx_volume:{rx},{ch},{new_vol};",
|
f"rx_volume:{rx},{ch},{new_vol};",
|
||||||
f"rx_volume:{rx},{ch},{old_vol};", dwell)
|
f"rx_volume:{rx},{ch},{old_vol};", dwell)
|
||||||
|
|
||||||
|
def visible_vfo_b(self, channels, dwell):
|
||||||
|
"""Move hardware VFO-B; this is not a UI slice on the main pan."""
|
||||||
|
rx, ch = 0, 1
|
||||||
|
self.ok("TCI VFO-B channel is present", ch in channels.get(rx, []),
|
||||||
|
str(channels))
|
||||||
|
if ch not in channels.get(rx, []):
|
||||||
|
return
|
||||||
|
print(f"VFO-B only: +15 kHz for {dwell:.1f}s, then restore")
|
||||||
|
vfo = self.query(f"vfo:{rx},{ch};", "vfo")
|
||||||
|
if len(vfo) >= 3:
|
||||||
|
old_vfo = int(float(vfo[2]))
|
||||||
|
self.visible_change("VFO-B", "vfo",
|
||||||
|
f"vfo:{rx},{ch},{old_vfo + 15000};",
|
||||||
|
f"vfo:{rx},{ch},{old_vfo};", dwell)
|
||||||
|
|
||||||
def sample_stats(self, payload, fmt, length):
|
def sample_stats(self, payload, fmt, length):
|
||||||
data = payload[HDR.size:]
|
data = payload[HDR.size:]
|
||||||
count = min(length, 4096)
|
count = min(length, 4096)
|
||||||
@@ -362,7 +377,8 @@ class LiveTest:
|
|||||||
rms = math.sqrt(sum(v * v for v in finite) / len(finite))
|
rms = math.sqrt(sum(v * v for v in finite) / len(finite))
|
||||||
return rms, max(abs(v) for v in finite), len(finite) == len(vals)
|
return rms, max(abs(v) for v in finite), len(finite) == len(vals)
|
||||||
|
|
||||||
def stream(self, rx, start, stop, wanted_type, requested_length=None):
|
def stream(self, rx, start, stop, wanted_type, requested_length=None,
|
||||||
|
expected_rate=None):
|
||||||
self.binary.clear()
|
self.binary.clear()
|
||||||
self.text.clear()
|
self.text.clear()
|
||||||
self.ws.send_text(f"{start}:{rx};")
|
self.ws.send_text(f"{start}:{rx};")
|
||||||
@@ -400,6 +416,8 @@ class LiveTest:
|
|||||||
if not finite:
|
if not finite:
|
||||||
bad += 1
|
bad += 1
|
||||||
rms_values.append(rms)
|
rms_values.append(rms)
|
||||||
|
if expected_rate is not None and rates != {expected_rate}:
|
||||||
|
bad += 1
|
||||||
self.ok(label + " headers", bad == 0,
|
self.ok(label + " headers", bad == 0,
|
||||||
f"blocks={len(blocks)}, bad={bad}, rates={sorted(rates)}")
|
f"blocks={len(blocks)}, bad={bad}, rates={sorted(rates)}")
|
||||||
rms = max(rms_values) if rms_values else 0.0
|
rms = max(rms_values) if rms_values else 0.0
|
||||||
@@ -452,6 +470,28 @@ class LiveTest:
|
|||||||
finally:
|
finally:
|
||||||
self.seconds = old_seconds
|
self.seconds = old_seconds
|
||||||
|
|
||||||
|
def iq_rate_matrix(self, receivers):
|
||||||
|
"""Compare negotiated IQ rate with every live block header."""
|
||||||
|
rx = receivers[0]
|
||||||
|
old_seconds = self.seconds
|
||||||
|
self.seconds = 0.8
|
||||||
|
print(f"IQ rate matrix on rx={rx}")
|
||||||
|
try:
|
||||||
|
for requested in (48000, 96000, 192000, 384000):
|
||||||
|
self.text.clear()
|
||||||
|
self.ws.send_text(f"iq_samplerate:{requested};")
|
||||||
|
got, args = self.wait_text("iq_samplerate")
|
||||||
|
effective = int(args[0]) if got and args else 0
|
||||||
|
self.ok(f"IQ rate request {requested}", effective > 0,
|
||||||
|
got or "no response")
|
||||||
|
if effective > 0:
|
||||||
|
self.stream(rx, "iq_start", "iq_stop", 0,
|
||||||
|
expected_rate=effective)
|
||||||
|
finally:
|
||||||
|
self.ws.send_text("iq_samplerate:48000;")
|
||||||
|
self.wait_text("iq_samplerate")
|
||||||
|
self.seconds = old_seconds
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
ap = argparse.ArgumentParser()
|
ap = argparse.ArgumentParser()
|
||||||
@@ -465,10 +505,16 @@ def main():
|
|||||||
help="skip command matrix and capture only IQ/audio")
|
help="skip command matrix and capture only IQ/audio")
|
||||||
ap.add_argument("--format-matrix", action="store_true",
|
ap.add_argument("--format-matrix", action="store_true",
|
||||||
help="test every audio sample type/channel count and rate")
|
help="test every audio sample type/channel count and rate")
|
||||||
|
ap.add_argument("--iq-rate-matrix", action="store_true",
|
||||||
|
help="test negotiated IQ rates against binary headers")
|
||||||
ap.add_argument("--visible", action="store_true",
|
ap.add_argument("--visible", action="store_true",
|
||||||
help="visibly change RX parameters and restore every value")
|
help="visibly change RX parameters and restore every value")
|
||||||
|
ap.add_argument("--vfo-b-visible", action="store_true",
|
||||||
|
help="move hardware VFO-B by 15 kHz and restore it")
|
||||||
ap.add_argument("--dwell", type=float, default=1.5,
|
ap.add_argument("--dwell", type=float, default=1.5,
|
||||||
help="seconds to leave each visible test value active")
|
help="seconds to leave each visible test value active")
|
||||||
|
ap.add_argument("--expect-channels", type=int, default=0,
|
||||||
|
help="require this exact total TCI channel count")
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
|
|
||||||
print(f"Connecting to ws://{args.host}:{args.port} (RX-only safety mode)")
|
print(f"Connecting to ws://{args.host}:{args.port} (RX-only safety mode)")
|
||||||
@@ -491,16 +537,27 @@ def main():
|
|||||||
receivers = sorted({int(row[0]) for row in test.initial_by_name("dds") if row})
|
receivers = sorted({int(row[0]) for row in test.initial_by_name("dds") if row})
|
||||||
print("Discovered receivers/channels:", receivers, channels)
|
print("Discovered receivers/channels:", receivers, channels)
|
||||||
test.ok("live receivers found", bool(receivers), str(receivers))
|
test.ok("live receivers found", bool(receivers), str(receivers))
|
||||||
test.ok("three slices/channels visible", sum(map(len, channels.values())) >= 3,
|
slice_count = sum(map(len, channels.values()))
|
||||||
|
if args.expect_channels > 0:
|
||||||
|
test.ok(f"expected {args.expect_channels} TCI channels visible",
|
||||||
|
slice_count == args.expect_channels, str(channels))
|
||||||
|
else:
|
||||||
|
test.ok("at least one slice/channel visible", slice_count > 0,
|
||||||
str(channels))
|
str(channels))
|
||||||
|
|
||||||
if args.format_matrix:
|
if args.vfo_b_visible:
|
||||||
|
test.visible_vfo_b(channels, args.dwell)
|
||||||
|
elif args.iq_rate_matrix:
|
||||||
|
test.iq_rate_matrix(receivers)
|
||||||
|
elif args.format_matrix:
|
||||||
test.format_matrix(receivers)
|
test.format_matrix(receivers)
|
||||||
elif args.visible:
|
elif args.visible:
|
||||||
test.visible_checks(receivers, channels, args.dwell)
|
test.visible_checks(receivers, channels, args.dwell)
|
||||||
elif not args.streams_only:
|
elif not args.streams_only:
|
||||||
test.command_checks(receivers, channels)
|
test.command_checks(receivers, channels)
|
||||||
if not args.commands_only and not args.visible and not args.format_matrix:
|
if (not args.commands_only and not args.visible and
|
||||||
|
not args.vfo_b_visible and
|
||||||
|
not args.format_matrix and not args.iq_rate_matrix):
|
||||||
test.stream_checks(receivers)
|
test.stream_checks(receivers)
|
||||||
print("Skipped by RF-safety policy: START, STOP, TRX/TUNE setters, TX audio, "
|
print("Skipped by RF-safety policy: START, STOP, TRX/TUNE setters, TX audio, "
|
||||||
"CW text/keying, SPOT_CLEAR and SET_IN_FOCUS")
|
"CW text/keying, SPOT_CLEAR and SET_IN_FOCUS")
|
||||||
|
|||||||
Executable
+283
@@ -0,0 +1,283 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Стенд «как MSHV»: повторяет TCI-клиента MSHV по его исходникам и говорит,
|
||||||
|
почему он не подключается.
|
||||||
|
|
||||||
|
Зачем: MSHV — самый распространённый TCI-клиент для цифры, и ведёт он себя не
|
||||||
|
так, как наш собственный стенд. Две его особенности определяют всё:
|
||||||
|
|
||||||
|
1. ВСЕ входящие строки он фильтрует по номеру приёмника:
|
||||||
|
if (ls2.at(0)!=tci_trx) continue; // network.cpp
|
||||||
|
то есть клиенту в режиме RX2 (tci_trx = "1") строка `trx:0,false`
|
||||||
|
не существует в природе.
|
||||||
|
|
||||||
|
2. Инициализация — это ожидание ОДНОГО ответа. Он шлёт `vfo:<rx>,0;` раз в
|
||||||
|
500 мс, максимум 5 попыток, и ждёт `vfo:<rx>,0,<частота>`. Не дождался —
|
||||||
|
«Error To Initialize TCI Server» (network.cpp, Network::initAll,
|
||||||
|
ветка s_ModelID==3||4). Никаких других причин у этой ошибки нет.
|
||||||
|
|
||||||
|
Соответствие настроек MSHV: «TCI Client rx1» → приёмник 0, «TCI Client rx2» →
|
||||||
|
приёмник 1 (hvrigcontrol.cpp: netServPort[3]/[4], network.cpp: tci_trx).
|
||||||
|
|
||||||
|
Стенд по умолчанию БЕЗОПАСЕН на приём: ни TRX, ни TUNE, ни START/STOP, ни
|
||||||
|
TX-аудио не отправляются. Передающую часть включает явный --tx.
|
||||||
|
|
||||||
|
Запуск (приложение уже работает, TCI-сервер поднят):
|
||||||
|
test/tci/mshv_sim.py --rx 1
|
||||||
|
test/tci/mshv_sim.py --rx 0 --audio
|
||||||
|
Только стандартная библиотека; транспорт переиспользуется из live_rx_test.py.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
from live_rx_test import WS, split_commands, parse_command # noqa: E402
|
||||||
|
|
||||||
|
HDR = struct.Struct("<16I")
|
||||||
|
|
||||||
|
# Умолчания MSHV из его же диалога настроек (hvrigcontrol.cpp): каналов 2,
|
||||||
|
# сэмплов 2048, тип float32, частота 48 кГц, буфер TX 24 мс.
|
||||||
|
MSHV_CHANNELS = "2"
|
||||||
|
MSHV_SAMPLES = "2048"
|
||||||
|
MSHV_TYPE = "float32"
|
||||||
|
MSHV_RATE = "48000"
|
||||||
|
MSHV_TXBUF = "24"
|
||||||
|
|
||||||
|
|
||||||
|
class Verdict:
|
||||||
|
def __init__(self):
|
||||||
|
self.ok_count = 0
|
||||||
|
self.fail_count = 0
|
||||||
|
|
||||||
|
def ok(self, label, detail=""):
|
||||||
|
self.ok_count += 1
|
||||||
|
print(f" ok {label}" + (f" — {detail}" if detail else ""))
|
||||||
|
|
||||||
|
def fail(self, label, detail=""):
|
||||||
|
self.fail_count += 1
|
||||||
|
print(f" FAIL {label}" + (f" — {detail}" if detail else ""))
|
||||||
|
|
||||||
|
def note(self, label, detail=""):
|
||||||
|
print(f" .. {label}" + (f" — {detail}" if detail else ""))
|
||||||
|
|
||||||
|
|
||||||
|
class MshvClient:
|
||||||
|
"""Ровно та логика, что в Network::* у MSHV — включая её ограничения."""
|
||||||
|
|
||||||
|
def __init__(self, ws, rx, verdict):
|
||||||
|
self.ws = ws
|
||||||
|
self.rx = str(rx)
|
||||||
|
self.v = verdict
|
||||||
|
# Состояние, которое MSHV ведёт по ФИЛЬТРОВАННОМУ потоку строк.
|
||||||
|
self.tx_enable = False
|
||||||
|
self.rx_enable = False
|
||||||
|
self.rx_mute = False
|
||||||
|
self.split_enable = False
|
||||||
|
self.drive = 0
|
||||||
|
self.is_trx = False
|
||||||
|
self.vfo_hz = None
|
||||||
|
self.mode = None
|
||||||
|
self.device = "NONE"
|
||||||
|
self.vendor = "ExpertSDR3"
|
||||||
|
self.proto_id = 0 # 0, 1 = 1.5.0 и выше, 2 = 1.9.0 и выше
|
||||||
|
self.audio_blocks = 0
|
||||||
|
self.audio_foreign = 0 # блоки чужих приёмников (MSHV их выбрасывает)
|
||||||
|
self.seen_for_me = [] # что вообще дошло до логики MSHV
|
||||||
|
self.seen_all = []
|
||||||
|
|
||||||
|
# ── приём ────────────────────────────────────────────────────────────
|
||||||
|
def pump(self, seconds):
|
||||||
|
deadline = time.monotonic() + seconds
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
frame = self.ws.recv_frame(min(0.2, deadline - time.monotonic()))
|
||||||
|
if frame is None:
|
||||||
|
continue
|
||||||
|
opcode, payload = frame
|
||||||
|
if opcode == 1:
|
||||||
|
for command in split_commands(payload.decode("utf-8", "replace")):
|
||||||
|
self.on_text(command)
|
||||||
|
elif opcode == 2:
|
||||||
|
self.on_binary(payload)
|
||||||
|
|
||||||
|
def on_text(self, command):
|
||||||
|
name, args = parse_command(command)
|
||||||
|
self.seen_all.append(command)
|
||||||
|
args = args + ["", "", ""]
|
||||||
|
|
||||||
|
# protocol разбирается ДО фильтра по приёмнику (так же у MSHV).
|
||||||
|
if name == "protocol":
|
||||||
|
self.vendor = args[0]
|
||||||
|
parts = (args[1].split(".") + ["0", "0", "0"])[:3]
|
||||||
|
v0, v1, v2 = (int(p) if p.isdigit() else 0 for p in parts)
|
||||||
|
self.proto_id = 2
|
||||||
|
if v0 < 2 and v1 < 9 and v2 < 1000:
|
||||||
|
self.proto_id = 1
|
||||||
|
if v0 < 2 and v1 < 5 and v2 < 1000:
|
||||||
|
self.proto_id = 0
|
||||||
|
return
|
||||||
|
if name == "device":
|
||||||
|
self.device = args[0]
|
||||||
|
return
|
||||||
|
|
||||||
|
# ★Фильтр MSHV: всё, адресованное не нашему приёмнику, не существует.
|
||||||
|
if args[0] != self.rx:
|
||||||
|
return
|
||||||
|
self.seen_for_me.append(command)
|
||||||
|
|
||||||
|
if name == "vfo" and args[1] == "0":
|
||||||
|
self.vfo_hz = args[2]
|
||||||
|
elif name == "modulation":
|
||||||
|
self.mode = args[1]
|
||||||
|
elif name == "trx":
|
||||||
|
self.is_trx = args[1] == "true"
|
||||||
|
elif name == "tx_enable":
|
||||||
|
self.tx_enable = args[1] == "true"
|
||||||
|
elif name == "rx_enable":
|
||||||
|
self.rx_enable = args[1] == "true"
|
||||||
|
elif name == "rx_mute":
|
||||||
|
self.rx_mute = args[1] == "true"
|
||||||
|
elif name == "split_enable":
|
||||||
|
self.split_enable = args[1] == "true"
|
||||||
|
elif name == "drive":
|
||||||
|
self.drive = int(args[1]) if args[1].lstrip("-").isdigit() else 0
|
||||||
|
|
||||||
|
def on_binary(self, payload):
|
||||||
|
if len(payload) < HDR.size:
|
||||||
|
return
|
||||||
|
head = HDR.unpack(payload[:HDR.size])
|
||||||
|
# MSHV: if (pStream->receiver != tci_trx) return;
|
||||||
|
if head[0] != int(self.rx):
|
||||||
|
self.audio_foreign += 1
|
||||||
|
return
|
||||||
|
self.audio_blocks += 1
|
||||||
|
|
||||||
|
# ── инициализация: единственная причина «Error To Initialize» ────────
|
||||||
|
def initialize(self):
|
||||||
|
for attempt in range(1, 6):
|
||||||
|
self.ws.send_text(f"vfo:{self.rx},0;")
|
||||||
|
self.pump(0.5)
|
||||||
|
if self.vfo_hz is not None:
|
||||||
|
self.v.ok(f"инициализация прошла с попытки {attempt}",
|
||||||
|
f"vfo:{self.rx},0,{self.vfo_hz}")
|
||||||
|
return True
|
||||||
|
self.v.note(f"попытка {attempt}: ответа vfo:{self.rx},0,… нет")
|
||||||
|
self.v.fail("Error To Initialize TCI Server",
|
||||||
|
f"за 5 попыток не пришло vfo:{self.rx},0,<частота> — "
|
||||||
|
f"приёмника {self.rx} для сервера не существует")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ── то, что MSHV делает сразу после успешной инициализации ───────────
|
||||||
|
def after_connect(self):
|
||||||
|
if not self.tx_enable:
|
||||||
|
self.ws.send_text(f"tx_enable:{self.rx},true;")
|
||||||
|
if not self.rx_enable:
|
||||||
|
self.ws.send_text(f"rx_enable:{self.rx},true;")
|
||||||
|
if self.rx_mute:
|
||||||
|
self.ws.send_text(f"rx_mute:{self.rx},false;")
|
||||||
|
if self.split_enable:
|
||||||
|
self.ws.send_text(f"split_enable:{self.rx},false;")
|
||||||
|
if self.drive <= 0:
|
||||||
|
self.ws.send_text(f"drive:{self.rx},25;")
|
||||||
|
self.pump(1.0)
|
||||||
|
|
||||||
|
def start_audio(self):
|
||||||
|
self.ws.send_text(f"audio_samplerate:{MSHV_RATE};")
|
||||||
|
if self.proto_id > 1:
|
||||||
|
self.ws.send_text(f"audio_stream_sample_type:{MSHV_TYPE};")
|
||||||
|
self.ws.send_text(f"audio_stream_channels:{MSHV_CHANNELS};")
|
||||||
|
self.ws.send_text(f"audio_stream_samples:{MSHV_SAMPLES};")
|
||||||
|
self.ws.send_text(f"tx_stream_audio_buffering:{MSHV_TXBUF};")
|
||||||
|
self.ws.send_text(f"audio_start:{self.rx};")
|
||||||
|
|
||||||
|
def stop_audio(self):
|
||||||
|
self.ws.send_text(f"audio_stop:{self.rx};")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
|
ap.add_argument("--host", default="127.0.0.1")
|
||||||
|
ap.add_argument("--port", type=int, default=40001)
|
||||||
|
ap.add_argument("--rx", type=int, default=0,
|
||||||
|
help="приёмник TCI: 0 = «TCI Client rx1» в MSHV, 1 = «rx2»")
|
||||||
|
ap.add_argument("--audio", action="store_true",
|
||||||
|
help="запросить аудиопоток, как это делает MSHV")
|
||||||
|
ap.add_argument("--seconds", type=float, default=3.0,
|
||||||
|
help="сколько слушать поток")
|
||||||
|
ap.add_argument("--tx", action="store_true",
|
||||||
|
help="★ВКЛЮЧАЕТ ПЕРЕДАЧУ: короткий trx:<rx>,true,tci")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
v = Verdict()
|
||||||
|
print(f"Стенд MSHV: приёмник {args.rx} "
|
||||||
|
f"(в MSHV это «TCI Client rx{args.rx + 1}»)")
|
||||||
|
try:
|
||||||
|
ws = WS(args.host, args.port)
|
||||||
|
except Exception as exc:
|
||||||
|
v.fail("подключение к TCI-серверу", str(exc))
|
||||||
|
print(f"\nИтого: {v.ok_count} ok, {v.fail_count} FAIL")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
client = MshvClient(ws, args.rx, v)
|
||||||
|
try:
|
||||||
|
# Пачка инициализации приходит сама; MSHV её тоже читает.
|
||||||
|
client.pump(1.5)
|
||||||
|
v.ok("пачка инициализации получена",
|
||||||
|
f"{client.vendor} {client.device}, protocol id {client.proto_id}")
|
||||||
|
|
||||||
|
if not client.initialize():
|
||||||
|
return 1
|
||||||
|
|
||||||
|
client.after_connect()
|
||||||
|
|
||||||
|
# Дальше — то, без чего MSHV работает молча неправильно.
|
||||||
|
if client.tx_enable:
|
||||||
|
v.ok(f"tx_enable для приёмника {args.rx} получен")
|
||||||
|
else:
|
||||||
|
v.fail(f"tx_enable для приёмника {args.rx} НЕ получен",
|
||||||
|
"MSHV начинает set_ptt со строки `if (!tci_tx_enable) return;` "
|
||||||
|
"— PTT будет молча мёртв")
|
||||||
|
if client.mode:
|
||||||
|
v.ok("модуляция известна", client.mode)
|
||||||
|
else:
|
||||||
|
v.fail("modulation для этого приёмника не приходила",
|
||||||
|
"MSHV не узнает вид связи")
|
||||||
|
|
||||||
|
if args.audio:
|
||||||
|
client.start_audio()
|
||||||
|
client.pump(args.seconds)
|
||||||
|
if client.audio_blocks > 0:
|
||||||
|
v.ok("аудиопоток идёт", f"{client.audio_blocks} блоков")
|
||||||
|
else:
|
||||||
|
v.fail("блоков аудио нет",
|
||||||
|
f"чужих (не нашего приёмника) отброшено: {client.audio_foreign}")
|
||||||
|
client.stop_audio()
|
||||||
|
client.pump(0.3)
|
||||||
|
|
||||||
|
if args.tx:
|
||||||
|
print(" .. ★передача: trx:%d,true,tci" % args.rx)
|
||||||
|
client.ws.send_text(f"trx:{args.rx},true,tci;")
|
||||||
|
client.pump(0.7)
|
||||||
|
if client.is_trx:
|
||||||
|
v.ok("MSHV увидел бы себя в эфире", f"trx:{args.rx},true")
|
||||||
|
else:
|
||||||
|
v.fail("подтверждения передачи для своего приёмника нет",
|
||||||
|
"MSHV фильтрует ответ по номеру приёмника: "
|
||||||
|
"строку с чужим номером он не увидит")
|
||||||
|
client.ws.send_text(f"trx:{args.rx},false;")
|
||||||
|
client.pump(0.5)
|
||||||
|
|
||||||
|
print(f"\nСтрок, дошедших до логики MSHV: {len(client.seen_for_me)} "
|
||||||
|
f"из {len(client.seen_all)} присланных")
|
||||||
|
finally:
|
||||||
|
ws.close()
|
||||||
|
|
||||||
|
print(f"\nИтого: {v.ok_count} ok, {v.fail_count} FAIL")
|
||||||
|
return 1 if v.fail_count else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
+176
-2
@@ -997,11 +997,115 @@ begin
|
|||||||
|
|
||||||
C.SendText('audio_start:0;');
|
C.SendText('audio_start:0;');
|
||||||
C.SendText('trx:0,true,tci;');
|
C.SendText('trx:0,true,tci;');
|
||||||
C.WaitText('trx:', 1000);
|
C.Pump(300);
|
||||||
Check('trx tci с потоком берёт модуляцию', Ctrl.TCIMicRequested);
|
Check('trx tci с потоком берёт модуляцию', Ctrl.TCIMicRequested);
|
||||||
|
// ★Проверяем и сам эфир, а не только флаг источника: раньше SetMOX по этой
|
||||||
|
// дороге падал в Access violation (SyncCWKeyer → SendDUCSpecificFromSettings
|
||||||
|
// при FNetwork = nil), ошибку глотал обработчик команды, и «передача» жила
|
||||||
|
// только в ответе клиенту.
|
||||||
|
Check('trx:0,true поднял передачу', Ctrl.FTransmitting);
|
||||||
|
|
||||||
|
// Второй TCI-клиент не должен перехватывать ни аудио, ни
|
||||||
|
// владение уже идущей TCI-передачей. Ждём истечения hold,
|
||||||
|
// иначе вторая команда будет отклонена ещё до этой проверки.
|
||||||
|
C2 := TRawClient.Create;
|
||||||
|
try
|
||||||
|
Check('конкурирующий клиент подключился', C2.Connect(PORT));
|
||||||
|
Check('конкурирующему пришёл ready', C2.WaitText('ready;', 2000) <> '');
|
||||||
|
C2.SendText('audio_start:0;');
|
||||||
|
Sleep(300);
|
||||||
|
C2.SendText('trx:0,true,tci;');
|
||||||
|
C2.Pump(300);
|
||||||
|
GotBinary := False;
|
||||||
|
while C2.NextFrame(Op, Pay) do
|
||||||
|
if Op = $02 then GotBinary := True;
|
||||||
|
Check('конкурирующий клиент не получил TX_CHRONO', not GotBinary);
|
||||||
|
C2.Close_;
|
||||||
|
Sleep(600);
|
||||||
|
Check('уход второго TCI-клиента не снял эфир первого',
|
||||||
|
Ctrl.FTransmitting);
|
||||||
|
Check('после ухода второго остался один клиент',
|
||||||
|
Ad.ClientCount = 1, IntToStr(Ad.ClientCount));
|
||||||
|
finally
|
||||||
|
C2.Close_;
|
||||||
|
C2.Free;
|
||||||
|
end;
|
||||||
|
|
||||||
C.SendText('trx:0,false;');
|
C.SendText('trx:0,false;');
|
||||||
C.WaitText('trx:', 1000);
|
C.Pump(300);
|
||||||
Check('снятие TRX снимает источник', not Ctrl.TCIMicRequested);
|
Check('снятие TRX снимает источник', not Ctrl.TCIMicRequested);
|
||||||
|
Check('снятие TRX сняло передачу', not Ctrl.FTransmitting);
|
||||||
|
|
||||||
|
// ★arg1 у TRX/TUNE — номер передатчика, и он обязан разбираться, как всюду:
|
||||||
|
// раньше он игнорировался целиком, и клиент доп. приёмника уводил в эфир
|
||||||
|
// слайс оператора (чужая частота, а с кросс-бандом — чужой диапазон).
|
||||||
|
C.SendText('trx:9,true;');
|
||||||
|
S := C.WaitText('tci_error', 1500);
|
||||||
|
Check('trx:9 → ошибка, а не эфир', Pos('bad receiver', S) > 0, S);
|
||||||
|
Check('trx:9 не поднял передачу', not Ctrl.FTransmitting);
|
||||||
|
|
||||||
|
C.SendText('trx:abc,true;');
|
||||||
|
S := C.WaitText('tci_error', 1500);
|
||||||
|
Check('trx:abc → ошибка', Pos('bad receiver', S) > 0, S);
|
||||||
|
Check('trx:abc не поднял передачу', not Ctrl.FTransmitting);
|
||||||
|
|
||||||
|
// Приёмник 1 без подключённого радио: либо вне потолка, либо не запущен —
|
||||||
|
// но в эфир по нему уходить нельзя ни в каком случае.
|
||||||
|
C.SendText('trx:1,true;');
|
||||||
|
S := C.WaitText('tci_error', 1500);
|
||||||
|
Check('trx на несуществующий приёмник → ошибка',
|
||||||
|
(Pos('not running', S) > 0) or (Pos('bad receiver', S) > 0), S);
|
||||||
|
Check('trx на несуществующий приёмник не поднял передачу',
|
||||||
|
not Ctrl.FTransmitting);
|
||||||
|
|
||||||
|
C.SendText('tune:9,true;');
|
||||||
|
S := C.WaitText('tci_error', 1500);
|
||||||
|
Check('tune:9 → ошибка', Pos('bad receiver', S) > 0, S);
|
||||||
|
Check('tune:9 не включил TUN', not Ctrl.FTuning);
|
||||||
|
|
||||||
|
// Ответ называет приёмник, чей слайс реально в эфире. Без радио источник
|
||||||
|
// передачи — главный VFO, то есть 0.
|
||||||
|
C.SendText('trx:0;');
|
||||||
|
S := C.WaitText('trx:', 1000);
|
||||||
|
Check('ответ trx называет передающий приёмник', Pos('trx:0,', S) > 0, S);
|
||||||
|
C.SendText('tune:0;');
|
||||||
|
S := C.WaitText('tune:', 1000);
|
||||||
|
Check('ответ tune называет передающий приёмник', Pos('tune:0,', S) > 0, S);
|
||||||
|
|
||||||
|
// TUN через TCI: включается и гасится, и снятие возвращает всё на место.
|
||||||
|
// Ждём временем, а не WaitText: на каждую смену состояния уходит ДВА
|
||||||
|
// сообщения (ответ автору + рассылка по rfTuning), и WaitText поймал бы
|
||||||
|
// хвост предыдущей команды, не дождавшись текущей.
|
||||||
|
C.SendText('tune:0,true;');
|
||||||
|
C.Pump(300);
|
||||||
|
Check('tune:0,true включил TUN', Ctrl.FTuning);
|
||||||
|
C.SendText('tune:0,false;');
|
||||||
|
C.Pump(300);
|
||||||
|
Check('tune:0,false выключил TUN', not Ctrl.FTuning);
|
||||||
|
Check('после TUN передача снята', not Ctrl.FTransmitting);
|
||||||
|
|
||||||
|
// ★Передача, начатая ОПЕРАТОРОМ: клиент не вправе ни увести из неё
|
||||||
|
// микрофон, ни подмешать в неё настроечный тон. SetMOX(True) поверх уже
|
||||||
|
// идущей передачи не выходит рано — он заново выбирает источник модуляции,
|
||||||
|
// и «trx:0,true,tci» посреди чужой передачи уводил её в ринг TCI, не
|
||||||
|
// становясь при этом хозяином эфира.
|
||||||
|
// Пауза перед командой обязательна: смену состояния оператором адаптер
|
||||||
|
// тоже захватывает (§3.5, 200 мс), и без неё команду отклонили бы по
|
||||||
|
// совсем другой причине — проверка прошла бы вхолостую.
|
||||||
|
Ctrl.SetMOX(True);
|
||||||
|
Check('оператор в эфире', Ctrl.FTransmitting);
|
||||||
|
Sleep(300);
|
||||||
|
C.SendText('trx:0,true,tci;');
|
||||||
|
C.Pump(300);
|
||||||
|
Check('trx поверх чужой передачи не уводит микрофон',
|
||||||
|
not Ctrl.TCIMicRequested);
|
||||||
|
Check('trx поверх чужой передачи её не трогает', Ctrl.FTransmitting);
|
||||||
|
Sleep(300);
|
||||||
|
C.SendText('tune:0,true;');
|
||||||
|
C.Pump(300);
|
||||||
|
Check('tune поверх чужой передачи не включает тон', not Ctrl.FTuning);
|
||||||
|
Ctrl.SetMOX(False);
|
||||||
|
Check('передачу оператора снимает оператор', not Ctrl.FTransmitting);
|
||||||
|
|
||||||
// Бинарный блок не нашего типа обязан быть проигнорирован, а соединение —
|
// Бинарный блок не нашего типа обязан быть проигнорирован, а соединение —
|
||||||
// остаться рабочим (клиент шлёт их пачками, рвать связь нельзя).
|
// остаться рабочим (клиент шлёт их пачками, рвать связь нельзя).
|
||||||
@@ -1019,6 +1123,13 @@ begin
|
|||||||
if Op = $02 then GotBinary := True;
|
if Op = $02 then GotBinary := True;
|
||||||
Check('без передачи маркеров нет', not GotBinary);
|
Check('без передачи маркеров нет', not GotBinary);
|
||||||
|
|
||||||
|
// ★Уход клиента снимает только ЕГО передачу. Ставим в эфир оператора,
|
||||||
|
// клиент безуспешно просит TRX поверх — и уходит: передача обязана остаться.
|
||||||
|
Ctrl.SetMOX(True);
|
||||||
|
Sleep(300);
|
||||||
|
C.SendText('trx:0,true,tci;');
|
||||||
|
C.Pump(300);
|
||||||
|
|
||||||
// ★Обычный TCP-разрыв БЕЗ close-кадра: так уходит и упавший клиент, и
|
// ★Обычный TCP-разрыв БЕЗ close-кадра: так уходит и упавший клиент, и
|
||||||
// выдернутый кабель. recv отдаёт 0, и это EOF, а не таймаут — errno при
|
// выдернутый кабель. recv отдаёт 0, и это EOF, а не таймаут — errno при
|
||||||
// нём не трогается и вполне может нести EAGAIN от прошлого истёкшего
|
// нём не трогается и вполне может нести EAGAIN от прошлого истёкшего
|
||||||
@@ -1026,6 +1137,8 @@ begin
|
|||||||
// после нескольких аварийных отключений новые клиенты не подключались.
|
// после нескольких аварийных отключений новые клиенты не подключались.
|
||||||
C.Close_;
|
C.Close_;
|
||||||
Sleep(300);
|
Sleep(300);
|
||||||
|
Check('уход клиента не снял чужую передачу', Ctrl.FTransmitting);
|
||||||
|
Ctrl.SetMOX(False);
|
||||||
Check('после ухода клиента источник снят', not Ctrl.TCIMicRequested);
|
Check('после ухода клиента источник снят', not Ctrl.TCIMicRequested);
|
||||||
Check('TCP-разрыв без close-кадра освобождает слот', Ad.ClientCount = 0,
|
Check('TCP-разрыв без close-кадра освобождает слот', Ad.ClientCount = 0,
|
||||||
IntToStr(Ad.ClientCount));
|
IntToStr(Ad.ClientCount));
|
||||||
@@ -1083,6 +1196,9 @@ var
|
|||||||
Pay: TBytes;
|
Pay: TBytes;
|
||||||
H: TTCIStreamHeader;
|
H: TTCIStreamHeader;
|
||||||
AudioBlocks, IQBlocks, BadHdr: Integer;
|
AudioBlocks, IQBlocks, BadHdr: Integer;
|
||||||
|
SliceId: Integer;
|
||||||
|
SV: TSliceView;
|
||||||
|
S: string;
|
||||||
begin
|
begin
|
||||||
WriteLn('E. Сквозной прогон через движок');
|
WriteLn('E. Сквозной прогон через движок');
|
||||||
|
|
||||||
@@ -1183,6 +1299,64 @@ begin
|
|||||||
Check('сквозной: блоки IQ пришли', IQBlocks > 0, IntToStr(IQBlocks));
|
Check('сквозной: блоки IQ пришли', IQBlocks > 0, IntToStr(IQBlocks));
|
||||||
Check('сквозной: заголовки верны', BadHdr = 0, IntToStr(BadHdr));
|
Check('сквозной: заголовки верны', BadHdr = 0, IntToStr(BadHdr));
|
||||||
|
|
||||||
|
// ── ★Слайс главного пана = приёмник 1 ────────────────────────────────
|
||||||
|
// Ровно случай Pluto: панов больше одного там не бывает, и «второй
|
||||||
|
// приёмник» существует только как слайс. Приёмник = слот слайса, поэтому
|
||||||
|
// первый созданный слайс (слот 0, буква B) обязан стать приёмником 1 —
|
||||||
|
// единственным номером сверх нулевого, который умеют клиенты вроде MSHV.
|
||||||
|
SliceId := Ctrl.AddSlice(Ctrl.FCenterFreq + 3000, MODE_USB, 200, 2800,
|
||||||
|
agcMedium, 0.5, -1, '', 0);
|
||||||
|
Check('слайс на главном пане создан', SliceId > 0, IntToStr(SliceId));
|
||||||
|
C.Pump(300);
|
||||||
|
while C.NextFrame(Op, Pay) do ; // выгребаем рассылку о появлении
|
||||||
|
|
||||||
|
// Инициализация MSHV — это ровно один запрос и ровно один ответ
|
||||||
|
// (network.cpp, Network::initAll: `vfo:<rx>,0;` пять раз, потом
|
||||||
|
// «Error To Initialize TCI Server»).
|
||||||
|
C.SendText('vfo:1,0;');
|
||||||
|
S := C.WaitText('vfo:1,0,', 1500);
|
||||||
|
Check('слайс отвечает на vfo:1,0 (инициализация MSHV)', S <> '', S);
|
||||||
|
Check('слайс отдаёт свою частоту',
|
||||||
|
Pos(IntToStr(Round(Ctrl.FCenterFreq + 3000)), S) > 0, S);
|
||||||
|
|
||||||
|
// Управление: то, чего от слайса и хотят.
|
||||||
|
C.SendText('vfo:1,0,' + IntToStr(Round(Ctrl.FCenterFreq + 5000)) + ';');
|
||||||
|
C.Pump(300);
|
||||||
|
Check('слайсом можно управлять по TCI',
|
||||||
|
Ctrl.GetSliceView(SliceId, SV) and
|
||||||
|
(Abs(SV.TargetHz - (Ctrl.FCenterFreq + 5000)) < 2),
|
||||||
|
FloatToStr(SV.TargetHz));
|
||||||
|
|
||||||
|
// Аудиопоток приёмника 1 — это звук слайса, и в заголовке стоит его номер
|
||||||
|
// (MSHV отбрасывает блоки с чужим receiver: network.cpp:231).
|
||||||
|
C.SendText('audio_start:1;');
|
||||||
|
C.Pump(100);
|
||||||
|
for k := 0 to 40 do
|
||||||
|
begin
|
||||||
|
Ctrl.FDSPEngine.PushDDCPacket(Pkt, 0, PAIRS);
|
||||||
|
Sleep(2);
|
||||||
|
end;
|
||||||
|
C.Pump(400);
|
||||||
|
n := 0;
|
||||||
|
while C.NextFrame(Op, Pay) do
|
||||||
|
if (Op = $02) and (Length(Pay) >= SizeOf(H)) then
|
||||||
|
begin
|
||||||
|
Move(Pay[0], H, SizeOf(H));
|
||||||
|
if (H.StreamType = LongWord(Ord(tstRXAudio))) and (H.Receiver = 1) then
|
||||||
|
Inc(n);
|
||||||
|
end;
|
||||||
|
Check('аудио слайса идёт под номером 1', n > 0, IntToStr(n));
|
||||||
|
C.SendText('audio_stop:1;');
|
||||||
|
C.Pump(200);
|
||||||
|
|
||||||
|
// Удалили слайс — приёмник исчез, и врать про его частоту нельзя.
|
||||||
|
Ctrl.RemoveSlice(SliceId);
|
||||||
|
C.Pump(300);
|
||||||
|
while C.NextFrame(Op, Pay) do ;
|
||||||
|
C.SendText('vfo:1,0;');
|
||||||
|
S := C.WaitText('vfo:1,0,', 800);
|
||||||
|
Check('после удаления слайса приёмник 1 молчит', S = '', S);
|
||||||
|
|
||||||
// Остановка потока обязана прекратить подачу.
|
// Остановка потока обязана прекратить подачу.
|
||||||
C.SendText('audio_stop:0;');
|
C.SendText('audio_stop:0;');
|
||||||
C.SendText('iq_stop:0;');
|
C.SendText('iq_stop:0;');
|
||||||
|
|||||||
Reference in New Issue
Block a user