mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
fix(tci): передача с слайса — маркер TX_CHRONO под номером клиента, фронт rfTransmitting
Живой прогон с 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 <noreply@anthropic.com>
This commit is contained in:
+38
-10
@@ -152,7 +152,9 @@ type
|
|||||||
// ── TX-аудио от клиента (§3.4) ──
|
// ── TX-аудио от клиента (§3.4) ──
|
||||||
FTxLock: TCriticalSection;
|
FTxLock: TCriticalSection;
|
||||||
FTxClient: TTCIClient; // кто модулирует (nil — никто)
|
FTxClient: TTCIClient; // кто модулирует (nil — никто)
|
||||||
|
FTxRx: Integer; // ЕГО номер приёмника (из TRX) — см. PushTxChrono
|
||||||
FTrxOwner: TTCIClient; // кто поставил трансивер в эфир (§4.2)
|
FTrxOwner: TTCIClient; // кто поставил трансивер в эфир (§4.2)
|
||||||
|
FLastTxOn: Boolean; // было ли радио в эфире на прошлом rfTransmitting
|
||||||
FTxInterp: TTCIInterpolator;
|
FTxInterp: TTCIInterpolator;
|
||||||
FTxInRate: Integer; // частота дискретизации подачи клиента
|
FTxInRate: Integer; // частота дискретизации подачи клиента
|
||||||
FTxRunning: Boolean; // маркеры TX_CHRONO идут
|
FTxRunning: Boolean; // маркеры TX_CHRONO идут
|
||||||
@@ -394,6 +396,9 @@ begin
|
|||||||
FStreamLock := TCriticalSection.Create;
|
FStreamLock := TCriticalSection.Create;
|
||||||
FTxLock := TCriticalSection.Create;
|
FTxLock := TCriticalSection.Create;
|
||||||
FWriter := nil; // заводится на первом SAVE (см. EnqueueWav)
|
FWriter := nil; // заводится на первом SAVE (см. EnqueueWav)
|
||||||
|
// Состояние передачи на старте: фронт «было-стало» ловим с него (см. OnState).
|
||||||
|
FLastTxOn := (AController <> nil) and
|
||||||
|
(AController.FTransmitting or AController.FTuning);
|
||||||
FTapsOn := False;
|
FTapsOn := False;
|
||||||
SetLength(FTxRaw, TCI_STREAM_DATA_MAX div 2); // худший случай: int16
|
SetLength(FTxRaw, TCI_STREAM_DATA_MAX div 2); // худший случай: int16
|
||||||
SetLength(FTxMono, TCI_STREAM_DATA_MAX div 2);
|
SetLength(FTxMono, TCI_STREAM_DATA_MAX div 2);
|
||||||
@@ -1959,8 +1964,11 @@ begin
|
|||||||
FromTCI := B and (not Tune) 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
|
// ★Вместе с клиентом запоминаем НОМЕР ПРИЁМНИКА, которым он назвался:
|
||||||
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;
|
finally FTxLock.Leave; end;
|
||||||
Started := False;
|
Started := False;
|
||||||
FLock.Enter;
|
FLock.Enter;
|
||||||
@@ -3227,6 +3235,7 @@ begin
|
|||||||
if (FTxClient <> nil) and ((C = nil) or (FTxClient = C)) then
|
if (FTxClient <> nil) and ((C = nil) or (FTxClient = C)) then
|
||||||
begin
|
begin
|
||||||
FTxClient := nil;
|
FTxClient := nil;
|
||||||
|
FTxRx := 0;
|
||||||
FTxRunning := False;
|
FTxRunning := False;
|
||||||
Drop := True;
|
Drop := True;
|
||||||
end;
|
end;
|
||||||
@@ -3295,14 +3304,15 @@ procedure TTCIAdapter.PushTxChrono;
|
|||||||
var
|
var
|
||||||
C: TTCIClient;
|
C: TTCIClient;
|
||||||
Now_: QWord;
|
Now_: QWord;
|
||||||
Rate, Chans, Block: Integer;
|
Rate, Chans, Block, Rx: Integer;
|
||||||
ST: TTCISampleType;
|
ST: TTCISampleType;
|
||||||
H: TTCIStreamHeader;
|
H: TTCIStreamHeader;
|
||||||
Active: Boolean;
|
Active: Boolean;
|
||||||
begin
|
begin
|
||||||
FTxLock.Enter;
|
FTxLock.Enter;
|
||||||
try
|
try
|
||||||
C := FTxClient;
|
C := FTxClient;
|
||||||
|
Rx := FTxRx;
|
||||||
finally
|
finally
|
||||||
FTxLock.Leave;
|
FTxLock.Leave;
|
||||||
end;
|
end;
|
||||||
@@ -3341,7 +3351,13 @@ begin
|
|||||||
end;
|
end;
|
||||||
while FTxOwed >= Block do
|
while FTxOwed >= Block do
|
||||||
begin
|
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);
|
C.SendBin(H, nil, 0);
|
||||||
FTxOwed := FTxOwed - Block;
|
FTxOwed := FTxOwed - Block;
|
||||||
end;
|
end;
|
||||||
@@ -3446,7 +3462,7 @@ procedure TTCIAdapter.OnState(Sender: TObject; Field: TRadioField);
|
|||||||
var
|
var
|
||||||
Rx, Ch: Integer;
|
Rx, Ch: Integer;
|
||||||
TxHz: Double;
|
TxHz: Double;
|
||||||
MapChanged: Boolean;
|
MapChanged, TxNow: Boolean;
|
||||||
Sig: string;
|
Sig: string;
|
||||||
begin
|
begin
|
||||||
// Снимок слайсов обновляем ДО всего остального и НЕЗАВИСИМО от того, есть ли
|
// Снимок слайсов обновляем ДО всего остального и НЕЗАВИСИМО от того, есть ли
|
||||||
@@ -3466,11 +3482,23 @@ begin
|
|||||||
// уход когда-нибудь потом снял бы уже чужую передачу) и снимаем просьбу
|
// уход когда-нибудь потом снял бы уже чужую передачу) и снимаем просьбу
|
||||||
// «модулируй из потока TCI»: она относилась ровно к той передаче, которую
|
// «модулируй из потока TCI»: она относилась ровно к той передаче, которую
|
||||||
// клиент и начал, а следующий PTT оператора обязан идти с его микрофона.
|
// клиент и начал, а следующий PTT оператора обязан идти с его микрофона.
|
||||||
if (Field = rfTransmitting) and (FController <> nil)
|
// ★Ловим именно ФРОНТ «было-стало», а не всякий rfTransmitting. Это поле
|
||||||
and not FController.FTransmitting then
|
// контроллер шлёт и просто «перерисуй TX-бейджи»: SetTxSlice заканчивается
|
||||||
|
// Changed(rfTransmitting), хотя эфира ещё нет. По прежнему условию такой
|
||||||
|
// сигнал приходил ПОСЕРЕДИНЕ нашей же команды trx:<N>,true,tci (SyncSetTRX
|
||||||
|
// ставит просьбу → RequestSliceTx → SetTxSlice → Changed) и стирал её ДО
|
||||||
|
// SetMOX, который её и читает. Итог на живом железе: клиент на слайсе
|
||||||
|
// поднимал эфир, а модуляция шла с микрофона оператора, то есть в эфир —
|
||||||
|
// тишина.
|
||||||
|
if (Field = rfTransmitting) and (FController <> nil) then
|
||||||
begin
|
begin
|
||||||
ForgetTxOwner;
|
TxNow := FController.FTransmitting or FController.FTuning;
|
||||||
FController.TCIMicRequested := False;
|
if FLastTxOn and (not TxNow) then
|
||||||
|
begin
|
||||||
|
ForgetTxOwner;
|
||||||
|
FController.TCIMicRequested := False;
|
||||||
|
end;
|
||||||
|
FLastTxOn := TxNow;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
MapChanged := False;
|
MapChanged := False;
|
||||||
|
|||||||
+26
-2
@@ -495,6 +495,27 @@ web-клиента: явная просьба сильнее умолчания
|
|||||||
микрофона, что и web-аудио. Аудио от клиента, который не просил `tci`,
|
микрофона, что и 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 с)
|
пишет он то, что слышно в аппарате. Буфер на запрошенное время (потолок 300 с)
|
||||||
в int16 48 кГц стерео — это ровно то, что уйдёт в WAV, и вдвое меньше памяти,
|
в int16 48 кГц стерео — это ровно то, что уйдёт в WAV, и вдвое меньше памяти,
|
||||||
@@ -933,7 +954,7 @@ ExpertSDR3 давно бы не было.
|
|||||||
движков и сети валится с AV — клиент получает `tci_error`, соединение живо) и
|
движков и сети валится с AV — клиент получает `tci_error`, соединение живо) и
|
||||||
неразрывность пачки инициализации под крутящейся ручкой.
|
неразрывность пачки инициализации под крутящейся ручкой.
|
||||||
|
|
||||||
### Стенд этапа 2 (бинарные потоки) — 214 проверок, все зелёные
|
### Стенд этапа 2 (бинарные потоки) — 217 проверок, все зелёные
|
||||||
|
|
||||||
Отдельная программа (`test/tci/tcitest.pas`, прогон — `test/tci/run.sh`,
|
Отдельная программа (`test/tci/tcitest.pas`, прогон — `test/tci/run.sh`,
|
||||||
внешних библиотек не требует) проверяет потоки на четырёх уровнях:
|
внешних библиотек не требует) проверяет потоки на четырёх уровнях:
|
||||||
@@ -1001,7 +1022,10 @@ ExpertSDR3 давно бы не было.
|
|||||||
- **Слайс как приёмник** (часть E, живой движок): созданный на ГЛАВНОМ пане
|
- **Слайс как приёмник** (часть E, живой движок): созданный на ГЛАВНОМ пане
|
||||||
слайс становится приёмником 1, отвечает на `vfo:1,0;` своей частотой (это и
|
слайс становится приёмником 1, отвечает на `vfo:1,0;` своей частотой (это и
|
||||||
есть вся инициализация MSHV), слушается командой `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`.
|
целиком — вместо прежнего `vfo:1,0,0`.
|
||||||
- **Сквозной прогон через живой WDSP:** синтетический 24-битный IQ подаётся
|
- **Сквозной прогон через живой WDSP:** синтетический 24-битный IQ подаётся
|
||||||
в движок, а клиент по WebSocket получает блоки RX-аудио 12 кГц и IQ 48 кГц
|
в движок, а клиент по WebSocket получает блоки RX-аудио 12 кГц и IQ 48 кГц
|
||||||
|
|||||||
+35
-1
@@ -1566,7 +1566,7 @@ var
|
|||||||
Op: Byte;
|
Op: Byte;
|
||||||
Pay: TBytes;
|
Pay: TBytes;
|
||||||
H: TTCIStreamHeader;
|
H: TTCIStreamHeader;
|
||||||
AudioBlocks, IQBlocks, BadHdr: Integer;
|
AudioBlocks, IQBlocks, BadHdr, BadRx: Integer;
|
||||||
SliceId: Integer;
|
SliceId: Integer;
|
||||||
SV: TSliceView;
|
SV: TSliceView;
|
||||||
S: string;
|
S: string;
|
||||||
@@ -1758,6 +1758,40 @@ begin
|
|||||||
Inc(n);
|
Inc(n);
|
||||||
end;
|
end;
|
||||||
Check('аудио слайса идёт под номером 1', n > 0, IntToStr(n));
|
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.SendText('audio_stop:1;');
|
||||||
C.Pump(200);
|
C.Pump(200);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user