Improve radio status bar

This commit is contained in:
2026-05-21 15:19:43 +03:00
parent 343735389e
commit 5800e9ec35
2 changed files with 262 additions and 102 deletions
+243 -101
View File
@@ -241,6 +241,12 @@ type
FActiveDDC: Integer; // DDC index для текущей платы (0 или 2)
FLastDDCSeq: LongWord; // последний seq (пишется из сетевого потока)
FLastDDCIndex: Integer; // последний DDC index
FDDCLastSeq: array[0..6] of LongWord;
FDDCSeqValid: array[0..6] of Boolean;
FSeqErrorCount: LongWord;
FLastSeqErrorDDC: Integer;
FLastSeqErrorDelta: Int64;
FSeqOkStreak: Integer;
FLastFwdW: Double;
FLastSWR: Double;
// Эти значения приходят с HP Status пакетами (50..200 раз/с). UI обновляется
@@ -427,7 +433,9 @@ type
PbWaterfall: TPaintBox;
// ---- Status bar ----
StatusBar1: TStatusBar;
StatusPanel: TPanel;
StatusCells: array[0..7] of TPanel;
StatusLabels: array[0..7] of TLabel;
// ---- Helpers ----
procedure BuildUI;
@@ -435,6 +443,9 @@ type
procedure SetLightTheme(V: Boolean);
procedure StyleButton(B: TFlatButton; Active: Boolean = False);
procedure StyleSpanButton(B: TFlatButton; Active: Boolean = False);
procedure SetStatusText(Index: Integer; const Text: string);
procedure SetRadioOfflineStatus(const ConnText: string; ClearDevice: Boolean = False);
procedure ApplyStatusTheme(const T: TAppTheme);
procedure PositionSpanOverlayButtons;
procedure UpdateVfoDisplay;
function FormatFreq(Hz: Double): string;
@@ -1299,6 +1310,12 @@ begin
FActiveDDC := 0;
FLastDDCSeq := 0;
FLastDDCIndex := 0;
FillChar(FDDCLastSeq, SizeOf(FDDCLastSeq), 0);
FillChar(FDDCSeqValid, SizeOf(FDDCSeqValid), 0);
FSeqErrorCount := 0;
FLastSeqErrorDDC := -1;
FLastSeqErrorDelta:= 0;
FSeqOkStreak := 0;
FLastFwdW := 0;
FLastSWR := 1;
FLastSupplyV:= -1;
@@ -1507,6 +1524,30 @@ var
L.Font.Size := 7;
end;
procedure MakeStatusCell(Index, CellWidth: Integer; const Cap: string);
begin
StatusCells[Index] := TPanel.Create(Self);
StatusCells[Index].Parent := StatusPanel;
StatusCells[Index].BevelOuter := bvNone;
StatusCells[Index].Align := alLeft;
StatusCells[Index].Width := CellWidth;
StatusCells[Index].BorderSpacing.Left := 1;
StatusCells[Index].BorderSpacing.Top := 1;
StatusCells[Index].BorderSpacing.Bottom := 1;
StatusLabels[Index] := TLabel.Create(Self);
StatusLabels[Index].Parent := StatusCells[Index];
StatusLabels[Index].AutoSize := False;
StatusLabels[Index].Align := alClient;
StatusLabels[Index].BorderSpacing.Left := 8;
StatusLabels[Index].BorderSpacing.Right := 8;
StatusLabels[Index].Alignment := taLeftJustify;
StatusLabels[Index].Layout := tlCenter;
StatusLabels[Index].Caption := Cap;
StatusLabels[Index].Font.Name := 'Courier New';
StatusLabels[Index].Font.Size := 8;
end;
begin
// ---- Toolbar ----
PanelToolbar := TPanel.Create(Self);
@@ -1537,19 +1578,23 @@ begin
// ---- Separator line ----
// ---- Status bar ----
StatusBar1 := TStatusBar.Create(Self);
StatusBar1.Parent := Self;
StatusBar1.Align := alBottom;
StatusBar1.SimplePanel := False;
with StatusBar1.Panels do
begin
Add; Items[0].Width := 150; Items[0].Text := 'IP: --';
Add; Items[1].Width := 120; Items[1].Text := 'Board: --';
Add; Items[2].Width := 200; Items[2].Text := 'Supply: --';
Add; Items[3].Width := 250; Items[3].Text := 'RX: waiting...';
Add; Items[4].Width := 200; Items[4].Text := 'Not connected';
Add; Items[5].Width := 280; Items[5].Text := 'TX: --';
end;
StatusPanel := TPanel.Create(Self);
StatusPanel.Parent := Self;
StatusPanel.Align := alBottom;
StatusPanel.Height := 26;
StatusPanel.BevelOuter := bvNone;
// alLeft controls are arranged by Z-order in LCL; create them in reverse
// visual order so the final bar reads left-to-right as intended.
MakeStatusCell(6, 130, 'SEQ --');
MakeStatusCell(5, 180, 'TX idle');
MakeStatusCell(4, 190, 'RX idle');
MakeStatusCell(7, 90, 'PLL --');
MakeStatusCell(3, 150, 'Supply --');
MakeStatusCell(1, 120, 'IP --');
MakeStatusCell(0, 220, 'Board --');
MakeStatusCell(2, 170, 'Disconnected');
StatusCells[6].Align := alClient;
// ---- Left panel ----
PanelLeft := TPanel.Create(Self);
@@ -2493,6 +2538,7 @@ begin
PbWaterfall.Color := T.BG;
if PanelSMeterRight <> nil then PanelSMeterRight.Color := T.Panel;
if PbSMeterRight <> nil then PbSMeterRight.Color := T.Panel;
ApplyStatusTheme(T);
// Метки
DL(LblAGCTop, False);
@@ -2555,6 +2601,62 @@ begin
UpdateVfoDisplay;
end;
procedure TMainForm.SetStatusText(Index: Integer; const Text: string);
begin
if (Index < Low(StatusLabels)) or (Index > High(StatusLabels)) then Exit;
if StatusLabels[Index] = nil then Exit;
if StatusLabels[Index].Caption <> Text then
StatusLabels[Index].Caption := Text;
end;
procedure TMainForm.SetRadioOfflineStatus(const ConnText: string; ClearDevice: Boolean);
begin
SetStatusText(2, ConnText);
SetStatusText(3, 'Supply --');
SetStatusText(4, 'RX idle');
SetStatusText(5, 'TX idle');
SetStatusText(6, 'SEQ --');
SetStatusText(7, 'PLL --');
FillChar(FDDCSeqValid, SizeOf(FDDCSeqValid), 0);
FSeqErrorCount := 0;
FLastSeqErrorDDC := -1;
FLastSeqErrorDelta := 0;
FSeqOkStreak := 0;
if ClearDevice then
begin
SetStatusText(0, 'Board --');
SetStatusText(1, 'IP --');
end;
end;
procedure TMainForm.ApplyStatusTheme(const T: TAppTheme);
var
I: Integer;
begin
if StatusPanel = nil then Exit;
StatusPanel.Color := T.Border;
for I := Low(StatusCells) to High(StatusCells) do
begin
if StatusCells[I] <> nil then
begin
StatusCells[I].Color := T.Panel;
StatusCells[I].Font.Color := T.Text;
end;
if StatusLabels[I] <> nil then
begin
if I in [4, 5] then
StatusLabels[I].Font.Color := T.TextDim
else if I = 6 then
StatusLabels[I].Font.Color := T.Amber
else if I = 2 then
StatusLabels[I].Font.Color := T.TbStartText
else
StatusLabels[I].Font.Color := T.Text;
StatusLabels[I].Color := T.Panel;
end;
end;
end;
procedure TMainForm.SetLightTheme(V: Boolean);
begin
if FLightTheme = V then Exit;
@@ -2878,22 +2980,17 @@ begin
// HP Status пакеты идут ~150 раз/с, при таком rate цифры дёргаются.
UpdateTXMeters;
// Диагностика TX mic-пути — в отдельной панели [5], чтобы её не перетирали
// другие источники (DDC seq, audio-логи и т.п.).
if FWDSPReady then
begin
if FTransmitting then
begin
if FDSPEngine.TXMicSource = txmsSoundCard then
StatusBar1.Panels[5].Text :=
Format('TX sc: avail=%d ring=%d',
[FAudioIn.Available, FDSPEngine.TXMicRingAvail])
SetStatusText(5, 'TX sound card')
else
StatusBar1.Panels[5].Text :=
Format('TX hw: ring=%d', [FDSPEngine.TXMicRingAvail]);
SetStatusText(5, 'TX radio mic');
end
else
StatusBar1.Panels[5].Text := 'TX: idle';
SetStatusText(5, 'TX idle');
end;
end;
@@ -2929,7 +3026,7 @@ begin
StyleButton(BtnStartStop, False);
FRunning := False;
FNetwork.SetRunAndFreq(False, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), 0);
StatusBar1.Panels[4].Text := 'ТРАНСИВЕР НЕДОСТУПЕН';
SetRadioOfflineStatus('Hardware timeout');
BtnMOX.Enabled := False;
Exit;
end;
@@ -2940,7 +3037,7 @@ begin
StyleButton(BtnStartStop, False);
FRunning := False;
FNetwork.SetRunAndFreq(False, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), 0);
StatusBar1.Panels[4].Text := 'ПОТЕРЯ СВЯЗИ С ТРАНСИВЕРОМ';
SetRadioOfflineStatus('Connection lost');
BtnMOX.Enabled := False;
Exit;
end;
@@ -2948,10 +3045,12 @@ begin
if FRunning then
begin
// Обновляем StatusBar из таймера — без Synchronize в сетевом потоке
StatusBar1.Panels[3].Text :=
Format('RX DDC%d | seq=%d | pkts=%d',
[FLastDDCIndex, FLastDDCSeq, FRXPacketCount]);
// Обновляем статус из таймера — без Synchronize в сетевом потоке.
SetStatusText(4, 'RX running');
if FSeqErrorCount > 0 then
SetStatusText(6, Format('SEQ ERR DDC%d', [FLastSeqErrorDDC]))
else
SetStatusText(6, 'SEQ OK');
if FWDSPReady then
begin
@@ -3082,7 +3181,7 @@ begin
if Assigned(FDeviceDialog) then
FDeviceDialog.AddDiscovered(Dev.IPAddress, Entry, Dev.BoardType);
StatusBar1.Panels[4].Text := Format('Found: %s', [Dev.IPAddress]);
SetStatusText(2, Format('Found: %s', [Dev.IPAddress]));
end;
procedure TMainForm.OnHPStatusCB(const Status: THighPriorityStatus);
@@ -3101,22 +3200,27 @@ begin
ExcPwr := (Status.ExciterPwr0Hi shl 8) or Status.ExciterPwr0Lo;
FwdPwr := (Status.FwdPwrAlex0Hi shl 8) or Status.FwdPwrAlex0Lo;
RevPwr := (Status.RevPwrAlex0Hi shl 8) or Status.RevPwrAlex0Lo;
SupplyV := -1.0;
SupplyA := -1.0;
// Board 5 (Orion MkII): voltage on UserADC0 (bytes 57-58),
// current on UserADC1 (bytes 55-56) — matching Thetis behaviour.
if FNetwork.Device.BoardType = 5 then
// Other supported boards report voltage in SupplyVolts bytes and no current.
if BoardSupportsSupplyVoltage(FNetwork.Device.BoardType) then
begin
SupplyV := ADCToSupplyVolts(
(Status.UserADC0Hi shl 8) or Status.UserADC0Lo,
FNetwork.Device.BoardType);
SupplyA := ADCToSupplyCurrent(
(Status.UserADC1Hi shl 8) or Status.UserADC1Lo,
FNetwork.Device.BoardType);
end else
begin
SupplyV := ADCToSupplyVolts(
(Status.SupplyVoltsHi shl 8) or Status.SupplyVoltsLo,
FNetwork.Device.BoardType);
SupplyA := -1.0;
if FNetwork.Device.BoardType = 5 then
SupplyV := ADCToSupplyVolts(
(Status.UserADC0Hi shl 8) or Status.UserADC0Lo,
FNetwork.Device.BoardType)
else
SupplyV := ADCToSupplyVolts(
(Status.SupplyVoltsHi shl 8) or Status.SupplyVoltsLo,
FNetwork.Device.BoardType);
if BoardSupportsSupplyCurrent(FNetwork.Device.BoardType) then
SupplyA := ADCToSupplyCurrent(
(Status.UserADC1Hi shl 8) or Status.UserADC1Lo,
FNetwork.Device.BoardType);
end;
// FLastSMeter обновляется только из WDSP (GetSMeterDBm) в SpectrumTimerTick —
// ExciterPwr это мощность TX, не уровень принятого сигнала.
@@ -3188,14 +3292,17 @@ begin
FLastSupplyV := SupplyV // первое значение — без сглаживания
else
FLastSupplyV := ALPHA_SUPPLY * SupplyV + (1.0 - ALPHA_SUPPLY) * FLastSupplyV;
end;
end else
FLastSupplyV := -1.0;
if SupplyA >= 0 then
begin
if FLastSupplyA < 0 then
FLastSupplyA := SupplyA
else
FLastSupplyA := ALPHA_SUPPLY * SupplyA + (1.0 - ALPHA_SUPPLY) * FLastSupplyA;
end;
end else
FLastSupplyA := -1.0;
FLastPLLLock := PLLLock;
// Hardware PTT (foot switch / mic PTT) — обнаружение фронта (нужно делать
@@ -3229,33 +3336,84 @@ end;
procedure TMainForm.UpdateTXMeters;
// Вызывается из MeterTimerTick (10 Гц). Использует FLastFwdW/FLastSWR/
// FLastSupplyV/FLastPLLLock — они обновляются на rate HP Status (~150/с)
// FLastSupplyV/FLastSupplyA обновляются на rate HP Status (~150/с)
// в DoUpdateStatus.
var
PLLStr: string;
begin
if FLastPLLLock then PLLStr := 'PLL OK' else PLLStr := 'PLL?';
if not FRunning then
begin
SetStatusText(3, 'Supply --');
SetStatusText(7, 'PLL --');
Exit;
end;
if FLastPLLLock then
SetStatusText(7, 'PLL OK')
else
SetStatusText(7, 'PLL?');
if not BoardSupportsSupplyVoltage(FNetwork.Device.BoardType) then
begin
SetStatusText(3, 'Supply n/a');
Exit;
end;
if FLastSupplyV >= 0 then
begin
if FLastSupplyA >= 0 then
StatusBar1.Panels[2].Text :=
Format('Supply: %.1fV / %.1fA | %s', [FLastSupplyV, FLastSupplyA, PLLStr])
SetStatusText(3,
Format('Supply %.1fV %.1fA', [FLastSupplyV, FLastSupplyA]))
else
StatusBar1.Panels[2].Text :=
Format('Supply: %.1fV | %s', [FLastSupplyV, PLLStr]);
SetStatusText(3,
Format('Supply %.1fV', [FLastSupplyV]));
end else
StatusBar1.Panels[2].Text := PLLStr;
SetStatusText(3, 'Supply --');
end;
procedure TMainForm.OnDDCIQCB(DDCIndex: Integer; const Data: TDDCIQPacket);
var
SamplesPerFrame: Integer;
Seq, ExpectedSeq: LongWord;
Delta: Int64;
begin
// Вызывается из сетевого потока — UI не трогаем напрямую
// Счётчик принятых пакетов
Inc(FRXPacketCount);
FRXLastPktTime := GetTickCount64; // фиксируем время последнего пакета
Seq := (LongWord(Data.Seq[0]) shl 24) or (LongWord(Data.Seq[1]) shl 16)
or (LongWord(Data.Seq[2]) shl 8) or LongWord(Data.Seq[3]);
if (DDCIndex >= Low(FDDCLastSeq)) and (DDCIndex <= High(FDDCLastSeq)) then
begin
if Seq <> 0 then
begin
ExpectedSeq := FDDCLastSeq[DDCIndex] + 1;
if FDDCSeqValid[DDCIndex] and (Seq <> ExpectedSeq) then
begin
Inc(FSeqErrorCount);
FLastSeqErrorDDC := DDCIndex;
Delta := Int64(Seq) - Int64(ExpectedSeq);
FLastSeqErrorDelta := Delta;
FSeqOkStreak := 0;
end
else if FDDCSeqValid[DDCIndex] then
begin
if FSeqErrorCount > 0 then
begin
Inc(FSeqOkStreak);
if FSeqOkStreak >= 20 then
begin
FSeqErrorCount := 0;
FLastSeqErrorDDC := -1;
FLastSeqErrorDelta := 0;
FSeqOkStreak := 0;
end;
end;
end;
end;
FDDCLastSeq[DDCIndex] := Seq;
FDDCSeqValid[DDCIndex] := True;
end;
// 1. Подаём IQ данные в DSP — только с активного DDC
if (DDCIndex = FActiveDDC) and FWDSPReady then
@@ -3266,16 +3424,14 @@ begin
FDSPEngine.PushDDCPacket(Data.IQData, 0, SamplesPerFrame);
end;
// 2. Запоминаем последний seq — StatusBar обновит таймер (без Synchronize в горячем пути)
FLastDDCSeq := (LongWord(Data.Seq[0]) shl 24) or (LongWord(Data.Seq[1]) shl 16)
or (LongWord(Data.Seq[2]) shl 8) or LongWord(Data.Seq[3]);
// 2. Запоминаем последний seq — статус обновит таймер (без Synchronize в горячем пути)
FLastDDCSeq := Seq;
FLastDDCIndex := DDCIndex;
end;
procedure TMainForm.DoUpdateDDCSeq(DDCIdx: Integer; Seq: LongWord);
begin
StatusBar1.Panels[3].Text :=
Format('RX DDC%d | seq=%d | pkts=%d', [DDCIdx, Seq, FRXPacketCount]);
SetStatusText(4, 'RX running');
end;
procedure TMainForm.OnMicPacketCB(const Data: TMicDataPacket);
@@ -3494,15 +3650,15 @@ procedure TMainForm.DoUpdateDDCSeqOrNoDevice(DDCIdx: Integer; Seq: LongWord);
begin
if DDCIdx = -1 then
begin
if Assigned(FDeviceDialog) then
begin
FDeviceDialog.ClearDiscovered;
FDeviceDialog.AddDiscovered('', '-- no device found --');
end;
StatusBar1.Panels[4].Text := 'No hardware found';
end
if Assigned(FDeviceDialog) then
begin
FDeviceDialog.ClearDiscovered;
FDeviceDialog.AddDiscovered('', '-- no device found --');
end;
SetStatusText(2, 'No hardware found');
end
else
StatusBar1.Panels[3].Text := Format('DDC%d seq=%d', [DDCIdx, Seq]);
SetStatusText(4, 'RX running');
end;
procedure TMainForm.RecreateDSPEngine(ASampleRate: Integer);
@@ -3546,7 +3702,7 @@ begin
end;
BtnStartStop.Enabled := False;
StatusBar1.Panels[3].Text := 'Creating WDSP wisdom...';
SetStatusText(2, 'Creating WDSP wisdom...');
BuildThread := TWisdomBuildThread.Create(WisdomDir);
try
@@ -3577,9 +3733,9 @@ begin
end;
if Result then
StatusBar1.Panels[3].Text := 'WDSP wisdom ready'
SetStatusText(2, 'DSP ready')
else
StatusBar1.Panels[3].Text := 'WDSP wisdom missing';
SetStatusText(2, 'WDSP wisdom missing');
end;
procedure TMainForm.BtnDiscoverClick(Sender: TObject);
@@ -3601,7 +3757,7 @@ procedure TMainForm.BtnDiscoverFromDialog(Sender: TObject);
begin
SetLength(FDevices, 0);
FDeviceCount := 0;
StatusBar1.Panels[4].Text := 'Discovering...';
SetStatusText(2, 'Discovering...');
FNetwork.DirectIP := '';
TDiscoverThread.Create(FNetwork, Self);
end;
@@ -3632,7 +3788,6 @@ begin
end;
FRXPacketCount := 0;
FActiveDDC := 0;
StatusBar1.Panels[3].Text := 'RX: waiting...';
// Сохраняем текущую частоту/настройки чтобы следующий START восстановил их
if FDevConnected then
begin
@@ -3652,9 +3807,7 @@ begin
FNetwork.Disconnect;
BtnStartStop.Caption := 'START';
StyleButton(BtnStartStop, False);
StatusBar1.Panels[4].Text := 'Not connected';
StatusBar1.Panels[0].Text := 'IP: --';
StatusBar1.Panels[1].Text := 'Board: --';
SetRadioOfflineStatus('Disconnected', True);
BtnMOX.Enabled := False;
Exit;
end;
@@ -3727,7 +3880,7 @@ begin
// --- Открываем WDSP асинхронно чтобы не блокировать UI ---
FPendingDev := Dev;
BtnStartStop.Enabled := False;
StatusBar1.Panels[3].Text := 'Opening WDSP...';
SetStatusText(2, 'Opening DSP...');
if FWDSPReady then
// WDSP уже открыт — сразу подключаемся
DoConnectDevice(Dev)
@@ -3742,7 +3895,7 @@ begin
FWDSPReady := Success;
if not Success then
begin
StatusBar1.Panels[3].Text := 'WDSP open failed';
SetStatusText(2, 'DSP failed');
BtnStartStop.Enabled := True;
BtnStartStop.Caption := 'START';
StyleButton(BtnStartStop, False);
@@ -3889,8 +4042,8 @@ begin
and FXvtrSettings.Entries[G_Settings.LastXvtr].Enabled then
ActivateXvtrBand(G_Settings.LastXvtr);
StatusBar1.Panels[0].Text := 'IP: ' + FNetwork.Device.IPAddress;
StatusBar1.Panels[1].Text := 'Board: ' + BoardTypeName(FNetwork.Device.BoardType);
SetStatusText(1, 'IP: ' + FNetwork.Device.IPAddress);
SetStatusText(0, 'Board: ' + BoardTypeName(FNetwork.Device.BoardType));
// General packet
FillChar(GenPkt, SizeOf(GenPkt), 0);
@@ -3937,6 +4090,13 @@ begin
FRunning := True;
FRXStartTime := GetTickCount64;
FRXLastPktTime := 0;
FRXPacketCount := 0;
FillChar(FDDCSeqValid, SizeOf(FDDCSeqValid), 0);
FSeqErrorCount := 0;
FLastSeqErrorDDC := -1;
FLastSeqErrorDelta := 0;
FSeqOkStreak := 0;
SetStatusText(6, 'SEQ OK');
// DDC и DUC Specific — теперь потоки эмулятора слушают на своих портах.
// DUC Specific содержит mic-конфигурацию (boost/bias/line/PTT) из FTXSettings.
@@ -3952,7 +4112,7 @@ begin
BtnStartStop.Caption := 'STOP';
StyleButton(BtnStartStop, True);
StatusBar1.Panels[4].Text := 'RUNNING';
SetStatusText(2, 'Running');
BtnMOX.Enabled := True;
end;
@@ -6678,13 +6838,8 @@ begin
FAudioOut.DeviceIndex := DevIndex;
FAudioOutDevName := DevName;
try
if not FAudioOut.Open then
StatusBar1.Panels[3].Text := 'Audio out: ' + FAudioOut.LastError
else
StatusBar1.Panels[3].Text := 'Audio out: ' + IfThen(DevName = '', 'Default', DevName);
FAudioOut.Open;
except
on E: Exception do
StatusBar1.Panels[3].Text := 'Audio out err: ' + E.Message;
end;
end;
@@ -6697,17 +6852,10 @@ begin
if DevIndex >= 0 then
begin
try
if not FAudioIn.Open then
StatusBar1.Panels[3].Text := 'Mic: ' + FAudioIn.LastError
else
StatusBar1.Panels[3].Text := 'Mic: ' + DevName;
FAudioIn.Open;
except
on E: Exception do
StatusBar1.Panels[3].Text := 'Mic err: ' + E.Message;
end;
end
else
StatusBar1.Panels[3].Text := 'Mic: none';
end;
end;
procedure TMainForm.ApplyVisibility(ShowSpectrum, ShowWaterfall: Boolean);
@@ -6830,15 +6978,9 @@ begin
// Вызывается один раз через 200 мс после старта формы
FAfterShowTimer.Enabled := False;
// Открываем PortAudio — теперь FPC уже перехватывает сигналы
try
if not FAudioOut.Open then
StatusBar1.Panels[3].Text := 'Audio: ' + FAudioOut.LastError
else
StatusBar1.Panels[3].Text := 'Audio OK (PortAudio)';
FAudioOut.Open;
except
on E: Exception do
StatusBar1.Panels[3].Text := 'Audio: ' + E.Message;
end;
EnsureWDSPWisdom;