mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +00:00
feat(beacon): автопоиск маяка в окне лупы (кнопка AUTO)
Лупа сделала прицеливание возможным, но целиться всё равно надо руками. AUTO находит маяк сам, в текущем окне лупы: что видно, то и обыскивается — заодно зумом можно сузить область поиска. Критерий — не «самый громкий», иначе поймается первая же SSB-станция. Копим min-hold по кадрам лупы (~1.6 с): у маяка несущая непрерывна и минимум остаётся высоким, а речь, телеграф и цифра в паузах проваливаются к шуму и срезаются минимумом. Это тот самый «непрерывный след», который видно на водопаде, только числом. По накопленному: шумовой пол = медиана, кандидат = максимум скользящего среднего шириной с полосу приёма (±450 Гц) — среднее по полосе, а не отдельный бин, потому что маяк это плато ~900 Гц, и по такой мере он выигрывает у узкой пораженки с той же высотой пика. Порог 6 дБ над полом; центр уточняется центроидом превышения, чтобы наведение село на середину плато. - Тумблер рядом с FOLLOW, состояние в настройках (beacon_zoom_auto). - На локе автопоиск молчит: в контур не лезем. - После наведения выдержка ~3 с — дать декодеру попробовать захватиться. - Нашли там же, где уже стоит наведение (ближе 150 Гц) — не трогаем. BeaconSeedAtHz дёргает Reseed, и без этой проверки при слабом сигнале автопоиск сбрасывал бы захват по кругу, мешая декодеру сойтись. - В накопление идут только свежие кадры: таймер формы (25 Гц) быстрее анализатора (15 к/с), и повторный учёт кадра сокращал бы окно наблюдения, ради которого min-hold и заведён. В строке захвата появляется пометка AUTO — и только пока автопоиск реально работает: на локе она гаснет, чтобы не обещать действие, которого нет. Сборка ewsdr (--ws=qt6) и ewsdrd — ОК. На эфире не проверено. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+194
-5
@@ -47,6 +47,13 @@ const
|
|||||||
BCN_CLR_REF = TColor($0000FF00); // clLime — где маяк ДОЛЖЕН быть (пунктир)
|
BCN_CLR_REF = TColor($0000FF00); // clLime — где маяк ДОЛЖЕН быть (пунктир)
|
||||||
BCN_CLR_TRK = TColor($000AA5FF); // отслеживаемый центроид (оранж)
|
BCN_CLR_TRK = TColor($000AA5FF); // отслеживаемый центроид (оранж)
|
||||||
BCN_CLR_DEC = TColor($0020D0FF); // грани полосы приёма + центр наведения
|
BCN_CLR_DEC = TColor($0020D0FF); // грани полосы приёма + центр наведения
|
||||||
|
// Автопоиск маяка. Копим min-hold по кадрам лупы: у маяка несущая непрерывна,
|
||||||
|
// и минимум за пару секунд остаётся высоким, а любая SSB/CW-станция в паузах
|
||||||
|
// проваливается к шуму — это и отсеивает «громко, но не маяк».
|
||||||
|
BCN_AUTO_FRAMES = 24; // кадров в min-hold (~1.6 с при 15 к/с анализатора)
|
||||||
|
BCN_AUTO_COOLDOWN = 75; // тиков паузы после наведения (~3 с при 25 Гц)
|
||||||
|
BCN_AUTO_MIN_DB = 6.0; // насколько кандидат должен возвышаться над полом
|
||||||
|
BCN_AUTO_MIN_MOVE = 150.0; // ближе этого к текущему наведению не пере-наводим
|
||||||
|
|
||||||
type
|
type
|
||||||
TBeaconScopeForm = class(TForm)
|
TBeaconScopeForm = class(TForm)
|
||||||
@@ -78,7 +85,7 @@ type
|
|||||||
FWfBox: TPaintBox; // мини-водопад лупы
|
FWfBox: TPaintBox; // мини-водопад лупы
|
||||||
FWf: TWaterfallView;
|
FWf: TWaterfallView;
|
||||||
FStrip: TBitmap; // офскрин спектра (без него мигает)
|
FStrip: TBitmap; // офскрин спектра (без него мигает)
|
||||||
FBtnZoomIn, FBtnZoomOut, FBtnReset, FBtnFollow: TFlatButton;
|
FBtnZoomIn, FBtnZoomOut, FBtnReset, FBtnFollow, FBtnAuto: TFlatButton;
|
||||||
|
|
||||||
FCenterHz: Double; // центр окна лупы (display Hz), 0 = ещё не задан
|
FCenterHz: Double; // центр окна лупы (display Hz), 0 = ещё не задан
|
||||||
FSpanHz: Double; // ширина окна лупы
|
FSpanHz: Double; // ширина окна лупы
|
||||||
@@ -100,6 +107,14 @@ type
|
|||||||
// лезть за копией TBeaconScope на каждую отрисовку незачем.
|
// лезть за копией TBeaconScope на каждую отрисовку незачем.
|
||||||
FState: TBeaconState;
|
FState: TBeaconState;
|
||||||
FSNRdB: Double;
|
FSNRdB: Double;
|
||||||
|
|
||||||
|
// --- Автопоиск ---
|
||||||
|
FAuto: Boolean; // кнопка AUTO взведена
|
||||||
|
FAutoMin: array[0..BCN_MAX_PIX - 1] of Single; // min-hold кадров
|
||||||
|
FAutoFrames: Integer; // сколько кадров уже в min-hold
|
||||||
|
FAutoCnt: Integer; // точек в кадре, под который копится min-hold
|
||||||
|
FAutoLo, FAutoHi: Double; // окно, под которое копится (сменилось — сброс)
|
||||||
|
FAutoCooldown: Integer; // тиков паузы после наведения
|
||||||
FCursorX: Integer; // -1 = курсор вне лупы
|
FCursorX: Integer; // -1 = курсор вне лупы
|
||||||
FDragging: Boolean; // ЛКМ зажата в лупе
|
FDragging: Boolean; // ЛКМ зажата в лупе
|
||||||
FDragMoved: Boolean; // увели мышь → это панорамирование, не клик
|
FDragMoved: Boolean; // увели мышь → это панорамирование, не клик
|
||||||
@@ -145,6 +160,10 @@ type
|
|||||||
procedure BtnZoomOutClick(Sender: TObject);
|
procedure BtnZoomOutClick(Sender: TObject);
|
||||||
procedure BtnResetClick(Sender: TObject);
|
procedure BtnResetClick(Sender: TObject);
|
||||||
procedure BtnFollowClick(Sender: TObject);
|
procedure BtnFollowClick(Sender: TObject);
|
||||||
|
procedure BtnAutoClick(Sender: TObject);
|
||||||
|
procedure AutoReset; // сбросить накопление min-hold
|
||||||
|
procedure AutoService(NewFrame: Boolean); // одна итерация автопоиска
|
||||||
|
function AutoFindBeacon(out FreqHz: Double): Boolean;
|
||||||
procedure StyleTuneButtons;
|
procedure StyleTuneButtons;
|
||||||
protected
|
protected
|
||||||
procedure DoShow; override;
|
procedure DoShow; override;
|
||||||
@@ -311,7 +330,8 @@ begin
|
|||||||
|
|
||||||
FTuneHint := TLabel.Create(Self);
|
FTuneHint := TLabel.Create(Self);
|
||||||
FTuneHint.Parent := FTune;
|
FTuneHint.Parent := FTune;
|
||||||
FTuneHint.Caption := 'click the beacon to point the decoder · wheel zooms · drag pans';
|
FTuneHint.Caption :=
|
||||||
|
'click the beacon to point the decoder · AUTO finds it in this window';
|
||||||
FTuneHint.AutoSize := False;
|
FTuneHint.AutoSize := False;
|
||||||
FTuneHint.SetBounds(DpiScale(120), DpiScale(11), DpiScale(360), DpiScale(16));
|
FTuneHint.SetBounds(DpiScale(120), DpiScale(11), DpiScale(360), DpiScale(16));
|
||||||
FTuneHint.Font.Size := 8;
|
FTuneHint.Font.Size := 8;
|
||||||
@@ -319,6 +339,8 @@ begin
|
|||||||
|
|
||||||
FBtnFollow := MakeFlatBtn(FTune, 'FOLLOW', 0, 0,
|
FBtnFollow := MakeFlatBtn(FTune, 'FOLLOW', 0, 0,
|
||||||
DpiScale(66), DpiScale(22), @BtnFollowClick);
|
DpiScale(66), DpiScale(22), @BtnFollowClick);
|
||||||
|
FBtnAuto := MakeFlatBtn(FTune, 'AUTO', 0, 0,
|
||||||
|
DpiScale(52), DpiScale(22), @BtnAutoClick);
|
||||||
FBtnReset := MakeFlatBtn(FTune, 'RESET', 0, 0,
|
FBtnReset := MakeFlatBtn(FTune, 'RESET', 0, 0,
|
||||||
DpiScale(58), DpiScale(22), @BtnResetClick);
|
DpiScale(58), DpiScale(22), @BtnResetClick);
|
||||||
FBtnZoomOut := MakeFlatBtn(FTune, '−', 0, 0,
|
FBtnZoomOut := MakeFlatBtn(FTune, '−', 0, 0,
|
||||||
@@ -384,7 +406,9 @@ begin
|
|||||||
Style(FBtnZoomOut);
|
Style(FBtnZoomOut);
|
||||||
Style(FBtnReset);
|
Style(FBtnReset);
|
||||||
Style(FBtnFollow);
|
Style(FBtnFollow);
|
||||||
|
Style(FBtnAuto);
|
||||||
if FBtnFollow <> nil then FBtnFollow.Active := FFollow;
|
if FBtnFollow <> nil then FBtnFollow.Active := FFollow;
|
||||||
|
if FBtnAuto <> nil then FBtnAuto.Active := FAuto;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TBeaconScopeForm.LayoutTunePanel;
|
procedure TBeaconScopeForm.LayoutTunePanel;
|
||||||
@@ -399,6 +423,8 @@ begin
|
|||||||
Y := DpiScale(8);
|
Y := DpiScale(8);
|
||||||
X := W - Pad;
|
X := W - Pad;
|
||||||
Dec(X, FBtnFollow.Width); FBtnFollow.SetBounds(X, Y, FBtnFollow.Width, FBtnFollow.Height);
|
Dec(X, FBtnFollow.Width); FBtnFollow.SetBounds(X, Y, FBtnFollow.Width, FBtnFollow.Height);
|
||||||
|
Dec(X, DpiScale(6) + FBtnAuto.Width);
|
||||||
|
FBtnAuto.SetBounds(X, Y, FBtnAuto.Width, FBtnAuto.Height);
|
||||||
Dec(X, DpiScale(6) + FBtnReset.Width);
|
Dec(X, DpiScale(6) + FBtnReset.Width);
|
||||||
FBtnReset.SetBounds(X, Y, FBtnReset.Width, FBtnReset.Height);
|
FBtnReset.SetBounds(X, Y, FBtnReset.Width, FBtnReset.Height);
|
||||||
Dec(X, DpiScale(10) + FBtnZoomIn.Width);
|
Dec(X, DpiScale(10) + FBtnZoomIn.Width);
|
||||||
@@ -517,6 +543,150 @@ begin
|
|||||||
FCenterHz := EnsureRange(FCenterHz, CapLo + Half, CapHi - Half);
|
FCenterHz := EnsureRange(FCenterHz, CapLo + Half, CapHi - Half);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// Автопоиск маяка
|
||||||
|
// ===========================================================================
|
||||||
|
// Ищем в ТЕКУЩЕМ окне лупы — что видно, то и обыскивается. Это и предсказуемо,
|
||||||
|
// и даёт управление: зумом пользователь сам сужает область поиска.
|
||||||
|
|
||||||
|
procedure TBeaconScopeForm.AutoReset;
|
||||||
|
begin
|
||||||
|
FAutoFrames := 0;
|
||||||
|
FAutoCnt := 0;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TBeaconScopeForm.AutoFindBeacon(out FreqHz: Double): Boolean;
|
||||||
|
// По накопленному min-hold: шумовой пол = медиана, кандидат = максимум
|
||||||
|
// скользящего среднего шириной с полосу приёма декодера. Именно среднее по
|
||||||
|
// полосе, а не отдельный бин: маяк — плато ~900 Гц, и по нему он выигрывает у
|
||||||
|
// узкой пораженки/бирди с той же высотой пика.
|
||||||
|
var
|
||||||
|
N, i, Half, i1, i2, Best: Integer;
|
||||||
|
BinHz, FloorDb, Sum, BestVal, V, WSum, FSum, Wt: Double;
|
||||||
|
Sorted: array of Double;
|
||||||
|
TmpD: Double;
|
||||||
|
a, b: Integer;
|
||||||
|
begin
|
||||||
|
Result := False;
|
||||||
|
FreqHz := 0;
|
||||||
|
N := FAutoCnt;
|
||||||
|
if (N < 16) or (FAutoFrames < BCN_AUTO_FRAMES) then Exit;
|
||||||
|
BinHz := (FAutoHi - FAutoLo) / (N - 1);
|
||||||
|
if BinHz <= 0 then Exit;
|
||||||
|
|
||||||
|
// Шумовой пол — медиана по прореженной выборке (как в шкале дБ кадра).
|
||||||
|
SetLength(Sorted, (N + 7) div 8);
|
||||||
|
for i := 0 to High(Sorted) do
|
||||||
|
Sorted[i] := FAutoMin[Min(i * 8, N - 1)];
|
||||||
|
for a := 1 to High(Sorted) do
|
||||||
|
begin
|
||||||
|
TmpD := Sorted[a];
|
||||||
|
b := a - 1;
|
||||||
|
while (b >= 0) and (Sorted[b] > TmpD) do
|
||||||
|
begin
|
||||||
|
Sorted[b + 1] := Sorted[b];
|
||||||
|
Dec(b);
|
||||||
|
end;
|
||||||
|
Sorted[b + 1] := TmpD;
|
||||||
|
end;
|
||||||
|
FloorDb := Sorted[Length(Sorted) div 2];
|
||||||
|
|
||||||
|
Half := Max(1, Round(BCN_DEC_BW_HZ / BinHz));
|
||||||
|
if 2 * Half + 1 >= N then Exit; // окно уже полосы приёма — не ищем
|
||||||
|
|
||||||
|
// Скользящее среднее одним проходом: сумма правится краями окна.
|
||||||
|
Sum := 0;
|
||||||
|
for i := 0 to 2 * Half do Sum := Sum + FAutoMin[i];
|
||||||
|
Best := Half;
|
||||||
|
BestVal := Sum / (2 * Half + 1);
|
||||||
|
for i := Half + 1 to N - Half - 1 do
|
||||||
|
begin
|
||||||
|
Sum := Sum - FAutoMin[i - Half - 1] + FAutoMin[i + Half];
|
||||||
|
V := Sum / (2 * Half + 1);
|
||||||
|
if V > BestVal then
|
||||||
|
begin
|
||||||
|
BestVal := V;
|
||||||
|
Best := i;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
if BestVal - FloorDb < BCN_AUTO_MIN_DB then Exit; // ничего похожего на маяк
|
||||||
|
|
||||||
|
// Уточнение центра: центроид превышения над полом внутри полосы. Декодер
|
||||||
|
// дотянет и с ошибкой в сотню герц (у него свой acquisition), но так наведение
|
||||||
|
// сразу садится на середину плато.
|
||||||
|
i1 := Max(0, Best - Half);
|
||||||
|
i2 := Min(N - 1, Best + Half);
|
||||||
|
WSum := 0;
|
||||||
|
FSum := 0;
|
||||||
|
for i := i1 to i2 do
|
||||||
|
begin
|
||||||
|
Wt := FAutoMin[i] - FloorDb;
|
||||||
|
if Wt <= 0 then Continue;
|
||||||
|
WSum := WSum + Wt;
|
||||||
|
FSum := FSum + Wt * i;
|
||||||
|
end;
|
||||||
|
if WSum <= 0 then Exit;
|
||||||
|
FreqHz := FAutoLo + (FSum / WSum) * BinHz;
|
||||||
|
Result := True;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TBeaconScopeForm.AutoService(NewFrame: Boolean);
|
||||||
|
// Раз в тик, при взведённой AUTO. Пока декодер солидно залочен — не мешаем
|
||||||
|
// контуру; потерял лок (или его и не было) — копим min-hold и наводимся сами.
|
||||||
|
// В накопление идут только СВЕЖИЕ кадры: таймер формы быстрее анализатора, и
|
||||||
|
// повторный учёт одного и того же кадра сокращал бы окно наблюдения, ради
|
||||||
|
// которого min-hold и заведён.
|
||||||
|
var
|
||||||
|
i: Integer;
|
||||||
|
F: Double;
|
||||||
|
begin
|
||||||
|
if not FAuto then Exit;
|
||||||
|
if FCtl = nil then Exit;
|
||||||
|
|
||||||
|
if FState = bsLock then
|
||||||
|
begin
|
||||||
|
AutoReset; // лок держится — искать нечего
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
if FAutoCooldown > 0 then
|
||||||
|
begin
|
||||||
|
Dec(FAutoCooldown); // ждём, пока декодер попробует захватиться
|
||||||
|
AutoReset;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
if (not NewFrame) or (FPixCount < 16) then Exit;
|
||||||
|
|
||||||
|
// Окно переехало (зум/пан/смена rate) — накопленное больше не про эти бины.
|
||||||
|
if (FPixCount <> FAutoCnt) or (Abs(FViewLo - FAutoLo) > 0.5)
|
||||||
|
or (Abs(FViewHi - FAutoHi) > 0.5) then
|
||||||
|
begin
|
||||||
|
FAutoCnt := FPixCount;
|
||||||
|
FAutoLo := FViewLo;
|
||||||
|
FAutoHi := FViewHi;
|
||||||
|
FAutoFrames := 0;
|
||||||
|
end;
|
||||||
|
|
||||||
|
if FAutoFrames = 0 then
|
||||||
|
for i := 0 to FAutoCnt - 1 do FAutoMin[i] := FPix[i]
|
||||||
|
else
|
||||||
|
for i := 0 to FAutoCnt - 1 do
|
||||||
|
if FPix[i] < FAutoMin[i] then FAutoMin[i] := FPix[i];
|
||||||
|
Inc(FAutoFrames);
|
||||||
|
if FAutoFrames < BCN_AUTO_FRAMES then Exit;
|
||||||
|
|
||||||
|
// Наводимся, только если нашли ДРУГОЕ место. Если декодер и так смотрит сюда,
|
||||||
|
// а лока нет (слабый сигнал) — пусть работает: BeaconSeedAtHz дёргает Reseed,
|
||||||
|
// и повторные наведения на ту же частоту сбрасывали бы захват по кругу.
|
||||||
|
if AutoFindBeacon(F) then
|
||||||
|
if (FCtl.BeaconDecodeFreqHz <= 0)
|
||||||
|
or (Abs(F - FCtl.BeaconDecodeFreqHz) > BCN_AUTO_MIN_MOVE) then
|
||||||
|
begin
|
||||||
|
FCtl.BeaconSeedAtHz(F); // тот же путь, что клик мышью
|
||||||
|
FAutoCooldown := BCN_AUTO_COOLDOWN;
|
||||||
|
end;
|
||||||
|
AutoReset; // не нашли (или не двигались) — копим заново
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TBeaconScopeForm.RestoreZoomState;
|
procedure TBeaconScopeForm.RestoreZoomState;
|
||||||
// Окно лупы переживает перезапуск: ширина, положение и FOLLOW лежат в
|
// Окно лупы переживает перезапуск: ширина, положение и FOLLOW лежат в
|
||||||
// глобальных настройках (персист — общий SaveGlobalSettings при выходе).
|
// глобальных настройках (персист — общий SaveGlobalSettings при выходе).
|
||||||
@@ -526,11 +696,14 @@ begin
|
|||||||
if FCtl = nil then Exit;
|
if FCtl = nil then Exit;
|
||||||
FSpanHz := EnsureRange(FCtl.FBcnZoomSpanHz, BCN_SPAN_MIN, BCN_SPAN_MAX);
|
FSpanHz := EnsureRange(FCtl.FBcnZoomSpanHz, BCN_SPAN_MIN, BCN_SPAN_MAX);
|
||||||
FFollow := FCtl.FBcnZoomFollow;
|
FFollow := FCtl.FBcnZoomFollow;
|
||||||
|
FAuto := FCtl.FBcnZoomAuto;
|
||||||
if FCtl.BeaconRefHz > 0 then
|
if FCtl.BeaconRefHz > 0 then
|
||||||
FCenterHz := FCtl.BeaconRefHz + FCtl.FBcnZoomOffsetHz
|
FCenterHz := FCtl.BeaconRefHz + FCtl.FBcnZoomOffsetHz
|
||||||
else
|
else
|
||||||
FCenterHz := DefaultCenterHz;
|
FCenterHz := DefaultCenterHz;
|
||||||
if FBtnFollow <> nil then FBtnFollow.Active := FFollow;
|
if FBtnFollow <> nil then FBtnFollow.Active := FFollow;
|
||||||
|
if FBtnAuto <> nil then FBtnAuto.Active := FAuto;
|
||||||
|
AutoReset;
|
||||||
ClampWindow;
|
ClampWindow;
|
||||||
// Наведение декодера могло остаться за краем восстановленного окна — его
|
// Наведение декодера могло остаться за краем восстановленного окна — его
|
||||||
// подтянет FOLLOW на первом же тике (PollZoom), отдельной логики не нужно.
|
// подтянет FOLLOW на первом же тике (PollZoom), отдельной логики не нужно.
|
||||||
@@ -545,6 +718,7 @@ begin
|
|||||||
if FCtl = nil then Exit;
|
if FCtl = nil then Exit;
|
||||||
FCtl.FBcnZoomSpanHz := FSpanHz;
|
FCtl.FBcnZoomSpanHz := FSpanHz;
|
||||||
FCtl.FBcnZoomFollow := FFollow;
|
FCtl.FBcnZoomFollow := FFollow;
|
||||||
|
FCtl.FBcnZoomAuto := FAuto;
|
||||||
if FCtl.BeaconRefHz > 0 then
|
if FCtl.BeaconRefHz > 0 then
|
||||||
FCtl.FBcnZoomOffsetHz := FCenterHz - FCtl.BeaconRefHz;
|
FCtl.FBcnZoomOffsetHz := FCenterHz - FCtl.BeaconRefHz;
|
||||||
end;
|
end;
|
||||||
@@ -555,7 +729,7 @@ procedure TBeaconScopeForm.PollZoom;
|
|||||||
var
|
var
|
||||||
Cnt: Integer;
|
Cnt: Integer;
|
||||||
Lo, Hi, Dec_: Double;
|
Lo, Hi, Dec_: Double;
|
||||||
GotWf: Boolean;
|
GotWf, Fresh: Boolean;
|
||||||
begin
|
begin
|
||||||
if FCtl = nil then Exit;
|
if FCtl = nil then Exit;
|
||||||
|
|
||||||
@@ -583,8 +757,8 @@ begin
|
|||||||
// Возврат GetBeaconZoomSpectrum = «в кэше был НЕпрочитанный кадр»; это и есть
|
// Возврат GetBeaconZoomSpectrum = «в кэше был НЕпрочитанный кадр»; это и есть
|
||||||
// главный триггер перерисовки трассы. Границы окна тут не помощник: пока
|
// главный триггер перерисовки трассы. Границы окна тут не помощник: пока
|
||||||
// никто не панорамирует, они не меняются, а пиксели — каждый кадр.
|
// никто не панорамирует, они не меняются, а пиксели — каждый кадр.
|
||||||
if FCtl.GetBeaconZoomSpectrum(FPix, Cnt, Lo, Hi) then
|
Fresh := FCtl.GetBeaconZoomSpectrum(FPix, Cnt, Lo, Hi);
|
||||||
FZoomDirty := True;
|
if Fresh then FZoomDirty := True;
|
||||||
if Cnt > 0 then
|
if Cnt > 0 then
|
||||||
begin
|
begin
|
||||||
if (Cnt <> FPixCount) or (Abs(Lo - FViewLo) > 0.01)
|
if (Cnt <> FPixCount) or (Abs(Lo - FViewLo) > 0.01)
|
||||||
@@ -613,6 +787,8 @@ begin
|
|||||||
FLastTrkHz := FCtl.BeaconTrackedFreqHz;
|
FLastTrkHz := FCtl.BeaconTrackedFreqHz;
|
||||||
FLastDecHz := FCtl.BeaconDecodeFreqHz;
|
FLastDecHz := FCtl.BeaconDecodeFreqHz;
|
||||||
|
|
||||||
|
AutoService(Fresh);
|
||||||
|
|
||||||
GotWf := FCtl.GetBeaconZoomWaterfall(FWfPix, Cnt);
|
GotWf := FCtl.GetBeaconZoomWaterfall(FWfPix, Cnt);
|
||||||
if GotWf and (Cnt > 0) and (FWf <> nil) then
|
if GotWf and (Cnt > 0) and (FWf <> nil) then
|
||||||
begin
|
begin
|
||||||
@@ -806,6 +982,15 @@ begin
|
|||||||
if FBtnFollow <> nil then FBtnFollow.Active := FFollow;
|
if FBtnFollow <> nil then FBtnFollow.Active := FFollow;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TBeaconScopeForm.BtnAutoClick(Sender: TObject);
|
||||||
|
begin
|
||||||
|
FAuto := not FAuto;
|
||||||
|
if FBtnAuto <> nil then FBtnAuto.Active := FAuto;
|
||||||
|
AutoReset;
|
||||||
|
FAutoCooldown := 0; // включили руками — ищем сразу, без выдержки
|
||||||
|
FZoomDirty := True; // в строке захвата появляется/пропадает пометка AUTO
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TBeaconScopeForm.DoShow;
|
procedure TBeaconScopeForm.DoShow;
|
||||||
begin
|
begin
|
||||||
inherited DoShow;
|
inherited DoShow;
|
||||||
@@ -1146,6 +1331,10 @@ begin
|
|||||||
end;
|
end;
|
||||||
if (FState <> bsIdle) and (FSNRdB > -50) then
|
if (FState <> bsIdle) and (FSNRdB > -50) then
|
||||||
Txt := Txt + Format(' · %.1f dB', [FSNRdB]);
|
Txt := Txt + Format(' · %.1f dB', [FSNRdB]);
|
||||||
|
// Автопоиск помечаем только пока он реально работает: на локе он молчит, и
|
||||||
|
// светить «AUTO» там значило бы обещать действие, которого не происходит.
|
||||||
|
if FAuto and (FState <> bsLock) then
|
||||||
|
Txt := Txt + ' · AUTO';
|
||||||
C.TextOut(DpiScale(6), DpiScale(21), Txt);
|
C.TextOut(DpiScale(6), DpiScale(21), Txt);
|
||||||
a := DpiScale(6) + C.TextWidth(Txt) + DpiScale(10);
|
a := DpiScale(6) + C.TextWidth(Txt) + DpiScale(10);
|
||||||
|
|
||||||
|
|||||||
@@ -339,6 +339,7 @@ type
|
|||||||
FBcnZoomSpanHz: Double; // ширина окна лупы, Гц
|
FBcnZoomSpanHz: Double; // ширина окна лупы, Гц
|
||||||
FBcnZoomOffsetHz: Double; // центр = FBeaconRefHz + этот сдвиг
|
FBcnZoomOffsetHz: Double; // центр = FBeaconRefHz + этот сдвиг
|
||||||
FBcnZoomFollow: Boolean; // держать наведение декодера в окне
|
FBcnZoomFollow: Boolean; // держать наведение декодера в окне
|
||||||
|
FBcnZoomAuto: Boolean; // автопоиск маяка в окне лупы
|
||||||
|
|
||||||
// ---- DMR 4FSK receive decoder ----
|
// ---- DMR 4FSK receive decoder ----
|
||||||
FDMRDec: TDMRDecoder; // 48 kHz FM discriminator -> DMR worker
|
FDMRDec: TDMRDecoder; // 48 kHz FM discriminator -> DMR worker
|
||||||
@@ -1327,6 +1328,7 @@ begin
|
|||||||
FBcnZoomSpanHz := 40000.0;
|
FBcnZoomSpanHz := 40000.0;
|
||||||
FBcnZoomOffsetHz := 0.0;
|
FBcnZoomOffsetHz := 0.0;
|
||||||
FBcnZoomFollow := True;
|
FBcnZoomFollow := True;
|
||||||
|
FBcnZoomAuto := False;
|
||||||
FBeaconDec.SetInvert(FBeaconDecInvert);
|
FBeaconDec.SetInvert(FBeaconDecInvert);
|
||||||
FDSPEngine.SetBeaconDecoder(FBeaconDec);
|
FDSPEngine.SetBeaconDecoder(FBeaconDec);
|
||||||
|
|
||||||
@@ -3959,6 +3961,7 @@ begin
|
|||||||
Result.BeaconZoomSpanHz := FBcnZoomSpanHz;
|
Result.BeaconZoomSpanHz := FBcnZoomSpanHz;
|
||||||
Result.BeaconZoomOffsetHz := FBcnZoomOffsetHz;
|
Result.BeaconZoomOffsetHz := FBcnZoomOffsetHz;
|
||||||
Result.BeaconZoomFollow := FBcnZoomFollow;
|
Result.BeaconZoomFollow := FBcnZoomFollow;
|
||||||
|
Result.BeaconZoomAuto := FBcnZoomAuto;
|
||||||
Result.AudioSampleRate := FAudioOut.SampleRate;
|
Result.AudioSampleRate := FAudioOut.SampleRate;
|
||||||
Result.ShowSpectrum := FShowSpectrum;
|
Result.ShowSpectrum := FShowSpectrum;
|
||||||
Result.ShowWaterfall := FShowWaterfall;
|
Result.ShowWaterfall := FShowWaterfall;
|
||||||
@@ -7355,6 +7358,7 @@ begin
|
|||||||
FBcnZoomSpanHz := EnsureRange(FLoadedGlobal.BeaconZoomSpanHz, 600.0, 200000.0);
|
FBcnZoomSpanHz := EnsureRange(FLoadedGlobal.BeaconZoomSpanHz, 600.0, 200000.0);
|
||||||
FBcnZoomOffsetHz := EnsureRange(FLoadedGlobal.BeaconZoomOffsetHz, -500000.0, 500000.0);
|
FBcnZoomOffsetHz := EnsureRange(FLoadedGlobal.BeaconZoomOffsetHz, -500000.0, 500000.0);
|
||||||
FBcnZoomFollow := FLoadedGlobal.BeaconZoomFollow;
|
FBcnZoomFollow := FLoadedGlobal.BeaconZoomFollow;
|
||||||
|
FBcnZoomAuto := FLoadedGlobal.BeaconZoomAuto;
|
||||||
if FWDSPReady then FDSPEngine.SetDisplayResLimit(FDisplayPixels);
|
if FWDSPReady then FDSPEngine.SetDisplayResLimit(FDisplayPixels);
|
||||||
FShowSpectrum := FLoadedGlobal.ShowSpectrum;
|
FShowSpectrum := FLoadedGlobal.ShowSpectrum;
|
||||||
FShowWaterfall := FLoadedGlobal.ShowWaterfall;
|
FShowWaterfall := FLoadedGlobal.ShowWaterfall;
|
||||||
|
|||||||
@@ -620,6 +620,7 @@ type
|
|||||||
BeaconZoomSpanHz: Double; // ширина окна лупы, Гц
|
BeaconZoomSpanHz: Double; // ширина окна лупы, Гц
|
||||||
BeaconZoomOffsetHz: Double; // центр окна = BeaconRefHz + этот сдвиг
|
BeaconZoomOffsetHz: Double; // центр окна = BeaconRefHz + этот сдвиг
|
||||||
BeaconZoomFollow: Boolean; // держать наведение декодера в окне
|
BeaconZoomFollow: Boolean; // держать наведение декодера в окне
|
||||||
|
BeaconZoomAuto: Boolean; // автопоиск маяка в окне лупы
|
||||||
// --- Audio settings ---
|
// --- Audio settings ---
|
||||||
AudioOutDevice: string; // PortAudio output device name ('' = default)
|
AudioOutDevice: string; // PortAudio output device name ('' = default)
|
||||||
AudioSampleRate: Integer; // PortAudio stream sample rate (e.g. 48000)
|
AudioSampleRate: Integer; // PortAudio stream sample rate (e.g. 48000)
|
||||||
@@ -988,6 +989,7 @@ begin
|
|||||||
G.BeaconZoomSpanHz := 40000.0;
|
G.BeaconZoomSpanHz := 40000.0;
|
||||||
G.BeaconZoomOffsetHz := 0.0;
|
G.BeaconZoomOffsetHz := 0.0;
|
||||||
G.BeaconZoomFollow := True;
|
G.BeaconZoomFollow := True;
|
||||||
|
G.BeaconZoomAuto := False;
|
||||||
G.AudioOutDevice := '';
|
G.AudioOutDevice := '';
|
||||||
G.AudioSampleRate := 48000;
|
G.AudioSampleRate := 48000;
|
||||||
G.AudioInDevice := '';
|
G.AudioInDevice := '';
|
||||||
@@ -1241,6 +1243,7 @@ begin
|
|||||||
G.BeaconZoomSpanHz := JD(GObj,'beacon_zoom_span',40000.0);
|
G.BeaconZoomSpanHz := JD(GObj,'beacon_zoom_span',40000.0);
|
||||||
G.BeaconZoomOffsetHz := JD(GObj,'beacon_zoom_offset',0.0);
|
G.BeaconZoomOffsetHz := JD(GObj,'beacon_zoom_offset',0.0);
|
||||||
G.BeaconZoomFollow := JB(GObj,'beacon_zoom_follow',True);
|
G.BeaconZoomFollow := JB(GObj,'beacon_zoom_follow',True);
|
||||||
|
G.BeaconZoomAuto := JB(GObj,'beacon_zoom_auto',False);
|
||||||
// Audio settings
|
// Audio settings
|
||||||
G.AudioOutDevice := JS(GObj,'audio_out_device','');
|
G.AudioOutDevice := JS(GObj,'audio_out_device','');
|
||||||
G.AudioSampleRate := JI(GObj,'audio_sample_rate',48000);
|
G.AudioSampleRate := JI(GObj,'audio_sample_rate',48000);
|
||||||
@@ -1391,6 +1394,7 @@ begin
|
|||||||
JW(O,'beacon_zoom_span',G.BeaconZoomSpanHz);
|
JW(O,'beacon_zoom_span',G.BeaconZoomSpanHz);
|
||||||
JW(O,'beacon_zoom_offset',G.BeaconZoomOffsetHz);
|
JW(O,'beacon_zoom_offset',G.BeaconZoomOffsetHz);
|
||||||
JW(O,'beacon_zoom_follow',G.BeaconZoomFollow);
|
JW(O,'beacon_zoom_follow',G.BeaconZoomFollow);
|
||||||
|
JW(O,'beacon_zoom_auto',G.BeaconZoomAuto);
|
||||||
// Audio settings
|
// Audio settings
|
||||||
JWS(O,'audio_out_device',G.AudioOutDevice);
|
JWS(O,'audio_out_device',G.AudioOutDevice);
|
||||||
JW(O,'audio_sample_rate',G.AudioSampleRate);
|
JW(O,'audio_sample_rate',G.AudioSampleRate);
|
||||||
|
|||||||
Reference in New Issue
Block a user