feat(dmr): show stable status on receiver slices

This commit is contained in:
2026-07-14 21:23:24 +03:00
parent 8b28986fec
commit 7c4b024186
3 changed files with 140 additions and 3 deletions
+41 -1
View File
@@ -34,7 +34,7 @@ uses
Classes, SysUtils, Math, Controls, ExtCtrls, StdCtrls, Graphics, Forms, Classes, SysUtils, Math, Controls, ExtCtrls, StdCtrls, Graphics, Forms,
LCLIntf, LCLType, LCLIntf, LCLType,
OpenGLContextEx, OpenGLContextEx,
AppTheme, WDSPEngine, RadioController, VfoOverlay, AppTheme, WDSPEngine, RadioController, DMRDecoder, VfoOverlay,
SpectrumView, SpectrumViewOpengl, PanZoomBar, FlatButton; SpectrumView, SpectrumViewOpengl, PanZoomBar, FlatButton;
type type
@@ -85,6 +85,7 @@ type
FPendingSliceClose: Integer; // SliceId к удалению после мыши (0=нет) FPendingSliceClose: Integer; // SliceId к удалению после мыши (0=нет)
FMainFlagPinned: Boolean; // True = левая панель хозяина скрыта FMainFlagPinned: Boolean; // True = левая панель хозяина скрыта
FLastSliceMeterMs: QWord; // троттлинг S-метра слайсов (~10 Гц) FLastSliceMeterMs: QWord; // троттлинг S-метра слайсов (~10 Гц)
FLastSliceDMRMs: QWord; // семантический DMR-статус (~4 Гц)
FOnWireFlag: TWireFlagEvent; FOnWireFlag: TWireFlagEvent;
FOnSliceClosed: TSliceClosedEvent; FOnSliceClosed: TSliceClosedEvent;
FOnFlagsInvalidate: TNotifyEvent; FOnFlagsInvalidate: TNotifyEvent;
@@ -282,6 +283,7 @@ type
procedure PushAllSliceFlagStates; // все флаги панели (после смены rate и т.п.) procedure PushAllSliceFlagStates; // все флаги панели (после смены rate и т.п.)
procedure PushMainFlagExtState; procedure PushMainFlagExtState;
procedure PushMainFlagAudioDevices; procedure PushMainFlagAudioDevices;
procedure UpdateSliceDMRFlags;
// Тик: S-метр/частота главного флага + позиции + троттленный S-метр слайсов. // Тик: S-метр/частота главного флага + позиции + троттленный S-метр слайсов.
procedure TickFlags; procedure TickFlags;
procedure SetFlagsTheme(const T: TAppTheme); procedure SetFlagsTheme(const T: TAppTheme);
@@ -1375,6 +1377,36 @@ begin
end; end;
end; end;
procedure TPanafallPanel.UpdateSliceDMRFlags;
var
I, Slot: Integer;
O: TVfoOverlay;
S: TDMRStatus;
Txt: string;
begin
if FController = nil then Exit;
for I := 0 to High(FSliceFlags) do
begin
O := FSliceFlags[I];
if (O = nil) or (O.DisplayMode <> MODE_DMR) then Continue;
if not FController.GetSliceDMRStatus(O.SliceId, S) or not S.Synced then
begin
O.UpdateDMRStatus('DMR...', False);
Continue;
end;
Txt := 'DMR SYNC';
Slot := -1;
if S.LinkControlValid then Slot := S.LCSlot
else if S.CACHValid then Slot := S.Slot;
if Slot in [0, 1] then Txt := 'DMR TS' + IntToStr(Slot + 1);
if S.SlotTypeValid then Txt := Txt + ' CC' + IntToStr(S.ColorCode);
if S.LinkControlValid and (S.TargetID <> 0) then
Txt := Txt + ' TG' + IntToStr(S.TargetID);
O.UpdateDMRStatus(Txt, True);
end;
end;
// Тик (~частота FSpectrumTimer хозяина): метр/частота главного флага, позиции // Тик (~частота FSpectrumTimer хозяина): метр/частота главного флага, позиции
// всех флагов, троттленный S-метр слайсов. // всех флагов, троттленный S-метр слайсов.
procedure TPanafallPanel.TickFlags; procedure TPanafallPanel.TickFlags;
@@ -1400,6 +1432,14 @@ begin
if FSliceFlags[I] <> nil then if FSliceFlags[I] <> nil then
FSliceFlags[I].UpdateSMeter(FController.SliceSMeter(FSliceFlags[I].SliceId)); FSliceFlags[I].UpdateSMeter(FController.SliceSMeter(FSliceFlags[I].SliceId));
end; end;
// Статус DMR содержит только устойчивые параметры и обновляется отдельно,
// заметно реже метра. Сам оверлей инвалидируется лишь после debounce и
// только если итоговая надпись действительно изменилась.
if GetTickCount64 - FLastSliceDMRMs >= 250 then
begin
FLastSliceDMRMs := GetTickCount64;
UpdateSliceDMRFlags;
end;
end; end;
end; end;
+20
View File
@@ -141,6 +141,7 @@ type
constructor Create(AOwner: TRadioController; ASliceId: Integer); constructor Create(AOwner: TRadioController; ASliceId: Integer);
destructor Destroy; override; destructor Destroy; override;
procedure FeedAudio(const Left, Right: array of Single; Count: Integer); procedure FeedAudio(const Left, Right: array of Single; Count: Integer);
procedure GetStatus(out S: TDMRStatus);
end; end;
// Слайс уровня контроллера: логическая обёртка над WDSP-слайс-каналом // Слайс уровня контроллера: логическая обёртка над WDSP-слайс-каналом
@@ -697,6 +698,7 @@ type
function GetBeaconScope(out S: TBeaconScope): Boolean; function GetBeaconScope(out S: TBeaconScope): Boolean;
function GetBeaconFrame(out Frame: TBeaconFrame; out RSErrors: Integer): Boolean; function GetBeaconFrame(out Frame: TBeaconFrame; out RSErrors: Integer): Boolean;
function GetDMRStatus(out S: TDMRStatus): Boolean; function GetDMRStatus(out S: TDMRStatus): Boolean;
function GetSliceDMRStatus(SliceId: Integer; out S: TDMRStatus): Boolean;
// Отображение / приём // Отображение / приём
procedure SetCTun(On_: Boolean); procedure SetCTun(On_: Boolean);
@@ -839,6 +841,12 @@ begin
if Assigned(FDecoder) then FDecoder.FeedAudio(Left, Right, Count); if Assigned(FDecoder) then FDecoder.FeedAudio(Left, Right, Count);
end; end;
procedure TDMRSliceDecoder.GetStatus(out S: TDMRStatus);
begin
if Assigned(FDecoder) then FDecoder.GetStatus(S)
else FillChar(S, SizeOf(S), 0);
end;
procedure TDMRSliceDecoder.AudioReady(const PCM: array of Single; procedure TDMRSliceDecoder.AudioReady(const PCM: array of Single;
Count: Integer); Count: Integer);
begin begin
@@ -2380,6 +2388,18 @@ begin
else FillChar(S, SizeOf(S), 0); else FillChar(S, SizeOf(S), 0);
end; end;
function TRadioController.GetSliceDMRStatus(SliceId: Integer;
out S: TDMRStatus): Boolean;
var
Idx: Integer;
begin
Idx := FindSliceIndex(SliceId);
Result := (Idx >= 0) and (FSlices[Idx].Mode = MODE_DMR) and
Assigned(FSlices[Idx].DMR);
if Result then FSlices[Idx].DMR.GetStatus(S)
else FillChar(S, SizeOf(S), 0);
end;
// Кромки полосы под режим/полосу (единый расчёт для главного канала и слайсов). // Кромки полосы под режим/полосу (единый расчёт для главного канала и слайсов).
class procedure TRadioController.FilterEdgesFor(Mode, BW: Integer; out Lo, Hi: Integer); class procedure TRadioController.FilterEdgesFor(Mode, BW: Integer; out Lo, Hi: Integer);
var Half: Integer; var Half: Integer;
+79 -2
View File
@@ -79,6 +79,11 @@ type
FTxSel: Boolean; // этот слайс выбран для передачи (янтарный) FTxSel: Boolean; // этот слайс выбран для передачи (янтарный)
FSlice: Char; // буква слайса ('A') FSlice: Char; // буква слайса ('A')
FSMeterThreshold: Double; // мин. изменение S-метра (дБ) для перерисовки FSMeterThreshold: Double; // мин. изменение S-метра (дБ) для перерисовки
FDMRText: string; // стабильный краткий статус рядом с полосой
FDMRSynced: Boolean;
FDMRCandidateText: string; // debounce: не мигать на кратком sync-loss
FDMRCandidateSynced: Boolean;
FDMRCandidateSince: QWord;
FDevNames: TStringList; // имена устройств вывода FDevNames: TStringList; // имена устройств вывода
FDevIdx: array of Integer; // соответствующие PortAudio-индексы (-1=default) FDevIdx: array of Integer; // соответствующие PortAudio-индексы (-1=default)
@@ -182,6 +187,7 @@ type
const CurName: string); const CurName: string);
procedure UpdateSMeter(DB: Double); procedure UpdateSMeter(DB: Double);
procedure UpdateVfo(AVfoHz: Double); procedure UpdateVfo(AVfoHz: Double);
procedure UpdateDMRStatus(const AText: string; Synced: Boolean);
// Тема приложения (цвета кнопок берутся отсюда — как левая панель). // Тема приложения (цвета кнопок берутся отсюда — как левая панель).
procedure SetTheme(const T: TAppTheme); procedure SetTheme(const T: TAppTheme);
// Привязка к слайсу: id, буква флага, признак главного (слайс A). // Привязка к слайсу: id, буква флага, признак главного (слайс A).
@@ -450,6 +456,9 @@ begin
FSQLOn := False; FSQLLevel := 40; FSqlDrag := False; FSQLOn := False; FSQLLevel := 40; FSqlDrag := False;
FVolume := 70; FMuted := False; FSplit := False; FTx := False; FVolume := 70; FMuted := False; FSplit := False; FTx := False;
FTxSel := True; FSlice := 'A'; FTxSel := True; FSlice := 'A';
FDMRText := ''; FDMRSynced := False;
FDMRCandidateText := ''; FDMRCandidateSynced := False;
FDMRCandidateSince := 0;
FSliceId := 0; FIsMain := True; // по умолчанию — главный приёмник (слайс A) FSliceId := 0; FIsMain := True; // по умолчанию — главный приёмник (слайс A)
FSMeterThreshold := 0.4; // главный флаг — чувствительный метр FSMeterThreshold := 0.4; // главный флаг — чувствительный метр
FPanel := fpNone; FPanel := fpNone;
@@ -977,8 +986,8 @@ end;
procedure TVfoOverlay.DrawSelf(C: TCanvas; W, H: Integer); procedure TVfoOverlay.DrawSelf(C: TCanvas; W, H: Integer);
var var
FreqStr, SStr, BWStr: string; FreqStr, SStr, BWStr, DMRStr: string;
CurY, X, RW, i: Integer; CurY, X, RW, i, DMRX, DMRRight: Integer;
R: TRect; R: TRect;
BarL, BarW, MeterX: Integer; BarL, BarW, MeterX: Integer;
begin begin
@@ -1013,6 +1022,21 @@ begin
C.Font.Name := 'Courier New'; C.Font.Size := 8; C.Font.Style := []; C.Font.Name := 'Courier New'; C.Font.Size := 8; C.Font.Style := [];
C.Font.Color := CLR_DIM; C.Brush.Style := bsClear; C.Font.Color := CLR_DIM; C.Brush.Style := bsClear;
C.TextOut(X + 22, CurY + 1, BWStr); C.TextOut(X + 22, CurY + 1, BWStr);
// Краткий статус дополнительного DMR-слайса. Он обновляется только при
// устойчивом изменении sync/slot/CC/TG, поэтому не заставляет GL-текстуру
// перезаливаться на каждом burst.
if (not FIsMain) and (FMode = 11) and (FDMRText <> '') then
begin
DMRStr := FDMRText;
DMRX := X + 22 + C.TextWidth(BWStr) + 8;
DMRRight := W - PAD - 26 - 6 - 14 - 5; // до кнопок × и TX
while (Length(DMRStr) > 1) and
(C.TextWidth(DMRStr) > DMRRight - DMRX) do
SetLength(DMRStr, Length(DMRStr) - 1);
if FDMRSynced then C.Font.Color := CLR_ACCENT
else C.Font.Color := CLR_SVAL;
C.TextOut(DMRX, CurY + 1, DMRStr);
end;
// TX badge: a DMR slice is visibly disabled and has no hit target. // TX badge: a DMR slice is visibly disabled and has no hit target.
R := Rect(W - PAD - 26, CurY, W - PAD, CurY + HDR_BADGE_H); R := Rect(W - PAD - 26, CurY, W - PAD, CurY + HDR_BADGE_H);
if not TxSelectionAllowed then if not TxSelectionAllowed then
@@ -1405,12 +1429,29 @@ end;
{ ---- Обновление состояния из контроллера ---- } { ---- Обновление состояния из контроллера ---- }
procedure TVfoOverlay.SetState(AMode, ABW: Integer; AVfoHz, ASMeterDB: Double); procedure TVfoOverlay.SetState(AMode, ABW: Integer; AVfoHz, ASMeterDB: Double);
var
OldMode: Integer;
begin begin
OldMode := FMode;
FMode := Max(0, Min(OVL_MODE_COUNT - 1, AMode)); FMode := Max(0, Min(OVL_MODE_COUNT - 1, AMode));
FFilterBW := ABW; FFilterBW := ABW;
FModeFilter[FMode] := ABW; FModeFilter[FMode] := ABW;
FVfoHz := AVfoHz; FVfoHz := AVfoHz;
FSMeterDB := ASMeterDB; FSMeterDB := ASMeterDB;
if FMode <> 11 then
begin
FDMRText := '';
FDMRSynced := False;
FDMRCandidateText := '';
FDMRCandidateSince := 0;
end
else if OldMode <> 11 then
begin
FDMRText := 'DMR...';
FDMRSynced := False;
FDMRCandidateText := '';
FDMRCandidateSince := 0;
end;
Height := ComputeHeight; Height := ComputeHeight;
RequestInvalidate; RequestInvalidate;
end; end;
@@ -1487,4 +1528,40 @@ begin
end; end;
end; end;
procedure TVfoOverlay.UpdateDMRStatus(const AText: string; Synced: Boolean);
var
NowTick, HoldMs: QWord;
begin
if FMode <> 11 then Exit;
if (FDMRText = AText) and (FDMRSynced = Synced) then
begin
FDMRCandidateText := '';
FDMRCandidateSince := 0;
Exit;
end;
NowTick := GetTickCount64;
if (FDMRCandidateText <> AText) or
(FDMRCandidateSynced <> Synced) then
begin
FDMRCandidateText := AText;
FDMRCandidateSynced := Synced;
FDMRCandidateSince := NowTick;
Exit;
end;
// Потерю уже показанной синхронизации держим дольше, чем появление/смену
// параметров вызова: короткие замирания не должны мигать в шапке слайса.
if FDMRSynced and not Synced then HoldMs := 1200
else if FDMRSynced and Synced then HoldMs := 750
else HoldMs := 250;
if NowTick - FDMRCandidateSince < HoldMs then Exit;
FDMRText := AText;
FDMRSynced := Synced;
FDMRCandidateText := '';
FDMRCandidateSince := 0;
RequestInvalidate;
end;
end. end.