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:
2026-08-19 21:47:30 +03:00
co-authored by Claude Opus 5
parent 3dc7035f9e
commit ecd42c32f8
3 changed files with 99 additions and 13 deletions
+38 -10
View File
@@ -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:<N>,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;