mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +00:00
feat(dxcluster): споты на всех панадаптерах + кнопка DX в блоке RX
Подписи спотов рисовались только на главном пане. Теперь их видит каждый пан, в чьё окно попадает частота спота. Оверлей — ЭКЗЕМПЛЯР НА ПАН, а не один общий: кэш полосы подписей ключуется центром/спаном/шириной, а у панов они свои — общий оверлей пересобирался бы на каждый пан каждый кадр, то есть ровно то, ради чего кэш и заводился. Живёт в TPanafallPanel (AttachDXSpots/DetachDXSpots/DXOverlay, Owner=панель), база спотов по-прежнему одна на всех. FDXSpotOverlay в MainForm стал алиасом на оверлей пана 0 — как FSpecView для FPan.View: настройки, тема и тик затухания идут общим циклом по FPans, и главный пан перестал быть особым случаем (иначе тумблер DX гасил бы подписи только на нём). ★ Version стора глобальна, а окно у пана своё, поэтому в EnsureRendered на смену версии сначала берётся снимок окна и сверяется его подпись (FNV-1a по позывному, частоте, моде и метке времени). Совпала — ни пересборки битмапа, ни перезаливки GL-текстуры: спот, севший на чужой диапазон, до этого пана не доходит. Без этого цена пересборки множилась бы на число панов. Снимок берётся один раз и переиспользуется раскладкой — стор второй раз не дёргаем. Клик по подписи на пане N = QSY слайса ЭТОГО пана (активного, если он здесь, иначе первого; нет ни одного — создаём в точке) с модой по комментарию кластера и полосой пресета этой моды. Своего VFO у панов N нет, а ретюнить их DDC под спот нельзя — увезло бы весь пан. Мода спота → режим вынесена в общую DXSpotRadioMode для обоих путей. Низкий пан (грид): полоса подписей и штрихи пропускаются целиком — раньше в GL-пути полоса легла бы поверх спектра, а штрихи пошли бы снизу вверх. Кнопка DX переехала из тулбара в блок RX левой панели, сразу после CTUN (ряд стал четырёхколоночным: CTUN | DX | Channel | BEACON): споты — часть приёмного вида, а не глобальная команда уровня DISCOVER/START/SETUP. Заодно снят расчёт SafeLeft, резервировавший под неё место в шапке. Подписи по умолчанию ВЫКЛЮЧЕНЫ (show_spots: дефолт и фолбэк чтения были True), состояние переживает перезапуск. Клик по кнопке подливает состояние в открытый SETUP: там своя копия конфига, и первая же правка любого поля страницы вернула бы подписи обратно. fix: Stop клиента кластера больше не держит главный поток. Он делал Terminate + WaitFor, а поток в этот момент сидит в блокирующем recv и просыпается лишь по своему кванту (до 1 с), на застрявшем send — до SO_SNDTIMEO. Всё это время окно висело на выходе и на переподключении после правки настроек. Теперь Stop рвёт живой сокет SockShutdown (хэндл — зеркало под тем же локом, поток снимает его ДО close, поэтому shutdown по закрытому fd невозможен), а FormDestroy зовёт неблокирующий RequestStop первой строкой: поток доживает параллельно с разборкой радио, и join в конце уже никого не ждёт. Сборка ewsdr (--ws=qt6) и ewsdrd — ОК. На железе не проверено: доп. паны требуют подключённого радио. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+51
-7
@@ -67,6 +67,13 @@ type
|
||||
|
||||
FStore: TDXSpotStore; // не владеет
|
||||
|
||||
// Сокет живого соединения — зеркало под FLock. Нужен ровно для того, чтобы
|
||||
// Stop() мог сделать SockShutdown и вывести поток из блокирующего recv:
|
||||
// иначе выход из программы ждал бы до конца кванта recv (и до конца
|
||||
// таймаута send, если кластер перестал забирать данные). Поток снимает
|
||||
// зеркало ДО close, поэтому под локом хэндл гарантированно ещё открыт.
|
||||
FActiveSock: TSocket;
|
||||
|
||||
FState: TDXClusterState;
|
||||
FStatusMsg: string;
|
||||
FSpotCount: Int64; // сколько спотов принято за сессию
|
||||
@@ -79,6 +86,7 @@ type
|
||||
FOutQueue: TStringList; // строки на отправку в кластер
|
||||
|
||||
procedure SetState(St: TDXClusterState; const Msg: string);
|
||||
procedure PublishSock(S: TSocket); // зеркало хэндла для Stop (см. FActiveSock)
|
||||
public
|
||||
constructor Create(AStore: TDXSpotStore);
|
||||
destructor Destroy; override;
|
||||
@@ -86,7 +94,11 @@ type
|
||||
procedure Configure(const AHost: string; APort: Integer;
|
||||
const ALogin, APassword, APostLogin: string);
|
||||
procedure Start;
|
||||
procedure Stop;
|
||||
// Неблокирующая половина Stop: разбудить поток и порвать сокет, не дожидаясь
|
||||
// его конца. Зовётся в начале выхода из программы, чтобы поток доживал
|
||||
// ПАРАЛЛЕЛЬНО с разборкой радио, а join в Stop уже никого не ждал.
|
||||
procedure RequestStop;
|
||||
procedure Stop; // RequestStop + join
|
||||
function Running: Boolean;
|
||||
|
||||
// Отправить сырую строку в кластер (окно списка: sh/dx, set/filter, …).
|
||||
@@ -274,6 +286,7 @@ begin
|
||||
FLogCount := 0;
|
||||
FLogVer := 0;
|
||||
FSpotCount := 0;
|
||||
FActiveSock := SOCK_INVALID;
|
||||
{$IFDEF MSWINDOWS}
|
||||
WSAStartup($0202, wsa);
|
||||
{$ENDIF}
|
||||
@@ -329,8 +342,7 @@ begin
|
||||
FThread := TDXClusterThread.Create(Self);
|
||||
end;
|
||||
|
||||
procedure TDXClusterClient.Stop;
|
||||
var T: TThread;
|
||||
procedure TDXClusterClient.RequestStop;
|
||||
begin
|
||||
// Всё, что не успели отправить, к следующему соединению уже неактуально.
|
||||
FLock.Enter;
|
||||
@@ -339,7 +351,28 @@ begin
|
||||
finally
|
||||
FLock.Leave;
|
||||
end;
|
||||
if FThread = nil then Exit;
|
||||
FThread.Terminate;
|
||||
FStopEvent.SetEvent;
|
||||
// Терминации мало: поток стоит в блокирующем recv и проснулся бы только по
|
||||
// SO_RCVTIMEO (до DX_RECV_MS), а на застрявшем send — до SO_SNDTIMEO. Всё это
|
||||
// время join держал бы главный поток, т.е. окно висело бы на выходе и на
|
||||
// переподключении после правки настроек. SockShutdown рвёт обе стороны
|
||||
// немедленно (на Linux одного close мало — см. WebUtils.SockShutdown).
|
||||
// Закрывает сокет по-прежнему сам поток: закрыть чужой хэндл значило бы
|
||||
// гонку с его же recv.
|
||||
FLock.Enter;
|
||||
try
|
||||
if FActiveSock <> SOCK_INVALID then SockShutdown(FActiveSock);
|
||||
finally
|
||||
FLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TDXClusterClient.Stop;
|
||||
var T: TThread;
|
||||
begin
|
||||
RequestStop;
|
||||
T := FThread;
|
||||
if T = nil then
|
||||
begin
|
||||
@@ -347,10 +380,6 @@ begin
|
||||
Exit;
|
||||
end;
|
||||
FThread := nil;
|
||||
T.Terminate;
|
||||
FStopEvent.SetEvent;
|
||||
// Сокет закрывает сам поток (TDXClusterThread.CloseSock из Execute);
|
||||
// FStopEvent + Terminate выводят его из ожиданий, а recv рвётся по таймауту.
|
||||
T.WaitFor;
|
||||
T.Free;
|
||||
SetState(dxsOff, '');
|
||||
@@ -361,6 +390,19 @@ begin
|
||||
Result := FThread <> nil;
|
||||
end;
|
||||
|
||||
procedure TDXClusterClient.PublishSock(S: TSocket);
|
||||
// Зеркало хэндла для Stop. Поток зовёт с валидным хэндлом сразу после connect
|
||||
// и с SOCK_INVALID ПЕРЕД close — так Stop под локом видит либо живой сокет,
|
||||
// либо ничего, но не закрытый.
|
||||
begin
|
||||
FLock.Enter;
|
||||
try
|
||||
FActiveSock := S;
|
||||
finally
|
||||
FLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TDXClusterClient.SetState(St: TDXClusterState; const Msg: string);
|
||||
begin
|
||||
FLock.Enter;
|
||||
@@ -606,6 +648,7 @@ begin
|
||||
{$ELSE}
|
||||
fpSetSockOpt(FSocket, SOL_SOCKET, SO_KEEPALIVE, @One, SizeOf(One));
|
||||
{$ENDIF}
|
||||
FOwner.PublishSock(FSocket); // с этого момента Stop может рвать нас shutdown'ом
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
@@ -613,6 +656,7 @@ procedure TDXClusterThread.CloseSock;
|
||||
begin
|
||||
if FSocket <> SOCK_INVALID then
|
||||
begin
|
||||
FOwner.PublishSock(SOCK_INVALID); // до close: Stop не должен увидеть мёртвый хэндл
|
||||
SockShutdown(FSocket);
|
||||
SockClose(FSocket);
|
||||
FSocket := SOCK_INVALID;
|
||||
|
||||
Reference in New Issue
Block a user