From ecd42c32f83adba121e42d4ac52f6afa295f0bf9 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 19 Aug 2026 21:47:30 +0300 Subject: [PATCH] =?UTF-8?q?fix(tci):=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4?= =?UTF-8?q?=D0=B0=D1=87=D0=B0=20=D1=81=20=D1=81=D0=BB=D0=B0=D0=B9=D1=81?= =?UTF-8?q?=D0=B0=20=E2=80=94=20=D0=BC=D0=B0=D1=80=D0=BA=D0=B5=D1=80=20TX?= =?UTF-8?q?=5FCHRONO=20=D0=BF=D0=BE=D0=B4=20=D0=BD=D0=BE=D0=BC=D0=B5=D1=80?= =?UTF-8?q?=D0=BE=D0=BC=20=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD=D1=82=D0=B0,=20?= =?UTF-8?q?=D1=84=D1=80=D0=BE=D0=BD=D1=82=20rfTransmitting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Живой прогон с MSHV на втором слайсе (openHPSDR): приём в порядке, эфир по trx:1,true,tci поднимается, а звук от клиента не доходит. Два независимых дефекта, оба видны только на приёмнике > 0 — на rx0 передача работала, потому стенд их и не ловил. 1. Маркеры TX_CHRONO уходили с receiver = 0 жёстко. MSHV шлёт TX-аудио ТОЛЬКО в ответ на маркер и фильтрует все входящие бинарные блоки по номеру приёмника первой же строкой обработчика (network.cpp: `if (pStream->receiver != tci_trx) return;`, ветка TxChrono там же и собирает блок). У клиента на слайсе tci_trx = 1, так что маркеры отбрасывались целиком. Теперь вместе с клиентом-модулятором запоминается номер приёмника из его же TRX (FTxRx), и маркеры идут под ним. 2. Changed(rfTransmitting) из SetTxSlice стирал TCIMicRequested. Контроллер шлёт это поле и просто как «перерисуй TX-бейджи», а адаптер понимал любой такой сигнал при FTransmitting = false как «передача кончилась». Приходил он посередине нашей же команды: SyncSetTRX ставит просьбу → RequestSliceTx → SetTxSlice → Changed → просьба стёрта → SetMOX выбирает микрофон уже без неё. В эфир шёл микрофон оператора (тишина), а TX-аудио клиента отбрасывалось — TCIMicActive не поднят. Теперь ловится фронт «было → стало» (FLastTxOn), а не всякое уведомление. Стенд test/tci: 217 проверок (было 214), все зелёные. Новое — часть E, слайс как приёмник 1: по trx:1,true,tci модуляция из TCI взята, маркеры TX_CHRONO идут и названы номером 1. Негативный контроль разделён: каждая правка краснит свою проверку. Проверено на железе: MSHV на втором слайсе передаёт. Co-Authored-By: Claude Opus 5 --- TCIAdapter.pas | 48 +++++++++++++++++++++++++++++++++++--------- doc/TCI.md | 28 ++++++++++++++++++++++++-- test/tci/tcitest.pas | 36 ++++++++++++++++++++++++++++++++- 3 files changed, 99 insertions(+), 13 deletions(-) diff --git a/TCIAdapter.pas b/TCIAdapter.pas index 928ccb0..8d225b4 100644 --- a/TCIAdapter.pas +++ b/TCIAdapter.pas @@ -152,7 +152,9 @@ type // ── TX-аудио от клиента (§3.4) ── FTxLock: TCriticalSection; FTxClient: TTCIClient; // кто модулирует (nil — никто) + FTxRx: Integer; // ЕГО номер приёмника (из TRX) — см. PushTxChrono FTrxOwner: TTCIClient; // кто поставил трансивер в эфир (§4.2) + FLastTxOn: Boolean; // было ли радио в эфире на прошлом rfTransmitting FTxInterp: TTCIInterpolator; FTxInRate: Integer; // частота дискретизации подачи клиента FTxRunning: Boolean; // маркеры TX_CHRONO идут @@ -394,6 +396,9 @@ begin FStreamLock := TCriticalSection.Create; FTxLock := TCriticalSection.Create; FWriter := nil; // заводится на первом SAVE (см. EnqueueWav) + // Состояние передачи на старте: фронт «было-стало» ловим с него (см. OnState). + FLastTxOn := (AController <> nil) and + (AController.FTransmitting or AController.FTuning); FTapsOn := False; SetLength(FTxRaw, TCI_STREAM_DATA_MAX div 2); // худший случай: int16 SetLength(FTxMono, TCI_STREAM_DATA_MAX div 2); @@ -1959,8 +1964,11 @@ begin FromTCI := B and (not Tune) and (Name = 'tci') and HasAudioStream(Client); FTxLock.Enter; try - if FromTCI then FTxClient := Client - else if (not Tune) and (FTxClient = Client) then FTxClient := nil; + // ★Вместе с клиентом запоминаем НОМЕР ПРИЁМНИКА, которым он назвался: + // маркеры TX_CHRONO обязаны идти под ним же (см. PushTxChrono). + if FromTCI then begin FTxClient := Client; FTxRx := Rx; end + else if (not Tune) and (FTxClient = Client) then + begin FTxClient := nil; FTxRx := 0; end; finally FTxLock.Leave; end; Started := False; FLock.Enter; @@ -3227,6 +3235,7 @@ begin if (FTxClient <> nil) and ((C = nil) or (FTxClient = C)) then begin FTxClient := nil; + FTxRx := 0; FTxRunning := False; Drop := True; end; @@ -3295,14 +3304,15 @@ procedure TTCIAdapter.PushTxChrono; var C: TTCIClient; Now_: QWord; - Rate, Chans, Block: Integer; + Rate, Chans, Block, Rx: Integer; ST: TTCISampleType; H: TTCIStreamHeader; Active: Boolean; begin FTxLock.Enter; try - C := FTxClient; + C := FTxClient; + Rx := FTxRx; finally FTxLock.Leave; end; @@ -3341,7 +3351,13 @@ begin end; while FTxOwed >= Block do begin - TCIFillHeader(H, tstTXChrono, 0, Rate, ST, Block, Chans); + // ★Номер приёмника — ТОТ, которым назвался клиент в TRX, а не 0. + // Клиент фильтрует ВХОДЯЩИЕ БИНАРНЫЕ блоки по receiver (MSHV, + // network.cpp:231: `if (pStream->receiver != tci_trx) return;`), а + // TX-аудио шлёт ровно в ответ на этот маркер (там же, ветка TxChrono). + // С нулём клиент на втором слайсе (tci_trx = 1) поднимал эфир и молчал: + // маркеры до него не доходили вовсе. + TCIFillHeader(H, tstTXChrono, Rx, Rate, ST, Block, Chans); C.SendBin(H, nil, 0); FTxOwed := FTxOwed - Block; end; @@ -3446,7 +3462,7 @@ procedure TTCIAdapter.OnState(Sender: TObject; Field: TRadioField); var Rx, Ch: Integer; TxHz: Double; - MapChanged: Boolean; + MapChanged, TxNow: Boolean; Sig: string; begin // Снимок слайсов обновляем ДО всего остального и НЕЗАВИСИМО от того, есть ли @@ -3466,11 +3482,23 @@ begin // уход когда-нибудь потом снял бы уже чужую передачу) и снимаем просьбу // «модулируй из потока TCI»: она относилась ровно к той передаче, которую // клиент и начал, а следующий PTT оператора обязан идти с его микрофона. - if (Field = rfTransmitting) and (FController <> nil) - and not FController.FTransmitting then + // ★Ловим именно ФРОНТ «было-стало», а не всякий rfTransmitting. Это поле + // контроллер шлёт и просто «перерисуй TX-бейджи»: SetTxSlice заканчивается + // Changed(rfTransmitting), хотя эфира ещё нет. По прежнему условию такой + // сигнал приходил ПОСЕРЕДИНЕ нашей же команды trx:,true,tci (SyncSetTRX + // ставит просьбу → RequestSliceTx → SetTxSlice → Changed) и стирал её ДО + // SetMOX, который её и читает. Итог на живом железе: клиент на слайсе + // поднимал эфир, а модуляция шла с микрофона оператора, то есть в эфир — + // тишина. + if (Field = rfTransmitting) and (FController <> nil) then begin - ForgetTxOwner; - FController.TCIMicRequested := False; + TxNow := FController.FTransmitting or FController.FTuning; + if FLastTxOn and (not TxNow) then + begin + ForgetTxOwner; + FController.TCIMicRequested := False; + end; + FLastTxOn := TxNow; end; MapChanged := False; diff --git a/doc/TCI.md b/doc/TCI.md index 7d1f000..f232958 100644 --- a/doc/TCI.md +++ b/doc/TCI.md @@ -495,6 +495,27 @@ web-клиента: явная просьба сильнее умолчания микрофона, что и web-аудио. Аудио от клиента, который не просил `tci`, отбрасывается молча: отвечать ошибкой на каждый чужой блок значит захлебнуться. +**★Маркер `TX_CHRONO` называет приёмник КЛИЕНТА, а не нулевой.** Клиент шлёт +TX-аудио не по своей воле, а строго в ответ на маркер, и все входящие бинарные +блоки фильтрует по номеру приёмника — у MSHV это первая строка обработчика +(`network.cpp`: `if (pStream->receiver != tci_trx) return;`, ветка `TxChrono` +там же собирает и отправляет блок). С жёстким нулём в заголовке клиент, +сидящий на **втором слайсе** (`tci_trx = 1`), маркеров не видел вовсе: эфир по +`trx:1,true,tci` поднимался, а звука не было ни одного блока. Поэтому вместе с +клиентом-модулятором запоминается номер приёмника из его же `TRX` +(`FTxRx`), и маркеры идут под ним. + +**★`rfTransmitting` — не всегда «эфир изменился».** Контроллер шлёт это поле и +просто «перерисуй TX-бейджи»: `SetTxSlice` заканчивается `Changed(rfTransmitting)`, +хотя передачи ещё нет. Адаптер раньше понимал любой такой сигнал при +`FTransmitting = false` как «передача кончилась» и снимал `TCIMicRequested` — +а приходил он **посередине нашей же команды**: `SyncSetTRX` ставит просьбу → +`RequestSliceTx` → `SetTxSlice` → `Changed` → просьба стёрта → `SetMOX` выбирает +микрофон уже без неё. На живом железе это выглядело так: клиент на слайсе +поднимает эфир, а модуляция идёт с микрофона оператора, то есть в эфир — +тишина (и TX-аудио клиента отбрасывалось, `TCIMicActive` не поднят). Теперь +ловится **фронт** «было → стало» (`FLastTxOn`), а не всякое уведомление. + **Запись линейного выхода.** Рекордер один на приёмник (а не на клиента): пишет он то, что слышно в аппарате. Буфер на запрошенное время (потолок 300 с) в int16 48 кГц стерео — это ровно то, что уйдёт в WAV, и вдвое меньше памяти, @@ -933,7 +954,7 @@ ExpertSDR3 давно бы не было. движков и сети валится с AV — клиент получает `tci_error`, соединение живо) и неразрывность пачки инициализации под крутящейся ручкой. -### Стенд этапа 2 (бинарные потоки) — 214 проверок, все зелёные +### Стенд этапа 2 (бинарные потоки) — 217 проверок, все зелёные Отдельная программа (`test/tci/tcitest.pas`, прогон — `test/tci/run.sh`, внешних библиотек не требует) проверяет потоки на четырёх уровнях: @@ -1001,7 +1022,10 @@ ExpertSDR3 давно бы не было. - **Слайс как приёмник** (часть E, живой движок): созданный на ГЛАВНОМ пане слайс становится приёмником 1, отвечает на `vfo:1,0;` своей частотой (это и есть вся инициализация MSHV), слушается командой `vfo:1,0,<Гц>`, отдаёт своё - аудио блоками с `receiver = 1`, а после удаления слайса приёмник 1 замолкает + аудио блоками с `receiver = 1`, ★по `trx:1,true,tci` берёт модуляцию из TCI + (то есть просьбу не стирает `Changed(rfTransmitting)` из `SetTxSlice`) и шлёт + маркеры `TX_CHRONO` **под номером 1** — оба этих места и ломали передачу + MSHV, сидящего на втором слайсе, а после удаления слайса приёмник 1 замолкает целиком — вместо прежнего `vfo:1,0,0`. - **Сквозной прогон через живой WDSP:** синтетический 24-битный IQ подаётся в движок, а клиент по WebSocket получает блоки RX-аудио 12 кГц и IQ 48 кГц diff --git a/test/tci/tcitest.pas b/test/tci/tcitest.pas index 1e0edf7..94b2d20 100644 --- a/test/tci/tcitest.pas +++ b/test/tci/tcitest.pas @@ -1566,7 +1566,7 @@ var Op: Byte; Pay: TBytes; H: TTCIStreamHeader; - AudioBlocks, IQBlocks, BadHdr: Integer; + AudioBlocks, IQBlocks, BadHdr, BadRx: Integer; SliceId: Integer; SV: TSliceView; S: string; @@ -1758,6 +1758,40 @@ begin Inc(n); end; Check('аудио слайса идёт под номером 1', n > 0, IntToStr(n)); + + // ── ★TX со слайса: маркер обязан нести НОМЕР ЭТОГО приёмника ───────── + // MSHV шлёт TX-аудио только в ответ на маркер TX_CHRONO и отбрасывает + // ЛЮБОЙ входящий блок с чужим receiver (network.cpp:231 — `if + // (pStream->receiver != tci_trx) return;`, ветка TxChrono — network.cpp:288 + // и далее). С жёстким нулём в заголовке клиент, сидящий на втором слайсе + // (tci_trx = 1), поднимал эфир и молчал: маркеры до него не доходили, а + // без них он не отправляет ни одного блока. Ровно это и наблюдалось на + // живом железе с MSHV. + Ctrl.FWDSPReady := True; + Ctrl.SetSliceSlotAutoTx(Ctrl.SliceSlotOf(SliceId), True); + C.SendText('trx:1,true,tci;'); + C.WaitText('trx:', 1500); + Check('TX слайса: модуляция из TCI взята', Ctrl.TCIMicActive); + C.Pump(400); + n := 0; + BadRx := 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(tstTXChrono)) then + begin + Inc(n); + if H.Receiver <> 1 then Inc(BadRx); + end; + end; + Check('TX слайса: маркеры TX_CHRONO идут', n > 0, IntToStr(n)); + Check('TX слайса: маркер назван номером приёмника (MSHV фильтрует)', + (n > 0) and (BadRx = 0), IntToStr(BadRx)); + C.SendText('trx:1,false;'); + C.WaitText('trx:', 1000); + Ctrl.SetSliceSlotAutoTx(Ctrl.SliceSlotOf(SliceId), False); + C.SendText('audio_stop:1;'); C.Pump(200);