From 11d8e11ba54cea4dff0d9df5d95d6b28d6e47a6f Mon Sep 17 00:00:00 2001 From: Uladzimir Karpenka Date: Thu, 20 Aug 2026 16:06:21 +0300 Subject: [PATCH] =?UTF-8?q?fix(tci):=20=D0=B2=D1=8B=D0=BA=D0=BB=D1=8E?= =?UTF-8?q?=D1=87=D0=B5=D0=BD=D0=B8=D0=B5=20TUNE=20=D0=BD=D0=B5=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=B1=D1=8B=D0=B2=D0=B0=D0=BB=D0=BE=20=D1=85=D0=BE=D0=B7?= =?UTF-8?q?=D1=8F=D0=B8=D0=BD=D0=B0=20=D1=8D=D1=84=D0=B8=D1=80=D0=B0=20?= =?UTF-8?q?=E2=80=94=20=D1=83=D1=85=D0=BE=D0=B4=20=D0=BA=D0=BB=D0=B8=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D0=B0=20=D1=80=D1=83=D0=B1=D0=B8=D0=BB=20=D1=87?= =?UTF-8?q?=D1=83=D0=B6=D1=83=D1=8E=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=D0=B0?= =?UTF-8?q?=D1=87=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Задний фронт передачи ловился только по rfTransmitting, а выключение TUN гасит эфир изнутри SetTune: там SetMOX(False) шлёт rfTransmitting, пока FTuning ещё True (флаг сбрасывается строкой ниже, см. RadioController.SetTune), поэтому TxNow оставался True и ForgetTxOwner не звался. Следующий за этим Changed(rfTuning) обработчик игнорировал вовсе. Итог: клиент сделал «tune:0,true» и стал хозяином эфира, оператор выключил TUN кнопкой (тогда сброс в DispatchCommand не срабатывает — команды-то не было), позже нажал PTT сам, а когда сокет клиента закрылся, HandleDisconnect → StopTxOf всё ещё видел его хозяином и снимал ОПЕРАТОРСКУЮ передачу. Ровно то, что комментарий у ForgetTxOwner обещает не допускать. Теперь фронт ловится и по rfTuning. На стенде — сценарий целиком: клиент поднимает TUN, оператор гасит его сам и встаёт в эфир, уход клиента передачу не трогает (негативный контроль: с прежним условием проверка падает). Co-Authored-By: Claude Opus 5 --- TCIAdapter.pas | 7 ++++++- test/tci/tcitest.pas | 26 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/TCIAdapter.pas b/TCIAdapter.pas index c4fcd73..df5fee6 100644 --- a/TCIAdapter.pas +++ b/TCIAdapter.pas @@ -3599,7 +3599,12 @@ begin // SetMOX, который её и читает. Итог на живом железе: клиент на слайсе // поднимал эфир, а модуляция шла с микрофона оператора, то есть в эфир — // тишина. - if (Field = rfTransmitting) and (FController <> nil) then + // ★Слушаем и rfTuning: выключение TUN гасит эфир изнутри SetTune, где + // SetMOX(False) шлёт rfTransmitting, пока FTuning ещё True (сброс флага идёт + // строкой ниже). Заднего фронта по одному rfTransmitting тут не видно, и + // хозяин эфира оставался протухшим: уход того клиента когда-нибудь потом + // снимал уже чужую — операторскую — передачу. + if (Field in [rfTransmitting, rfTuning]) and (FController <> nil) then begin TxNow := FController.FTransmitting or FController.FTuning; if FLastTxOn and (not TxNow) then diff --git a/test/tci/tcitest.pas b/test/tci/tcitest.pas index 51e6516..88ec04d 100644 --- a/test/tci/tcitest.pas +++ b/test/tci/tcitest.pas @@ -2217,6 +2217,32 @@ begin Sleep(300); Check('после ухода клиента источник снят', not Ctrl.TCIMicRequested); Check('после ухода клиента MOX снят', not Ctrl.FTransmitting); + + // ── TUN: хозяин эфира забывается и на выключении TUNE ── + // Клиент поднял TUN, оператор погасил его САМ (кнопкой, не командой TCI), + // а потом сам же встал на передачу. Уход клиента не имеет права снять эту + // чужую передачу: хозяина эфира обязано было забыть ещё выключение TUN. + // ★Задний фронт ловился только по rfTransmitting, а SetTune(False) шлёт + // его, пока FTuning ещё True, — владелец оставался протухшим навсегда. + C2 := TRawClient.Create; + try + Check('TUN: клиент подключился', C2.Connect(PORT)); + C2.WaitText('ready;', 2000); + C2.SendText('tune:0,true;'); + C2.WaitText('tune:', 1000); + Check('TUN: эфир поднят клиентом', Ctrl.FTuning and Ctrl.FTransmitting); + Ctrl.SetTune(False); // оператор гасит TUN сам + Ctrl.SetMOX(True); // и сам встаёт на передачу + Check('TUN: оператор в эфире', Ctrl.FTransmitting); + C2.Close_; + Sleep(300); + Check('TUN: уход клиента не снял чужую передачу', Ctrl.FTransmitting); + Ctrl.SetMOX(False); + finally + C2.Close_; + C2.Free; + end; + Ctrl.FWDSPReady := False; finally if C <> nil then begin C.Close_; C.Free; end;