The narrow beacon decoder filter marker rendered differently in the two
spectrum views: CPU drew a translucent filled band (BlendBand) plus a centre
line, while OpenGL drew three plain vertical lines (both edges + centre), so
it looked like one band on CPU and "two parts" on GL.
Make the CPU path draw the same three lines as GL — consistent look and
cheaper (drops the per-frame per-pixel alpha blend for this marker; BlendBand
is kept for the RX/TX passband shading).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the display-FFT centroid beacon lock with a decoder-based loop.
The beacon decoder already tracks the carrier precisely via its NCO
(squaring-FFT acquisition + Costas offload); its residual ResidHz+CarFreqHz
is an exact measurement of the beacon's offset from the aim point. Feed that
into LOError of the active transverter so the LO retunes to compensate LNB
drift — the whole downlink stays put, calibration persists.
RadioController:
- Remove MeasureBeaconFFT and all display-FFT lock state/constants
(lobe/search/track/slew/sym/shape/seed-gain/prom), fields
FBeaconManualSeed/FBeaconPromDB/FBeaconDecOn, methods
SetBeaconDecode/SetBeaconDecodeAtHz/BeaconDecodeEnabled.
- SetBeaconLock is now the master switch (lock = decoder); BeaconSeedAtHz
aims the decoder; ServiceBeaconLock measures beacon freq from the decoder
scope and trims LOError (gain 0.5, deadband 20 Hz, step clamp 400 Hz,
settle 8 ticks). Feed-forward of the aim keeps the decoder locked through
the retune (net baseband ~0). Lock/SNR now come from the decoder.
MainForm: single BEACON button — left-click toggles lock (+ opens the
constellation/bulletin scope, arms the spectrum click for aiming),
right-click shows/hides the scope. Spectrum click (armed or Shift) aims via
BeaconSeedAtHz. Simplified status field and markers.
Both targets build (lazbuild --ws=qt6 + build-ewsdrd.sh). Not yet verified
on air. Risk: BEACON_RESID_SIGN=+1.0 — flip to -1.0 if the loop runs away.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
After successful on-air validation, drop the temporary diagnostics:
- IQ baseband dump to ~/ewsdr_beacon_iq.f32 (FDbgStream/DbgWrite2).
- BeaconLog file logger (~/ewsdr_beacon.log) and per-second STATE / AIM lines.
- Dead Gardner timing fields (FMidI/FMidQ/FHalf/FPrevI/FPrevQ), superseded by
the ML signal×slope TED.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Decode and show the AO-40 frame payload as text in the beacon scope window.
- RadioController.GetBeaconFrame exposes the latest 256-byte frame.
- BeaconScopeForm: poll the frame counter; on each new frame, parse the
payload (drop trailing 2-byte CRC, printable ASCII, 64-char lines, as in
gr-satellites qo100.parse) and append to a scrolling read-only memo.
- Window widened/heightened; constellation size capped to leave room.
Frame format verified offline against a real on-air capture: the decoder
produces the live QO-100 AMSAT bulletin text (RS errors=0).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full chain now decodes the QO-100 central beacon to 256-byte AO-40 frames
on-air (frames>0, RS errors=0). Builds on M1 (stable BPSK demod).
FEC backend (BeaconFEC.pas, TBeaconFEC):
- AO-40 FEC: distributed sync (65 bit, step 80) -> 80x65 deinterleave ->
Viterbi r=1/2 k=7 -> CCSDS descramble -> 2x RS(160,128). Links system
libfec (Karn) load-time (libfec.so/.dll/.dylib). Viterbi AO-40 [0x4F,-0x6D]
remapped to libfec [0x6D,0x4F]. Validated against gr-satellites reference
vectors (fectest2: exact 256-byte frame, 0 mismatches).
DBPSK Manchester frontend (BeaconDecoder.pas):
- Chip-rate (800) processing: RRC matched filter -> ML signal-times-slope
timing recovery -> Costas -> Manchester combine (block phase select) ->
differential decode -> soft symbols to FEC.
- Carrier acquisition: squaring-FFT estimates the residual (carrier at +-pi/chip
defeats decision-directed Costas), one-shot pulls it to DC via residual NCO.
- Costas frequency offload into pre-RRC NCO (with deadband) tracks LNB drift
without hitting the +-pi Costas clamp.
- ML TED replaces Gardner, which degenerates on the Manchester chip stream.
Temporary on-air diagnostics retained (IQ dump + STATE log) pending wider
signal validation; to be removed before final cleanup.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The DUP toggle wrote FDisplayDuplex directly, bypassing the controller's
SetDuplex (which also sets KeepRXDuringTX live and fires rfDuplex). And
MainForm had no rfDuplex render case, so QO-100 auto-enabling duplex
(ActivateXvtr → SetDuplex) left the button unlit while FDisplayDuplex=True
— pressing DUP then turned it OFF when the user expected ON.
Single path now: ApplyDUP just calls SetDuplex; a new rfDuplex render case
styles the button and (when transmitting) switches the display source.
Both the button and QO-100 auto-enable go through it, so the button always
reflects state and KeepRXDuringTX stays in sync.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Document the TX-path work landed this session:
- §4.6: polyphase FIR interpolator + sample-rate-scaled TX buffer +
SoapyPlutoSDR confirmation of 32768 full-scale.
- §4.8 (new): power-linear drive→atten mapping, full WDSP digital level,
TX LO powerdown gated by PTT.
- Phase 4 status updated.
- Related fixes: per-band FM settings (SQL/CTCSS/step/repeater) restore on
startup — was dropped by LoadDevice; affects all backends.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TX LO (altvoltage1) is now powered down on RX and enabled on PTT, instead
of staying on continuously. Removes the TX carrier leakage that showed up
at the VFO frequency in the RX spectrum (and persisted after MOX), and
stops idle TX-LO radiation. ApplyTxLO sets powerdown from FTransmitting,
which also recovers the LO if external software (e.g. SDR Console) left it
powered down. RX LO (altvoltage0) is kept on — receive is continuous; QO-100
full-duplex still works (RX LO separate).
Trade-off: re-enabling the TX LO re-runs the AD9361 synth calibration
(a few ms), so the first syllable on SSB may be slightly clipped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LoadDevice (bulk startup band loader) parsed only freq/mode/filter/AGC/
CTun/center/span and skipped the FM fields, while SaveBand writes them and
LoadBand reads them. So FM squelch, CTCSS, step and repeater shift were
saved on exit but reset to defaults on the next start (band switching kept
working — that path goes through the in-memory band cache, not disk).
Added the missing fmsq_on/fmsq_level/ctcss_on/ctcss_idx/fmstep_on/
fmstep_idx/fmrpt_dir/fmrpt_offset reads to LoadDevice, mirroring LoadBand.
Affects all backends, not just Pluto.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TUN produced no tone: TUNLevel default 10% mapped via the old dB-linear
attenuation curve (−89.75..ceiling) to −81.8 dB → effectively off. Now
power-linear: dB = ceiling + 20*log10(pct/100), so 10%→−30, 50%→−16,
100%→ceiling — usable across the whole slider.
Also keep WDSP digital level at full-scale for Pluto (ApplyWDSPDrive):
power is set once via hardware attenuation, not by scaling the WDSP path.
HPSDR unchanged (drive byte still drives the radio PA).
Note: at a given slider position Pluto TX is now much hotter than before
(50%: −49.9→−16 dB). Ceiling stays PlutoTxMaxAttDb (default −10 dB).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaced linear interpolation upsampling (192k→device-rate) with a
polyphase FIR interpolator ×L. Prototype: windowed-sinc lowpass (Blackman,
~-58 dB sidelobes), TAPS*L long, cutoff TX_FIR_CUT=40k, normalized to unity
per-phase DC gain (loudness unchanged). Images at multiples of 192k are now
suppressed ~-58 dB instead of only sinc², cleaning up the uplink spectrum.
BuildTxResampler designs coefficients per FTxRatio in StartTX; FTxDlI/Q
delay line persists across SendDUCIQ calls, reset on StartTX/ClearDUCIQQueue.
Removed FTxPrevI/Q.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixed 4096-pair TX buffer was too small for full-duplex above ~1 Msps
("not enough ... samples getting dropped" per SoapyPlutoSDR), causing
clicks/dropouts. Now sized as round(rate/60) rounded up to a power of 2,
clamped [4096..65536] (576k→16384, 1536k→32768). FIFO cushion (~4 buffers)
resizes to match in StartTX.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
QO-100 reuses the transverter path (VFO/band-stack/beacon-lock untouched)
with a split LO for the geostationary transponder:
- TXvtrEntry += FullDuplexTx + TxLOOffset (persisted). Reserve the last
XVTR slot (QO100_SLOT) for QO-100; migration restores the template when
the slot is unset or has empty offsets (fixes old configs showing zeros).
- XvtrTranslateTX: TX LO = visible - TxLOOffset (no LNB LOError) for QO-100;
identical to RX translate for normal transverters. Switched all TX-side
translate calls (controller + MainForm).
- SetDuplex wired (live KeepRXDuringTX); auto display-duplex on QO-100 entry.
- New "QO-100" settings page (LNB LO / transponder offset / downlink edges /
RX gain / TX ceiling + live RX-IF/TX readout); QO slot hidden from the
transverter table. Reuses OnXvtrChange — no MainForm wiring needed.
- doc/PLUTO_INTEGRATION_PLAN.md: phase 5 done.
Builds clean (headless + qt6 GUI). Not yet verified on hardware.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Show current drive value next to the slider: percent for openHPSDR,
TX attenuation in dB for Pluto (via controller PlutoTxAttForDrive;
unit flips on backend connect). Refreshed on drive change, device
render, and TX max-att change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Status bar / web now show Pluto/LibreSDR model + serial instead of
"Unknown Board" (BoardDisplayName in controller; UI just renders it).
- Field 3 shows AD936x chip temperature + RX RSSI for Pluto (no PA →
Supply only for openHPSDR). Polled via backend.ReadTelemetry (~2 Hz),
filtered for sensor glitches (plausibility window + slew-limit, with
anti-stick) — fixes rare bogus -15°C readings.
- Add libiio binding iio_channel_attr_read_double.
- Remove stale root pluto_integration.MD (superseded by
doc/PLUTO_INTEGRATION_PLAN.md).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Builds on the beacon-lock baseline: keeps the proven display-FFT control
path (hardware-LO trim folded into the active transverter LOError), and
hardens it. The narrowband NCO/FLL scaffold (BeaconLock.pas) is removed —
it was never in the control path.
- RadioController:
* Persist live: each correction folds straight into the active xvtr
LOError (visible in Settings, survives restart as LNB calibration);
disk writes throttled. FBeaconLockHz is now only a session indicator.
* Robust tracking vs nearby signals: narrow ±2 kHz gate while locked
(±6 kHz only for acquisition), slew-rate outlier rejection (a sudden
centroid jump = interferer, not slow LNB drift → ignored), and lobe
shape validation in MeasureBeaconFFT (width + power symmetry) so a CW
carrier / one-sided neighbour isn't mistaken for the BPSK beacon.
Prominence measured over the in-window noise floor (gate-width robust).
* Fast convergence: feed-forward (predict post-retune position so the
window follows the beacon through the jump) + near-deadbeat correction
on the manual click → one click locks instead of repeated tapping.
* Single central BPSK beacon (10489.750); dead CoarseBeaconOffset and
unused throttle counters removed; stale NCO/FLL/4 Hz comments fixed.
- WDSPEngine: drop the per-sample TBeaconTracker RX-IQ tap (tracker gone).
- MainForm: BEACON is now a plain button, shown only on Pluto in a
transverter. Lock status (off / click / search / LOCK + correction +
prominence) moves to status-bar field 7 in place of PLL on Pluto;
openHPSDR keeps PLL there.
- SettingsForm/StatusBar: widen LO Error field to ±10 MHz (QO-100 LNBs
drift hundreds of kHz) and widen status field 7 for the beacon text.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Click the beacon on the spectrum; the controller tracks it and continuously
trims the transverter LO so the whole downlink stays put as the LNB drifts.
- BeaconLock.pas: TBeaconTracker scaffold (narrowband NCO/FLL) — kept but no
longer in the control path (FLL diverged on the suppressed-carrier BPSK
beacon + Pluto DC offset). Measurement now uses the proven display-FFT path.
- RadioController: FBeaconLockHz folded into XvtrTranslate (on top of LOError).
MeasureBeaconFFT finds the beacon by POWER-WEIGHTED CENTROID of the lobe
(BPSK has a suppressed carrier → broad symmetric ~2.4 kHz lobe, not a line;
a peak-finder jitters, the centroid sits on the carrier). Track position in
ABSOLUTE display Hz (FBeaconTrackHz) so scrolling the waterfall/centre never
moves the search window off the beacon. Closed loop: e = tracked - ref,
d(e)/d(LockHz) = -1 -> LockHz += gain*e; settle cooldown after each correction
(display-FFT lags the LO retune -> avoids overshoot); deadband 75 Hz +
centroid/prominence smoothing + lock hysteresis for a steady lock.
Correction applied only after the user clicks (manual seed), so a crowded
band can't pull a neighbour to the reference.
- WDSPEngine: SetBeaconTracker + RX-IQ tap (tracker idle now, ~1 check/sample).
- SpectrumView + GL: beacon markers — green = reference (10489.750), orange =
tracked centroid.
- MainForm: BEACON button (RX block, XVTR-only). Flow: BEACON -> "CLICK BCN"
-> click beacon -> "L<corr> /<prom>". ServiceBeaconLock driven by MeterTimer.
Reference defaults to the middle BPSK beacon 10489.750. Tested on-air against
Es'hail-2: locks and holds, survives waterfall drag.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CTun position was lost on band restore / restart, and VHF/UHF bands were
never saved on Pluto.
- SaveCurrentBand: the >61 MHz guard (HPSDR HF-only) skipped saving native
Pluto VHF/UHF bands → 2m/70cm reverted to defaults. Now HPSDR-only; Pluto
saves VU-plan bands (FreqToBandIdx guards cross-band pollution).
- Persist DDC center for CTun: new TBandSettings.CenterHz (+ JSON center_hz)
and TXvtrEntry.LastCenterHz (+ last_center_hz). RestoreBand / ActivateXvtr
restore the saved center when CTun is on and the offset fits the span,
instead of forcing center := VFO (which re-centered the receiver).
- UpdateFreqDisplayMax: freq-display upper bound from active transverter's
FreqEnd (lets QO-100 show 10 GHz past the 6 GHz Pluto cap).
- QO-100 transverter template (slot 2, disabled): downlink 10489.5–10490.0,
LO 9750. MigrateXvtrTemplate injects it into the first free slot for
devices that already have a saved xvtr config (template was otherwise
masked by the loaded slots).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- RX hw-gain (Pluto/AD936x): backend SetRxGain(ModeIdx, GainDb) — gain_control_mode
(manual/fast_attack/slow_attack/hybrid) + hardwaregain. Controller FRxGainMode/
FRxGainDb, commands SetRxGainMode/SetRxGain/RxGainBy, rfRxGain event, snapshot,
applied in StartRunning. Persisted per-device (rx_gain_mode/rx_gain_db). HPSDR
unaffected (backend no-op).
- UI: separate "RF AGC" panel below the (unchanged) WDSP AGC block, shown only for
Pluto — FAST/SLOW/HYB/MAN mode buttons + manual GAIN slider. LayoutLeftPanel
reflows lower panels via RelayoutBelowBands; no layout shift on HPSDR.
- Cold-init fix: direct sampling_frequency write doesn't load the AD9361 FIR
decimation filter, so a cold-booted Pluto showed a wide centre spike that didn't
track tuning (worked only after SDR Console pre-initialised it). Add optional
libad9361 binding (dynamic) and call ad9361_set_bb_rate() in ApplySampleRate,
which configures BBPLL + loads the FIR; falls back to direct write if absent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add ADALM-Pluto / AD9361 support as a second hardware backend alongside
openHPSDR, sharing the WDSP DSP pipeline and the existing controller API
(UI stays decoupled from logic).
- RadioBackend.pas: abstract TRadioBackend + TBackendCaps + TRadioDevice
(Kind/URI/Serial). THPSDRNetwork now derives from it (state via virtual
getters); TRadioController.FNetwork is the base type.
- IIOBindings.pas: dynamic libiio loader (runs without libiio present).
- PlutoBackend.pas: scan/probe-by-URI, connect, LO/rate/bandwidth/gain
control, RX streaming thread (int16->24bit BE -> OnDDCIQ), Q conjugated
to match WDSP IQ convention. Verified on LibreSDR (AD9361) over network.
- Unified discovery: TDiscoverThread scans both backends; network Plutos
found via direct ProbeURI (no mDNS needed). ConnectDevice dispatches by
Dev.Kind (EnsureBackend swaps backend, preserving callbacks).
- DeviceStore/DeviceForm: persist Kind/URI/Serial; save Pluto via
AddSavedPluto so saved/autostart devices reconnect across restarts.
- VHF/UHF band plan (BoardUtils, kind-aware): 6m..ADS-B for Pluto; fixes
HF clamps (band detect, mouse-wheel 60 MHz cap, freq-display max).
- SampleRateOverlay: configurable presets (Pluto 576k..5760k, >520 ksps),
auto-width to fit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
FLastSMeter is sourced from WDSP (GetSMeterDBm), not the network HP-status,
so only the GUI timer (MainForm) ever updated it. In headless mode nothing
did, leaving PushState to send web clients a frozen -130 dBm. Refresh it in
the daemon Run loop before each PushState, mirroring the GUI timer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1. StartRunning applied AGC mode via a raw enum cast (TWDSPAGCMode(FAGCMode))
instead of the index→enum table, inverting Fast↔Off on every connect/go-live
(GUI and daemon). Now uses ApplyAGCToEngine like the rest of the unit.
2. SetFilterIdx indexed the per-mode BW/deviation tables with an unchecked Idx
(FM tables hold only 2 entries) — out-of-bounds when driven from CAT/web.
Now clamps Idx per mode (EnsureRange).
3. ConnectDevice loaded FActiveVfo / FCurrentBand straight from config without
range validation; a corrupt/old blob could leave FCurrentBand invalid and
make later SetMode/SetFilterIdx write past FBandCache. Both now clamped.
4. VfoSwap did not run the bandstack DSP restore that SetActiveVfo does, so a
swap to a VFO on another band kept the old mode/filter/AGC while the band
button highlighted the new band. Now mirrors SetActiveVfo.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Without it Inno Setup skips files whose version matches the installed
one, so the exe was not replaced on updates.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
FPC's GetAppConfigDir on Windows falls back to the exe directory
if the shell API call fails, which breaks installs in Program Files.
Read %APPDATA% from the environment variable instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Program Files is read-only for normal users; GetAppConfigDir(False)
returns the proper per-user config location on all platforms.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wires the new wdsp WFM API into the engine and adds a WFM preset to the
FM filter row (200 kHz BW / 75 kHz deviation). Also fixes WDSP_SAM
constant which was 12 — that slot is now RXA_WFM in the updated enum.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- ApplyChannel now sets TrkDrive from Ch.Power; saves pre-channel level in
FPreChannelPower (works around TrkDriveChange clearing the field on assignment)
- Power is restored when tuning away from channel (CheckChannelActive) or on
first VFO movement after returning to band with no active channel (ApplyVfoA)
- Manual drive slider move clears FPreChannelPower so restore is skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New ChannelStore.pas: TChannel record + JSON persistence (channels.json)
- New ChannelsForm.pas: full channel editor (list + fields, pre-fills from current radio state)
- CH button in RX block: left-click opens dropdown, right-click opens editor
- Active channel: button highlights with channel name, deactivates on VFO move
- ApplyChannel: auto-switches XVTR/HF band based on channel frequency; saves old
band state before switching to prevent cache corruption
- SaveCurrentBand: skip save when VFO > 61 MHz (prevents HF cache corruption
from VHF channel application without XVTR)
- DeactivateXvtr: save full XVTR state (was only saving LastFreq)
- CTCSS/RPT from channel marked as auto: cleared automatically when tuning away
from channel frequency; survives band switches via FXvtrSettings auto-flags
- TXvtrEntry: add LastCTCSSAutoActive, LastFMRptAutoActive fields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add PanelRXBlock between TX and Band with CTUN, VOL, NR/NB/SNB/ANF/MUTE
- Move AGC panel to sit directly below RX block for logical RX grouping
- Move DUP button into TX panel below MOX/TUN
- Rename PanelRX → PanelAGC to match its content
- Standardize block label top (2px) and first-control top (16px) across all
panels: AGC, SQL, CTCSS, STEP were inconsistent
- Fix left panel width restore after hide: save actual DPI-scaled width before
zeroing instead of hardcoding 232, preventing panel truncation on Win32 with
display scaling enabled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(ClickFreq div StepHz)*StepHz always snapped downward; replaced with
((ClickFreq + StepHz/2) div StepHz)*StepHz so clicks above the midpoint
between two steps correctly snap to the higher one.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
RefreshAudioDevices was firing OnRXDevChange/OnTXDevChange when restoring
the combo selection, which called ApplyAudioDevice(-1,'') and wiped
FAudioOutDevName/FAudioInDevName before LoadValues could use them.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract helpers out of MainForm into structured modules:
- BoardUtils: BoardTypeName, FreqToBandIdx (removes duplication with DeviceForm)
- WisdomBuilder: TWisdomBuildThread + TWisdomProgressDialog
- UISync: TDeviceFoundSync/TStatusUISync/TDDCSeqSync via callbacks
- PlatformUtils: HasOpenGLSpectrumSwitch, CurrentScreenDPI
Minor cleanup: unused local constants, stale comments in AudioOutput.
Replaces TProgressBar in TWisdomProgressDialog with TFlatProgressBar,
drawn entirely via Canvas (plain rectangle, no native widget styling).
Dialog now receives the active TAppTheme so both dark and light themes
are applied consistently to background, label, and progress bar.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- LFM: Position = poDesigned (was poScreenCenter, overriding our restore)
- RestoreWindowBounds: remove position scaling (L,T invariant across DPI);
scale size correctly per backend: LCLWin32 uses CurDPI/SavedDPI (physical px),
Qt6/GTK/Cocoa use SavedDPI/CurDPI (logical px, inverse); fix min-size
inflation on Qt6 (was MulDiv(900,DPI,96) which exceeds logical screen width
at 150%+ scale, forcing window to 0,0)
- SaveWindowBounds moved from FormDestroy to FormClose while window is still
stable; add guard for bogus RestoredLeft/Top values on Qt6 when maximized
- RestoreWindowBounds stores FRestoreL/T and re-applies position in AfterShowTick
(200ms after show) when WM frame extents are already known to Qt, fixing the
systematic right/down drift caused by unknown frame extents at FormCreate time
- Save and restore WindowState (maximized flag); skip AfterShowTick position
re-apply when window is maximized
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove ResizeSMeter (Pango font measurement + multiple SetBounds) from 60 Hz timer tick;
it is still called on actual resize events
- Throttle UpdateAGCLines WDSP API calls from 60 Hz to ~10 Hz (every 6 ticks)
- Optimize spectrum alpha-fix loop: write-only byte access instead of read-modify-write DWORD
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SpectrumView: rewrite DrawSpectrumGradient to blend directly into
FSpectrumBitmap via ScanLine instead of TLazIntfImage+BitBlt (stale DC
on win32). Add alpha=$FF fixup pass so pf32bit Canvas.Draw renders via
StretchBlt not AlphaBlend. Remove FSpecGradImg/FSpecGradBmp fields.
MainForm: mask exZeroDivide/exInvalidOp in FormCreate so WDSP DLL
divide-by-zero does not crash the DSP thread on win32 (Qt6 already
masks FPU exceptions globally).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add SampleRateOverlay.pas: TCustomControl overlay (top-left of spectrum)
with hide-panel button and 6 span buttons (48k–1536k), same hit-test
pattern as VfoOverlay
- Consolidate toolbar: move PanelTopVfoGroup and PanelSMeterRight into
PanelToolbar (height 72) instead of floating on the form
- Remove PanelSpanButtons (alTop spacer) that caused the ~1cm black strip
at the top of the spectrum
- Add separator line inside PanelToolbar anchored to its bottom edge,
with 16px left/right margins
- Fix left panel blocks starting Y offset (was 38, now 0)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
AGC button in DSP row shows current mode (FAST/MED/SLOW/LONG/OFF).
Click toggles a picker row below; selecting a mode closes it, fires
OnAGCChange, and syncs MainForm BtnAGCMode buttons and DSP engine.
Overlay height expands 136→159 px while picker is open.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moves CTCSS tone picker and FM step picker from inline TPanel+TFlatButton
arrays into a standalone FlatDropDown.pas module. TFlatDropDown owns the
popup panel, handles positioning and item selection, and fires OnSelect.
ApplyStyle syncs button colors on theme change.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Third button row mirrors MainForm NR (cycle 0-4), NB (cycle 0-2),
SNB and ANF toggles. OnDSPChange event syncs state back to MainForm
buttons and DSP engine. Overlay height tightened to match top/bottom
padding.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- FM filter row now shows only NFM (11 kHz) and FM (16 kHz) matching MainForm
- OnModeFilterSelect updates FFMDeviation and FFilter index for FM mode
- Mode and filter button widths distributed evenly (proportional, not fixed BtnW)
- S-meter inset by extra 4 px on each side
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TSpectrumView remains the public facade for MainForm; waterfall, S-meter,
and ruler rendering are now in isolated units. Dead code removed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Gives 44ms jitter margin for WiFi connections. Sound card mic path
is unaffected — StallCount never increments when FOnPullMic fills
the ring every tick.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pre-roll bug: when MIC_PREROLL_SAMP is an exact multiple of OPUS_FRAME_SAMP
(1920/960=2), the N<=Want branch filled the buffer but never flushed it,
losing the first 40ms of audio and leaving the ring empty long enough for
silence blocks to be injected into WDSP.
StallCount threshold raised from 2 to 4 (~43ms) to cover OS scheduler
jitter of up to 22ms, preventing false silence injection when an Opus
frame arrives slightly late relative to the DSP tick.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Opus decodes 960 samples per frame; the TX DSP thread consumes
512-sample blocks. 960 mod 512 = 448, so the ring always kept a
sub-threshold tail, causing TTXDSPThread to skip ticks and starve
the DUC hardware every ~20 ms.
FlushMicToCallback accumulates samples in FMicStageBuf and calls
FOnWebMic only when a full 512-sample block is ready. The remainder
stays in the buffer until the next Opus frame arrives. This ensures
FTXMicRing always receives data in multiples of 512, eliminating
the 0 < Avail < 512 dead-zone entirely.
FMicStageLen is reset together with FMicPreRollPos on new TX session
to prevent stale samples from a previous session mixing in.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WebOnMic: drop intermediate Singles[] array, read PSingle directly.
PushAudio: replace per-frame SetLength/array of Byte with pre-allocated
FWsAudioBuf field — eliminates heap allocation from the DSP thread.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove manual "Mic Source" (Radio/SoundCard) setting from UI and storage;
source is now determined automatically in ApplyMOX: HW PTT → txmsRadio,
MOX button/CAT → txmsSoundCard if audio device configured, else txmsRadio
- Replace "Line in" checkbox with "Mic jack" combo (Mic In / Line In);
when Line In is selected, Mic Boost is hidden and Line In Gain (-34.5..+12 dB,
step 1.5 dB) is shown and sent as DUC Specific byte 51
- Add Tip/Ring connector selector (Orion/OrionMkII boards 4/5 only, byte 50 bit 3);
control is hidden for other board types
- Fix byte 50 bit 1 (Mic Boost) suppressed when Line In is active
- Fix byte 51 comment polarity (was reversed: 0=+12dB; correct: 0=-34.5dB)
- New JSON fields: mic_line_in, mic_tip_ring, line_in_gain_db; mic_source removed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WebPageHtml.pas:
- Add MIC gain slider (0..300%) with localStorage persistence; inserts
Web Audio GainNode between MediaStreamSource and Opus encoder.
- Disable autoGainControl in getUserMedia so the manual gain actually works.
- Pre-create+resume AudioContext synchronously inside the MOX click handler
(before any await) so user-gesture context isn't lost during getUserMedia
on macOS/Android.
- Call startTXMic() from the click handler too — the call from applyState
runs in a WS-message context which mobile browsers reject.
- Re-resume txMicACtx on every MOX→TX (Android can suspend it between
cycles) and reuse the existing context instead of recreating per session.
- stopTXMic now a no-op: the previous flag reset created a race that spun
up duplicate pipelines on rapid RX→TX toggles.
WebServer.pas:
- Pre-roll cushion: hold first 40ms of decoded mic samples before flushing
to FTXMicRing, raising steady-state ring depth from ~9ms to ~50ms so
WiFi jitter no longer drains the buffer into silence ("robotic" voice).
- Wall-clock-based Opus PLC: synthesize up to 5 frames via
opus_decode_float(NULL) when packet gap exceeds expected 20ms cadence.
- A gap > 200ms is treated as a new TX session: pre-roll resets, PLC
is skipped (no useful decoder state).
- Local (non-web) mic paths are untouched — they bypass WebOnMic entirely.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
UserADC1 (bytes 55-56) carries current sensor data on the ANAN-8000DLE.
Formula from Thetis: amps = ((adc/4095 * 5000) - 360) / 120 (Voff=360mV,
Sens=120mV/A). Status bar now shows "Supply: 13.8V / 2.3A | PLL OK"
when current is available.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PA calibration per band (HF and VHF):
- Add VHFBandCal[0..7] in TGlobalSettings (saved as vhf_cal_0..7 in JSON)
- Default HF cal updated to Thetis HERMES defaults (38.8-41.3 dB per band)
- Default VHF cal = 56.2 dB (Thetis HERMES XVTR default)
- SettingsForm: second group in PA tab "VHF Calibration (Transverters)" with
8 spinboxes VHF0..VHF7; labels dynamically show XVTR slot ButtonText
when LoadXvtrSettings is called (e.g. "VHF0 (2m):")
TX drive formula (CalcDriveByte) rewritten to match Thetis exactly:
drive_byte = min(sqrt(10^((10*log10(pwr_W*1000) - Cal)/10) * 0.05) / 0.8, 1.0) * 1.02 * 255
Cal = PA gain in dB (not a percentage); matches Thetis verified output
(Cal=63.1 + slider=100% + PAMax=100W → drive_byte=16, verified against Thetis)
Old formula Pos/100*Cal/100*255 was wrong: treated dB gain as linear percent
Fix stale FDriveLevel not updated on band/XVTR switch:
- Startup: FCurrentBand must be set before CalcDriveByte (was reversed)
- RestoreBand: recalculate FDriveLevel before ApplyVfoA network update
- ActivateXvtrBand: recalculate after FCurrentXvtr changes
- ApplyVfoA: recalculate when VFO crosses HF band boundary
- FreqDispBChanged: same for VFO-B band changes
- ApplyMOX(True): safety recalculate at PTT engage time
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Web UI: stepSel.onchange now sends fm_step command to server when in FM
mode; option values stay in Hz so tuneStep works unchanged.
WebServer: add fm_step command handler and OnFMStep callback.
MainForm: wire WebOnFMStep -> SyncWebFMStep -> SetFMStep + SaveCurrentBand.
Web spectrum/ruler: vertical FM-step grid lines in drawSpec; FM-aligned
ticks and labels in drawRuler with auto label-density (short ticks between
labeled lines, measureText-based labelMult to prevent overlap).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When FM mode is active, vertical grid lines and frequency labels snap to
FM channel boundaries (FM_STEP_HZ) instead of the fixed 8-division layout.
Grid and label density auto-adapt: lines thinner than 4px are thinned,
labels are spaced so they never overlap (short tick shown between labeled
lines). Grid cache invalidates on center-freq, step, or span changes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>