Клик по «NNN kHz · DDCn» в шапке пана — кроме сетки rate теперь есть пункты
«→ DDC N»: пан переезжает на другой аппаратный DDC (MovePanDDC), при этом
движок, анализатор и слайсы не пересоздаются — перепрошивается только
hw-индекс. Пригодилось при отладке: конкретный DDC платы может вести себя
хуже соседей, и пан можно просто увести на другой.
Диапазон ограничен реальными DDC железа: MaxDDCIndex = 7 (DDC8+ openHPSDR
enable-бит принимает, но поток не отдаёт). Ограничение действует и в меню,
и в DDCFree, и в самом MovePanDDC.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ApplyAntPorts брал оба разъёма по центру приёма, поэтому при мультислайс-TX
(или split на другой диапазон) передача могла уйти в разъём RX-диапазона.
Теперь RX-разъём — по FCenterFreq, TX — по ActiveTXFreqHz (TX-слайс/TX-VFO);
TX вне VU-плана падает обратно на диапазон приёма. Пересчёт добавлен в
SetTxSlice, SetSplit и в SetMOX(True) как страховка перед выходом в эфир.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- шапка колонок: «RX»/«TX» по центру своей пары, номера разъёмов отдельной
строкой ниже (не налезают на рамку группы);
- строки трансвертеров пакуются подряд (раньше выключенные слоты занимали
место → дыра между ADS-B и QO-100), высота группы — по последней видимой;
- RX- и TX-пары разнесены, шире колонка названий, крупнее шаг строки;
- дим-заголовок «Transverters» над секцией слотов.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Второй разъём у AD936x-плат — это второй КАНАЛ чипа (не порт B первого), и в
1R1T-образе он выбирается мультиплексором AD9361: debug-параметры
adi,1rx-1tx-mode-use-rx-num/-tx-num + биты RX1/RX2, TX1/TX2 регистров 0x3/0x2
(как switch_rfinput.sh/switch_rfoutput.sh прошивки). rf_port_select тут ни при
чём — прошивка его к тому же блокирует.
- IIOBindings: debug-атрибуты + прямой доступ к регистрам.
- PlutoBackend: детект второго разъёма (2R2T-каналы либо мультиплексор + модель
платы), SetAntPorts(RxPort,TxPort), ApplyRxMux/ApplyTxMux; Caps.HasAntSwitch.
⚠ApplyGain(Force): драйвер пропускает запись gain_control_mode, если считает
режим неизменным, — а у нового канала AGC-регистры не запрограммированы (в
железе крутится auto-AGC, hardwaregain отвергается EOPNOTSUPP). Форсируем
перепрог через другой режим после смены разъёма и после ad9361_set_bb_rate.
- RadioBackend: TBackendCaps.HasAntSwitch + virtual SetAntPorts (HPSDR — no-op,
там антеннами рулит Alex).
- Settings: секция ant936x (разъём RX/TX на диапазон и на слот трансвертера);
hw-AGC (режим + manual gain) переехал в TBandSettings/TXvtrEntry — теперь он
свой на каждом диапазоне и трансвертере.
- RadioController: FAnt936x + ApplyAntPorts (RestoreBand/ActivateXvtr/
StartRunning); ApplyBandDSP восстанавливает hw-AGC диапазона.
- SettingsForm: вкладка Antenna — Alex-таблица (openHPSDR) либо таблица
разъёмов AD936x по диапазонам/трансвертерам.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- MainForm: TXOverlay главной вьюхи взводился по FTransmitting без учёта
источника TX → красной становилась полоса главного VFO даже при передаче
со слайса. Теперь TXOverlay = FTransmitting and (FTxSliceId = 0).
- VfoOverlay: публичный TxActive (FTx) — вьюхи знают, какой слайс передаёт.
- SpectrumView/SpectrumViewOpengl: полоса/края/несущая передающего слайса
красные (alpha 100, как у главного при TX), иначе цвет бейджа слайса.
- Фикс цвета TX-полосы в GL: $00E03030 при ColorToRGB (младший байт = R)
давал синий вместо красного; общая константа CLR_TX_BAND = $003030E0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
У слайса нет своего VFO B — кросс-частотный TX делается TX-бейджем слайса.
Бейдж SPLIT (и его хит-зона) рисуется лишь при IsMain; «×» флага слайса
переехал вплотную к TX. SPLIT главного гаснет, пока TX-источник — слайс
(ActiveTXFreqHz берёт частоту слайса, а не VFO B).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- RadioController: FTxSliceId (0=главный VFO, >0=слайс), SetTxSlice
эксклюзивно; ActiveTXFreqHz/ActiveTXMode с TX-слайса, режим применяется
в SetMOX(True) перед SetTXRun; TxBandIdx для PA-кал и safety DoNotTx
при кросс-банде; RemoveSlice сбрасывает TX-слайс.
- HPSDRNetwork: TX-антенна выбиралась по RX-бэнду → по TX-частоте
(TxAnt[FreqToBandIndex(TXFreqHz)]).
- MainForm/PanafallPanel: клик TX на флаге слайса → SetTxSlice, TX-бейджи
(янтарь=выбран, красный=передаёт), RefreshTxIndicators по rfTransmitting.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Селекторы диапазона/samplerate панов N переведены с нативного TPopupMenu на
своё меню в стиле приложения (одинаково на всех платформах, в цветовой теме).
- FlatPopupMenu.pas (new): TFlatPopupMenu — owner-draw меню как ДОЧЕРНИЙ контрол
формы (не top-level окно: Wayland центрирует свои top-level окна). Hover,
галка текущего, клавиатура (↑↓/Enter/Esc), закрытие по клику-вне (MouseCapture)
и потере фокуса. Позиционируется под лейблом-якорем в клиентских координатах.
- PanDisplayPopup: поповер «◑» тоже переведён с top-level формы на дочерний
контрол (центрировался на Wayland) — открывается под кнопкой, закрытие «×»
или повторным «◑».
- MainForm: band/rate-селекторы на FlatPopupMenu; свёртка повторяющегося
выбора темы (9 инлайнов) в CurrentAppTheme.
- PanafallPanel: курсор-«палец» кликабельных лейблов шапки переприменяется в
конце Build (флаги *Clickable могли выставляться до создания лейблов —
samplerate-лейбл не получал crHandPoint).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Кликабельный бейдж диапазона («20m») в шапке пана N → popup-меню КВ-бандов.
На выбор — по-флексовому: ретюн DDC на VfoA диапазона (из общего band-кэша,
как пан 0), активный приёмник садится на VfoA с mode/filter/AGC + zoom/pan
этого диапазона, прочие слайсы пана убираются (вне новой полосы). Только на
настоящих КВ (не XVTR, не Pluto).
- TPanafallPanel: FHeaderBandLabel + OnBandClick/BandClickable/SetHeaderBand;
каскад left-лейблов шапки вынесен в LayoutHeaderLabels (band/rate/adc без
дыр при скрытии).
- MainForm: OnPanBandClick/PanBandMenuItemClick/ApplyPanBand +
PanBandSelectorVisible (гейт КВ); band-лейбл в UpdatePanHeaders (по
FreqToBandIdx частоты DDC), тема в ApplyPanHeaderTheme.
Ограничение: per-slice AGC-Top в движке нет — подтягиваются
mode/filter/AGC-mode/zoom/pan, но не индивидуальный AGC-Top диапазона.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Сохранение и восстановление панадаптеров, слайсов и раскладки per-device,
раздельно по контексту: КВ ('hf') и каждый трансвертер ('xvtr_N') — паны
одного контекста не имеют смысла в другом.
Settings: секция "pans"/<ctx> под MAC (grid-режим/доли, на пан —
freq/rate/adc/zoom/pan/доли стека+колонки+сплиттер спектр/водопад, на слайс —
freq/mode/фильтр/agc/vol/mute/squelch/аудиоустройство). TSliceCfg/TPanCfg/
TPansConfig + Default/Save/LoadPans(Ctx).
MainForm: CreatePanCore (вынос ядра из OnAddPanClick), SavePanWorkspaceCtx/
RestorePanWorkspaceCtx/TearDownWorkspace, PanContextKey. Хук контроллера
OnBeforeStop (снимок до гашения DDC в StopRunning). Переходы ActivateXvtrBand/
DeactivateXvtr обёрнуты save-старого/teardown/restore-нового (гейт FRunning);
restore XVTR-панов после ActivateXvtr (FCurrentXvtr целевой ⇒ XvtrTranslate
верный). PanafallPanel.DestroyAllSliceFlags. Смена КВ-банда контекст не меняет.
Round-trip сериализации и разделения контекстов проверены автономно; на
экране не проверено.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Частота пана хранится ВИДИМОЙ (как FVfoA/FCenterFreq) и транслируется в
IF через XvtrTranslate только на границе с железом (AddPanDDC,
SetPanDDCFreq, SetPanDDCRate, SetPanADC) — раньше DDC пана получал
видимую частоту сырьём, а drag водопада пана N клампился пределами АЦП
(~61 МГц) вместо диапазона трансвертера.
Новое: controller.VisibleFreqBounds (диапазон активного XVTR | пределы
железа) для UI-клампа драга; RefreshPanDDCs — на входе/выходе XVTR и
правке LO/LOError клампит видимые центры живых панов, перешивает
freq-слова DDC и прижимает выпавшие слайсы к краям полосы захвата.
Подтверждено юзером на трансвертере.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Грид: пан 0 сверху во всю ширину, доп. паны колонками снизу; тумблер
«▦/▤» в ряду зума пана 0, вертикальные сплиттеры-призраки колонок
(FPanColShare), доля ряда FPanGridRowShare; автоткат в стек при <2 доп.
панах. Регион пана обобщён до прямоугольника (StackLeft/StackWidth).
Фиксы по экранному тесту:
- наезд панов: раздача высот и драг сплиттеров клампятся минимумом
содержимого пана (MinLayoutHeight/PanMinHeight, пан 0 += wideband);
- «блуждающие» кнопки после STOP: MakeFlatBtn даёт Owner=контейнер,
TPanafallPanel.Destroy теперь явно освобождает −/⌂/+/⊞/▦;
- жор CPU ~40%/пан: убран безусловный Invalidate зум-бара из тика
(SyncPanZoomUI), инвалидация только при реальной смене значений;
- пропорция спектр/водопад пана 0 не восстанавливалась после закрытия
панов: кламп ratio минимумами больше не пишется в FSplitterRatio.
Подтверждено юзером на экране: грид/стек/STOP/pop-out работают,
CPU со всеми панами 17-20%.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Движок: TPanChan.ZoomFactor/PanSlider, span-clip в ApplyPanAnalyzerSettings
(математика главного, ZOOM_LIMIT 100), SetPanZoomPan с no-op guard (SetAnalyzer
сбрасывает FFT-историю). Зум переживает смену rate/ADC пана.
Контроллер: формула окна вынесена в ViewWindowFor (главный GetViewWindow тоже
через неё), GetPanViewWindow (PanId=0 делегирует), PanZoomIn/Out по лестнице
кратностей с якорем на центре окна, PanZoomDefault, геттеры.
UI: TPanZoomEvent получил Sender; у панов N включён ряд зума, бар
диспетчеризуется по объекту, кнопки −/⌂/+ общие через PanOfZoomButton (матч по
идентичности — MakeFlatBtn создаёт кнопки с Owner=контейнер, PanFromSender их
не видит); SyncPanZoomUI/RefreshPanZoom, вся мышь панов зум-корректна через
GetPanViewWindow. Темизация баров панов N.
Смоук panzoomtest ALL PASS (пик тона совпадает с формулой окна ±0.6%),
подтверждено на экране.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1) LayoutPanStack прятал шапку пана 0 по числу застекованных панов, а wideband
ставится со сдвигом Pan0HeaderH по общему числу (вкл. плавающие) — спектр
накрывал линейку частот wideband. Теперь видимость шапки по PanCount.
2) Лейблы шапки наследовали шрифт формы (ParentFont) — в pop-out окне текст
был дефолтно-тёмным на тёмном. Темизация шапки вынесена в ApplyPanHeaderTheme
(зовётся из UpdatePanHeaders: создание пана/pop-out/dock), окну — шрифт формы.
3) Бейдж ADC-источника переименован A1/A2 → RX1/RX2.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ChangeSampleRate больше не делает полный Close/Open движка (стоп DSP-потока,
разрушение всех анализаторов, сброс общей IQ-очереди): гейт FMainGated+FMainLock
дропает только main-пакеты, пересоздаются только RXA_CHAN и слайсы пана 0,
анализаторы панов живут. Контроллер не сбрасывает FWDSPReady на время смены
(OnDDCIQ продолжает кормить паны). Общий код RXA — Open/CloseMainRXChannel.
Бонус: SetChannelState dmode=1 на idle-каналах зря ждал флаш ~130мс → dmode=0
в CloseSliceChannel/CloseMainRXChannel (каналы всегда idle по локам).
Итог: смена 331→120мс, пауза аудио пан-слайса 137→40мс. Смоук ratetest ALL PASS,
подтверждено на экране.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Создание пана: rate наследуется от главного с клампом ≤768к
(вне сетки → 384к); за сменой rate главного паны не следуют
- Клик по «NNN kHz» в шапке пана N → меню полной сетки железа
(PanRateAllowed = Caps.RatePresets ∩ кратные 48 кГц; захардкоженный
PAN_DDC_RATES удалён)
- WDSPEngine.SetPanRate: под FSliceLock пересбор аккумулятора/анализатора
пана + переоткрытие слайс-каналов на новом rate (state сохраняется)
- RadioController.SetPanDDCRate: движок → реконфиг DDC (SetPanDDC) →
кламп слайсов к новой полосе; FPanADC[] хранится (база под 3.4)
- Шапка: rate отдельным лейблом (у панов N кликабелен, акцент/рука)
Смоук-тест без железа ALL PASS (тон: спектр+аудио слайса живы через
384→192→768). Проверено юзером на экране.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Несколько панадаптеров в главном окне (кнопка «⊞», до MAX_PANS):
- TPanafallPanel: регион в стеке (StackTop/Height), шапка с «×»,
pan-aware ViewWindow, FirstSliceId; явный crArrow GL-канвам
(WA_NativeWindow-поверхность на Wayland навсегда наследует курсор
момента создания — прилипала «рука» кнопки «⊞»)
- MainForm: FPans[]/LayoutPanStack (доли + межпанные сплиттеры с живым
призраком), создание/закрытие панов (DDC+слайс+вьюха), пиксели панов
из display-потока под FPanCbLock, рендер по dirty в тике
- Мышь панов N: Ctrl+ЛКМ = слайс, drag маркера = ретюн слайса,
drag фона = live-ретюн DDC (гистерезис 4px), клик = тюн слайса пана
(PanClickTune, снап 100 Гц), флаг-диспетчер только на спектре
- Сплиттер спектр/водопад: drag-математика от региона пана
(FSplitTopOff/FSplitAvailH из Layout), OnSplitterMoved у панов N
- Паны живут в Run-сессии, умирают на STOP
Проверено юзером на экране (3 раунда фидбека) + e2e против hpsdrsim.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SetZoomPan вызывался из RestoreBand безусловно и переармировал RX-анализатор
(SetAnalyzer = сброс FFT-истории) даже когда zoom/pan не менялись — дисплей
замирал на время наполнения окна. Теперь no-op guard: без изменений выходим
без переарма.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- общая IQ-очередь с TIQQueueItem.PanId (64→256 слотов); DSP-поток
диспетчеризует пан-пакеты в PushPanItemToDSP: декод, feed анализатора
PAN_DISP_BASE+PanId, аккумулятор, ProcessSlicesFor(PanId) под FSliceLock
- пан-анализаторы: Create/DestroyPanAnalyzer + ApplyPanAnalyzerSettings
(rate пана, без зума — пан-зум в 3.3+), живут в OpenAnalyzer/CloseAnalyzer,
общие настройки дисплея перекатываются (ReapplyPanDisplaySettings)
- слайсы в панах: TSliceChan.PanId, AddSlice(..., APanId), OpenSliceChannel
от rate/блока своего пана, SliceDSPRateFor (WFM от rate пана); блок пана
= FAudioBufSize*Rate/FAudioRate => аудио-блок слайсов неизменен
- выдача пикселей: OnPanSpectrum/OnPanWaterfall(PanId,...) из display-потока,
GetPixels пана под FSliceLock, колбэки вне лока
- MAX_PANS=4 переехал в WDSPEngine (единый движок/контроллер/UI)
- контроллер: AddPanDDC создаёт пан в движке до включения DDC, RemovePanDDC
убирает слайсы (со звуковухами)+движок+DDC, гейт/NCO слайсов от центра
своего пана, ретюн пана двигает его слайсы
- смоук-тест на libwdsp без железа ALL PASS: пик спектра пана точно на месте,
слайс пана выдал 1 кГц RMS 0.68, главный тракт жив, RemovePan чисто
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- TBackendCaps: MaxPans/IndependentPanFreq (HPSDR 4/True, Pluto 1/False);
SetPanDDC/SetPanDDCFreq — виртуальные no-op в TRadioBackend
- HPSDRNetwork: ConfigureDDCs → кэш главного DDC + RebuildDDCSpecific
(главный + активные пан-DDC, каждый со своим rate/ADC); SendFullHP пишет
частотные слова всех активных пан-DDC; хелпер DDCBaseIndex; сброс
пан-состояния в Connect
- RadioController: MAX_PANS=4, PAN_DDC_RATES 192/384/768; AddPanDDC/
RemovePanDDC/SetPanDDCFreq/PanDDC* (аллокатор hw-индексов от FActiveDDC+1,
обратный маппинг FDDCPanMap); OnDDCIQ роутит пан-DDC → PushPanIQ;
паны гасятся в StopRunning, пул сбрасывается в StartRunning
- WDSPEngine: PushPanIQ — заглушка-дроп (аккумулятор/анализатор — этап 3.2)
- фикс: StartRunning не включал Saturn (BoardType 10) в список плат с
DDCBase=2 — на Saturn гейт FActiveDDC ждал бы IQ с DDC0
- doc/SLICES_PLAN.md: статус 3.1 + B1 проверена на экране
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Панель владеет коллекцией флагов B+ (создание/закрытие/заливка состояния из
контроллера), раскладкой LayoutFlags, диспетчером мыши флагов, тиком S-метров
(TickFlags) и видимостью флага A (UpdateMainFlagVisibility). Главный флаг A
создаёт MainForm (проводка семантических событий у него) и отдаёт через
AttachMainFlag; новые флаги проводятся через OnWireFlag. Роутинг по слайсу —
FPan.CurSliceId; закрытие «×» — RequestSliceClose/OnSliceClosed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TWebAdapter.SyncMode и TCATAdapter.SyncMode клампили индекс режима по
MODE_SAM (7), тогда как новые режимы дописаны в конец: DIGU=8, DIGL=9,
WFM=10. Команда на WFM/DIGI молча выходила из SyncMode, SetMode не
вызывался — приёмник оставался в прежнем режиме. У web-клиента окно
pend("mode") (600мс) истекало и UI откатывался на старый режим, что
выглядело как «переключение не успело по таймауту». Дело не в самплрейте.
Верхняя граница поднята до MODE_WFM.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WFM использует новые wfmd/wfmmod из нашего форка WDSP. Демодулятор —
квадратурный дискриминатор на dsp_rate, поэтому при штатных 48 кГц
девиация 75 кГц не помещается в полосу. В WFM цепь RXA поднимается до
WFMDSPRate (наибольший FAudioRate*2^k <= 192 кГц, делящий SampleRate
нацело), rsmpin децимирует вход сам. Выше 192 кГц смысла нет: энергия
демодулированного тона выходит на полку (33.5k при 192 кГц против 31.8k
при 576 кГц), а FFT дорожает.
Две ловушки WDSP, найденные по дороге:
* firmin.c: nfor = nc / size, idxmask = nfor - 1. Все fircore создаются
с nc = max(2048, dsp_size), поэтому смена dsp_size без RXASetNC даёт
nfor = 0 — пустой массив планов и падение в fftw_execute.
* fir.c/bandpass.c планируют с FFTW_PATIENT, а wisdom.c пишет в
wdspWisdom00 только степени двойки. При SampleRate 576 кГц наивный
dsp_size = in_size = 12288 требовал FFT на 24576 точек — FFTW мерил
план 25 секунд. Отсюда потолок 192 кГц и dsp_size = степень двойки.
RXA и TXA гейтятся раздельно: рейты TXA (48 кГц вход, 192 кГц выход) от
SampleRate не зависят, поэтому wfmmod уходит на широкий dsp_rate даже
когда приёмник остался на 48 кГц.
Заодно исправлен WDSP_SAM: было 12, а RXA_SAM = 10. Раньше 12 было вне
диапазона и WDSP молча игнорировал (SAM работал как DSB); после появления
RXA_WFM = 12 это включало бы WFM-демодулятор вместо SAM. txaMode
расходится с rxaMode после DRM (WFM: RXA 12, TXA 14) — отсюда ModeToWDSPTX.
DIGI-U/DIGI-L получили свою таблицу фильтров (3.0k…300) и полосу от нуля,
а не от среза на 100 Гц, как у SSB. В CAT они замаплены на коды Kenwood 6
(FSK) и 9 (FSK-R) — раньше эти коды сваливались в USB/LSB.
Новые индексы дописаны в конец (DIGU=8, DIGL=9, WFM=10): индекс режима
уходит в settings.json, band-cache, CAT и web-API. В VfoOverlay пришлось
развести хит-коды — режимы кодировались как -(I+1) и налезли на HIT_NR/
HIT_NB/HIT_SNB, а полосы кодировались значением BW, и WFM-овские
120000/160000/200000 налезли на HIT_AGC_PICK_BASE/HIT_DEV_BASE.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
backingScaleFactor главного экрана врёт, когда окно лежит на другом
мониторе. Спрашиваем масштаб у NSWindow того NSView, которому принадлежит
паинтбокс: это ровно то число, в котором Cocoa рисует эту канву, и оно
меняется само при перетаскивании окна между мониторами.
Objective-C по-прежнему живёт только в MacScale; наружу торчит
кроссплатформенная GetControlScale, которая на Windows/Linux сворачивается
в Result := 1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cocoa рисует канву в физических пикселях, TBitmap всегда 1x, поэтому кадр,
собранный в offscreen-битмап логического размера, растягивался вдвое.
PlatformUtils.GetScreenScale возвращает 2 на Retina и 1 на остальных
платформах. Масштаб — обычная переменная, а не условная компиляция: на
Windows/Linux умножение на 1 даёт прежние числа, а на канву кадр кладётся
через Draw, как и раньше (StretchDraw только при масштабе > 1).
В SMeterView, RulerView и PanZoomBar битмап растёт в S раз, пиксельные
константы, размеры шрифтов и толщина пера умножаются на S. Перо важно
отдельно: иначе на Retina все риски стали бы вдвое тоньше.
RulerView кэширует битмап, поэтому смена масштаба сбрасывает кэш.
PanZoomBar.ThumbGeom получил параметр AScale: отрисовка зовёт его с
физической шириной, хит-тест мыши — с логической, код мыши не менялся.
Objective-C изолирован в MacScale.pas: {$MODESWITCH OBJECTIVEC1} несовместим
с {$MODE Delphi}, а DWARF-3 (-gw3, Debug-режим lazbuild) роняет FPC 3.2.4 с
internal error 200609171 на любых Objective-C типах — отсюда {$DEBUGINFO OFF}.
Замер после правки: 37.5% CPU против 38% базовых, отрисовка этих трёх
элементов ниже уровня шума в профиле.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Бандл под LaunchServices не может передать argv, поэтому GL-режим
включался через bash-обёртку в CFBundleExecutable. Теперь тот же
переключатель читается из окружения, что позволяет задать его в
Info.plist через LSEnvironment и обойтись без обёртки.
Разбор аргументов командной строки не изменён.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
На каждый сэмпл (576к/с при 576 кГц) цикл делал заметно больше работы, чем
требовалось:
- IR * SCALE и QR * SCALE вычислялись ТРИЖДЫ — в аргументы FeedDisplaySample,
в FBeaconDec.Feed и при записи в FRXAcc;
- FeedDisplaySample был невстроенным вызовом метода ради двух записей в буфер,
то есть 576 тысяч вызовов в секунду;
- FAnalyzerOpen и FBeaconDec перечитывались из полей на каждой итерации, хотя
внутри цикла не меняются;
- индекс считался умножением Pos := i * 6.
Теперь DI/DQ считаются один раз, FeedDisplaySample развёрнут в цикл (и удалён
как мёртвый код — других вызывающих не было), флаг анализатора и декодер маяка
лежат в локальных, а по данным идёт бегущий указатель.
Замерено зонным профайлером в режиме CPU-времени потока: dsp.push_iq avg
12.5 -> 9.3 мкс, доля ядра 3.0% -> 2.3%.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ReadTelemetry делал 5-6 синхронных iio_channel_attr_read_* — каждое это USB
round-trip, вместе ~30 мс. Звался из MeterTimerTick, то есть прямо в главном
потоке, дважды в секунду (POLL_MS=500). Замер зонным профайлером: тик метра
занимал 5633 мкс настенного времени при пиках до 51 мс, тогда как CPU-времени
в нём было всего 140 мкс — чистое ожидание USB, ронявшее кадры.
Опрос переехал в TPlutoTeleThread: он раз в 500 мс читает те же атрибуты и
публикует снимок под FTeleLock. ReadTelemetry теперь только отдаёт снимок и
никогда не блокирует, поэтому RadioController.ServicePlutoTelemetry и
MainForm.MeterTimerTick не менялись — вся фильтрация температуры на месте.
Прежнее блокирующее тело стало PollTelemetryBlocking (только из потока).
Конкуренция за контекст libiio не выросла: UI-поток и раньше читал атрибуты
параллельно с iio_buffer_refill в RX-потоке. Сменился лишь тот, кто ждёт.
StopTelemetry идёт первой строкой Disconnect — до iio_context_destroy, иначе
поток продолжил бы читать атрибуты разрушенного контекста.
Результат: ui.meter_tick 5633 -> 110 мкс. Значение телеметрии теперь может
отставать до секунды (троттл контроллера + период потока) — на 10-герцовом
индикаторе температуры это незаметно.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Полоса фильтра, края и линия несущей теперь берут цвет из
SliceColor(SliceLetter) вместо захардкоженного янтарного, который
совпадал с бейджем слайса B. Каждый слайс (B/C/D…) получает свой оттенок.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
В fly-out DSP появляется ряд SQL (тумблер + порог 0..100) только в режиме FM.
Squelch работает per-slice: у слайсов свои WDSP-каналы, поэтому добавлены
поля/сеттеры FMSQ в TSliceChan/TCtrlSlice, ApplyFMSQToChan и восстановление
при переоткрытии канала. Роутинг события по SliceId (0=RXA, иначе слайс).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace OpenGLContextEx with OpenGLContext in MainForm, SpectrumViewOpengl, WaterfallViewOpengl, and WidebandView.
Switch the Lazarus project/program dependencies from LazOpenGLContextEx to LazOpenGLContext in ewsdr.lpi and ewsdr.lpr.
PixelScale was introduced in 092a202; restore the previous viewport/texture path: glViewport(0, 0, W, H), EnsureTexture(W, H), and UploadCurrentRow(W).
Две сохраняемые громкости: FVolume (RX) и FTxMonVolume (self-monitor
даунлинка на передаче, дефолт 40). Контекст монитора = TX + DUP + RX MUTE
off (тот же гейт, что аудио в движке). В этом контексте слайдер/SetVolume
правят FTxMonVolume, иначе FVolume; ActiveVolume — активная под контекст.
Автопереключение громкости RX<->TX-монитор на T/R (SetMOX) и при смене
RX MUTE / DUP во время передачи (ApplyActiveVolume). Обе персистятся
(tx_mon_volume в конфиге). Веб выровнен на ActiveVolume.
Фикс: слайдер громкости не синхронизировался с загруженным значением после
подключения (в rfDevice синкался только drive, volume пропущен) → слайдер
оставался на 70, движок брал сохранённое → «прыжок» при первом касании.
Добавлен TrkVolume.Position := ActiveVolume в rfDevice.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
RX MUTE (self-monitor даунлинка на передаче): кнопка теперь доступна на
ВСЕХ бэкендах, а не только Pluto/QO-100. Активна только в DUP; без DUP
кнопка disabled и звук на TX не проходит (движок гейтит по
FKeepRXDuringTX=FDisplayDuplex — двойная защита: аудио-гейт + RXA не
кормится). Добавлен RadioController.DisplayDuplex. TFlatButton теперь
рисует disabled-состояние приглушённым (текст/рамка к фону).
TX pre-roll 8144->6096 (2 блока подушки вместо 3): обоснование «нужно 3
блока» опиралось на щелчок ~0.5с после старта, который на деле оказался
bfo-глитчом тона (устранён f2d0519), а не underrun'ом подушки. Латентность
старта TX ~-10мс; startup-щелчков под нагрузкой DUP не слышно (подтверждено).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Всплески на водопаде + щелчки в аудио при TX (openHPSDR TUN/DUP):
периодический ~72° фазовый разрыв передаваемого PostGen-тона на границе
TX-блока, в среднем ~5 раз за сессию, привязан к фазе тона.
Причина: TX-канал открывался с bfo=0 (13-й параметр OpenChannel). При
bfo=0 fexchange0 читает выход из кольца r2 асинхронно (заполняет DSP-поток),
а r2_outidx продвигается на out_size каждый вызов; при рассинхроне DSP-потока
блок изредка читается со швом → разрыв фазы тона (err=0, амплитуда постоянна).
В DUP этот сплаттер принимается обратно = широкополосная строка на водопаде
и слышимый щелчок.
Диагностика A/B: pihpsdr на том же WDSP/радио и на той же частоте 750 Гц —
чист; единственное отличие в открытии TX-канала — bfo=1 (transmitter.c:1316).
При bfo=1 fexchange0 ждёт Sem_OutReady и отдаёт полный выходной блок
синхронно/фазонепрерывно (wdsp/iobuffs.c). Продюсер даёт ровно один вход-блок
на вызов → стойла нет.
Исключено прямыми зондами: рендер, NB/NB2, перегруз ADC, потеря пакетов DDC
(seq), переполнение RX-очереди, временно́й разрыв в принятом IQ, burst
catch-up продюсера, TX-пейсинг.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Два источника фабрикации данных в TX IQ-поток рисовали всплески на водопаде
при передаче (openHPSDR, особенно TUN/DUP):
1. HPSDRNetwork (sender): анти-старв zero-fill при пустой очереди впрыскивал
нулевые пакеты = 1.25мс тишины прямо в поток = разрыв данных = всплеск.
Теперь пустая очередь → просто ждём следующий блок (модель dl1ycf pihpsdr
txiq_thread: данные не инжектируются никогда; короткий разрыв покрывает
pre-roll в FPGA-FIFO радио). Убраны DUC_FIFO_LOW/REANCHOR/ZeroPkt.
2. WDSPEngine (продюсер TTXDSPThread): silence-инъекция при стойле mic
впрыскивала лишний блок сверх mic-темпа (для TUN он нёс PostGen-тон →
боковые/разрыв темпа). Продюсер теперь строго mic-гейтед.
Замерено на эфире: всплески при TUN 5-6/сессию → ~1. Остаточный редкий клик
по прямому замеру НЕ из TX-пейсинга (sender ровно на радиоклоке 800 пак/с
через supply-limiting, дропов/underrun нет) — источник отдельный (дисплей/RX),
разбор позже.
Windows-ветка sender'а не тронута. Pluto — свой TX-тракт (TPlutoTXThread +
libiio DMA), задета только общая silence-removal (push-поток сам добивает
нулями). Обе не перепроверены на железе.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Диагностика временным TXLog (снят) вскрыла цепочку проблем TX-тракта:
- Задержка ~200мс своего сигнала в DUP: за время приёма ринг PortAudio-
микрофона наполнялся до упора (9600 сэмплов) и на старте TX бёрстом
оседал стоячим запасом в DUC-очереди. Фикс: TAudioInput.Flush на RX→TX
(заодно убрана передача в эфир аудио, записанного до нажатия PTT).
- Всплески на TUN: (1) старт — радио начинало с пустого DUC-FIFO; (2) по
ходу — пейсинг со строгой каденцией держал запас FIFO у нуля, джиттер
3-6мс давал underrun; (3) клип: голос из бэклога складывался с PostGen-
тоном (стоит после ALC) поверх шкалы, а сам тон при mag=1.0 давал
выброс Гиббса x1.18 на старте. Фиксы: pre-roll PrimeDUCIQ (THROTTLE +
3 блока подушки в очереди — sender метрит непрерывно, FIFO радио прижат
2000..2400), анти-старв доливка нулями до REANCHOR при просадке,
TUN_TONE_MAG=0.83, мьют mic-входа TXA при активном тоне.
- Огрызок TUN-тона в эфир на первом блоке следующего TX: стоп канала
dmode=1 ждал фейд, который некому прокрутить (продюсер уже остановлен) —
канал стопился по таймауту Sleep(1)x100 с недобранным сигналом в
буферах. Это же было и «остаточным фризом ~106мс» GUI на отпускании
PTT: чистый таймаут, фейд никогда не исполнялся. Фикс: dmode=0 +
синхронный слив/продувка канала 4 нулевыми fexchange0 на стопе и
старте (<1мс, штатные down-slew и flush).
- Дисплей: ResetPixelBuffers(TX_DISP_ID) на старте TX — усредняющая
история TX-анализатора показывала спектр тона прошлой сессии.
- StopRunning при активном TUN не чистил DUC-очередь и не сбрасывал
FTuning/тон — закрыто.
Латентность тракта mic→радио теперь ~43мс (WDSP 11 + подушка 21 + FIFO
10.4). Проверено на эфире: q=0..30, fill/drops/underrun=0, clip=0,
клоки радио/ПК без дрейфа (micRx 48000/с).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TX-DSP поток (TTXDSPThread) больше не пересоздаётся на каждом T/R — он
создаётся один раз и живёт весь сеанс, на приём/передачу переключается
флагом FTXActive (поток при False просто Continue, не трогает канал/ring).
Тердаун перенесён в TWDSPEngine.Close. Раньше Terminate+WaitFor+Free на
каждом отпускании PTT давал фиксированные ~100мс пустого join-оверхеда
pthread_join, блокируя GUI-поток. Итог: 206→106мс, крупные фризы ушли.
Остаток ~106мс = плавный WDSP fade-out TXA (SetChannelState dmode=1) —
оставлен как есть (dmode=0 дал бы trailing key-click в эфир).
Попутно:
- MainForm: убраны прямые PbSpectrum.Invalidate из drag/wheel VFO/центра —
кадр рисует таймер по FSpectrumDirty (FPS-гейт; прямой Invalidate давал
GL-кадр на каждое событие мыши мимо гейта → треск в HDMI-звуке AMD APU).
- AudioOutput: const-секция после {$ENDIF} блока PA-констант.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- SPECTRUM_PIXELS 1024→4096 + настройка display_pixels (лимит точек
анализатора, SetDisplayResLimit); web-зеркало остаётся 1024 —
max-децимация в SnapshotPixels
- палитры водопада Classic/Inferno/Turbo + регулятор gamma (в LUT),
настройки wf_palette/wf_gamma, страница Waterfall → группа Rendering
- GL-водопад переведён на общий LUT/AGC-уровни базового класса
(убраны дубли ColorForDB/UpdateLevels)
- явные glViewport в GL-вьюхах умножаются на PixelScale (HiDPI),
текстура водопада — в физической ширине
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Пакет LazOpenGLContext заменён на LazOpenGLContextEx (форк в
../lazOpengl): Qt6-бэкенд работает поверх настоящего QOpenGLWidget
(своя C++-либа libqlclglwidget) вместо GLX-контекста на winId(),
поэтому --opengl теперь работает в нативной Wayland-сессии, а под
ws=qt6 открывается путь на Windows/macOS.
- uses OpenGLContext -> OpenGLContextEx (класс TOpenGLControl тот же)
- удалена заглушка QtPlatformAllowsOpenGLControls с советом про
XWayland: больше не нужна
- рантайм-зависимость: libqlclglwidget.so рядом с бинарником
(сборка: make -C ../lazOpengl/csrc install-app APPDIR=bin/x86_64-linux)
Проверено на живом эфире (QO-100): спектр/водопад через GL под
Wayland и xcb, прозрачность FBO-альфы починена в обёртке (forceOpaque).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Canvas на битмапе кадра больше не используется вообще (кроме редкого
ADC-overload пути с восстановлением альфы) — убраны все raw<->canvas
переходы, каждый из которых синкал весь битмап на Qt6 (~2мс):
- RawBegin/RawEnd: прямой доступ к пикселям через RawImage.Data +
BytesPerLine (bottom-up DIB win32 учтён отрицательным шагом)
- RawVLine/RawHLine (с пунктиром), RawFillRect, RawTriangleDown,
RawCurve (кривая = связные вертикальные сегменты по колонкам)
- кэш текстовых масок (TRawTextMask, LRU 48): строка рендерится canvas'ом
один раз белым-на-чёрном в свой мини-битмап, блитится raw с цветом;
все тексты кадра теперь Courier New
- конвертированы: маркер (метка без случайной цветной плашки), бикон-
маркеры, буквы слайсов, слайс-линии, AGC (пунктир 4/2 и 1/2, подпись
с фоном SpecFilter как раньше), края/VFO/TX-курсоры с треугольниками
- пофреймовый альфа-OR проход удалён: альфа чинится один раз в кэше
СЕТКИ после ребилда (canvas-текст там), кадр наследует её через memcpy
Дворд-блендеры (два 16-бит лейна на умножение, /256 вместо /255,
расхождение <0.4%) вместо побайтовых с div 255:
- BandPlanOverlay.CompositeStrip (~1мс -> ~0.2мс)
- SampleRateOverlay.CompositeCache (per-px альфа)
- VfoOverlay.BlendBitmapKey (keyed-магента дворд-сравнением)
- SpectrumView.DrawSpectrumGradient (source-лейны 1 раз на строку,
~1.6мс -> ~0.8мс)
Порог перестройки бэндплана: полпикселя вместо 1 Гц (на QO-100 с
decoder-lock центр подтюнивается непрерывно — был canvas-ребилд полоски
каждый кадр). Композиты внутри raw-лока (их BeginUpdate вложенные).
PerfLog: зона ui.composites отделена от ui.spec_alpha (=RawEnd).
Замер на эфире (Pluto, 576k, 52fps): кадр 8.3мс -> 1.67мс (5x),
со слайсом 2.1-2.3мс; UI-поток ~45% -> ~26% ядра, из них 15% — блиты
paint_spectrum/paint_waterfall (потолок Qt6-вывода). Визуально проверено.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Инструментация (ВРЕМЕННАЯ, не мержить в main):
- PerfLog.pas: зонные таймеры, отчёт раз в 5с в stderr (core% = доля ядра),
EWSDR_PERF=0 выкл, EWSDR_PERF_FILE=путь — дублировать в файл
- Замеры // PERF: сеть (DDC IQ/wideband), Pluto (rx_conv/tx_resamp/tx_fill),
DSP (push_iq/fexchange/slices/tx), аудио, UI (тики/draw/paint/gl_swap,
разбивка draw_spectrum по фазам)
- doc/PERF_PROFILING.md: карта зон, методика, выводы
Оптимизация CPU-рендера спектра (по результатам замеров на эфире):
диагноз — дорога не пиксельная работа, а чередование raw(ScanLine)/
canvas(QPainter) доступа к битмапу на Qt6 (~6 переходов за кадр, каждый
синкает весь битмап; полоса фильтра в тысячи пикселей стоила 4x дороже
полного memcpy кадра). DrawSpectrum перестроен в фазы calc->raw->canvas->raw
(2 перехода): FillBandRaw вместо Canvas.FillRect, DrawSliceFilterMarkers
разделён на raw-полосы + canvas-линии, композиты флагов и альфа-OR в
финальной raw-фазе. Кадр 8.3мс -> 6.1мс; spec_bands 1.87мс -> 0.03мс.
Альфа-OR проход на Qt6 подтверждён нужным (без него canvas-пиксели теряют
альфу и пропадают при блите), сам цикл ~0.08мс — оставлен; тумблер
EWSDR_ALPHA_FIX=0 только для экспериментов. Кисть перед AGC-подписями
теперь ставится явно (была остаточной после перестройки фаз).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Record grounded data-path analysis, per-DDC/RX2 deltas, the soft-vs-hard
WDSP channel split, preliminary decisions (4-6 hard DDCs, static first),
and the UI concept (edge markers, slices rail, ADC badge). Marked as a
work-in-progress draft.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Share one button paint routine: extract TFlatButton.Paint into
PaintFlatButton(canvas, rect, colors) and call it from both TFlatButton
and VfoOverlay.DrawButton — flag buttons now render identically to the
left panel (flat fill + border, no gloss), using theme Btn* colors.
Theme the whole flag card: the structural CLR_* palette (card bg,
border, text, dim, accents, freq, meter bg) is now derived from the
active TAppTheme via ApplyThemeColors, so flags follow light/dark like
the rest of the UI. Status accents (TX/SPLIT) stay fixed on purpose and
use a fixed dark text over their bright badges. Theme is pushed to
flags from ApplyDarkTheme and on creation via WireOverlayEvents.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Manual TextWidth/TextHeight centering pushed the glyph up-left on the
substituted monospace font (Linux). Let the widgetset center it with a
taCenter/tlCenter TTextStyle, fixing the A/×/SPLIT/TX badges.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Flags (A + B+) now lay out in one pass: each sits beside its own filter
band as before, but a left neighbour pushes the next flag right so they
never cross (forward pack + backward pull-in on right-edge overflow).
LayoutFlags replaces the per-flag positioning; PositionVfoOverlay/
PositionSliceFlags are thin wrappers.
Identity is shown by a letter drawn at the top of each filter band in
the spectrum (both CPU + GL renderers), so a flag pushed away from its
band is still traceable. Letters and the flag letter-badge share one
per-slice palette (SliceColor); F is violet, not red, to avoid clashing
with the TX indicator.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Slice flags (B+) were staggered by i*12 which barely helped with the
96px-tall card and looked untidy. Put every flag — main A and slices —
on one row. Overlap of near-frequency flags to be handled next (leader
lines).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Independent RX slices sharing one panadapter: each has its own
freq/mode/filter/AGC/NR-NB-SNB-ANF, S-meter, volume and sound card.
VFO A = main receiver; B+ are soft fan-out WDSP channels within the
captured span (ideal for Pluto/QO-100; HW DDC path is Stage 3).
- WDSPEngine: soft fan-out (TSliceChan, ProcessSlices in DSP thread,
per-slice NCO/mode/filter/AGC/NR/NB/SNB/ANF + S-meter, OnSliceAudio),
FSliceLock serialises add/remove/set vs DSP; reopened on rate change.
- RadioController: TCtrlSlice with its own TAudioOutput/device,
Add/Remove/SetSlice*, RefreshSliceShifts keeps slices on target Hz
across centre changes. Slice letters start at 'B' (A = main receiver).
- UI: per-slice flags (VfoOverlay) with letter badge/close, filter-band
markers in BOTH GL and CPU renderers, drag-retune, wheel; flags placed
beside the filter band with flip, like the main flag. Ctrl+click adds.
- Main flag (slice A) visible whenever the left panel is hidden OR any
slice exists; synced with panel-driven DSP/mode/AGC changes.
- Perf: S-meter decoupled from flag chrome cache (FMeterDirty +
meter-only RefreshMeter via EnsureRendered); InvalidateVfoOverlay
avoids global overlay-cache churn on meter ticks.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Список устройств вывода ограничен AUD_MAX_VIS=6 видимыми строками — панель
больше не растёт бесконечно и не уезжает за низ спектра. При большем числе
устройств справа появляется полоса прокрутки ▲/▼ (строки сужаются под
колонку стрелок), клик листает FAudScroll не закрывая панель; стрелки гаснут
на краях. ≤6 устройств — как раньше, без стрелок.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DrawButton теперь рисует стеклянную кнопку как ползунок зум-бара: тёмная
скруглённая база-фаска + вертикальный глянцевый градиент (ярче вверху,
темнее внизу) + блик сверху и тень снизу. Хелпер GlossColor масштабирует
яркость цвета. Только при ребилде кэша — CPU-паритет сохранён.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TX-индикатор теперь кликабельный (хит HIT_TX → колбэк OnTxSelect) — задел
под выбор TX-слайса в мультислайсах. Три состояния цвета:
- передача идёт → красный;
- слайс выбран для TX → янтарный (CLR_TX_SEL);
- не выбран → нейтральный.
Новое состояние FTxSel прокидывается через SetExtState (пока True — один
слайс всегда TX-слайс). MainForm.OnVfoOverlayTxSelect — заглушка с TODO под
мультислайсы.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PositionVfoOverlay считает обе кромки полосы фильтра и ставит накладку справа
от неё по умолчанию; если справа не влезает (маркер у правого края) — рисует
слева от левой кромки. Возврат на правую сторону автоматически, когда снова
помещается. Убран прежний clamp-оверлап с маркером.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- S-метр: убрана метка +60 (вылезала за правый край), шрифт подписей
5→7 (читаемость), подписи сдвинуты ниже тика. Бар оставлен компактным,
ряду добавлено 3px под крупные метки.
- Градиент S-метра привязан к уровню dBm (S1→S7→S9→S9+18), а не к длине
бара — сильные сигналы уходят в красный раньше.
- Бейджи A/SPLIT/TX: высота 15→12px (верх на месте, низ приподнят) —
текст по центру, больше зазор до S-метра.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Полный редизайн накладки поверх спектра: шапка (слайс-бейдж A/B, ширина
фильтра, кликабельный SPLIT, индикатор TX), метр-ряд (индикаторы NR/NB/ANF/
SNB + сигнал-бар), крупная частота + S, нижний бар с fly-out пикерами.
- Fly-out движок MODE/DSP/AUD (раскрываются под баром, карточка растёт):
MODE — сетка режимов 4×2 + фильтры; DSP — NR/NB/SNB/ANF + AGC; AUD —
выбор аудио-устройства вывода (задел под слайсы).
- Слайдер громкости (click+drag) + mute; SPLIT-toggle; live TX-индикатор.
- Все контролы подключены к RadioController (SetVolume/ToggleMute/SetSplit/
EnumOutputDevices→ApplyAudioDevice); wire в MainForm + HandleMouseUp, чтобы
клик по накладке не уходил в спектр.
- FormatFreq на Int64 (double накапливал ошибку в десятки Гц на ~10 ГГц QO-100).
- S-метр ниже по высоте + плавный градиент по уровню dBm: тёмно-зелёный→
зелёный→жёлтый→красный (красный к ~S9+18).
- Кэш+dirty рендер сохранён (CPU-паритет со старой накладкой), GL-путь через
DrawOverlayBitmap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Зум был per-band только для HF (TBandSettings). XVTR-слоты хранятся отдельно
(TXvtrEntry) и зум туда не попадал — при входе в трансвертер не восстанавливался.
- TXvtrEntry: LastZoomFactor/LastPanSlider + Default/Load/Save (JSON).
- SaveCurrentBand (XVTR-ветка): пишет зум/пан в слот.
- ActivateXvtr: восстанавливает зум слота + SetZoomPan (re-arm анализатора).
Покрывает все входы (кнопка, старт по LastXvtr, CAT/web).
- ApplyZoom: кэширует в активный слот — XVTR при активном трансверторе,
иначе HF-band.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Button/encoder indices follow Thetis MakeNewAndromedaDataset (our 0-based
index = Thetis Number - 1), verified against andromeda_test/AndDecode.pas.
The old map assumed sequential 0,1,2.. and truncated at 32; the real panel
sends sparse indices up to 47.
- AndromedaMap: AND_MAX_BUTTONS 32->48, renumbered button/encoder tables;
new actions abVFOAtoB/abVFOBtoA/abVFOLock/abStartStop/abShift.
- RadioController: VFO Lock (FVfoLock, rfVfoLock, SetVfoLock, gate in
TuneActiveBy).
- AndromedaPanel: wire A>B/B>A/SDR-ON/Lock; Shift band-keypad layer
(#28 latch, #29..39 -> SetBand 160m..6m, non-sticky); aiVFOLock/aiShift
indicators.
- Filter High/Low (#4/#5) left as width approximation per decision.
- doc/ANDROMEDA.md updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bidirectional Apache Labs Andromeda/G2 front-panel protocol on top of CAT.
Panel logic lives in dedicated units; existing files get only thin hooks.
New units:
- AndromedaMap.pas — data only: action enums + default button/encoder
layout tables and LED (ZZZI) numbers (mirroring the Thetis defaults)
- AndromedaPanel.pas — TAndromedaPanel: inbound panel events -> controller
actions (marshalled via Invoke), state subscription -> ZZZI push via SendProc
Hooks:
- CATEngine: parse ZZZD/ZZZU/ZZZE/ZZZP/ZZZS into generic TCATContext callbacks
- CATAdapter: creates the panel, routes engine callbacks to it, supplies the
SendProc to the Andromeda port, AddStateListener, ZZZS handshake
- CATSerial: Andromeda flag in port config + HasAndromeda/SendToAndromeda
- RadioController: AddStateListener (multicast, does not steal MainForm's
OnStateChanged)
- Settings/SettingsForm/MainForm: CATSerialAndromeda[0..3] + persistence + checkbox
First version: serial transport, hardcoded default mapping, LED indicators
(MOX/Tune/CTUN/VFO A-B/Split/NB/NR/SNB/ANF/Squelch). Doc: doc/ANDROMEDA.md.
TODO: default button mapping does not match the physical panel (hardcoded) —
to be fixed later; also ZZMF/LCD text, TCP G2V2 transport, configurable mapping.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Реализованы команды, ранее бывшие заглушками — функционал уже был в
TRadioController, но не пробрасывался в CAT:
- Split TX: ZZSP/FT/FR + split-байт в IF (новый RadioController.SetSplit)
- FM Squelch: SQ (0–255), ZZSQ (0–100%), ZZSO (on/off)
- FM CTCSS: CT/ZZTA (on), CN (1-based)/ZZTB (0-based тон)
- FM-репитер: OF (offset), OS (направление, с трансляцией Kenwood↔RPT_*)
- FM step: ZZST (00–03)
- CTUN: ZZCN; AGC-T/RF gain: ZZAR
- ANF: ZZNT (раньше ошибочно алиас NB)
- Шаги VFO: ZZAU/ZZBP (исправлен баг — были алиасы ZZFA/ZZAP), ZZSU
TCATContext расширен callback'ами (split, squelch, CTCSS, repeater, step,
AGC-T, CTUN); CATAdapter — геттеры/сеттеры/sync с маршалингом через Invoke.
Добавлен doc/CAT_STATUS.md — полный статус CAT и план работ.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SyncWebConnect (web-коннект на десктопной GUI) собирал устройство сам
только из IP+BoardType, оставляя Kind=0 (=bkHPSDR) и пустые URI/Serial.
Из-за этого сохранённый Pluto подключался как openHPSDR → UDP discovery
→ Hardware Timeout (web при этом оптимистично показывал connected /
Unknown Board). Десктоп-путь и демон уже шли через ResolveDevice.
Теперь SyncWebConnect тоже зовёт FController.ResolveDevice(IP) —
единый резолв (восстанавливает Kind/URI/Serial/BoardType/MAC из стора
по IP или URI) для всех трёх входов.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Заменил дропдаун+слайдер на одну кнопку RF AGC (с подписью текущего
режима). По клику — оверлей (стиль как FNP-попап) с кнопками
FAST/SLOW/HYBRID/MANUAL + ползунок GAIN 0–73 дБ, активный только в
MANUAL. Крестик/клик мимо закрывают; выбор режима оставляет оверлей
открытым (чтобы сразу подвинуть слайдер). Бэкенд не тронут — те же
команды set_rx_gain_mode/set_rx_gain и state-поля. Только WebPageHtml.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Прокинул в web-интерфейс управление, которого там не хватало (раньше
только отображение):
- RF-AGC (Pluto hw-gain): дропдаун режима FAST/SLOW/HYB/MAN + слайдер
GAIN 0–73 дБ (активен только в MAN, драг переводит в manual). Виден
по BackendCaps.HasHWGain (= Pluto подключён). Команды set_rx_gain_mode
/ set_rx_gain → SetRxGainMode / SetRxGain.
- RX MUTE (QO-100 self-monitor): toggle-кнопка рядом с BCN, видна по
InQO100, подсветка по состоянию. Команда set_rx_mute → SetRxMuteOnTx.
Гейтинг зеркалит десктоп. Работает в GUI-web-сервере и headless-демоне.
Проверено на железе пользователем.
WebServer: команды + state-поля (rf_agc_visible/rx_gain_mode/rx_gain_db,
rx_mute_visible/rx_mute_on) + SetRxGainStatus/SetRxMuteStatus.
WebAdapter: колбэки OnRxGain*/OnRxMute (Invoke на поток контроллера) +
публикация в PushState. MainForm + ewsdrd.lpr: привязка колбэков.
WebPageHtml: виджеты, JS-обработчики, updRfAgc/updRxMute, PEND_MAP.
Также актуализирован doc/PLUTO_INTEGRATION_PLAN.md: TX-тракт и режим
QO-100 помечены проверенными на железе; раскрыт TODO «XO ppm-калибровка»
(ручной TxLOOffset как текущий обходной путь, xo_correction/GPSDO как
постоянное решение).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
After tying the ruler font to the ruler height, the labels (drawn at a
fixed H/2-1 top) sat below center because the taller font extended
downward. Shorten the ticks (major H*0.34, minor H*0.20) and center the
label in the area beneath them using TextHeight, so the digits are
vertically centered at any DPI. Applies to both the normal grid and the
FM-step grid branch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ruler drew labels at a fixed Font.Size := 7, so on Windows display
scaling (125/150/200%) the text no longer matched the DPI-scaled ruler
height — labels came out too small or clipped. Tie the font height to
the ruler height (-Max(8, Round(H*0.45))) so labels scale with DPI,
matching the approach used by the new bandplan overlay.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a thin colored strip along the bottom of the spectrum (above the
ruler) showing the QO-100 narrow-band transponder bandplan: mode
segments (CW ONLY / NB DIGI / DIGI / SSB / MIXED MODES) plus the three
beacons (CW / PSK / EXP) as muted-red cells at their ranges.
- New unit BandPlanOverlay.pas (TBandPlanOverlay), modelled on
VfoOverlay: content rendered once into a cache bitmap, per-frame cost
is just a constant-alpha composite (CPU) / one textured quad (GL).
Cache invalidates only on view change (center/span/width) so it idles
while the QO-100 downlink display is static.
- Wired into both render paths: SpectrumView (CPU composite) and
SpectrumViewOpengl (texture, re-uploaded only when dirty).
- MainForm: SetView fed from SyncSpecViewFreq; visibility gated by
InQO100 (Pluto + full-duplex transponder), like RX MUTE / BEACON.
- DPI-robust: strip height scales with Screen.PixelsPerInch and the font
height is tied to the strip height (not point size), so labels always
fit at any Windows scaling.
- Segment/beacon frequencies and colors are a single editable table.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a per-operator toggle to hear your own downlink off the satellite
while transmitting (full-duplex self-monitor), and restrict QO-100-only
controls to the QO-100 mode.
- WDSPEngine: new MonitorRXDuringTX flag; audio gate becomes
`not FTXActive or (FKeepRXDuringTX and FMonitorRXDuringTX)` so RX audio
can pass to speakers during TX in full-duplex.
- Controller: FRxMuteOnTx (default True = mute RX on TX), SetRxMuteOnTx/
ApplyRxMuteToEngine, rfRxMuteOnTx notify; new InQO100 predicate
(Pluto + full-duplex XVTR slot) as the single gate for QO-100 UI.
- Settings: global RxMuteOnTx (rx_mute_on_tx, default True), persisted.
- MainForm: new RX MUTE button in TX panel next to DUP, visible only in
QO-100; BEACON button visibility tightened from "any transverter" to
InQO100 so it no longer shows in plain VHF/UHF modes.
- doc: plan updated (RF-AGC done, two temps, TX/RX locked split,
self-monitor, phase 6 done; remaining TODOs incl. web wiring).
Web intentionally left unchanged for now.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pluto exposes two temperature sensors: the AD9361 transceiver
(ad9361-phy temp0/input, milli-°C) and the Zynq SoC/FPGA (xadc temp0,
raw+offset+scale → C = (raw+offset)*scale/1000). Only the former was
read. Now both are read and shown as "Temp 37/53C" (chip/SoC) in the
desktop status bar and web UI.
- ReadTelemetry gains an out SocTempC param (base + Pluto override);
Pluto reads xadc via the IIO context.
- Controller stores FLastPlutoSocTempC; SoC temp validated with a
plausibility window (no slew-limit — stable local sensor, unlike the
AD9361 sensor that spikes during mid-conversion).
- Status formatting handles one or both temps in MainForm and WebAdapter.
- Widen desktop status-bar field 3 (150→190) so both temps + RSSI fit;
web layout unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The PLL column (also used for QO-100 beacon status on Pluto) was
minmax(70px,90px), clipping "BCN LOCK 105Hz /7dB". Widen to
minmax(120px,185px), matching the desktop field-7 width. Frontend only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the single tracked-beacon line with a filter band spanning the
beacon width, split into two halves (left/right Manchester lobes at
±400 Hz) with a central divider (carrier null) and band edges at
±450 Hz, matching the desktop dec-marker width. Reference frequency is
now a green dashed line. Frontend only (WebPageHtml).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirror the desktop QO-100 beacon lock into the web interface and make it
work in the headless daemon.
Backend:
- WebServer: set_beacon/beacon_seed commands (+OnBeacon/OnBeaconSeed
events), beacon state fields, SetBeaconStatus, and beacon_visible/on/
text/ref_hz/track_hz in BuildStateJson. beacon_seed carries 'frac'
(0..1 click position); absolute Hz is computed controller-side from
live FCenterFreq/FSpanHz (mirrors desktop PixelToFreq).
- WebAdapter: OnBeacon->SetBeaconLock, OnBeaconSeed->BeaconSeedAtHz;
PushState mirrors compact status (matches MainForm.BeaconStatusText)
into the PLL status cell when visible (Pluto + active XVTR).
- MainForm/ewsdrd: wire the two callbacks.
- ewsdrd: call ServiceBeaconLock in the main loop @~10Hz (FRunning+
FWDSPReady gated). Without this the lock loop never ran headless.
Frontend (WebPageHtml):
- BCN button (visible only on Pluto+XVTR), highlighted while locked.
- Seed-on-mousedown with immediate return (like desktop FormMouseDown):
no drag/set_center so the gesture never moves the center / tears IQ.
Armed (first click after BCN) or Shift, mirroring desktop arm/Shift.
- PLL status cell shows beacon status (field-7 parity with desktop).
- Ref (green) + tracked (orange) beacon markers on spectrum/waterfall.
Known issue (unresolved): after lock the beacon can slowly drift off and
drop to "BCN sync". Suspected residual-sign anti-drift or retune-timing
on the web/daemon path; needs on-air diagnostics. See memory
project_web_beacon for the investigation state.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The headless daemon never set the web server's FreqMhzDigits, so it
stayed at the constructor default (3 = up to 999 MHz) and VHF/UHF/SHF
frequencies didn't fit the web VFO display. The GUI mirrors this per-
device setting in ApplyFreqMhzDigits; the daemon now does the same on
connect (rfDevice) via PushFreqDigits, reading the value the controller
loaded into FLoadedGlobal.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fix: dragging/wheeling the spectrum in the web UI clamped frequency to a
hardcoded HF ceiling (vfoMax=60 MHz), so on Pluto VHF/UHF it snapped down
to 60 MHz and couldn't pan up (only the VFO wheel worked, via a different
limit). Push the backend's tuning range (BackendCaps.MinFreqHz/MaxFreqHz)
to the web alongside the band plan; JS vfoMin/vfoMax use freq_min/freq_max
from state. The transverter branch (XVTR_CUR>=0) is unchanged — it keeps
the wide-open range since the displayed RF freq is translated to IF and
clamped server-side.
Dead-code cleanup (audit of this session's work):
- SampleRateMode/TSampleRateMode: orphaned when the sample-rate
unification replaced the srmContinuous branch in ApplyBackendCapsToUI;
no readers left. Removed (+ now-unused PLUTO_MAX_SR).
- Pre-existing write-only/unused: TBackendCaps.MaxSampleRate,
SampleRateOverlay.SPAN_NAMES, MainForm.BAND_FREQ.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Web band selector was hardcoded to the HF plan, so on Pluto it showed
160m..6m instead of the VHF/UHF plan and highlighted the wrong entry
(band switching itself already worked: web sends idx, controller maps it
via the IsPluto plan). Mirror the sample-rate unification.
Single source of truth = BoardUtils -> controller:
- BoardUtils: TBandInfo/TBandPlanArray + BuildBandPlan(Pluto).
- TRadioController.BandPlan returns BuildBandPlan(IsPluto).
Both UIs derive from it:
- Desktop already plan-aware (RelabelBands + RestoreBand) - unchanged.
- Web: WebServer.SetBands + "bands" in state JSON; hosts push
BandPlan on connect/startup; JS builds the band selector from
state.bands (BAND_N/BAND_F now dynamic).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sample-rate (span) presets were duplicated in three places and none was
authoritative: HPSDR set hardcoded in the desktop overlay (SPAN_RATES),
Pluto set in backend caps, and a separate hardcoded list in the web JS.
The web showed HPSDR rates even on Pluto, and the web adapter silently
dropped any rate outside a hardcoded HPSDR whitelist (so Pluto-only rates
like 960k/2304k never switched).
Single source of truth = backend caps -> controller:
- HPSDRNetwork.Caps now fills RatePresets [48k..1536k] (was nil).
- RadioBackend: named type TBackendRateArray.
- TRadioController.SampleRatePresets returns BackendCaps.RatePresets.
Both UIs derive from it:
- Desktop: ApplyBackendCapsToUI always feeds the overlay from
SampleRatePresets (overlay no longer owns the HPSDR list).
- Web: WebServer.SetRatePresets + rate_presets in state JSON; hosts
(daemon OnState/startup, GUI ApplyBackendCapsToUI/startup) push
SampleRatePresets; JS builds span buttons dynamically from it.
- WebAdapter.SyncSpan validates against SampleRatePresets instead of a
hardcoded whitelist, so any backend rate is accepted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>