mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +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:
+349
-161
@@ -47,7 +47,16 @@ uses
|
||||
DXSpotStore, TCIProtocol, TCIServer, TCIStreams;
|
||||
|
||||
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), от него и
|
||||
// считаются все прореживания и пересчёты потоков.
|
||||
TCI_AUDIO_ENGINE_RATE = 48000;
|
||||
@@ -170,6 +179,8 @@ type
|
||||
FsInt3: Integer;
|
||||
FsBool: Boolean;
|
||||
FsBool2: Boolean;
|
||||
FsBool3: Boolean;
|
||||
FsRes: Boolean; // ОБРАТНО из Sync-метода (под тем же FLock)
|
||||
FsStr: string;
|
||||
|
||||
// ── Sync-методы (поток контроллера) ──
|
||||
@@ -179,7 +190,6 @@ type
|
||||
procedure SyncSetFilter;
|
||||
procedure SyncSetTRX;
|
||||
procedure SyncStopTRX;
|
||||
procedure SyncSetTune;
|
||||
procedure SyncSetDrive;
|
||||
procedure SyncSetTuneDrive;
|
||||
procedure SyncSetSplit;
|
||||
@@ -235,6 +245,9 @@ type
|
||||
function RxCount: Integer;
|
||||
function ValidRx(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 FreqLimits(out LoHz, HiHz: Double); // из снимка (любой поток)
|
||||
function FreqSaneLive(Hz: Double): Boolean; // проверка перед установкой
|
||||
@@ -250,6 +263,7 @@ type
|
||||
function SliceIdLive(Rx, Ch: Integer): Integer; // из живой таблицы (Sync*)
|
||||
function RxSlice(Rx: Integer; out S: TSliceView): Boolean;
|
||||
function SliceRxCh(Id: Integer; out Rx, Ch: Integer): Boolean;
|
||||
function TxRx: Integer; // приёмник, чей слайс сейчас источник передачи
|
||||
{ Захват параметра (§3.5). True — параметр наш (свободен, наш или отпущен
|
||||
по таймауту), захват продлевается. False — им сейчас управляет другой. }
|
||||
function Claim(const Key: string; Owner: TTCIClient): Boolean;
|
||||
@@ -277,8 +291,8 @@ type
|
||||
function StrDds(Rx: Integer): string;
|
||||
function StrModulation(Rx: Integer): string;
|
||||
function StrFilterBand(Rx: Integer): string;
|
||||
function StrTrx: string;
|
||||
function StrTune: string;
|
||||
function StrTrx(Rx: Integer): string;
|
||||
function StrTune(Rx: Integer): string;
|
||||
function StrDrive: string;
|
||||
function StrVolume: string;
|
||||
function StrMute: string;
|
||||
@@ -294,6 +308,7 @@ type
|
||||
function StrANF(Rx: Integer): string;
|
||||
{ Полный набор строк по приёмнику — рассылка после правки слайса. }
|
||||
procedure BroadcastRxState(Rx, Ch: Integer);
|
||||
procedure BroadcastTxEnable; // всем живым приёмникам, каждому со своим номером
|
||||
|
||||
procedure SendInit(Client: TTCIClient);
|
||||
procedure SendState(Client: TTCIClient);
|
||||
@@ -480,14 +495,15 @@ begin
|
||||
end;
|
||||
|
||||
{ ═══════════════════════════════════════════════════════════════════════════
|
||||
Модель: приёмник = панадаптер, канал = VFO A/B (пан 0) или слайс (паны 1..)
|
||||
Модель: приёмник 0 = главный тракт (каналы = VFO A/B), приёмник N = слайс
|
||||
слота N−1 (буквы B..G). См. комментарий у TCI_MAX_RX.
|
||||
═══════════════════════════════════════════════════════════════════════════ }
|
||||
|
||||
function TTCIAdapter.RxCount: Integer;
|
||||
// Потолок, а не число живых: TRX_COUNT протокол объявляет один раз, при
|
||||
// подключении (§4.1). Про приёмник, которого сейчас нет, просто молчим.
|
||||
begin
|
||||
Result := DevSnap.MaxPans;
|
||||
if Result < 1 then Result := 1;
|
||||
if Result > TCI_MAX_RX then Result := TCI_MAX_RX;
|
||||
Result := TCI_MAX_RX;
|
||||
end;
|
||||
|
||||
function TTCIAdapter.ValidRx(Rx: Integer): Boolean;
|
||||
@@ -495,13 +511,34 @@ begin
|
||||
Result := (Rx >= 0) and (Rx < RxCount);
|
||||
end;
|
||||
|
||||
function TTCIAdapter.RxActive(Rx: Integer): Boolean;
|
||||
// Приёмник существует физически. TRX_COUNT в протоколе объявляется один раз и
|
||||
// равен потолку железа, но пан из этого потолка может быть ещё не создан —
|
||||
// врать про его частоту нельзя (см. SendState).
|
||||
function TTCIAdapter.LiveRx(Rx: Integer): Boolean;
|
||||
// Адресат команды. Про приёмник, которого нет (слот пуст), молчим целиком:
|
||||
// ответ с нулями клиент принял бы за настоящее состояние.
|
||||
begin
|
||||
if Rx = 0 then Result := True
|
||||
else Result := ValidRx(Rx) and FController.PanDDCActive(Rx);
|
||||
Result := ValidRx(Rx) and RxActive(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;
|
||||
|
||||
procedure TTCIAdapter.CalcFreqLimits(out LoHz, HiHz: Double);
|
||||
@@ -670,40 +707,51 @@ begin
|
||||
begin
|
||||
if not RxActive(Rx) then Continue;
|
||||
N := ChanCount(Rx);
|
||||
FServer.Broadcast(StrDds(Rx));
|
||||
for Ch := 0 to N - 1 do BroadcastRxState(Rx, Ch);
|
||||
FServer.Broadcast(TCIBuild('rx_channel_enable',
|
||||
[TCIIntStr(Rx), '1', TCIBoolStr(N > 1)]));
|
||||
// ★С адресом приёмника — иначе клиент его не увидит. Клиенты фильтруют
|
||||
// входящие по arg1 (см. StrTrx), а MSHV на этом ещё и завязывает передачу:
|
||||
// `set_ptt` начинается с `if (!tci_tx_enable) return;`. Слайс, созданный
|
||||
// ПОСЛЕ подключения клиента, без этой строки получал бы молча мёртвую PTT.
|
||||
FServer.Broadcast(StrTxEnable(Rx));
|
||||
end;
|
||||
end;
|
||||
|
||||
function TTCIAdapter.SnapSlice(Rx, Ch: Integer; out V: TSliceView): Boolean;
|
||||
// Слайс канала Ch приёмника Rx из снимка. Для Rx=0 слайса нет: главный тракт
|
||||
// живёт в полях контроллера.
|
||||
var i, Seen: Integer;
|
||||
// Слайс приёмника Rx из снимка. У приёмника-слайса канал один — A: второго
|
||||
// «саб-приёмника» внутри слайса не бывает. Для Rx=0 слайса нет вовсе: главный
|
||||
// тракт живёт в полях контроллера.
|
||||
var Slot: Integer;
|
||||
begin
|
||||
Result := False;
|
||||
FillChar(V, SizeOf(V), 0);
|
||||
if (Rx <= 0) or (Ch < 0) then Exit;
|
||||
Seen := 0;
|
||||
Slot := RxSlot(Rx);
|
||||
if (Slot < 0) or (Ch <> 0) then Exit;
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
for i := 0 to MAX_SLICES - 1 do
|
||||
if FSliceSnap[i].Used and (FSliceSnap[i].V.PanId = Rx) then
|
||||
begin
|
||||
if Seen = Ch then
|
||||
begin
|
||||
V := FSliceSnap[i].V;
|
||||
Exit(True);
|
||||
end;
|
||||
Inc(Seen);
|
||||
end;
|
||||
if FSliceSnap[Slot].Used then
|
||||
begin
|
||||
V := FSliceSnap[Slot].V;
|
||||
Result := True;
|
||||
end;
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
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;
|
||||
// Слайсы пана Rx по порядку в таблице: 0-й = канал A, 1-й = канал B.
|
||||
// Слайс приёмника Rx (канал A) из снимка.
|
||||
var V: TSliceView;
|
||||
begin
|
||||
if SnapSlice(Rx, Ch, V) then Result := V.Id else Result := 0;
|
||||
@@ -713,9 +761,12 @@ function TTCIAdapter.SliceIdLive(Rx, Ch: Integer): Integer;
|
||||
// То же, но из живой таблицы. Только для Sync-методов: они идут в потоке
|
||||
// контроллера, где чтение безопасно, а снимок мог бы отстать на такт — команда
|
||||
// на установку обязана попасть в тот слайс, который есть сейчас.
|
||||
var Slot: Integer;
|
||||
begin
|
||||
if Rx <= 0 then Result := 0
|
||||
else Result := FController.PanSliceId(Rx, Ch);
|
||||
Result := 0;
|
||||
Slot := RxSlot(Rx);
|
||||
if (Slot < 0) or (Ch <> 0) then Exit;
|
||||
Result := FController.SliceIdBySlot(Slot);
|
||||
end;
|
||||
|
||||
function TTCIAdapter.RxSlice(Rx: Integer; out S: TSliceView): Boolean;
|
||||
@@ -726,35 +777,21 @@ end;
|
||||
|
||||
function TTCIAdapter.SliceRxCh(Id: Integer; out Rx, Ch: Integer): Boolean;
|
||||
// Обратное отображение слайс → (приёмник, канал). Нужно уведомлениям: контроллер
|
||||
// сообщает об изменении Id, а клиенту адресуются номера TCI.
|
||||
var i, Seen, Pan: Integer;
|
||||
// сообщает об изменении Id, а клиенту адресуются номера TCI. Слот слайса и есть
|
||||
// его номер приёмника — искать по панам больше нечего.
|
||||
var i: Integer;
|
||||
begin
|
||||
Result := False;
|
||||
Rx := 0; Ch := 0;
|
||||
if Id <= 0 then Exit;
|
||||
Pan := -1;
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
for i := 0 to MAX_SLICES - 1 do
|
||||
if FSliceSnap[i].Used and (FSliceSnap[i].V.Id = Id) then
|
||||
begin
|
||||
Pan := FSliceSnap[i].V.PanId;
|
||||
Break;
|
||||
end;
|
||||
// Пан 0 в модели TCI — это VFO A/B главного приёмника, а не его слайсы:
|
||||
// слайсу на главном пане в протоколе места нет.
|
||||
if (Pan <= 0) or not ValidRx(Pan) then Exit;
|
||||
Seen := 0;
|
||||
for i := 0 to MAX_SLICES - 1 do
|
||||
if FSliceSnap[i].Used and (FSliceSnap[i].V.PanId = Pan) then
|
||||
begin
|
||||
if FSliceSnap[i].V.Id = Id then
|
||||
begin
|
||||
Rx := Pan;
|
||||
Ch := Seen;
|
||||
Exit(Seen < TCI_CHANNELS);
|
||||
end;
|
||||
Inc(Seen);
|
||||
Rx := i + 1;
|
||||
Ch := 0;
|
||||
Exit(True);
|
||||
end;
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
@@ -830,22 +867,11 @@ begin
|
||||
end;
|
||||
|
||||
function TTCIAdapter.ChanCount(Rx: Integer): Integer;
|
||||
var i: Integer;
|
||||
// У главного тракта два канала (VFO A/B), у приёмника-слайса — один: канал B
|
||||
// в протоколе это второй саб-приёмник, а внутри слайса такого нет.
|
||||
begin
|
||||
if Rx = 0 then begin Result := 2; Exit; end; // VFO A/B всегда есть
|
||||
Result := 0;
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
for i := 0 to MAX_SLICES - 1 do
|
||||
if FSliceSnap[i].Used and (FSliceSnap[i].V.PanId = Rx) then Inc(Result);
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
end;
|
||||
if Result > TCI_CHANNELS then Result := TCI_CHANNELS;
|
||||
// Канал A в TCI выключить нечем: он есть у приёмника всегда. Пока пан жив, но
|
||||
// слайсов на нём не осталось, показываем канал A на центре пана — иначе после
|
||||
// удаления последнего слайса клиент навсегда остался бы с его частотой.
|
||||
if (Result = 0) and RxActive(Rx) then Result := 1;
|
||||
if Rx = 0 then Exit(2);
|
||||
if RxActive(Rx) then Result := 1 else Result := 0;
|
||||
end;
|
||||
|
||||
function TTCIAdapter.ChanFreq(Rx, Ch: Integer): Double;
|
||||
@@ -857,15 +883,21 @@ begin
|
||||
if Ch = 1 then Result := FController.FVfoB else Result := FController.FVfoA;
|
||||
Exit;
|
||||
end;
|
||||
if SnapSlice(Rx, Ch, S) then Exit(S.TargetHz);
|
||||
// Слайса нет: у живого пана канал A стоит на его центре (см. ChanCount).
|
||||
if (Ch = 0) and RxActive(Rx) then Result := RxCenterHz(Rx);
|
||||
// Слайса нет — приёмника нет: частоту не выдумываем (SendState про такой
|
||||
// номер молчит целиком).
|
||||
if SnapSlice(Rx, Ch, S) then Result := S.TargetHz;
|
||||
end;
|
||||
|
||||
function TTCIAdapter.RxCenterHz(Rx: Integer): Double;
|
||||
// DDS приёмника — центр панорамы, на которой он живёт. У слайса это центр его
|
||||
// пана: для главного пана — FCenterFreq, для дополнительного — его DDC.
|
||||
var Pan: Integer;
|
||||
begin
|
||||
if Rx = 0 then Result := FController.FCenterFreq
|
||||
else Result := FController.PanDDCFreq(Rx);
|
||||
Result := 0;
|
||||
Pan := RxPanId(Rx);
|
||||
if Pan < 0 then Exit;
|
||||
if Pan = 0 then Result := FController.FCenterFreq
|
||||
else Result := FController.PanDDCFreq(Pan);
|
||||
end;
|
||||
|
||||
function TTCIAdapter.RxMode(Rx: Integer): Integer;
|
||||
@@ -1002,14 +1034,36 @@ begin
|
||||
Result := TCIBuild('rx_filter_band', [TCIIntStr(Rx), TCIIntStr(Lo), TCIIntStr(Hi)]);
|
||||
end;
|
||||
|
||||
function TTCIAdapter.StrTrx: string;
|
||||
function TTCIAdapter.TxRx: Integer;
|
||||
// Номер приёмника, чей слайс СЕЙЧАС источник передачи (0 = главный VFO).
|
||||
// Ответы и уведомления TRX/TUNE обязаны называть именно его: клиент на
|
||||
// приёмнике 1, получивший «trx:0,true», решил бы, что его команда не прошла, а
|
||||
// клиент на приёмнике 0 — что в эфире он, хотя передаёт чужой слайс.
|
||||
var
|
||||
Rx, Ch: Integer;
|
||||
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;
|
||||
|
||||
function TTCIAdapter.StrTune: string;
|
||||
function TTCIAdapter.StrTrx(Rx: Integer): string;
|
||||
// Состояние передатчика ДЛЯ ПРИЁМНИКА Rx: «true» значит, что в эфире именно
|
||||
// его слайс. Клиенты фильтруют входящие строки по номеру приёмника (MSHV,
|
||||
// например, отбрасывает всё, что адресовано не ему), поэтому автору команды
|
||||
// отвечаем ЕГО номером: `trx:1,false` — это «твоя заявка не прошла», а
|
||||
// `trx:0,false` он бы просто не увидел. В рассылку уходит номер того, чей
|
||||
// слайс сейчас источник передачи (`TxRx`).
|
||||
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;
|
||||
|
||||
function TTCIAdapter.StrDrive: string;
|
||||
@@ -1107,6 +1161,18 @@ begin
|
||||
FServer.Broadcast(StrSqlLevel(Rx));
|
||||
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;
|
||||
|
||||
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, TCIBuild('tune_drive', ['0', TCIIntStr(FController.FTXSettings.TUNLevel)]));
|
||||
Reply(Client, StrVolume);
|
||||
@@ -1295,11 +1362,15 @@ end;
|
||||
|
||||
procedure TTCIAdapter.SyncSetCenter;
|
||||
begin
|
||||
// Окончательная проверка — здесь: см. FreqSaneLive. Ни SetCenter, ни
|
||||
// SetPanDDCFreq границ не клампят, число уходит прямо в backend.
|
||||
// Окончательная проверка — здесь: см. FreqSaneLive. SetCenter границ не
|
||||
// клампит, число уходит прямо в backend.
|
||||
if not FreqSaneLive(FsFreq) then Exit;
|
||||
if FsInt = 0 then FController.SetCenter(FsFreq)
|
||||
else FController.SetPanDDCFreq(FsInt, FsFreq);
|
||||
// Двигать можно только центр главной панорамы — она и есть приёмник 0.
|
||||
// У приёмника-слайса DDS только читается: панорама под ним общая, и увести
|
||||
// её по просьбе одного клиента значит утащить за собой всех соседей по пану
|
||||
// и картинку оператора. Слайсу двигаться незачем — за окном DDC следит
|
||||
// TuneSliceInBand (см. SyncSetVfo).
|
||||
if FsInt = 0 then FController.SetCenter(FsFreq);
|
||||
end;
|
||||
|
||||
procedure TTCIAdapter.SyncSetMode;
|
||||
@@ -1319,12 +1390,71 @@ begin
|
||||
end;
|
||||
|
||||
procedure TTCIAdapter.SyncSetTRX;
|
||||
// Поток контроллера. FsInt — номер приёмника из arg1, FsBool3 — это TUNE.
|
||||
// Наружу отдаёт FsRes: «эфир подняла ИМЕННО ЭТА команда» — по нему в потоке
|
||||
// клиента назначается хозяин передачи.
|
||||
//
|
||||
// ★Все решения принимаются здесь, а не в потоке клиента. Между разбором
|
||||
// команды и её исполнением оператор успевает нажать PTT, и снимок «шла ли
|
||||
// передача», снятый заранее, соврал бы — а по нему назначается хозяин эфира,
|
||||
// то есть право снять чужую передачу.
|
||||
var
|
||||
Id: Integer;
|
||||
WasTx, Started: Boolean;
|
||||
begin
|
||||
FsRes := False;
|
||||
WasTx := FController.FTransmitting or FController.FTuning;
|
||||
|
||||
// Источник модуляции ставим ДО SetMOX — именно он его и читает при выборе
|
||||
// микрофона. Выключение передачи флаг снимает всегда: следующий раз оператор
|
||||
// может нажать PTT сам, и тогда в эфир должен идти его микрофон.
|
||||
FController.TCIMicRequested := FsBool and FsBool2;
|
||||
FController.SetMOX(FsBool);
|
||||
// ★Ставим его ТОЛЬКО когда эфир поднимаем мы сами. 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);
|
||||
|
||||
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;
|
||||
|
||||
procedure TTCIAdapter.SyncStopTRX;
|
||||
@@ -1332,9 +1462,14 @@ procedure TTCIAdapter.SyncStopTRX;
|
||||
// решение принято там, где известно, кто её начал, а здесь остаётся только
|
||||
// проверить, что она вообще идёт (оператор мог отпустить PTT сам).
|
||||
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.SetMOX(False);
|
||||
// TUN снимаем именно SetTune: он гасит и тон, и PTT, и возвращает drive.
|
||||
// SetMOX(False) поверх включённого TUN оставил бы взведённым генератор тона,
|
||||
// а FTuning — поднятым, то есть аппарат в состоянии, которого на экране нет.
|
||||
if FController.FTuning then FController.SetTune(False)
|
||||
else FController.SetMOX(False);
|
||||
end;
|
||||
|
||||
procedure TTCIAdapter.SyncTaps;
|
||||
@@ -1352,11 +1487,6 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TTCIAdapter.SyncSetTune;
|
||||
begin
|
||||
FController.SetTune(FsBool);
|
||||
end;
|
||||
|
||||
procedure TTCIAdapter.SyncSetDrive;
|
||||
begin
|
||||
FController.SetDrive(FsInt);
|
||||
@@ -1546,6 +1676,11 @@ begin
|
||||
if not TCITryArgInt(M, 1, Ch) then Ch := 0;
|
||||
if not ValidRx(Rx) then Exit;
|
||||
if (Ch < 0) or (Ch >= TCI_CHANNELS) then Exit;
|
||||
// ★Про приёмник, которого нет (слайс не создан или уже удалён), молчим
|
||||
// целиком — и на чтение тоже. Ответ «vfo:1,0,0» клиент принял бы за
|
||||
// настоящую частоту, а MSHV именно этим ответом завершает инициализацию
|
||||
// (network.cpp, Network::initAll) — то есть подключился бы к пустоте.
|
||||
if (Ch >= ChanCount(Rx)) or not RxActive(Rx) then Exit;
|
||||
|
||||
// Частота ставится, только если её удалось разобрать И она годная: «vfo:0,0,abc»
|
||||
// раньше превращалось в честный ноль и уводило приёмник на 0 Гц.
|
||||
@@ -1682,7 +1817,7 @@ procedure TTCIAdapter.DispatchCommand(Client: TTCIClient; const M: TTCIMessage);
|
||||
var
|
||||
Rx, Ch, V: Integer;
|
||||
Lo, Hi: Integer;
|
||||
B, FromTCI: Boolean;
|
||||
B, FromTCI, Tune, Started: Boolean;
|
||||
D: Double;
|
||||
Name: string;
|
||||
begin
|
||||
@@ -1708,8 +1843,14 @@ begin
|
||||
if M.Name = 'DDS' then
|
||||
begin
|
||||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||||
if not ValidRx(Rx) then Exit;
|
||||
if (M.ArgCount >= 2) and TCITryArgFloat(M, 1, D) and FreqSane(D) and
|
||||
if not LiveRx(Rx) then Exit;
|
||||
// Двигать центр можно только у приёмника 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
|
||||
begin
|
||||
FLock.Enter;
|
||||
@@ -1726,7 +1867,7 @@ begin
|
||||
if M.Name = 'MODULATION' then
|
||||
begin
|
||||
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
|
||||
begin
|
||||
V := TCIModeIndex(TCIArg(M, 1), RxMode(Rx), ChanFreq(Rx, 0));
|
||||
@@ -1746,7 +1887,7 @@ begin
|
||||
if M.Name = 'RX_FILTER_BAND' then
|
||||
begin
|
||||
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 и приёмник глох.
|
||||
if (M.ArgCount >= 3) and TCITryArgInt(M, 1, Lo) and TCITryArgInt(M, 2, Hi) and
|
||||
@@ -1765,51 +1906,75 @@ begin
|
||||
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
|
||||
// arg3 — источник сигнала. Наш только 'tci': модуляция берётся из
|
||||
// аудиопотока этого клиента. Остальные значения (mic1/mic2/micpc/ecoder2)
|
||||
// называют физические входы ExpertSDR3, которых у нас нет, — они значат
|
||||
// «микрофон, выбранный в программе», то есть ровно то, что и без arg3.
|
||||
// Требование «включен аудиопоток по TCI» (§4.2) проверяем буквально:
|
||||
// без AUDIO_START модулировать нечем, и молча оставить оператора с
|
||||
// тишиной в эфире хуже, чем передавать с его микрофона.
|
||||
if not TCITryArgInt(M, 0, Rx) or not ValidRx(Rx) then
|
||||
begin
|
||||
Reply(Client, TCIBuild('tci_error', [LowerCase(M.Name), 'bad receiver']));
|
||||
Exit;
|
||||
end;
|
||||
// Номер объявлен (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
|
||||
begin
|
||||
Name := LowerCase(Trim(TCIArg(M, 2)));
|
||||
FromTCI := B and (Name = 'tci') and HasAudioStream(Client);
|
||||
// arg3 — источник сигнала (только у TRX). Наш только 'tci': модуляция
|
||||
// берётся из аудиопотока этого клиента. Остальные значения
|
||||
// (mic1/mic2/micpc/ecoder2) называют физические входы ExpertSDR3, которых
|
||||
// у нас нет, — они значат «микрофон, выбранный в программе», то есть ровно
|
||||
// то, что и без arg3. Требование «включен аудиопоток по TCI» (§4.2)
|
||||
// проверяем буквально: без AUDIO_START модулировать нечем, и молча
|
||||
// оставить оператора с тишиной в эфире хуже, чем передавать с его
|
||||
// микрофона.
|
||||
Tune := M.Name = 'TUNE';
|
||||
Name := LowerCase(Trim(TCIArg(M, 2)));
|
||||
FromTCI := B and (not Tune) and (Name = 'tci') and HasAudioStream(Client);
|
||||
FTxLock.Enter;
|
||||
try
|
||||
if FromTCI then FTxClient := Client
|
||||
else if FTxClient = Client then FTxClient := nil;
|
||||
// Кто поставил трансивер в эфир: его уход обязан эфир и снять
|
||||
// (см. StopTxOf). Источник модуляции тут ни при чём — несущую без
|
||||
// хозяина оставлять нельзя в любом случае.
|
||||
if B then FTrxOwner := Client
|
||||
else if FTrxOwner = Client then FTrxOwner := nil;
|
||||
else if (not Tune) and (FTxClient = Client) then FTxClient := nil;
|
||||
finally FTxLock.Leave; end;
|
||||
Started := False;
|
||||
FLock.Enter;
|
||||
try
|
||||
FsInt := Rx;
|
||||
FsBool := B;
|
||||
FsBool2 := FromTCI;
|
||||
FsBool3 := Tune;
|
||||
FsRes := False;
|
||||
if CanInvoke then FController.Invoke(SyncSetTRX);
|
||||
Started := FsRes; // ответ Sync-метода — под тем же локом, что и вопрос
|
||||
finally FLock.Leave; end;
|
||||
end;
|
||||
Reply(Client, StrTrx);
|
||||
Exit;
|
||||
end;
|
||||
|
||||
if M.Name = 'TUNE' then
|
||||
begin
|
||||
if TCITryArgBool(M, 1, B) and Claim(HoldKey('TUNE', 0, 0), Client) then
|
||||
begin
|
||||
FLock.Enter;
|
||||
// Кто поставил трансивер в эфир: его уход обязан эфир и снять (см.
|
||||
// StopTxOf). Источник модуляции тут ни при чём — несущую без хозяина
|
||||
// оставлять нельзя в любом случае. ★Хозяином записываемся, только если
|
||||
// эфир и правда начался ИМЕННО от этой команды — а знает об этом только
|
||||
// сам Sync-метод: он один видит состояние передатчика до и после, на
|
||||
// потоке контроллера и без окна, в которое влезает PTT оператора.
|
||||
FTxLock.Enter;
|
||||
try
|
||||
FsBool := B;
|
||||
if CanInvoke then FController.Invoke(SyncSetTune);
|
||||
finally FLock.Leave; end;
|
||||
if Started then FTrxOwner := Client
|
||||
else if (not B) and (FTrxOwner = Client) then FTrxOwner := nil;
|
||||
finally FTxLock.Leave; end;
|
||||
end;
|
||||
Reply(Client, StrTune);
|
||||
// ★Автору отвечаем ЕГО номером приёмника (см. StrTrx): клиент фильтрует
|
||||
// входящие по arg1, и отказ, названный чужим номером, до него не дойдёт.
|
||||
if M.Name = 'TUNE' then Reply(Client, StrTune(Rx)) else Reply(Client, StrTrx(Rx));
|
||||
Exit;
|
||||
end;
|
||||
|
||||
@@ -1893,7 +2058,7 @@ begin
|
||||
if M.Name = 'RX_MUTE' then
|
||||
begin
|
||||
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
|
||||
begin
|
||||
FLock.Enter;
|
||||
@@ -1911,7 +2076,7 @@ begin
|
||||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||||
if not TCITryArgInt(M, 1, Ch) then Ch := 0;
|
||||
Ch := EnsureRange(Ch, 0, TCI_CHANNELS - 1);
|
||||
if not ValidRx(Rx) then Exit;
|
||||
if not LiveRx(Rx) then Exit;
|
||||
if (M.ArgCount >= 3) and TCITryArgFloat(M, 2, D) and
|
||||
Claim(HoldKey('RXVOL', Rx, Ch), Client) then
|
||||
begin
|
||||
@@ -1933,7 +2098,7 @@ begin
|
||||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||||
if not TCITryArgInt(M, 1, Ch) then Ch := 0;
|
||||
Ch := EnsureRange(Ch, 0, TCI_CHANNELS - 1);
|
||||
if not ValidRx(Rx) then Exit;
|
||||
if not LiveRx(Rx) then Exit;
|
||||
FEchoLock.Enter;
|
||||
try
|
||||
if (M.ArgCount >= 3) and TCITryArgInt(M, 2, V) then
|
||||
@@ -1980,7 +2145,7 @@ begin
|
||||
if M.Name = 'AGC_MODE' then
|
||||
begin
|
||||
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
|
||||
begin
|
||||
Name := LowerCase(TCIArg(M, 1));
|
||||
@@ -2004,7 +2169,7 @@ begin
|
||||
if M.Name = 'AGC_GAIN' then
|
||||
begin
|
||||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||||
if not ValidRx(Rx) then Exit;
|
||||
if not LiveRx(Rx) then Exit;
|
||||
// AGC-T (порог АРУ) в ewsdr один на приёмный тракт: у слайса своего нет.
|
||||
// Правка от имени доп. приёмника трогала бы главный — не делаем этого,
|
||||
// просто отвечаем текущим значением (ограничение, см. doc/TCI.md).
|
||||
@@ -2025,7 +2190,7 @@ begin
|
||||
(M.Name = 'RX_ANF_ENABLE') then
|
||||
begin
|
||||
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
|
||||
begin
|
||||
FLock.Enter;
|
||||
@@ -2045,7 +2210,7 @@ begin
|
||||
if M.Name = 'RX_NB_PARAM' then
|
||||
begin
|
||||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||||
if not ValidRx(Rx) then Exit;
|
||||
if not LiveRx(Rx) then Exit;
|
||||
FEchoLock.Enter;
|
||||
try
|
||||
if M.ArgCount >= 3 then
|
||||
@@ -2069,7 +2234,7 @@ begin
|
||||
(M.Name = 'RX_NF_ENABLE') then
|
||||
begin
|
||||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||||
if not ValidRx(Rx) then Exit;
|
||||
if not LiveRx(Rx) then Exit;
|
||||
FEchoLock.Enter;
|
||||
try
|
||||
if TCITryArgBool(M, 1, B) then
|
||||
@@ -2096,7 +2261,7 @@ begin
|
||||
if M.Name = 'LOCK' then
|
||||
begin
|
||||
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
|
||||
begin
|
||||
FLock.Enter;
|
||||
@@ -2112,7 +2277,7 @@ begin
|
||||
if M.Name = 'SQL_ENABLE' then
|
||||
begin
|
||||
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
|
||||
begin
|
||||
FLock.Enter;
|
||||
@@ -2128,7 +2293,7 @@ begin
|
||||
if M.Name = 'SQL_LEVEL' then
|
||||
begin
|
||||
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
|
||||
begin
|
||||
FLock.Enter;
|
||||
@@ -2146,7 +2311,7 @@ begin
|
||||
if (M.Name = 'RIT_ENABLE') or (M.Name = 'XIT_ENABLE') then
|
||||
begin
|
||||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||||
if not ValidRx(Rx) then Exit;
|
||||
if not LiveRx(Rx) then Exit;
|
||||
FEchoLock.Enter;
|
||||
try
|
||||
if TCITryArgBool(M, 1, B) then
|
||||
@@ -2165,7 +2330,7 @@ begin
|
||||
if (M.Name = 'RIT_OFFSET') or (M.Name = 'XIT_OFFSET') then
|
||||
begin
|
||||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||||
if not ValidRx(Rx) then Exit;
|
||||
if not LiveRx(Rx) then Exit;
|
||||
FEchoLock.Enter;
|
||||
try
|
||||
if TCITryArgInt(M, 1, V) then
|
||||
@@ -2188,7 +2353,7 @@ begin
|
||||
// вторым каналом был бы второй слайс (создание слайсов по TCI — этап 2).
|
||||
if not TCITryArgInt(M, 0, Rx) then Exit;
|
||||
if not TCITryArgInt(M, 1, Ch) then Ch := 1;
|
||||
if not ValidRx(Rx) then Exit;
|
||||
if not LiveRx(Rx) then Exit;
|
||||
if TCITryArgBool(M, 2, B) then
|
||||
begin
|
||||
FEchoLock.Enter;
|
||||
@@ -2533,7 +2698,9 @@ begin
|
||||
// держать при этом FStreamLock значило бы связать два лока без нужды.
|
||||
if K = tstIQ then
|
||||
begin
|
||||
SrcRate := FController.IQTapRateHz(Rx);
|
||||
// ★Частота IQ — свойство ПАНОРАМЫ, а приёмник теперь слайс: спрашиваем
|
||||
// рейт того пана, на котором он стоит.
|
||||
SrcRate := FController.IQTapRateHz(RxPanId(Rx));
|
||||
WantRate := C.IQRate;
|
||||
Chans := 2; // IQ комплексный по определению
|
||||
ST := tsyFloat32; // формат IQ в TCI не настраивается
|
||||
@@ -2695,19 +2862,26 @@ begin
|
||||
end;
|
||||
|
||||
function TTCIAdapter.StreamRxOf(PanId, SliceId: Integer): Integer;
|
||||
// Какому приёмнику TCI принадлежит это аудио. Главный тракт — приёмник 0.
|
||||
// У доп. пана в потоках участвует только канал A (первый слайс): «аудиопоток
|
||||
// приёмника» в протоколе один на приёмник, второго канала у него нет.
|
||||
// Зовётся из DSP-потока и берёт FSliceLock — ДО FStreamLock (порядок!).
|
||||
// Какому приёмнику TCI принадлежит это аудио. Главный тракт (SliceId = 0 на
|
||||
// пане 0) — приёмник 0, любой слайс — приёмник своего слота, на каком бы пане
|
||||
// он ни стоял. Зовётся из DSP-потока и берёт FSliceLock — ДО FStreamLock
|
||||
// (порядок!).
|
||||
var i: Integer;
|
||||
begin
|
||||
Result := -1;
|
||||
if PanId <= 0 then
|
||||
if SliceId <= 0 then
|
||||
begin
|
||||
if SliceId = 0 then Result := 0; // слайсы главного пана в модель не входят
|
||||
if PanId = 0 then Result := 0;
|
||||
Exit;
|
||||
end;
|
||||
if not ValidRx(PanId) then Exit;
|
||||
if SliceIdOf(PanId, 0) = SliceId then Result := PanId;
|
||||
FSliceLock.Enter;
|
||||
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;
|
||||
|
||||
procedure TTCIAdapter.OnAudioTap(Kind: TRadioAudioKind; PanId, SliceId: Integer;
|
||||
@@ -2742,14 +2916,23 @@ procedure TTCIAdapter.OnIQTap(PanId: Integer; PI_, PQ_: PDouble;
|
||||
N, RateHz: Integer);
|
||||
// DSP-поток, один вызов на накопленный блок. У пана этот вызов идёт под
|
||||
// FSliceLock движка, поэтому здесь тем более нельзя ждать.
|
||||
var i: Integer;
|
||||
//
|
||||
// IQ — величина ПАНОРАМЫ, а приёмник теперь слайс: поток получают все
|
||||
// приёмники, чьи слайсы стоят на этом пане (и приёмник 0, если пан главный).
|
||||
// Карту «приёмник → пан» строим ДО FStreamLock: порядок локов один на весь
|
||||
// адаптер — FSliceLock, потом FStreamLock.
|
||||
var
|
||||
i: Integer;
|
||||
Mine: array[0..TCI_MAX_RX-1] of Boolean;
|
||||
begin
|
||||
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;
|
||||
try
|
||||
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
|
||||
// Rate устройства могли сменить уже после START (смена sample rate,
|
||||
// другой rate DDC пана): пересчитываем прореживание на месте, иначе
|
||||
@@ -3181,8 +3364,11 @@ begin
|
||||
rfVolume: Claim(HoldKey('VOL', 0, 0), nil);
|
||||
rfMute: Claim(HoldKey('MUTE', 0, 0), nil);
|
||||
rfDrive: Claim(HoldKey('DRIVE', 0, 0), nil);
|
||||
rfTransmitting: Claim(HoldKey('TRX', 0, 0), nil);
|
||||
rfTuning: Claim(HoldKey('TUNE', 0, 0), nil);
|
||||
// Передатчик один — и ключ захвата у него один на всех, кто его дёргает:
|
||||
// и TRX, и TUNE, и оператор. Отдельный ключ 'TUNE' делал защиту дырявой —
|
||||
// TUN поверх чужого MOX не спорил с ним ни за что.
|
||||
rfTransmitting,
|
||||
rfTuning: Claim(HoldKey('TRX', 0, 0), nil);
|
||||
rfVfoLock: Claim(HoldKey('LOCK', 0, 0), nil);
|
||||
rfCenterFreq: Claim(HoldKey('DDS', 0, 0), nil);
|
||||
rfSliceFreq:
|
||||
@@ -3212,8 +3398,8 @@ begin
|
||||
rfVolume: FServer.Broadcast(StrVolume);
|
||||
rfMute: FServer.Broadcast(StrMute);
|
||||
rfDrive: FServer.Broadcast(StrDrive);
|
||||
rfTransmitting: FServer.Broadcast(StrTrx);
|
||||
rfTuning: FServer.Broadcast(StrTune);
|
||||
rfTransmitting: FServer.Broadcast(StrTrx(TxRx));
|
||||
rfTuning: FServer.Broadcast(StrTune(TxRx));
|
||||
rfVfoLock:
|
||||
begin
|
||||
FServer.Broadcast(StrLock(0));
|
||||
@@ -3253,10 +3439,12 @@ begin
|
||||
if FController.FRunning then FServer.Broadcast(TCIBuild('start'))
|
||||
else FServer.Broadcast(TCIBuild('stop'));
|
||||
rfPanFreq:
|
||||
// Центр пана уехал: у его каналов изменилась и IF (она отсчитывается
|
||||
// от центра), хотя абсолютная частота могла остаться прежней.
|
||||
// Центр пана уехал: у приёмников, которые на нём стоят, изменилась IF
|
||||
// (она отсчитывается от центра), хотя абсолютная частота могла остаться
|
||||
// прежней. Какие это приёмники — знает RxPanId: слайсы одного пана
|
||||
// раскиданы по слотам, подряд они не лежат.
|
||||
for Rx := 1 to RxCount - 1 do
|
||||
if FController.PanDDCActive(Rx) then
|
||||
if RxPanId(Rx) > 0 then
|
||||
begin
|
||||
FServer.Broadcast(StrDds(Rx));
|
||||
for Ch := 0 to ChanCount(Rx) - 1 do FServer.Broadcast(StrIf(Rx, Ch));
|
||||
@@ -3268,7 +3456,7 @@ begin
|
||||
BroadcastRxState(Rx, Ch);
|
||||
rfBand, rfXvtr:
|
||||
begin
|
||||
FServer.Broadcast(StrTxEnable(0));
|
||||
BroadcastTxEnable;
|
||||
FServer.Broadcast(StrVfo(0, 0));
|
||||
end;
|
||||
rfActiveVfo:
|
||||
@@ -3312,7 +3500,7 @@ begin
|
||||
if TxEnabled <> FLastTxEnable then
|
||||
begin
|
||||
FLastTxEnable := TxEnabled;
|
||||
FServer.Broadcast(StrTxEnable(0));
|
||||
BroadcastTxEnable;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
Reference in New Issue
Block a user