start work tx

This commit is contained in:
2026-03-26 22:50:55 +03:00
parent 7664907b3e
commit ae532a33cd
9 changed files with 1373 additions and 149 deletions
+569 -46
View File
@@ -26,7 +26,7 @@ uses
StdCtrls, ExtCtrls, ComCtrls, Buttons, Menus, Math, Types,
LCLIntf, LCLType, GraphType,
HPSDRProtocol, HPSDRNetwork,
WDSPEngine, AudioOutput,
WDSPEngine, AudioOutput, AudioInput,
IntfGraphics, FPImage,
Settings,
WebServer;
@@ -150,8 +150,9 @@ type
FSWRV: Double;
FSupplyV: Double;
FPLLLock: Boolean;
FHWPTT: Boolean;
public
constructor Create(AForm: TObject; FW, SW, SV: Double; PLL: Boolean);
constructor Create(AForm: TObject; FW, SW, SV: Double; PLL, HWPTT: Boolean);
procedure Execute;
end;
@@ -180,9 +181,12 @@ type
FPendingBoardType: Integer; // BoardType устройства для подключения
// ---- DSP + Audio ----
FDSPEngine: TWDSPEngine;
FAudioOut: TAudioOutput;
FWDSPReady: Boolean; // True когда WDSP открыт и работает
FDSPEngine: TWDSPEngine;
FAudioOut: TAudioOutput;
FAudioIn: TAudioInput;
FAudioOutDevName: string; // текущее имя выходного устройства
FAudioInDevName: string; // текущее имя входного устройства (TX mic)
FWDSPReady: Boolean; // True когда WDSP открыт и работает
// ---- State ----
FVfoA: Double;
@@ -203,8 +207,11 @@ type
FMarkerActive: Boolean; // True = линия видима
FMarkerX: Integer; // X в пикселях (относительно ширины панели)
FDriveLevel: Byte;
FPAMaxPower: Double;
FPABandCal: array[0..BAND_COUNT-1] of Double; // калибровка 38.8..100.0 на диапазон
FRunning: Boolean;
FTransmitting: Boolean;
FHWPTTActive: Boolean; // True = hardware PTT нажата
FMuted: Boolean;
FVolume: Integer;
FLastSMeter: Double;
@@ -223,16 +230,25 @@ type
// ---- Spectrum ----
FSpectrumBuf: array[0..1023] of Single;
FWfAvgBuf: array[0..1023] of Single;
FWaterfallBuf: array[0..1023] of Single; // WDSP waterfall pixout=1 data
FSpectrumBitmap: TBitmap;
FWfAGCEnabled: Boolean;
FWfNFEnabled: Boolean;
FWfHigh: Double;
FWfLow: Double;
// Spectrum grid settings
FSpecRefLevel: Double; // dBm top of scale (e.g. -20)
FSpecRange: Double; // dB range (e.g. 110 → bottom = -130)
FSpecGridStep: Double; // grid step in dB (e.g. 10)
FWaterfallBitmap:TBitmap;
FWaterfallTemp: TBitmap;
FSpectrumWidth: Integer;
FSpectrumHeight: Integer;
FWaterfallHeight:Integer;
// ---- Visibility settings ----
FShowSpectrum: Boolean;
FShowWaterfall: Boolean;
FDisplayFPS: Integer; // таймер спектра: кадров в секунду (5..30)
// ---- Splitter между спектром и водопадом ----
FSplitter: TPanel; // визуальная полоса-разделитель
FSplitterDrag: Boolean; // True = идёт перетаскивание
@@ -268,6 +284,7 @@ type
FWebSyncBool: Boolean;
FWebSyncM: TThreadMethod;
FCurrentBand: Integer; // текущий активный диапазон 0..10
FSettingsForm: TObject; // TSettingsForm (cast при использовании)
FDevMAC: array[0..5] of Byte; // MAC подключённого трансивера
FPendingDev: THPSDRDevice; // устройство ожидающее открытия WDSP
FBandCache: array[0..CFG_BAND_COUNT-1] of TBandSettings; // кэш диапазонов
@@ -283,8 +300,9 @@ type
// ---- Toolbar ----
PanelToolbar: TPanel;
BtnDiscover: TFlatButton;
BtnDiscover: TFlatButton;
BtnStartStop: TFlatButton; // START / STOP (connect+run в одном)
BtnSettings: TFlatButton; // открыть диалог настроек
// ---- Left panel ----
PanelLeft: TPanel;
@@ -397,10 +415,11 @@ type
// DSP/Audio callbacks (вызываются из рабочих потоков)
procedure OnAudioReady(const Left, Right: array of Single; Count: Integer);
procedure OnSpectrumReady(const Pixels: array of Single; Count: Integer);
procedure OnWaterfallReady(const Pixels: array of Single; Count: Integer);
// Public UI update (called from sync objects)
procedure DoAddDevice(const Dev: THPSDRDevice; const Entry: string);
procedure DoUpdateStatus(FwdW, SWRV, SupplyV: Double; PLLLock: Boolean);
procedure DoUpdateStatus(FwdW, SWRV, SupplyV: Double; PLLLock, HWPTT: Boolean);
procedure DoUpdateDDCSeq(DDCIdx: Integer; Seq: LongWord);
procedure DoUpdateDDCSeqOrNoDevice(DDCIdx: Integer; Seq: LongWord);
@@ -503,11 +522,18 @@ type
procedure BtnWfAGCClick(Sender: TObject);
procedure BtnWfNFClick(Sender: TObject);
procedure BtnHidePanelClick(Sender: TObject);
procedure BtnSettingsClick(Sender: TObject);
procedure InvalidateGridCache;
procedure PositionVfoOverlay;
procedure OnModeFilterSelect(Mode: Integer; FilterBW: Integer);
procedure PbSpectrumDblClick(Sender: TObject);
procedure TrkDriveChange(Sender: TObject);
procedure TrkVolumeChange(Sender: TObject);
function CalcDriveByte: Byte;
function ActiveTXFreqHz: Double;
procedure ApplyMOX(Active: Boolean);
procedure OnTXIQReady(const Buf: array of Double; Count: Integer);
procedure OnPASettingsChange(MaxPower: Double; const BandCal: array of Double);
procedure BtnAGCModeClick(Sender: TObject);
procedure TrkAGCChange(Sender: TObject);
procedure PbFwdPowerPaint(Sender: TObject);
@@ -527,6 +553,18 @@ type
procedure SplitterMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
public
// Методы для SettingsForm — немедленное применение настроек
procedure ApplyDisplayParams(FFTSize, WinType, SpecDet, SpecAvgMode,
SpecAvgCount: Integer; SpecBackmult: Double);
procedure ApplyWaterfallParams(WfDet, WfAvgMode, WfAvgCount: Integer;
WfBackmult: Double);
procedure ApplyGridParams(RefLevel, Range, GridStep: Double);
procedure ApplyAudioDevice(DevIndex: Integer; const DevName: string);
procedure ApplyAudioInputDevice(DevIndex: Integer; const DevName: string);
procedure ApplyVisibility(ShowSpectrum, ShowWaterfall: Boolean);
procedure ApplyFPS(FPS: Integer);
published
// Обработчики событий формы — должны быть в published для RTTI/LFM
procedure FormCreate(Sender: TObject);
@@ -542,6 +580,8 @@ var
implementation
uses SettingsForm;
{$R *.lfm}
// ===========================================================================
@@ -581,7 +621,7 @@ begin
end;
constructor TStatusUISync.Create(AForm: TObject;
FW, SW, SV: Double; PLL: Boolean);
FW, SW, SV: Double; PLL, HWPTT: Boolean);
begin
inherited Create;
FForm := AForm;
@@ -589,11 +629,12 @@ begin
FSWRV := SW;
FSupplyV := SV;
FPLLLock := PLL;
FHWPTT := HWPTT;
end;
procedure TStatusUISync.Execute;
begin
TMainForm(FForm).DoUpdateStatus(FFwdW, FSWRV, FSupplyV, FPLLLock);
TMainForm(FForm).DoUpdateStatus(FFwdW, FSWRV, FSupplyV, FPLLLock, FHWPTT);
end;
constructor TDDCSeqSync.Create(AForm: TObject; Idx: Integer; Seq: LongWord);
@@ -647,9 +688,13 @@ begin
end;
function TMainForm.MakeGlobalSettings: TGlobalSettings;
var i: Integer;
begin
Result.Volume := FVolume;
Result.DriveLevel := FDriveLevel;
Result.DriveLevel := TrkDrive.Position;
Result.PAMaxPower := FPAMaxPower;
for i := 0 to BAND_COUNT - 1 do
Result.PABandCal[i] := FPABandCal[i];
Result.ActiveVfo := FActiveVfo;
Result.NRMode := BtnNR.Tag;
Result.NBMode := BtnNB.Tag;
@@ -661,6 +706,43 @@ begin
Result.WfNFEnabled := FWfNFEnabled;
Result.LastBand := FCurrentBand;
Result.SampleRate := FSampleRate;
// Display settings
if FWDSPReady then
begin
Result.FFTSize := FDSPEngine.FFTSize;
Result.WindowType := FDSPEngine.WindowType;
Result.SpecDetector := FDSPEngine.SpecDetector;
Result.SpecAvgMode := FDSPEngine.SpecAvgMode;
Result.SpecAvgCount := FDSPEngine.SpecAvgCount;
Result.SpecBackmult := FDSPEngine.SpecBackmult;
Result.WfDetector := FDSPEngine.WfDetector;
Result.WfAvgMode := FDSPEngine.WfAvgMode;
Result.WfAvgCount := FDSPEngine.WfAvgCount;
Result.WfBackmult := FDSPEngine.WfBackmult;
end
else
begin
Result.FFTSize := 4096;
Result.WindowType := 5;
Result.SpecDetector := 0;
Result.SpecAvgMode := 3;
Result.SpecAvgCount := 2;
Result.SpecBackmult := 0.45;
Result.WfDetector := 2;
Result.WfAvgMode := 3;
Result.WfAvgCount := 2;
Result.WfBackmult := 0.45;
end;
Result.SpecRefLevel := FSpecRefLevel;
Result.SpecRange := FSpecRange;
Result.SpecGridStep := FSpecGridStep;
Result.AudioSampleRate := FAudioOut.SampleRate;
Result.ShowSpectrum := FShowSpectrum;
Result.ShowWaterfall := FShowWaterfall;
Result.DisplayFPS := FDisplayFPS;
// Audio device names
Result.AudioOutDevice := FAudioOutDevName;
Result.AudioInDevice := FAudioInDevName;
end;
procedure TMainForm.SaveCurrentBand;
@@ -751,7 +833,7 @@ end;
procedure TMainForm.FormCreate(Sender: TObject);
var
bi_, StartupRate: Integer;
bi_, StartupRate, i: Integer;
StartupVfoA, StartupVfoB: Double;
begin
FVfoA := 14200000;
@@ -765,9 +847,11 @@ begin
FCTun := False;
FFilterBW := 2700;
FSpecDrag := False;
FDriveLevel := 100;
FRunning := False;
FTransmitting := False;
FDriveLevel := 0;
FPAMaxPower := 100.0;
for i := 0 to BAND_COUNT - 1 do FPABandCal[i] := 100.0;
FRunning := False;
FTransmitting := False;
FMuted := False;
FVolume := 70;
FCenterFreq := FVfoA;
@@ -782,6 +866,11 @@ begin
FWfAGCEnabled := False;
FWfNFEnabled := False;
FDevConnected := False;
// Spectrum grid settings defaults
FSpecRefLevel := -20.0;
FSpecRange := 110.0;
FSpecGridStep := 10.0;
FSettingsForm := nil;
FillChar(FDevMAC, SizeOf(FDevMAC), 0);
// Инициализируем кэш диапазонов умолчаниями
@@ -847,7 +936,10 @@ begin
FVfoOverlay := TVfoOverlay.Create(Self);
FVfoOverlay.OnSelect := OnModeFilterSelect;
FVfoOverlay.Visible := False;
FPanelHidden := False;
FPanelHidden := False;
FShowSpectrum := True;
FShowWaterfall := True;
FDisplayFPS := 20;
FSplitterRatio := 0.40; // 40% спектр, 60% водопад по умолчанию
FSplitterDrag := False;
@@ -894,12 +986,15 @@ begin
// Уменьшение с 1024 вдвое сокращает время построения downsampler в OpenChannel RXA
// (~1300ms → ~650ms). Латентность: 512/48000 ≈ 10.7ms — допустимо для SDR.
FDSPEngine := TWDSPEngine.Create(FSampleRate, 48000, 512);
FDSPEngine.OnAudio := OnAudioReady;
FDSPEngine.OnSpectrum := OnSpectrumReady;
FDSPEngine.OnAudio := OnAudioReady;
FDSPEngine.OnSpectrum := OnSpectrumReady;
FDSPEngine.OnWaterfall := OnWaterfallReady;
FDSPEngine.OnTXIQ := OnTXIQReady;
// Audio output — создаём объект сейчас, открываем после показа формы
// Audio output/input — создаём объекты сейчас, открываем после показа формы
// (Pa_Initialize на Linux пишет в stderr до перехвата сигналов FPC)
FAudioOut := TAudioOutput.Create(48000);
FAudioIn := TAudioInput.Create(48000);
FMeterTimer := TTimer.Create(Self);
FMeterTimer.Interval := 100;
@@ -937,6 +1032,8 @@ begin
FNetwork.Free;
FAudioOut.Close;
FAudioOut.Free;
FAudioIn.Close;
FAudioIn.Free;
FDSPEngine.Close;
FDSPEngine.Free;
FSpectrumBitmap.Free;
@@ -1034,6 +1131,7 @@ begin
X := 4;
BtnDiscover := MakeBtn(PanelToolbar, 'DISCOVER', X, 3, 80, BTN_H, BtnDiscoverClick);
BtnStartStop := MakeBtn(PanelToolbar, 'START', X+84, 3, 76, BTN_H, BtnStartStopClick);
BtnSettings := MakeBtn(PanelToolbar, 'SETUP', X+164, 3, 66, BTN_H, BtnSettingsClick);
// ---- Status bar ----
StatusBar1 := TStatusBar.Create(Self);
@@ -1546,6 +1644,125 @@ begin
TopOff := PanelSpanButtons.Height;
// S-метр позиционируется отдельно в ResizeSMeter
ResizeSMeter;
// ---- Оба скрыты ----
if (not FShowSpectrum) and (not FShowWaterfall) then
begin
PbSpectrum.Visible := False;
PbRuler.Visible := False;
PanelSplitter.Visible := False;
PbWaterfall.Visible := False;
FSpectrumWidth := RW;
FSpectrumHeight := 0;
FWaterfallHeight := 0;
PositionSpanOverlayButtons;
Exit;
end;
// ---- Только спектр (водопад скрыт) ----
if FShowSpectrum and (not FShowWaterfall) then
begin
AvailH := RH - TopOff - RULER_H;
if AvailH < MIN_SH then AvailH := MIN_SH;
SH := AvailH;
WH := 0;
PbSpectrum.SetBounds(0, TopOff, RW, SH);
PbSpectrum.Visible := True;
PbRuler.SetBounds(0, TopOff + SH, RW, RULER_H);
PbRuler.Visible := True;
PanelSplitter.Visible := False;
PbWaterfall.Visible := False;
PositionSpanOverlayButtons;
FSpectrumWidth := RW;
FSpectrumHeight := SH;
FWaterfallHeight := 0;
if (FSpectrumBitmap <> nil) and (RW > 0) and (SH > 0) then
begin
FSpectrumBitmap.SetSize(RW, SH);
FSpectrumBitmap.Canvas.Brush.Color := CLR_BG;
FSpectrumBitmap.Canvas.FillRect(Rect(0, 0, RW, SH));
FreeAndNil(FSpecGradImg);
FGridBitmapW := 0;
FGridBitmapH := 0;
FSpPtsLen := 0;
end;
if (FWaterfallBitmap <> nil) then
begin
FWaterfallBitmap.SetSize(1, 1);
FWfBitmapW := 0;
FWfBitmapH := 0;
SetLength(FWfPixels, 0);
end;
if (FRulerBitmap <> nil) and (RW > 0) then
begin
FRulerBitmap.SetSize(RW, RULER_H);
FRulerLastFreq := -1.0;
FRulerLastVfo := -1.0;
end;
if RW > 0 then
begin
DrawSpectrum;
PbSpectrum.Invalidate;
PbRuler.Invalidate;
end;
Exit;
end;
// ---- Только водопад (спектр скрыт): линейка под водопадом ----
if (not FShowSpectrum) and FShowWaterfall then
begin
AvailH := RH - TopOff - RULER_H;
if AvailH < MIN_WH then AvailH := MIN_WH;
SH := 0;
WH := AvailH;
PbSpectrum.Visible := False;
PanelSplitter.Visible := False;
PbWaterfall.SetBounds(0, TopOff, RW, WH);
PbWaterfall.Visible := True;
PbRuler.SetBounds(0, TopOff + WH, RW, RULER_H);
PbRuler.Visible := True;
PositionSpanOverlayButtons;
FSpectrumWidth := RW;
FSpectrumHeight := 0;
FWaterfallHeight := WH;
if (FSpectrumBitmap <> nil) then
begin
FSpectrumBitmap.SetSize(1, 1);
FreeAndNil(FSpecGradImg);
FGridBitmapW := 0;
FGridBitmapH := 0;
FSpPtsLen := 0;
end;
if (FWaterfallBitmap <> nil) and (RW > 0) and (WH > 0) then
begin
FWaterfallBitmap.SetSize(RW, WH);
FWaterfallBitmap.Canvas.Brush.Color := clBlack;
FWaterfallBitmap.Canvas.FillRect(Rect(0, 0, RW, WH));
FWfBitmapW := 0;
FWfBitmapH := 0;
SetLength(FWfPixels, 0);
end;
if (FRulerBitmap <> nil) and (RW > 0) then
begin
FRulerBitmap.SetSize(RW, RULER_H);
FRulerLastFreq := -1.0;
FRulerLastVfo := -1.0;
end;
if RW > 0 then
begin
DrawWaterfall;
PbWaterfall.Invalidate;
PbRuler.Invalidate;
end;
Exit;
end;
// ---- Оба видимы: стандартный режим со сплиттером ----
PbSpectrum.Visible := True;
PbRuler.Visible := True;
PanelSplitter.Visible := True;
PbWaterfall.Visible := True;
AvailH := RH - TopOff - RULER_H - SPLITTER_H;
if AvailH < (MIN_SH + MIN_WH) then Exit;
@@ -1564,8 +1781,7 @@ begin
// Линейка частот — всегда прижата к низу спектра
PbRuler.SetBounds(0, TopOff + SH, RW, RULER_H);
// Сплиттер — между линейкой и водопадом
if PanelSplitter <> nil then
PanelSplitter.SetBounds(0, TopOff + SH + RULER_H, RW, SPLITTER_H);
PanelSplitter.SetBounds(0, TopOff + SH + RULER_H, RW, SPLITTER_H);
// Водопад — под сплиттером
PbWaterfall.SetBounds(0, TopOff + SH + RULER_H + SPLITTER_H, RW, WH);
@@ -1814,12 +2030,21 @@ end;
procedure TMainForm.ResetSpectrumBuf;
var i: Integer;
begin
for i := 0 to 1023 do FSpectrumBuf[i] := -130.0;
for i := 0 to 1023 do FWfAvgBuf[i] := -120.0;
for i := 0 to 1023 do FSpectrumBuf[i] := -130.0;
for i := 0 to 1023 do FWfAvgBuf[i] := -120.0;
for i := 0 to 1023 do FWaterfallBuf[i] := -120.0;
FWfHigh := -50.0;
FWfLow := -120.0;
end;
procedure TMainForm.InvalidateGridCache;
// Сбрасываем кэш фона/сетки — перерисуется на следующем тике DrawSpectrum
begin
FGridBitmapW := 0;
FGridBitmapH := 0;
FSpectrumDirty := True;
end;
procedure TMainForm.FillDemoSpectrum;
var
i: Integer;
@@ -2035,7 +2260,8 @@ begin
if (W <= 0) or (H <= 0) then Exit;
C := FSpectrumBitmap.Canvas;
DBmin := -130.0; DBmax := -20.0;
DBmax := FSpecRefLevel;
DBmin := FSpecRefLevel - FSpecRange;
InvRange := 1.0 / (DBmax - DBmin);
// ── 1. Фон+сетка из кэша ───────────────────────────────────────────────
@@ -2056,7 +2282,7 @@ begin
GC.Font.Name := 'Courier New';
GC.Font.Color := TColor($00A8C9D9);
GC.Brush.Style := bsClear;
dB := -40.0;
dB := DBmax - FSpecGridStep;
while dB >= DBmin do
begin
Yp := Round((DBmax - dB) * InvRange * H);
@@ -2064,7 +2290,7 @@ begin
GC.Pen.Width := 1;
GC.MoveTo(0, Yp); GC.LineTo(W-1, Yp);
GC.TextOut(1, Yp - 9, Format('%4.0f', [dB]));
dB := dB - 10;
dB := dB - FSpecGridStep;
end;
for i := 0 to 8 do
begin
@@ -2206,14 +2432,31 @@ begin
H := FWaterfallBitmap.Height;
if (W <= 0) or (H <= 0) then Exit;
// ── EMA + Peak + NF — один цикл по 1024 бинам ─────────────────────────
// ── Источник данных водопада ─────────────────────────────────────────────
// При работе WDSP: FWaterfallBuf заполнен из pixout=1 (уже усреднённые WDSP данные)
// Без WDSP (demo): делаем EMA FSpectrumBuf → FWfAvgBuf
PeakDB := -200.0; NFSum := 0.0; NFCount := 0;
for X := 0 to 1023 do
if FWDSPReady then
begin
AvgDB := FWfAvgBuf[X] * (1.0 - ALPHA_SPEC) + FSpectrumBuf[X] * ALPHA_SPEC;
FWfAvgBuf[X] := AvgDB;
if AvgDB > PeakDB then PeakDB := AvgDB;
if AvgDB < FWfLow + 15.0 then begin NFSum := NFSum + AvgDB; Inc(NFCount); end;
// Используем WDSP waterfall данные напрямую
for X := 0 to 1023 do
begin
AvgDB := FWaterfallBuf[X];
FWfAvgBuf[X] := AvgDB; // синхронизируем для AGC логики
if AvgDB > PeakDB then PeakDB := AvgDB;
if AvgDB < FWfLow + 15.0 then begin NFSum := NFSum + AvgDB; Inc(NFCount); end;
end;
end
else
begin
// Fallback: Pascal EMA от демо-спектра
for X := 0 to 1023 do
begin
AvgDB := FWfAvgBuf[X] * (1.0 - ALPHA_SPEC) + FSpectrumBuf[X] * ALPHA_SPEC;
FWfAvgBuf[X] := AvgDB;
if AvgDB > PeakDB then PeakDB := AvgDB;
if AvgDB < FWfLow + 15.0 then begin NFSum := NFSum + AvgDB; Inc(NFCount); end;
end;
end;
// ── AGC High/Low ────────────────────────────────────────────────────────
@@ -2789,10 +3032,16 @@ var
NowTick: QWord;
ElapsedTick: QWord;
begin
// Синхронизируем размеры если PaintBox изменился (после ресайза)
if (PbSpectrum.Width <> FSpectrumWidth) or
(PbSpectrum.Height <> FSpectrumHeight) or
(FSpectrumWidth = 0) then
// Синхронизируем размеры если размер панели изменился (после ресайза).
// Важно: проверяем только видимые панели — когда спектр скрыт,
// PbSpectrum.Height может быть ненулевым при FSpectrumHeight=0,
// что вызвало бы сброс буфера водопада на каждом тике.
if (FSpectrumWidth = 0) or
(FShowSpectrum and ((PbSpectrum.Width <> FSpectrumWidth) or
(PbSpectrum.Height <> FSpectrumHeight))) or
(FShowWaterfall and not FShowSpectrum and
((PbWaterfall.Width <> FSpectrumWidth) or
(PbWaterfall.Height <> FWaterfallHeight))) then
ResizeSpectrumPanels;
ResizeSMeter;
if FSpectrumWidth <= 0 then Exit;
@@ -2977,7 +3226,8 @@ begin
else
SWRV := 1.0;
Sync := TStatusUISync.Create(Self, FwdW, SWRV, SupplyV,
(Status.StatusBits and HPS_PLL_LOCKED) <> 0);
(Status.StatusBits and HPS_PLL_LOCKED) <> 0,
(Status.StatusBits and HPS_PTT) <> 0);
try
M := Sync.Execute;
TThread.Synchronize(nil, M);
@@ -2986,7 +3236,7 @@ begin
end;
end;
procedure TMainForm.DoUpdateStatus(FwdW, SWRV, SupplyV: Double; PLLLock: Boolean);
procedure TMainForm.DoUpdateStatus(FwdW, SWRV, SupplyV: Double; PLLLock, HWPTT: Boolean);
var
PLLStr: string;
begin
@@ -2998,7 +3248,15 @@ begin
StatusBar1.Panels[2].Text :=
Format('Supply: %.1fV | FWD: %.0fW | SWR: %.1f | %s',
[SupplyV, FwdW, SWRV, PLLStr]);
// Panels[3] зарезервирована для RX stats (DDC seq/pkt count)
// Hardware PTT (foot switch / mic PTT) — обнаружение фронта
if HWPTT <> FHWPTTActive then
begin
FHWPTTActive := HWPTT;
// Hardware PTT включает/выключает передачу только если BtnMOX не нажата
if not BtnMOX.Active then
ApplyMOX(HWPTT);
end;
end;
procedure TMainForm.OnDDCIQCB(DDCIndex: Integer; const Data: TDDCIQPacket);
@@ -3034,7 +3292,8 @@ end;
procedure TMainForm.OnMicPacketCB(const Data: TMicDataPacket);
begin
// TODO: WDSP TXA
if FTransmitting and FWDSPReady then
FDSPEngine.PushTXMicSamples16(Data.Samples, 64);
end;
// ===========================================================================
@@ -3147,8 +3406,10 @@ begin
end;
FWDSPReady := False;
FDSPEngine := TWDSPEngine.Create(ASampleRate, 48000, 512);
FDSPEngine.OnAudio := OnAudioReady;
FDSPEngine.OnSpectrum := OnSpectrumReady;
FDSPEngine.OnAudio := OnAudioReady;
FDSPEngine.OnSpectrum := OnSpectrumReady;
FDSPEngine.OnWaterfall := OnWaterfallReady;
FDSPEngine.OnTXIQ := OnTXIQReady;
end;
procedure TMainForm.BtnDiscoverClick(Sender: TObject);
@@ -3297,6 +3558,7 @@ var
GenPkt: TGeneralPacket;
DUCPkt: TDUCSpecificPacket;
G_Settings: TGlobalSettings;
i: Integer;
begin
BtnStartStop.Enabled := True;
@@ -3312,8 +3574,14 @@ begin
FDevConnected := True;
FSettings.LoadDevice(FDevMAC, G_Settings, FBandCache);
FVolume := G_Settings.Volume;
FDriveLevel := G_Settings.DriveLevel;
FActiveVfo := G_Settings.ActiveVfo;
// PA settings
FPAMaxPower := G_Settings.PAMaxPower;
for i := 0 to BAND_COUNT - 1 do
FPABandCal[i] := G_Settings.PABandCal[i];
// Slider position (0..100) and calibrated drive byte
TrkDrive.Position := EnsureRange(G_Settings.DriveLevel, 0, 100);
FDriveLevel := CalcDriveByte;
FCurrentBand := G_Settings.LastBand;
// SampleRate — глобальный, загружаем до RestoreBand
if G_Settings.SampleRate > 0 then
@@ -3327,6 +3595,53 @@ begin
BtnANF.Tag := Ord(G_Settings.ANFEnabled); UpdateANFButton;
FWfAGCEnabled := G_Settings.WfAGCEnabled; StyleButton(BtnWfAGC, FWfAGCEnabled);
FWfNFEnabled := G_Settings.WfNFEnabled; StyleButton(BtnWfNF, FWfNFEnabled);
FShowSpectrum := G_Settings.ShowSpectrum;
FShowWaterfall := G_Settings.ShowWaterfall;
if G_Settings.DisplayFPS > 0 then
ApplyFPS(G_Settings.DisplayFPS);
// Настройки дисплея и сетки
if G_Settings.FFTSize > 0 then
begin
FSpecRefLevel := G_Settings.SpecRefLevel;
FSpecRange := G_Settings.SpecRange;
if G_Settings.SpecGridStep > 0 then FSpecGridStep := G_Settings.SpecGridStep;
InvalidateGridCache;
if FWDSPReady then
begin
FDSPEngine.SetFFTParams(G_Settings.FFTSize, G_Settings.WindowType);
FDSPEngine.SetSpectrumDisplay(G_Settings.SpecDetector, G_Settings.SpecAvgMode,
G_Settings.SpecAvgCount, G_Settings.SpecBackmult);
FDSPEngine.SetWaterfallDisplay(G_Settings.WfDetector, G_Settings.WfAvgMode,
G_Settings.WfAvgCount, G_Settings.WfBackmult);
end;
// Аудио устройство вывода
FAudioOut.Close;
if G_Settings.AudioSampleRate > 0 then
FAudioOut.SampleRate := G_Settings.AudioSampleRate;
if G_Settings.AudioOutDevice <> '' then
begin
FAudioOut.DeviceIndex := FAudioOut.FindDeviceByName(G_Settings.AudioOutDevice);
FAudioOutDevName := G_Settings.AudioOutDevice;
end
else
begin
FAudioOut.DeviceIndex := -1;
FAudioOutDevName := '';
end;
FAudioOut.Open;
// Аудио устройство ввода (TX mic)
if G_Settings.AudioInDevice <> '' then
begin
FAudioIn.DeviceIndex := FAudioIn.FindDeviceByName(G_Settings.AudioInDevice);
FAudioInDevName := G_Settings.AudioInDevice;
FAudioIn.Open;
end
else
begin
FAudioIn.DeviceIndex := -1;
FAudioInDevName := '';
end;
end;
RestoreBand(FCurrentBand);
StatusBar1.Panels[0].Text := 'IP: ' + FNetwork.Device.IPAddress;
@@ -4117,9 +4432,14 @@ begin
end;
end;
procedure TMainForm.BtnMOXClick(Sender: TObject);
function TMainForm.ActiveTXFreqHz: Double;
begin
FTransmitting := not FTransmitting;
if FActiveVfo = 0 then Result := FVfoA else Result := FVfoB;
end;
procedure TMainForm.ApplyMOX(Active: Boolean);
begin
FTransmitting := Active;
if FTransmitting then
begin
BtnMOX.ClrNorm := TColor($00000044);
@@ -4128,7 +4448,19 @@ begin
BtnMOX.Active := True;
end else
StyleButton(BtnMOX, False);
if FWDSPReady then FDSPEngine.SetTXRun(FTransmitting);
if FWDSPReady then
FDSPEngine.SetTXRun(FTransmitting);
if FNetwork.Connected and FNetwork.Running then
begin
FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel,
FTransmitting, True, True);
FNetwork.SendFullHP;
end;
end;
procedure TMainForm.BtnMOXClick(Sender: TObject);
begin
ApplyMOX(not FTransmitting);
end;
procedure TMainForm.BtnMuteClick(Sender: TObject);
@@ -4784,11 +5116,62 @@ end;
procedure TMainForm.TrkDriveChange(Sender: TObject);
begin
FDriveLevel := Round(TrkDrive.Position / 100.0 * 255);
FDriveLevel := CalcDriveByte;
if FWDSPReady then
FDSPEngine.SetDriveLevel(TrkDrive.Position / 100.0);
end;
function TMainForm.CalcDriveByte: Byte;
var Cal: Double;
begin
if (FCurrentBand >= 0) and (FCurrentBand < BAND_COUNT) and (FPABandCal[FCurrentBand] > 0.0) then
Cal := FPABandCal[FCurrentBand]
else
Cal := 100.0;
Result := Round(TrkDrive.Position / 100.0 * Cal / 100.0 * 255.0);
end;
procedure TMainForm.OnTXIQReady(const Buf: array of Double; Count: Integer);
var
IData: array[0..239] of Integer;
QData: array[0..239] of Integer;
Offset, n, i: Integer;
begin
if not FNetwork.Connected then Exit;
Offset := 0;
while Offset < Count do
begin
FillChar(IData, SizeOf(IData), 0);
FillChar(QData, SizeOf(QData), 0);
n := Count - Offset;
if n > 240 then n := 240;
for i := 0 to n - 1 do
begin
IData[i] := Round(Buf[(Offset + i) * 2] * 8388607.0);
QData[i] := Round(Buf[(Offset + i) * 2 + 1] * 8388607.0);
end;
FNetwork.SendDUCIQ(IData, QData);
Inc(Offset, 240);
end;
end;
procedure TMainForm.OnPASettingsChange(MaxPower: Double;
const BandCal: array of Double);
var i: Integer;
begin
FPAMaxPower := MaxPower;
for i := 0 to BAND_COUNT - 1 do
begin
if i <= High(BandCal) then
FPABandCal[i] := BandCal[i]
else
FPABandCal[i] := 100.0;
end;
FDriveLevel := CalcDriveByte;
if FNetwork.Connected and FNetwork.Running then
FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel, FTransmitting, True, True);
end;
procedure TMainForm.TrkVolumeChange(Sender: TObject);
begin
FVolume := TrkVolume.Position;
@@ -4903,6 +5286,146 @@ begin
FSpectrumDirty := True;
end;
procedure TMainForm.OnWaterfallReady(const Pixels: array of Single;
Count: Integer);
var
i, N: Integer;
begin
// Заполняем буфер водопада из WDSP pixout=1.
// Вызывается из DSP-потока — Single-запись атомарна.
N := Min(Count, 1024);
for i := 0 to N - 1 do
FWaterfallBuf[i] := Pixels[i];
end;
// ===========================================================================
// Settings apply methods (вызываются из TSettingsForm)
// ===========================================================================
procedure TMainForm.ApplyDisplayParams(FFTSize, WinType, SpecDet, SpecAvgMode,
SpecAvgCount: Integer; SpecBackmult: Double);
begin
if FWDSPReady then
FDSPEngine.SetFFTParams(FFTSize, WinType);
if FWDSPReady then
FDSPEngine.SetSpectrumDisplay(SpecDet, SpecAvgMode, SpecAvgCount, SpecBackmult);
end;
procedure TMainForm.ApplyWaterfallParams(WfDet, WfAvgMode, WfAvgCount: Integer;
WfBackmult: Double);
begin
if FWDSPReady then
FDSPEngine.SetWaterfallDisplay(WfDet, WfAvgMode, WfAvgCount, WfBackmult);
end;
procedure TMainForm.ApplyGridParams(RefLevel, Range, GridStep: Double);
begin
FSpecRefLevel := RefLevel;
FSpecRange := Range;
if GridStep > 0 then FSpecGridStep := GridStep;
InvalidateGridCache;
end;
procedure TMainForm.ApplyAudioDevice(DevIndex: Integer; const DevName: string);
begin
if FAudioOut.IsOpen then
FAudioOut.Close;
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);
except
on E: Exception do
StatusBar1.Panels[3].Text := 'Audio out err: ' + E.Message;
end;
end;
procedure TMainForm.ApplyAudioInputDevice(DevIndex: Integer; const DevName: string);
begin
if FAudioIn.IsOpen then
FAudioIn.Close;
FAudioIn.DeviceIndex := DevIndex;
FAudioInDevName := DevName;
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;
except
on E: Exception do
StatusBar1.Panels[3].Text := 'Mic err: ' + E.Message;
end;
end
else
StatusBar1.Panels[3].Text := 'Mic: none';
end;
procedure TMainForm.ApplyVisibility(ShowSpectrum, ShowWaterfall: Boolean);
begin
FShowSpectrum := ShowSpectrum;
FShowWaterfall := ShowWaterfall;
ResizeSpectrumPanels;
end;
procedure TMainForm.ApplyFPS(FPS: Integer);
begin
if FPS < 1 then FPS := 1;
if FPS > 60 then FPS := 60;
FDisplayFPS := FPS;
FSpectrumTimer.Interval := 1000 div FPS;
end;
procedure TMainForm.BtnSettingsClick(Sender: TObject);
var
SF: TSettingsForm;
begin
if not Assigned(FSettingsForm) then
begin
SF := TSettingsForm.Create(Self);
FSettingsForm := SF;
SF.OnDisplayChange := ApplyDisplayParams;
SF.OnWaterfallChange := ApplyWaterfallParams;
SF.OnGridChange := ApplyGridParams;
SF.OnAudioDevChange := ApplyAudioDevice;
SF.OnAudioInDevChange := ApplyAudioInputDevice;
SF.OnVisibilityChange := ApplyVisibility;
SF.OnFPSChange := ApplyFPS;
SF.OnPAChange := OnPASettingsChange;
end;
SF := TSettingsForm(FSettingsForm);
SF.LoadPASettings(FPAMaxPower, FPABandCal);
// Перечисляем PA устройства
SF.RefreshAudioDevices(FAudioOut, FAudioIn);
SF.LoadVisibility(FShowSpectrum, FShowWaterfall);
SF.LoadFPS(FDisplayFPS);
// Загружаем текущие значения
if FWDSPReady then
SF.LoadValues(
FDSPEngine.FFTSize,
FDSPEngine.WindowType,
FDSPEngine.SpecDetector,
FDSPEngine.SpecAvgMode,
FDSPEngine.SpecAvgCount,
FDSPEngine.SpecBackmult,
FDSPEngine.WfDetector,
FDSPEngine.WfAvgMode,
FDSPEngine.WfAvgCount,
FDSPEngine.WfBackmult,
FSpecRefLevel, FSpecRange, FSpecGridStep,
FAudioOutDevName, FAudioInDevName)
else
SF.LoadValues(
4096, 5, 0, 3, 2, 0.45,
2, 3, 2, 0.45,
FSpecRefLevel, FSpecRange, FSpecGridStep,
FAudioOutDevName, FAudioInDevName);
SF.Show;
end;
procedure TMainForm.AfterShowTick(Sender: TObject);
begin