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,
LCLIntf, LCLType,
OpenGLContextEx,
AppTheme, WDSPEngine, RadioController, VfoOverlay,
AppTheme, WDSPEngine, RadioController, DMRDecoder, VfoOverlay,
SpectrumView, SpectrumViewOpengl, PanZoomBar, FlatButton;
type
@@ -85,6 +85,7 @@ type
FPendingSliceClose: Integer; // SliceId к удалению после мыши (0=нет)
FMainFlagPinned: Boolean; // True = левая панель хозяина скрыта
FLastSliceMeterMs: QWord; // троттлинг S-метра слайсов (~10 Гц)
FLastSliceDMRMs: QWord; // семантический DMR-статус (~4 Гц)
FOnWireFlag: TWireFlagEvent;
FOnSliceClosed: TSliceClosedEvent;
FOnFlagsInvalidate: TNotifyEvent;
@@ -282,6 +283,7 @@ type
procedure PushAllSliceFlagStates; // все флаги панели (после смены rate и т.п.)
procedure PushMainFlagExtState;
procedure PushMainFlagAudioDevices;
procedure UpdateSliceDMRFlags;
// Тик: S-метр/частота главного флага + позиции + троттленный S-метр слайсов.
procedure TickFlags;
procedure SetFlagsTheme(const T: TAppTheme);
@@ -1375,6 +1377,36 @@ begin
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 хозяина): метр/частота главного флага, позиции
// всех флагов, троттленный S-метр слайсов.
procedure TPanafallPanel.TickFlags;
@@ -1400,6 +1432,14 @@ begin
if FSliceFlags[I] <> nil then
FSliceFlags[I].UpdateSMeter(FController.SliceSMeter(FSliceFlags[I].SliceId));
end;
// Статус DMR содержит только устойчивые параметры и обновляется отдельно,
// заметно реже метра. Сам оверлей инвалидируется лишь после debounce и
// только если итоговая надпись действительно изменилась.
if GetTickCount64 - FLastSliceDMRMs >= 250 then
begin
FLastSliceDMRMs := GetTickCount64;
UpdateSliceDMRFlags;
end;
end;
end;
+20
View File
@@ -141,6 +141,7 @@ type
constructor Create(AOwner: TRadioController; ASliceId: Integer);
destructor Destroy; override;
procedure FeedAudio(const Left, Right: array of Single; Count: Integer);
procedure GetStatus(out S: TDMRStatus);
end;
// Слайс уровня контроллера: логическая обёртка над WDSP-слайс-каналом
@@ -697,6 +698,7 @@ type
function GetBeaconScope(out S: TBeaconScope): Boolean;
function GetBeaconFrame(out Frame: TBeaconFrame; out RSErrors: Integer): Boolean;
function GetDMRStatus(out S: TDMRStatus): Boolean;
function GetSliceDMRStatus(SliceId: Integer; out S: TDMRStatus): Boolean;
// Отображение / приём
procedure SetCTun(On_: Boolean);
@@ -839,6 +841,12 @@ begin
if Assigned(FDecoder) then FDecoder.FeedAudio(Left, Right, Count);
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;
Count: Integer);
begin
@@ -2380,6 +2388,18 @@ begin
else FillChar(S, SizeOf(S), 0);
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);
var Half: Integer;
+79 -2
View File
@@ -79,6 +79,11 @@ type
FTxSel: Boolean; // этот слайс выбран для передачи (янтарный)
FSlice: Char; // буква слайса ('A')
FSMeterThreshold: Double; // мин. изменение S-метра (дБ) для перерисовки
FDMRText: string; // стабильный краткий статус рядом с полосой
FDMRSynced: Boolean;
FDMRCandidateText: string; // debounce: не мигать на кратком sync-loss
FDMRCandidateSynced: Boolean;
FDMRCandidateSince: QWord;
FDevNames: TStringList; // имена устройств вывода
FDevIdx: array of Integer; // соответствующие PortAudio-индексы (-1=default)
@@ -182,6 +187,7 @@ type
const CurName: string);
procedure UpdateSMeter(DB: Double);
procedure UpdateVfo(AVfoHz: Double);
procedure UpdateDMRStatus(const AText: string; Synced: Boolean);
// Тема приложения (цвета кнопок берутся отсюда — как левая панель).
procedure SetTheme(const T: TAppTheme);
// Привязка к слайсу: id, буква флага, признак главного (слайс A).
@@ -450,6 +456,9 @@ begin
FSQLOn := False; FSQLLevel := 40; FSqlDrag := False;
FVolume := 70; FMuted := False; FSplit := False; FTx := False;
FTxSel := True; FSlice := 'A';
FDMRText := ''; FDMRSynced := False;
FDMRCandidateText := ''; FDMRCandidateSynced := False;
FDMRCandidateSince := 0;
FSliceId := 0; FIsMain := True; // по умолчанию — главный приёмник (слайс A)
FSMeterThreshold := 0.4; // главный флаг — чувствительный метр
FPanel := fpNone;
@@ -977,8 +986,8 @@ end;
procedure TVfoOverlay.DrawSelf(C: TCanvas; W, H: Integer);
var
FreqStr, SStr, BWStr: string;
CurY, X, RW, i: Integer;
FreqStr, SStr, BWStr, DMRStr: string;
CurY, X, RW, i, DMRX, DMRRight: Integer;
R: TRect;
BarL, BarW, MeterX: Integer;
begin
@@ -1013,6 +1022,21 @@ begin
C.Font.Name := 'Courier New'; C.Font.Size := 8; C.Font.Style := [];
C.Font.Color := CLR_DIM; C.Brush.Style := bsClear;
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.
R := Rect(W - PAD - 26, CurY, W - PAD, CurY + HDR_BADGE_H);
if not TxSelectionAllowed then
@@ -1405,12 +1429,29 @@ end;
{ ---- Обновление состояния из контроллера ---- }
procedure TVfoOverlay.SetState(AMode, ABW: Integer; AVfoHz, ASMeterDB: Double);
var
OldMode: Integer;
begin
OldMode := FMode;
FMode := Max(0, Min(OVL_MODE_COUNT - 1, AMode));
FFilterBW := ABW;
FModeFilter[FMode] := ABW;
FVfoHz := AVfoHz;
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;
RequestInvalidate;
end;
@@ -1487,4 +1528,40 @@ begin
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.