From 7c4b0241867a41b51212a970e851715a1cc6305c Mon Sep 17 00:00:00 2001 From: Vladimir Date: Tue, 14 Jul 2026 21:23:24 +0300 Subject: [PATCH] feat(dmr): show stable status on receiver slices --- PanafallPanel.pas | 42 ++++++++++++++++++++++- RadioController.pas | 20 +++++++++++ VfoOverlay.pas | 81 +++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 140 insertions(+), 3 deletions(-) diff --git a/PanafallPanel.pas b/PanafallPanel.pas index 0c9acdb..6636451 100644 --- a/PanafallPanel.pas +++ b/PanafallPanel.pas @@ -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; diff --git a/RadioController.pas b/RadioController.pas index a3fcd7a..f7d64d7 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -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; diff --git a/VfoOverlay.pas b/VfoOverlay.pas index 38b556c..bb82b0e 100644 --- a/VfoOverlay.pas +++ b/VfoOverlay.pas @@ -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.