diff --git a/MainForm.pas b/MainForm.pas index 6d3580b..38bca4c 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -1394,6 +1394,25 @@ begin FSyncingFromController := True; try case Field of + rfVfoA: + begin + FreqDispA.Frequency := Round(FController.FVfoA); + // Не рисуем синхронно из wheel/drag: частые события забивают UI-поток. + // Invalidate лишь планирует; DrawSpectrum откладываем (FSpectrumDirty). + // Линейку дёргаем сразу — её рендер дешёвый, иначе отстаёт от спектра. + SyncSpecViewFreq; + FSpectrumDirty := True; + PbSpectrum.Invalidate; + if PbRuler <> nil then PbRuler.Invalidate; + if FController.FLastVfoScrolled then + begin + FWaterfallDirty := True; + if PbWaterfall <> nil then PbWaterfall.Invalidate; + end; + end; + rfBand: + for i := 0 to BAND_COUNT - 1 do + StyleButton(BtnBand[i], i = FController.FCurrentBand); rfMute: begin StyleButton(BtnMute, FController.FMuted); @@ -4616,88 +4635,15 @@ end; // hits the edge of the display") // --------------------------------------------------------------------------- procedure TMainForm.ApplyVfoA(NewFreq: Int64); -var - Offset: Double; - FiltLo: Double; - FiltHi: Double; - HalfSpan: Double; - Scrolled: Boolean; - BandIdx: Integer; begin - // Клэмп в пределах XVTR-band — не даём VFO уйти за FreqBegin/FreqEnd - if (FController.FCurrentXvtr >= 0) and (FController.FCurrentXvtr < CFG_XVTR_COUNT) and - FController.FXvtrSettings.Entries[FController.FCurrentXvtr].Enabled then - begin - if NewFreq < FController.FXvtrSettings.Entries[FController.FCurrentXvtr].FreqBegin then - NewFreq := Round(FController.FXvtrSettings.Entries[FController.FCurrentXvtr].FreqBegin); - if NewFreq > FController.FXvtrSettings.Entries[FController.FCurrentXvtr].FreqEnd then - NewFreq := Round(FController.FXvtrSettings.Entries[FController.FCurrentXvtr].FreqEnd); - end; - FController.FVfoA := NewFreq; - Scrolled := False; + // Движок (XVTR-клэмп, CTUN shift/center/scroll, детект диапазона, FM авто-RPT, + // сеть) — в контроллере SetVfoA; рендер FreqDisp/спектра/band-кнопок — в + // OnControllerState(rfVfoA/rfBand). + FController.SetVfoA(NewFreq); - if not FController.FCTun then - begin - // ---- CTUN OFF ---- - // DDC = VFO, сдвига нет - FController.FCenterFreq := FController.FVfoA; - if FController.FWDSPReady then - FController.FDSPEngine.SetShift(0.0); - end - else - begin - // ---- CTUN ON ---- - // DDC (FController.FCenterFreq) стоит на месте. - // SetRXAShiftFreq сдвигает спектр внутри WDSP так чтобы - // демодулятор принимал сигнал на FController.FVfoA, а не на FController.FCenterFreq. - // Shift = FController.FVfoA - FController.FCenterFreq (в Гц) - Offset := FController.FVfoA - FController.FCenterFreq; - if FController.FWDSPReady then - FController.FDSPEngine.SetShift(Offset); - - // Проверяем не вышла ли полоса фильтра за край дисплея - HalfSpan := FController.FSpanHz / 2; - case FController.FMode of - 0: begin FiltLo := Offset - FController.FFilterBW; FiltHi := Offset - 100; end; - 1: begin FiltLo := Offset + 100; FiltHi := Offset + FController.FFilterBW; end; - else begin FiltLo := Offset - FController.FFilterBW/2; FiltHi := Offset + FController.FFilterBW/2; end; - end; - - if (FiltHi > HalfSpan) or (FiltLo < -HalfSpan) then - begin - // Полоса вышла за край → прокручиваем центр - if FiltHi > HalfSpan then - FController.FCenterFreq := FController.FVfoA - HalfSpan * 0.5 - else - FController.FCenterFreq := FController.FVfoA + HalfSpan * 0.5; - // Пересчитываем сдвиг после прокрутки - if FController.FWDSPReady then - FController.FDSPEngine.SetShift(FController.FVfoA - FController.FCenterFreq); - // DDC перестраивается на новый FController.FCenterFreq - Scrolled := True; - end; - end; - - // Обновляем VFO дисплей - FreqDispA.Frequency := Round(FController.FVfoA); - - // Обновляем кнопку диапазона если частота перешла в другой диапазон. - // В XVTR-режиме HF band-кнопки не подсвечиваем (мы клэмпим VFO внутри - // XVTR-диапазона — FreqToBandIdx всегда вернёт -1 для VHF/UHF). - if FController.FCurrentXvtr < 0 then - begin - BandIdx := FreqToBandIdx(FController.FVfoA); - if (BandIdx >= 0) and (BandIdx <> FController.FCurrentBand) then - begin - StyleButton(BtnBand[FController.FCurrentBand], False); - FController.FCurrentBand := BandIdx; - StyleButton(BtnBand[FController.FCurrentBand], True); - FController.FDriveLevel := CalcDriveByte; - end; - end; - - // Если нет активного канала, но CTCSS/RPT были установлены каналом (авто-флаги), - // сбрасываем их при первом же изменении частоты + // Канальная оркестрация (UI). Только при отсутствии активного канала. + // Авто-RPT сбрасывает сам SetVfoA через UpdateRptAutoState; здесь — авто-CTCSS + // и восстановление pre-channel drive (виджет TrkDrive). if FActiveChannelIdx < 0 then begin if FController.FFMCTCSSAutoActive then @@ -4707,13 +4653,6 @@ begin if BtnFMCTCSS <> nil then StyleButton(BtnFMCTCSS, False); if FController.FWDSPReady then FController.FDSPEngine.SetTXCTCSS(False, CTCSS_TONES[FController.FFMCTCSSToneIdx]); end; - if FController.FFMRptAutoActive then - begin - FController.FFMRptDir := RPT_NONE; - FController.FFMRptAutoActive := False; - if BtnFMRptMinus <> nil then StyleButton(BtnFMRptMinus, False); - if BtnFMRptPlus <> nil then StyleButton(BtnFMRptPlus, False); - end; if FPreChannelPower >= 0 then begin TrkDrive.Position := FPreChannelPower; @@ -4721,34 +4660,10 @@ begin FController.FDriveLevel := CalcDriveByte; if FController.FWDSPReady then FController.FDSPEngine.SetDriveLevel(FController.FDrivePercent / 100.0); + FController.PushNetworkState; // протолкнуть восстановленный drive end; end; - // Auto-enable repeater minus for 2 m repeater input segment - if FController.FMode = MODE_FM then UpdateRptAutoState; - - // DDC: передаём FController.FCenterFreq (при CTUN OFF = FController.FVfoA, при CTUN ON = фиксирован) - if FController.FNetwork.Connected and FController.FNetwork.Running then - begin - FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True); - FController.FNetwork.SendFullHP; - end; - - // Не рисуем синхронно из wheel/drag path: частые события мыши иначе - // забивают UI-поток и мешают аудио. Таймер подхватит ближайший кадр. - // Сам DrawSpectrum/DrawWaterfall откладываем (FSpectrumDirty), но Invalidate - // дёргаем сразу — он лишь планирует перерисовку. Линейку (PbRuler) тоже: - // её рендер дешёвый и без него она отставала от спектра при клике/смене VFO. - SyncSpecViewFreq; - FSpectrumDirty := True; - PbSpectrum.Invalidate; - if PbRuler <> nil then PbRuler.Invalidate; - if Scrolled then - begin - FWaterfallDirty := True; - PbWaterfall.Invalidate; - end; - CheckChannelActive; end; @@ -6559,55 +6474,10 @@ begin end; function TMainForm.CalcDriveByte: Byte; -// Thetis-compatible formula: -// drive_byte = int( min(target_volts/0.8, 1.0) * 1.02 * 255 ) -// where: -// target_volts = sqrt( 10^((target_dBm)/10) * 0.05 ) [E=sqrt(P*R), R=50, P in mW] -// target_dBm = 10*log10(pwr_W * 1000) - Cal -// pwr_W = Pos/100 * FController.FPAMaxPower -// Cal = per-band PA gain in dB (HF: ~38.8-41.3, VHF default: ~56.2) -var - Cal: Double; - Pos: Integer; - XvtrActive: Boolean; - pwr_W: Double; - target_dBm: Double; - target_volts: Double; - audio_vol: Double; begin - XvtrActive := (FController.FCurrentXvtr >= 0) and (FController.FCurrentXvtr < CFG_XVTR_COUNT) and - FController.FXvtrSettings.Entries[FController.FCurrentXvtr].Enabled; - - if XvtrActive then - Cal := FController.FVHFBandCal[FController.FCurrentXvtr] - else if (FController.FCurrentBand >= 0) and (FController.FCurrentBand < BAND_COUNT) then - Cal := FController.FPABandCal[FController.FCurrentBand] - else - Cal := 38.8; - - // Pos — итоговый процент драйва 0..100 - if FController.FTuning then - begin - if XvtrActive and FController.FXvtrSettings.UseXVTRTunePower then - Pos := EnsureRange(FController.FXvtrSettings.Entries[FController.FCurrentXvtr].TXPower, 0, 100) - else - Pos := EnsureRange(FController.FTXSettings.TUNLevel, 0, 100); - end - else - begin - if XvtrActive then - Pos := Round(FController.FDrivePercent * FController.FXvtrSettings.Entries[FController.FCurrentXvtr].TXPower / 100.0) - else - Pos := FController.FDrivePercent; - end; - - if Pos <= 0 then begin Result := 0; Exit; end; - - pwr_W := Pos / 100.0 * FController.FPAMaxPower; - target_dBm := 10.0 * Log10(pwr_W * 1000.0) - Cal; - target_volts := Sqrt(Power(10.0, target_dBm * 0.1) * 0.05); - audio_vol := Min(target_volts / 0.8, 1.0); - Result := Min(Round(audio_vol * 1.02 * 255.0), 255); + // Формула — в контроллере (чистая над состоянием). Делегат для существующих + // вызовов MainForm. + Result := FController.CalcDriveByte; end; procedure TMainForm.OnTXIQReady(const Buf: array of Double; Count: Integer); diff --git a/RadioController.pas b/RadioController.pas index 48f55d3..57d5220 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -47,10 +47,10 @@ unit RadioController; interface uses - Classes, SysUtils, + Classes, SysUtils, Math, HPSDRProtocol, HPSDRNetwork, WDSPEngine, AudioOutput, AudioInput, - Settings, ChannelStore, FMRepeater; + Settings, ChannelStore, FMRepeater, BoardUtils; type // Поле, которое изменилось — для гранулярных уведомлений наружу. @@ -110,7 +110,6 @@ type procedure Changed(Field: TRadioField); procedure ApplyAGCToEngine; // применяет FAGCMode к WDSP (единый маппинг) procedure ApplyModeDefaults; // дефолтный фильтр/девиация под текущий FMode - procedure PushNetworkState; // полный HP-кадр в радио (RX/TX freq+drive) public // ============================================================ // ПУБЛИЧНЫЕ ПОЛЯ (миграционные — имена совпадают с TMainForm). @@ -147,6 +146,7 @@ type FMuted: Boolean; FDrivePercent: Integer; // драйв 0..100 % (UI-уровень, источник истины) FDriveLevel: Byte; // вычисляемый hardware-байт (CalcDriveByte) + FLastVfoScrolled: Boolean; // последний SetVfoA прокрутил центр (для водопада) FAtten: Integer; FPAMaxPower: Double; FRunning: Boolean; @@ -216,6 +216,14 @@ type // FM-девиация, squelch). Вызывается командами и UI-путями смены режима. procedure ApplyModeFilter; + // Вычисляет hardware drive-байт (Thetis-формула) из текущего состояния + // (band/xvtr cal, TUN/drive%, PA max). Чистая функция над состоянием. + function CalcDriveByte: Byte; + + // Полный HP-кадр в радио (RX/TX freq через трансвертор + drive + MOX). + // Вызывается командами смены частоты/центра и UI после правки drive. + procedure PushNetworkState; + // ==================================================================== // КОМАНДЫ — единый API для GUI / Web / CAT / Arduino. // Фаза 1: обновляют поле + шлют событие; логика движка — TODO-wiring. @@ -441,8 +449,126 @@ end; // Команды (Фаза 1 — каркас). Полная логика переносится из TMainForm. // ===================================================================== +function TRadioController.CalcDriveByte: Byte; +// Thetis-совместимая формула: +// drive_byte = int( min(target_volts/0.8, 1.0) * 1.02 * 255 ) +// target_volts = sqrt( 10^(target_dBm/10) * 0.05 ) +// target_dBm = 10*log10(pwr_W*1000) - Cal ; pwr_W = Pos/100 * FPAMaxPower +var + Cal: Double; + Pos: Integer; + XvtrActive: Boolean; + pwr_W: Double; + target_dBm: Double; + target_volts: Double; + audio_vol: Double; +begin + XvtrActive := (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and + FXvtrSettings.Entries[FCurrentXvtr].Enabled; + if XvtrActive then + Cal := FVHFBandCal[FCurrentXvtr] + else if (FCurrentBand >= 0) and (FCurrentBand < CFG_BAND_COUNT) then + Cal := FPABandCal[FCurrentBand] + else + Cal := 38.8; + + if FTuning then + begin + if XvtrActive and FXvtrSettings.UseXVTRTunePower then + Pos := EnsureRange(FXvtrSettings.Entries[FCurrentXvtr].TXPower, 0, 100) + else + Pos := EnsureRange(FTXSettings.TUNLevel, 0, 100); + end + else + begin + if XvtrActive then + Pos := Round(FDrivePercent * FXvtrSettings.Entries[FCurrentXvtr].TXPower / 100.0) + else + Pos := FDrivePercent; + end; + + if Pos <= 0 then begin Result := 0; Exit; end; + + pwr_W := Pos / 100.0 * FPAMaxPower; + target_dBm := 10.0 * Log10(pwr_W * 1000.0) - Cal; + target_volts := Sqrt(Power(10.0, target_dBm * 0.1) * 0.05); + audio_vol := Min(target_volts / 0.8, 1.0); + Result := Min(Round(audio_vol * 1.02 * 255.0), 255); +end; + procedure TRadioController.SetVfoA(Hz: Double); -begin FVfoA := Hz; Changed(rfVfoA); { TODO wiring: ApplyVfoA } end; +// Чистое ядро перестройки VFO A: XVTR-клэмп, CTUN shift/center (с прокруткой +// центра при выходе полосы фильтра за край), детект диапазона, FM авто-RPT, +// проталкивание в сеть. Рендер (FreqDisp/спектр/band-кнопки) — в UI через +// OnControllerState(rfVfoA/rfBand). Канальную оркестрацию (авто-CTCSS, +// pre-channel drive) делает вызывающий UI-путь. +var + NewFreq: Int64; + Offset: Double; + FiltLo, FiltHi, HalfSpan: Double; + BandIdx: Integer; +begin + NewFreq := Round(Hz); + // Клэмп в пределах XVTR-band — не даём VFO уйти за FreqBegin/FreqEnd + if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and + FXvtrSettings.Entries[FCurrentXvtr].Enabled then + begin + if NewFreq < FXvtrSettings.Entries[FCurrentXvtr].FreqBegin then + NewFreq := Round(FXvtrSettings.Entries[FCurrentXvtr].FreqBegin); + if NewFreq > FXvtrSettings.Entries[FCurrentXvtr].FreqEnd then + NewFreq := Round(FXvtrSettings.Entries[FCurrentXvtr].FreqEnd); + end; + FVfoA := NewFreq; + FLastVfoScrolled := False; + + if not FCTun then + begin + // CTUN OFF: DDC = VFO, сдвига нет + FCenterFreq := FVfoA; + if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0); + end + else + begin + // CTUN ON: DDC фиксирован, демодулятор сдвигаем на FVfoA-FCenterFreq + Offset := FVfoA - FCenterFreq; + if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(Offset); + // Полоса фильтра вышла за край дисплея → прокручиваем центр + HalfSpan := FSpanHz / 2; + case FMode of + 0: begin FiltLo := Offset - FFilterBW; FiltHi := Offset - 100; end; + 1: begin FiltLo := Offset + 100; FiltHi := Offset + FFilterBW; end; + else begin FiltLo := Offset - FFilterBW/2; FiltHi := Offset + FFilterBW/2; end; + end; + if (FiltHi > HalfSpan) or (FiltLo < -HalfSpan) then + begin + if FiltHi > HalfSpan then + FCenterFreq := FVfoA - HalfSpan * 0.5 + else + FCenterFreq := FVfoA + HalfSpan * 0.5; + if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(FVfoA - FCenterFreq); + FLastVfoScrolled := True; + end; + end; + + // Детект диапазона (только HF: в XVTR клэмп держит VFO внутри band, + // FreqToBandIdx вернёт -1 для VHF/UHF). + if FCurrentXvtr < 0 then + begin + BandIdx := FreqToBandIdx(FVfoA); + if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then + begin + FCurrentBand := BandIdx; + FDriveLevel := CalcDriveByte; + Changed(rfBand); + end; + end; + + // Авто-MINUS в 2 м репитерном сегменте (само снимает при уходе из сегмента). + if FMode = MODE_FM then UpdateRptAutoState; + + PushNetworkState; + Changed(rfVfoA); +end; procedure TRadioController.SetVfoB(Hz: Double); begin FVfoB := Hz; Changed(rfVfoB); { TODO wiring } end;