mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
fix(tci): дефекты живого прогона — length аудио, маршруты тапов, MOX, рекордер, EOF сокета
Восемь дефектов, найденных прогоном настоящего TCI-клиента (три приёмника: NFM, DIGU, FMRAW) и его отчётом. 1. UI доп. панорам не перерисовывался: rfSliceState рассылался, но ветки в MainForm.OnControllerState не было (частоту несёт отдельный rfSliceFreq). 2. Stream.length у аудио — сэмплы НА КАНАЛ (§4.3), у IQ — вещественные отсчёты (§3.4: комплексных = length/channels). Было ×каналы везде, у стерео получалось вдвое больше. Развилка в TCIFillHeader + разбор TX-аудио в HandleBinary. 3+4. Дыры в маршрутах аудио движка: demod-тап звался только для DMR/FMRAW (у DIGU не было RX_AUDIO), а пост-громкостный — только для нецифровых (у FMRAW не было LINEOUT). Плюс мьют слайса больше не убивает RX_AUDIO: движку сообщают SetAudioTapsActive. 5. Клиент, поставивший TRX, уходил — MOX оставался. FTrxOwner + StopTxOf; TCIMicRequested снимается и по окончании любой передачи. 6. Гонка снятия IQ-тапа: SetIQTap(nil) возвращался раньше, чем DSP-поток выходил из вызова. FIQTapLock (порядок FSliceLock → FIQTapLock). 7. Рекордер был кольцом «последние N секунд», а §4.3 говорит про МАКСИМАЛЬНОЕ время записи с удалением по истечении. Переделан в линейный буфер с окном по часам от START. 8. TCIServer.HandleClient считал recv = 0 таймаутом: ноль — это EOF, errno при нём не трогается и несёт EAGAIN от прошлого истёкшего TCI_POLL_MS. Обычный TCP-разрыв без close-кадра не освобождал слот до остановки сервера, и после нескольких аварийных отключений новые клиенты упирались в TCI_MAX_CLIENTS. Теперь R = 0 рвёт связь безусловно, errno спрашивается только при R < 0. Попутно: MainForm.RecreateDSPEngine (смена sample rate до START) терял внутренние колбэки контроллера — введён AttachEngineCallbacks. Стенд test/tci заведён в репозиторий (run.sh, 126/126 зелёных, включая сквозной прогон через живой WDSP), доп. проверки на оба пути отключения клиента. doc/TCI.md приведена в соответствие: правило про recv = 0 в §1.1, единицы Stream.length, линейный буфер рекордера. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+95
-7
@@ -142,6 +142,7 @@ type
|
||||
// ── TX-аудио от клиента (§3.4) ──
|
||||
FTxLock: TCriticalSection;
|
||||
FTxClient: TTCIClient; // кто модулирует (nil — никто)
|
||||
FTrxOwner: TTCIClient; // кто поставил трансивер в эфир (§4.2)
|
||||
FTxInterp: TTCIInterpolator;
|
||||
FTxInRate: Integer; // частота дискретизации подачи клиента
|
||||
FTxRunning: Boolean; // маркеры TX_CHRONO идут
|
||||
@@ -177,6 +178,7 @@ type
|
||||
procedure SyncSetMode;
|
||||
procedure SyncSetFilter;
|
||||
procedure SyncSetTRX;
|
||||
procedure SyncStopTRX;
|
||||
procedure SyncSetTune;
|
||||
procedure SyncSetDrive;
|
||||
procedure SyncSetTuneDrive;
|
||||
@@ -225,6 +227,8 @@ type
|
||||
procedure CmdStream(Client: TTCIClient; const M: TTCIMessage);
|
||||
procedure CmdRecorder(Client: TTCIClient; const M: TTCIMessage);
|
||||
procedure ClearTxClient(C: TTCIClient); // клиент ушёл/перестал модулировать
|
||||
procedure StopTxOf(C: TTCIClient); // ★снять эфир, начатый этим клиентом
|
||||
procedure ForgetTxOwner; // передача кончилась не по TCI
|
||||
|
||||
// ── Помощники модели ──
|
||||
function CanInvoke: Boolean;
|
||||
@@ -1259,6 +1263,10 @@ begin
|
||||
DropHolds(Client);
|
||||
DropClientStreams(Client);
|
||||
ClearTxClient(Client);
|
||||
// ★И только теперь — сама передача: если в эфир нас поставил именно этот
|
||||
// клиент, снимаем MOX. Оставить включённый передатчик за ушедшим клиентом
|
||||
// нельзя ни при какой модуляции (§4.2).
|
||||
StopTxOf(Client);
|
||||
end;
|
||||
|
||||
{ ═══════════════════════════════════════════════════════════════════════════
|
||||
@@ -1319,6 +1327,16 @@ begin
|
||||
FController.SetMOX(FsBool);
|
||||
end;
|
||||
|
||||
procedure TTCIAdapter.SyncStopTRX;
|
||||
// Поток контроллера. Передачу, начатую ушедшим клиентом, снимаем безусловно:
|
||||
// решение принято там, где известно, кто её начал, а здесь остаётся только
|
||||
// проверить, что она вообще идёт (оператор мог отпустить PTT сам).
|
||||
begin
|
||||
if (FController = nil) or not FController.FTransmitting then Exit;
|
||||
FController.TCIMicRequested := False;
|
||||
FController.SetMOX(False);
|
||||
end;
|
||||
|
||||
procedure TTCIAdapter.SyncTaps;
|
||||
// Поток контроллера: движок и список тапов трогаем только отсюда.
|
||||
begin
|
||||
@@ -1764,6 +1782,11 @@ begin
|
||||
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;
|
||||
finally FTxLock.Leave; end;
|
||||
FLock.Enter;
|
||||
try
|
||||
@@ -2638,6 +2661,8 @@ begin
|
||||
FStreamLock.Leave;
|
||||
end;
|
||||
ClearTxClient(nil);
|
||||
// Страховка: клиент мог держать эфир и без своего аудио (TRX без 'tci').
|
||||
StopTxOf(nil);
|
||||
end;
|
||||
|
||||
function TTCIAdapter.EffIQRate(C: TTCIClient): Integer;
|
||||
@@ -2890,9 +2915,55 @@ begin
|
||||
if not Drop then Exit;
|
||||
// Пишем прямо, без Invoke: это один Boolean, который SetMOX только читает, и
|
||||
// зовут нас откуда угодно — в том числе из деструктора адаптера, где ждать
|
||||
// поток контроллера уже некому. Передачу при этом НЕ трогаем: оператор мог
|
||||
// нажать PTT сам, и обрывать его эфир из-за ухода клиента нельзя.
|
||||
if FController <> nil then FController.TCIMicRequested := False;
|
||||
// поток контроллера уже некому.
|
||||
if FController = nil then Exit;
|
||||
FController.TCIMicRequested := False;
|
||||
// ★А вот саму передачу, если она ИДЁТ ИМЕННО ИЗ ЭТОГО ПОТОКА, оставлять
|
||||
// нельзя: источник модуляции только что исчез, и в эфире осталась бы
|
||||
// несущая с тишиной (или с последним, что застряло в ринге), которую никто
|
||||
// не снимет. Передачу с микрофона оператора это не трогает — там
|
||||
// TCIMicActive не поднят.
|
||||
if FController.TCIMicActive then StopTxOf(nil);
|
||||
end;
|
||||
|
||||
procedure TTCIAdapter.StopTxOf(C: TTCIClient);
|
||||
// ★Безопасность (§4.2). Клиент, поставивший трансивер в эфир, ушёл — MOX
|
||||
// обязан упасть. C = nil — снять эфир, кем бы из клиентов он ни был начат
|
||||
// (остановка сервера, потеря источника модуляции).
|
||||
var Mine: Boolean;
|
||||
begin
|
||||
Mine := False;
|
||||
FTxLock.Enter;
|
||||
try
|
||||
if (FTrxOwner <> nil) and ((C = nil) or (FTrxOwner = C)) then
|
||||
begin
|
||||
FTrxOwner := nil;
|
||||
Mine := True;
|
||||
end;
|
||||
finally
|
||||
FTxLock.Leave;
|
||||
end;
|
||||
if not Mine or (FController = nil) then Exit;
|
||||
if CanInvoke then
|
||||
FController.Invoke(SyncStopTRX)
|
||||
else
|
||||
// CanInvoke=False бывает ровно в одном случае — сервер останавливается, а
|
||||
// останавливает его поток контроллера: он же сейчас и исполняет нас
|
||||
// (TTCIServer.Stop, шаг 5 → Disconnected). Ждать самого себя нельзя, и не
|
||||
// нужно — вызов и так на правильном потоке.
|
||||
SyncStopTRX;
|
||||
end;
|
||||
|
||||
procedure TTCIAdapter.ForgetTxOwner;
|
||||
// Передача кончилась сама (оператор, CAT, PTT, тайм-аут) — забываем, кто её
|
||||
// начал. Иначе уход того клиента через час снимал бы уже чужой эфир.
|
||||
begin
|
||||
FTxLock.Enter;
|
||||
try
|
||||
FTrxOwner := nil;
|
||||
finally
|
||||
FTxLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TTCIAdapter.PushTxChrono;
|
||||
@@ -2966,7 +3037,7 @@ var
|
||||
H: TTCIStreamHeader;
|
||||
ST: TTCISampleType;
|
||||
P: PByte;
|
||||
N, i, k, Chans, Rate, Factor, Bytes: Integer;
|
||||
N, i, k, Chans, Rate, Factor, Bytes, Want: Integer;
|
||||
Mine: Boolean;
|
||||
begin
|
||||
if (Data = nil) or (Len <= SizeOf(H)) then Exit;
|
||||
@@ -3001,10 +3072,16 @@ begin
|
||||
try
|
||||
N := TCIUnpackSamples(P, Bytes, ST, FTxRaw);
|
||||
if N <= 0 then Exit;
|
||||
// Заголовок обещает length вещественных отсчётов — верим меньшему из двух:
|
||||
// В аудиопотоке length — это сэмплы НА КАНАЛ (см. TCIFillHeader), значит
|
||||
// вещественных отсчётов в блоке length × channels. Верим меньшему из двух:
|
||||
// клиент вправе прислать короткий хвост, но не длиннее уместившегося.
|
||||
if (H.DataLength > 0) and (Integer(H.DataLength) < N) then
|
||||
N := Integer(H.DataLength);
|
||||
// Заведомо чужое число (мусор в заголовке) просто игнорируем — длину нам
|
||||
// и так ограничил размер кадра.
|
||||
if (H.DataLength > 0) and (H.DataLength <= LongWord(N)) then
|
||||
begin
|
||||
Want := Integer(H.DataLength) * Chans;
|
||||
if Want < N then N := Want;
|
||||
end;
|
||||
|
||||
// В тракт идёт моно: TXA у нас один, а стерео от клиента — это его
|
||||
// собственный формат вывода, а не два независимых сигнала.
|
||||
@@ -3063,6 +3140,17 @@ begin
|
||||
// а поток остался бы висеть на несуществующем приёмнике.
|
||||
if Field in [rfDevice, rfConnected] then DropDeadRxStreams;
|
||||
|
||||
// Передача кончилась — чья бы она ни была. Забываем хозяина эфира (иначе его
|
||||
// уход когда-нибудь потом снял бы уже чужую передачу) и снимаем просьбу
|
||||
// «модулируй из потока TCI»: она относилась ровно к той передаче, которую
|
||||
// клиент и начал, а следующий PTT оператора обязан идти с его микрофона.
|
||||
if (Field = rfTransmitting) and (FController <> nil)
|
||||
and not FController.FTransmitting then
|
||||
begin
|
||||
ForgetTxOwner;
|
||||
FController.TCIMicRequested := False;
|
||||
end;
|
||||
|
||||
MapChanged := False;
|
||||
if Field in [rfSliceFreq, rfSliceState, rfDevice, rfPanFreq, rfSampleRate,
|
||||
rfBand, rfXvtr, rfCenterFreq] then
|
||||
|
||||
Reference in New Issue
Block a user