mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +00:00
867da5921e90cf5cdfb88ea60172f753d69d43f3
161
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
867da5921e |
chore(tx): детектор голодания снят — всплесков не видно, вернём при нужде
Прибор из
|
||
|
|
b173b7f542 |
feat(tx): тихий детектор голодания очереди TX
Всплеск, рождённый нашим трактом, невозможен без одного события: очередь DUC
простаивает дольше подушки отправителя (DUC_FIFO_THROTTLE = 2000 отсчётов при
192 кГц = 10.42 мс). Съём 2026-08-21 это показал прямо: шесть всплесков — шесть
простоев 10.3-23.5 мс, а паузы 8 мс и короче не дали ни одного. Значит счётчик
таких простоев — полный детектор «наш/не наш», и оператору больше не нужно
называть интервал на глаз.
Новый юнит TxHealth.pas. Включён всегда: переменной окружения нет намеренно —
прибор, который надо не забыть включить, к моменту дефекта выключен.
* поток отправителя DUC мерит каждый простой очереди на передаче — одно чтение
монотонных часов на ПЕРЕХОД, ни файлов, ни локов (обе ветки, win и unix);
* планировщик TCI публикует рядом состояние пейсинга (аванс, долг, в полёте,
квант, шаг, период пачек клиента) — простыми записями, без лока: числа
диагностические, а лок в потоке отправителя DUC недопустим для тайминга;
* сетевой поток считает бит HPS_FIFO_EMPTY из HP-статуса радио — поле ГЛУБИНЫ
FIFO на этой прошивке статично (256) и веры ему нет, а бит мы не читали вовсе;
* ★журнал пишет ТОЛЬКО потребитель — таймер UI (100 мс) и цикл демона через
TxHealthService. В тракте передачи диска нет по построению.
Последнее — правило, оплаченное регрессом: прошлый прибор (TxTrace) сбрасывал
дамп синхронно в хвосте SetMOX, и эти десятки миллисекунд закрывали гонку в
пейсинге TCI (лечение —
|
||
|
|
166fc65fc8 |
chore(tx): сняты зонды трассировки — пейсинг проверен на железе
Всплесков на передаче нет, инструмент своё отработал и больше не нужен в горячем пути. Снято ровно то, что ставил |
||
|
|
d2096976df |
fix(tci): всплески на передаче — TX-аудио просилось общим тиком 20 мс
Посреди передачи из MSHV на водопаде появлялись всплески своего сигнала. Цепочка: очередь DUC пустеет дольше подушки отправителя (DUC_FIFO_THROTTLE = 2000 отсчётов = 10.4 мс) → FIFO радио сохнет → модуляция обрывается → в эфире остаётся голая несущая на частоте гетеродина DUC, в стороне от тона ровно на звуковой сдвиг. Доказано pcap-съёмом: шесть всплесков в дампе — ровно столько, сколько видел оператор, и каждый стоит за паузой 10.3-23.5 мс, а паузы 8 мс и короче не дали ни одного. Виноват не клиент и не блокировка UI, а зернистость НАШЕГО запроса. Слой первый: PushTxChrono жил на общем тике сервера 20 мс, а просил блок клиента целиком (2048 отсчётов = 42.7 мс) — маркер выходил через два или три тика, то есть через 40 или 60 мс. Слой второй: MSHV отвечает пачками по 4-5 блоков раз в ~44 мс (STREAM_C = 4096 при 96 кГц), и мелкий квант этого не лечит — нужен запас не меньше пачки. Сделано: * квант запроса = один блок TXA (512 отсчётов движка), а не блок клиента; * свой поток-планировщик TxTickLoop с АБСОЛЮТНЫМИ дедлайнами (опоздание одного пробуждения не сдвигает сетку); общий тик маркеров больше не шлёт; * бухгалтерия Owed/InFlight в кадрах на канал, гасится по k до интерполятора; потолок долга обязан быть выше окна в полёте (TCI_TX_OWED_HEADROOM_Q), иначе связывающим становится он и подача падает до 58% реального времени при полностью исправном клиенте; * SendBinNow: маркеры пишутся в сокет напрямую под FWriteLock, минуя очередь (та выпускается лишь на пробуждении потока клиента, TCI_POLL_MS = 20 мс — вдвое больше кванта, и подача снова рвалась); * FReapLock: планировщик TX — новый поток, а правило «клиента освобождает только тик-поток» держалось на том, что им же он и пользуется; * аванс под зернистость клиента: измеряется по ПЕРИОДУ между пачками (размер пачки зависит от того, сколько мы запросили ⇒ положительная обратная связь), переживает конец передачи, умеет уменьшаться по выдержке TCI_TX_LEAD_DOWN_MS, потолок TCI_TX_LEAD_MAX_MS; * старт передачи: KickTxTick будит планировщика на фронте PTT, TxPreWarm шлёт один маркер ДО SetMOX (41 мс раздумий клиента накладываются на нашу же подготовку тракта) под гейтом «передатчик свободен и чужого источника нет», PrimeDUCIQ для источника TCI растянут до Max(6096, аванс×4) — путь микрофона радио, CW и web не затронут; * посев аванса TCI_TX_LEAD_DEF_MS = 50 мс, пока про клиента ничего не известно: обучение к первому осушению физически не успевает. Монотонные часы одного источника для всех потоков — PlatformUtils.MonotonicUs (абсолютные дедлайны не терпят часов, способных прыгнуть от NTP). На железе: опасных осушений посреди передачи НОЛЬ (было 12 за 11 с), четыре передачи из пяти вообще без единого, включая старт; прогон 15:21 чист везде, в том числе на первой передаче после подключения. Всплесков оператор больше не видит. Приборы: TxTrace.pas (EWSDR_TXTRACE=1) и стенд test/hpsdr — кольцевой tcpdump capture.sh, разбор дампа pcap_tx_scan.py, разбор трассы txtrace_scan.py. Стенд test/tci: часть F «Пейсинг TX», 260 проверок, провалов нет; живой клиент с рампой, RTT и потерями — test/tci/tx_chrono_bench.py. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gmVQnna1i4EbSZ2VGm6KD |
||
|
|
c2fa1da2b0 |
perf(dsp): молчащие слайсы считались от одного факта, что поднят TCI-сервер
RX_AUDIO по TCI снимается ДО громкости и мьюта, поэтому замьюченный слайс обязан продолжать считаться, пока его кто-то качает по сети — иначе клиент остаётся без звука ровно потому, что оператор убрал его у себя в комнате. Гейт в ProcessSlicesFor это и делал, но спрашивал ГЛОБАЛЬНЫЙ флаг «тапы аудио установлены», а его взводит SetTaps(True) на старте сервера. То есть галка «Enable TCI server» одна, без единого клиента, заставляла гонять полный fexchange0 по каждому молчащему слайсу — до шести штук, ~47 блоков в секунду на каждый, плюс два входа в критические секции на блок в DSP-потоке. Клиент, качающий один слайс из шести или вообще только IQ, оплачивал остальные так же. Флаг стал ПОСЛАЙСОВЫМ (TSliceChan.TapWanted, SetSliceTapWanted). Кто его выставляет — владелец потоков: TTCIAdapter.PushTapWants полностью пересчитывает картину по FStreams и раскладывает по слотам. Считаются только потоки RX_AUDIO: LINE_OUT и рекордер снимаются ПОСЛЕ мьюта, и на молчащем слайсе им достаётся тишина независимо от того, посчитали его или нет. Пересчёт зовётся отовсюду, где множество потоков или карта слайсов меняются: StartStream (синхронно и ДО ответа клиенту — тогда не теряется ни один блок), StopStream, DropClientStreams, DropDeadRxStreams, StopAllStreams и OnState по MapChanged — слот мог сменить хозяина, и разрешение обязано переехать вместе с ним. Список слушателей снимается под FStreamLock, и лок отпускается ДО похода в движок: DSP-поток входит в тап, уже держа FSliceLock движка, и берёт в нём FStreamLock — обратный порядок дал бы клин. Глобальные FAudioTapsActive/SetAudioTapsActive/PushAudioTapsActive удалены. FAudioTapCount остаётся: он по-прежнему гейтит сам вызов тапов (FireAudioTap) и сборку звука DMR. Из AttachEngineCallbacks флаг не восстанавливается намеренно — слайсы пересозданный движок не наследует вовсе. Заодно: StopStream завершал перебор через Exit прямо из-под try, минуя хвост процедуры, — заменено на Break. Стенд (+3 проверки, негативный контроль в обе стороны): мьют слайса не глушит RX_AUDIO по TCI; без потока молчащий слайс не считается вовсе (наблюдатель — обычный аудио-тап, который теперь права требовать демодуляции не имеет); попросили поток снова — слайс снова в работе. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
84b90b23c0 |
feat(tci): KEYER — чужой ключ очередью элементов, а не сетевыми фронтами
Последняя невыполненная команда протокола. Ключ к ней в том, что arg3 — длительность интервала, который ТОЛЬКО ЧТО кончился, а не начинающегося. Документ задаёт это алгоритмом: первое нажатие keyer:0,true,0, отпускание keyer:0,false,142 («посылка длилась 142 мс»), следующее нажатие keyer:0,true,58 («пауза длилась 58 мс»). Отсюда перевод: тип элемента — это состояние ключа ДО фронта, то есть обратное пришедшему; arg3 = 0 играть нечего. Почему не дёргать ключ по приходу пакета: приход говорит, что интервал кончился, а не сколько он длился, и манипуляция «по приходу» — это сетевой джиттер прямо в эфир, тот самый «пьяный матрос», ради которого третий аргумент в протоколе и появился. Новый TCWElemPlayer (CWMorse.pas) держит очередь элементов и играет их подряд по абсолютным дедлайнам: сумма длительностей равна времени у клиента, значит отставание постоянно (сеть + один элемент) и не накапливается. Очередь опустела — ключ отпускается и следующая пачка начинается с чистого дедлайна: элементы чередуются, искажения нет, зато оборвавшийся клиент не оставляет в эфире несущую. Контроллер: CWKeyerElement(Mark, Ms) ставит элемент в очередь, CWElemKey раздаёт фронты — чужая манипуляция это прямой ключ с точными длительностями, поэтому у Pluto она идёт во вход прямого ключа локального генератора (он сам поднимает сессию, рисует огибающую и сайдтон), а у openHPSDR в бит CWX прошивки (тем же путём идёт передача текста). Гейт CWTXActive, как у CWXSend; обрыв общий с текстом — касание манипулятора, снятие MOX и уход из телеграфа гасят чужую манипуляцию тем же CWXAbort. Передачу KEYER не поднимает: при break-in PTT даёт прошивка (или сессия генератора), без него оператор держит MOX сам. Адаптер: номер передатчика разбирается как у TRX (bad receiver / receiver is not running), захват §3.5 общий с TRX — передатчик один, и ключ держит тот же, кто держит эфир. Паузы обрезаются TCI_KEYER_GAP_MAX_MS = 1 с (пауза целиком прибавляется к отставанию от клиента, а дольше секунды — это «оператор задумался», и честнее догнать реальное время), посылки — 5 с. Стенд test/tci: 238 проверок (было 219). Новая часть C2 меряет ДЛИТЕЛЬНОСТИ по фронтам ключа (посылка 150 / пауза 60 / посылка 150, допуск 30 мс), проверяет отпускание на пустой очереди, старт следующей пачки без «догона» дедлайна, обрыв и нулевую длительность; в части D — разбор аргументов команды и сквозная проверка, что keyer:0,true,<мс> ключ не замыкает (это пауза), а keyer:0,false,<мс> замыкает. Негативный контроль на инверсию перевода. ★Локальный генератор вооружается только при живом устройстве, поэтому сквозная проверка подставляет FDevConnected/FRunning на время. doc/TCI.md: §2.6 описывает команду целиком, §3.1 и §4 переписаны под то, что из пары KEYER/TX_FOOTSWITCH остался только второй; попутно убран устаревший абзац §2.3 про «потоки — этап 2». На железе с настоящим ключом по сети ещё не гонялось. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
bb48f5d3fa |
feat(tci): приёмник = слот слайса; TRX/TUNE адресуют передатчик; стенд «как MSHV»
Модель приёмников переделана: приёмник TCI — это СЛОТ СЛАЙСА, а не панадаптер.
rx0 — главный тракт (каналы A/B = VFO A/B), rx N — слайс слота N−1, то есть
буквы B..G с флага, на каком бы пане он ни стоял. Панорама стала свойством
приёмника: от неё берутся DDS (у слайса только чтение) и поток IQ.
Почему: у Pluto панорама ровно одна (MaxPans = 1), и второй приёмник там
существует ТОЛЬКО как слайс главного пана — при нумерации по панам он был
недоступен вовсе, а TRX_COUNT навсегда равнялся единице. С другого конца —
клиенты: у MSHV в настройках всего «TCI Client rx1/rx2», то есть приёмники 0 и
1, третьего номера ввести некуда. Со слотами правило «первый созданный слайс =
приёмник 1» держится на любом железе: на openHPSDR слайс второго пана и на
Pluto слайс главного одинаково занимают слот B. Номер совпадает с буквой на
экране и с портом слайс-CAT. Цена: панорама без слайсов из TCI пропала, а
второй слайс пана перестал быть «каналом B» и стал своим приёмником — у канала
B в протоколе только частота, IF и громкость, у приёмника же всё.
TRX/TUNE раньше игнорировали arg1 (номер передатчика) целиком: клиент доп.
приёмника уводил в эфир слайс ОПЕРАТОРА — чужая частота, а с кросс-бандовым
мультислайс-TX и чужой диапазон, с чужими антенной и фильтрами; trx:9,true жал
PTT. Теперь номер разбирается и проверяется, приёмник N > 0 идёт через
RequestSliceTx (та же дверь, что у CAT-порта слайса: «в эфире только один» и
Auto TX), у контроллера появился параметр Tune для TUN тем же путём. Чужую
передачу не трогаем вовсе — ни источник модуляции, ни тон: SetMOX(True) поверх
идущей передачи не выходит рано, а заново выбирает микрофон. Хозяином эфира
клиент становится, только если передача началась именно от его команды, и
решает это Sync-метод в потоке контроллера (снимок «шла ли передача», взятый в
потоке клиента, врал: между разбором и исполнением влезает PTT оператора).
Разбор исходников MSHV (он фильтрует ВСЕ строки и бинарные блоки по номеру
приёмника) дал ещё три правки:
* ответ на TRX/TUNE адресуется номером АВТОРА, состояние в нём — «в эфире
именно твой слайс»; в рассылку идёт номер реально передающего;
* tx_enable рассылается каждому живому приёмнику со своим номером и входит в
картину нового приёмника — без этого у MSHV молча мёртвая PTT
(set_ptt начинается с `if (!tci_tx_enable) return;`);
* про несуществующий приёмник молчим целиком (LiveRx), в том числе на чтение:
ответ «vfo:1,0,0» MSHV принимал бы за конец инициализации.
Попутно, вне TCI: SendDUCSpecificFromSettings трогала FNetwork без Assigned, а
зовут её по любому PTT/TUN (SetMOX → SyncCWKeyer → она) — до подключения
устройства это была Access violation, у TCI её глотал обработчик команды.
Стенды: новый test/tci/mshv_sim.py — точная копия логики клиента MSHV, отвечает
на вопрос «почему он не подключается» одной строкой (на живом приложении
воспроизвёл ошибку инициализации для rx2 до правки). tcitest — 158/158, в
сквозном прогоне добавлено создание слайса на главном пане: он становится
приёмником 1, отвечает на vfo:1,0, слушается командой, отдаёт аудио с
receiver = 1 и замолкает после удаления.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
dc6f996e70 |
fix(tci): дефекты живого прогона — length аудио, маршруты тапов, MOX, рекордер, EOF сокета
Восемь дефектов, найденных прогоном настоящего TCI-клиента (три приёмника: NFM, DIGU, FMRAW) и его отчётом. 1. UI доп. панорам не перерисовывался: rfSliceState рассылался, но ветки в MainForm.OnControllerState не было (частоту несёт отдельный rfSliceFreq). 2. Stream.length у аудио — сэмплы НА КАНАЛ (§4.3), у IQ — вещественные отсчёты (§3.4: комплексных = length/channels). Было ×каналы везде, у стерео получалось вдвое больше. Развилка в TCIFillHeader + разбор TX-аудио в HandleBinary. 3+4. Дыры в маршрутах аудио движка: demod-тап звался только для DMR/FMRAW (у DIGU не было RX_AUDIO), а пост-громкостный — только для нецифровых (у FMRAW не было LINEOUT). Плюс мьют слайса больше не убивает RX_AUDIO: движку сообщают SetAudioTapsActive. 5. Клиент, поставивший TRX, уходил — MOX оставался. FTrxOwner + StopTxOf; TCIMicRequested снимается и по окончании любой передачи. 6. Гонка снятия IQ-тапа: SetIQTap(nil) возвращался раньше, чем DSP-поток выходил из вызова. FIQTapLock (порядок FSliceLock → FIQTapLock). 7. Рекордер был кольцом «последние N секунд», а §4.3 говорит про МАКСИМАЛЬНОЕ время записи с удалением по истечении. Переделан в линейный буфер с окном по часам от START. 8. TCIServer.HandleClient считал recv = 0 таймаутом: ноль — это EOF, errno при нём не трогается и несёт EAGAIN от прошлого истёкшего TCI_POLL_MS. Обычный TCP-разрыв без close-кадра не освобождал слот до остановки сервера, и после нескольких аварийных отключений новые клиенты упирались в TCI_MAX_CLIENTS. Теперь R = 0 рвёт связь безусловно, errno спрашивается только при R < 0. Попутно: MainForm.RecreateDSPEngine (смена sample rate до START) терял внутренние колбэки контроллера — введён AttachEngineCallbacks. Стенд test/tci заведён в репозиторий (run.sh, 126/126 зелёных, включая сквозной прогон через живой WDSP), доп. проверки на оба пути отключения клиента. doc/TCI.md приведена в соответствие: правило про recv = 0 в §1.1, единицы Stream.length, линейный буфер рекордера. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
de0830fd19 |
feat(tci): этап 2 — бинарные потоки IQ/аудио, TX-аудио и запись линейного выхода
Реализованы все четыре потока §3.4 и рекордер:
• RX_AUDIO_STREAM — тап ДО громкости и мьюта (OnDemodAudioReady): скиммеру
и цифре нужен звук приёмника, а не то, что осталось после ручки;
• LINEOUT_STREAM — тап ПОСЛЕ (OnAudioReady), то есть что слышно;
• IQ_STREAM — тап сырого IQ в движке, ОДИН вызов на накопленный блок;
• TX_AUDIO_STREAM + TX_CHRONO — TRX:0,true,tci берёт модуляцию из потока
клиента (флаг TCIMicRequested впереди web в SetMOX), маркеры времени идут
из тика по часам, аудио клиента разворачивается в 48 кГц моно в тот же
ринг, что и web-микрофон;
• LINE_OUT_RECORDER_* — кольцо int16 на приёмник, WAV пишет отдельный поток.
Тапы аудио в контроллере многоадресные (AddAudioTap): слушают, ничего не
забирая, в отличие от OnAudioConsume, которым владеет web. Блоки нарезает и
раскладывает по кольцам клиентов сам DSP-поток, в сокет пишет поток клиента —
та же дисциплина, что у команд. Порядок локов везде FSliceLock → FStreamLock.
У очереди команд и кольца блоков разная политика переполнения: команду терять
нельзя, блок потока — можно (теряется самый старый).
Пересчёт частоты многоступенчатый (TCIStreams). Одноступенчатый FIR на верхнем
пресете Pluto (5760 кГц, коэффициент 120) упирался в потолок отводов и давал
завал 1.3 дБ в полосе при подавлении зеркала 16 дБ — то есть поток IQ с
мусором. Теперь коэффициент раскладывается на множители, спецификацию фильтра
каждой ступени задаёт ИТОГОВАЯ полоса, а свёртка идёт со сложением
симметричных пар: −83 дБ на любом коэффициенте, ≈10% ядра на 5.76 МГц.
Согласование частот с железом: из пресетов Pluto 576 и 960 кГц на 384 не
делятся, поэтому отдаём наибольшую ЗАКОННУЮ частоту, делящую источник нацело
(576/960 → 192 кГц). Ответ на IQ_SAMPLERATE называет достижимое, а не просьбу
клиента, и переобъявляется без запроса при смене rate и устройства.
Приёмный буфер соединения 4 → 32 КБ: блок TX-аудио это 64 байта заголовка плюс
data[16384], а кадр крупнее буфера не собирается никогда.
Настройки TCI переехали из Advanced на вкладку CAT, справа от TCP CAT Server:
это такой же канал внешнего управления трансивером.
Стенд (scratchpad, tcitest.pas): 112 проверок, все зелёные — включая сквозной
прогон через живой WDSP (синтетический IQ → блоки RX-аудио и IQ у настоящего
WS-клиента, и обратно TX-аудио клиента → блоки TX-IQ).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
4165cbe9a5 |
fix(tci): ревизия — потоки, валидация, арбитраж и синхронизация клиентов
Разбор семи проходов ревью ветки. Ниже — по сути, а не по списку. Потоки. Сетевые потоки больше не читают модель контроллера напрямую. Слайсы снимаются в потоке контроллера (RefreshSlices → FSliceSnap, на событиях rfSliceFreq/rfSliceState/rfDevice/…), железо — тоже (RefreshDev → TTCIDevSnap: имя платы, границы, число панов, HasTX). Копия TCtrlSlice из чужого потока портила счётчик ссылок managed-строк, а BackendCaps и BoardDisplayName смотрят в FNetwork, который UI освобождает на смене устройства. По той же причине ActiveTXFreqHz переведён на GetSliceView. Sync-методы читают живую таблицу: они уже в потоке контроллера. Жизненный цикл. Stop ждёт выхода клиентских потоков БЕЗ таймаута, прокачивая очередь Synchronize: выйти по таймауту нельзя — следом освобождаются и клиенты, и сам сервер. OnDisconnect зовётся и при остановке (иначе захваты параметров ушедших клиентов доживали до следующего запуска). Отправка переехала на поток самого клиента (recv с TCI_POLL_MS): общий поток задерживал всех на таймаут записи в один медленный сокет. WebUtils.SockSend шлёт с MSG_NOSIGNAL — SIGPIPE убивал headless-процесс. Транспорт. Слот протокола выдаётся только после Upgrade, а сокет до него живёт по таймауту handshake: восемь молчащих соединений закрывали дверь настоящим клиентам. Handshake с заголовком Origin получает 403 — авторизации в TCI нет, и без этого открытая вкладка браузера дотягивалась до TRX и VFO. Заголовки разбираются построчно, текстовые кадры проверяются на UTF-8, close длиной один байт отвергается, на close отвечаем close. Валидация. Все установки ходят через TCITryArg* — «vfo^0~0~abc» больше не превращается в честный ноль. Частота проверяется дважды: в потоке клиента по снимку и в SyncSetVfo/SyncSetCenter по живым границам (устройство успевают сменить между разбором и исполнением). Границы теперь из ОДНОГО источника (FreqLimits поверх VisibleFreqBounds) — тот же, что уходит в VFO_LIMITS; сами VFO_LIMITS переобъявляются при смене железа, и их кэш ведётся независимо от того, подключён ли кто-то. Слайс двигается только TuneSliceInBand, как у CAT: прямой SetSliceTarget уводил TX-слайс в DUC на чужой диапазон без антенн и фильтров. Параметры потоков сверяются со списками спецификации, а IQ_START и прочие запуски честно отвечают ошибкой вместо молчания. Синхронизация клиентов (§3.5). Появился захват параметра на 200 мс: два логгера больше не перетягивают частоту. Пачка инициализации уходит под FClientLock — изменение между строкой снимка и READY терялось навсегда. Глобальные величины (tune_drive, cw_macros_*, split_enable, mon_volume) рассылаются всем, а правки оператора приходят событиями: rfTXProfile, rfActiveVfo, rfMonVolume и новый rfCWSettings. Создание и удаление слайса рассылается по rfDevice (сравнение расстановки), у живого пана без слайсов канал A показывает центр — иначе клиент навсегда оставался с частотой удалённого слайса. Прочее. SliceFreqChanged переехал внутрь SetSliceTarget — один путь для мыши, CAT и TCI (перетаскивание флага мимо клиентов проходило молча). VOLUME и MON_VOLUME развели: SetVolume правит АКТИВНУЮ громкость, поэтому команда на DUP-передаче уезжала в монитор — добавлен адресный SetRxVolume. Настройки сохраняются только после успешного применения, при отказе поднимается прежний слушатель. Время спота — UTC. Подписки на измерители читаются и пишутся под локом клиента. Проверено стендом (сырой WS-клиент + живой TRadioController без железа): 73 проверки, включая изоляцию медленного клиента, остановку под Synchronize, арбитраж до и после 200 мс, отбраковку по живым границам и переобъявление VFO_LIMITS. На реальном железе и с реальным клиентом по-прежнему не гонялось. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
84c9e60b93 |
fix(tci): жизненный цикл, синхронизация слайсов и WebSocket по RFC
Разбор ревью ветки. Критичное — четыре отказа жизненного цикла и один пробел синхронизации. Use-after-free стора спотов: FTCIAdapter освобождается ДО FDXStore. Команда SPOT/SPOT_DELETE, пришедшая между их гибелью, обращалась к освобождённой памяти. Bind-адрес: TCIParseIPv4 стал строгим (out + Boolean, ровно четыре октета 0..255). Кривой адрес — отказ поднимать сокет, а не молчаливый INADDR_ANY: авторизации в TCI нет. В UI порт и адрес применяются по уходу фокуса и по Close, а не на каждую букву — набор «127.0.0.1» по дороге проходил через «127.0.0.» и открывал порт наружу. Остановка при висящем Synchronize: флаг Stopping (адаптер не начинает новых Invoke), прокачка CheckSynchronize в цикле ожидания Stop и запрет освобождать клиента, чей поток не вышел. Владение переделано: клиента освобождает только тик-поток (ReapClients), клиентский лишь помечает себя закрытым. Отправка больше не блокирует вызывающего: Send/Broadcast кладут строку в очередь клиента, в сокет пишет тик-поток вне общего лока, склеивая очередь в общие кадры. Медленный клиент морозил UI на таймаут отправки за каждое движение ручки VFO; теперь он просто вылетает. Слайсы: в контроллере появилось rfSliceState (нагрузка — FSliceFreqId), его шлют сами сеттеры слайса; SyncSetVfo зовёт SliceFreqChanged, как CAT. Адаптер разворачивает Id в пару (приёмник, канал) и рассылает состояние именно этого канала, а не канала 0 каждого пана. WebSocket по RFC 6455: маска обязательна, FIN/continuation собираются, RSV и незнакомые opcode рвут соединение, control-кадры ≤125 и только целиком, 64-битная длина не сворачивается в отрицательный Integer, пустой Sec-WebSocket-Key получает 400. Хвост пакета handshake больше не выбрасывается — первая команда не теряется. Клиент после исключения в разборе не остаётся висеть в массиве. Ещё: DSP и squelch доп. приёмников читаются и пишутся из TCtrlSlice (парные сеттеры сохраняли соседние поля значениями главного тракта); параметры потоков — в TTCIClient, они клиентские по спецификации; эхо под своим локом; ApplySettings возвращает результат, отказ старта виден оператору; инициализация объявляет только существующие каналы; SET_IN_FOCUS реализован через OnFocusRequest. Осознанно не сделано и записано в doc/TCI.md §3.1: AGC_GAIN для приёмников >0 (AGC-T один на тракт), цвет спота, KEYER, TX_FOOTSWITCH, арбитраж нескольких клиентов. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
82f0e4f771 |
feat(tci): TCI 2.0 — EWSDR как сервер (команды и уведомления)
Протокол Expert Electronics поверх WebSocket, порт 40001. EWSDR слушает, клиенты — логгеры, скиммеры, цифровые программы. - TCIProtocol.pas — чистый слой протокола: разбор/сборка `имя:арг;`, экранирование `^ ~ *`, словарь видов связи, пересчёт громкости в дБ. - TCIServer.pas — WS-сервер поверх WebUtils/WsClient: accept-поток, поток на клиента, HTTP-Upgrade, фреймы, рассылка, тик 20 мс. - TCIAdapter.pas — мост к TRadioController по схеме CAT: геттеры читают поля напрямую, сеттеры через Invoke, уведомления через AddStateListener. Маппинг: приёмник TCI = панадаптер, канал A/B = VFO A/B (пан 0) либо первый/второй слайс (паны 1..). Попутно: TRadioController.RemoveStateListener (адаптер умирает раньше контроллера) и TDXSpotStore.RemoveCall (spot_delete). Настройки — секция "tci" в settings.json (умолчание: выключено, 127.0.0.1, так как авторизации в протоколе нет) и вкладка Advanced → TCI Server. Бинарные потоки (IQ/аудио) — этап 2. Статус, таблица команд и список осознанных эхо-заглушек — в doc/TCI.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f7bed07831 |
refactor(controller): единая дверь SetTXSettings для правки TX-настроек
Применение TX-настроек (WDSP, DUC Specific при смене mic-битов/ATT, живое обновление TUN, запись в активный профиль, персист) жило в обработчике MainForm — то есть было недоступно ничему, кроме вкладки Transmit. CAT и web по архитектуре ходят только в контроллер и дотянуться туда не могли. Логика переехала в TRadioController.SetTXSettings по образцу уже существовавшего SetCWSettings; MainForm делегирует ей и оставляет себе только рендер. Notify=False у формы — иначе rfTXProfile перезагрузил бы вкладку Transmit прямо под руками у того, кто её правит. Заодно SetTXMonVolume: громкость self-monitor'а адресно, вне TX-контекста (слайдер добирается до неё только на передаче, CAT-клиенту такой контекст не нужен). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8569236214 |
feat(beacon): автопоиск маяка в окне лупы (кнопка AUTO)
Лупа сделала прицеливание возможным, но целиться всё равно надо руками. AUTO находит маяк сам, в текущем окне лупы: что видно, то и обыскивается — заодно зумом можно сузить область поиска. Критерий — не «самый громкий», иначе поймается первая же SSB-станция. Копим min-hold по кадрам лупы (~1.6 с): у маяка несущая непрерывна и минимум остаётся высоким, а речь, телеграф и цифра в паузах проваливаются к шуму и срезаются минимумом. Это тот самый «непрерывный след», который видно на водопаде, только числом. По накопленному: шумовой пол = медиана, кандидат = максимум скользящего среднего шириной с полосу приёма (±450 Гц) — среднее по полосе, а не отдельный бин, потому что маяк это плато ~900 Гц, и по такой мере он выигрывает у узкой пораженки с той же высотой пика. Порог 6 дБ над полом; центр уточняется центроидом превышения, чтобы наведение село на середину плато. - Тумблер рядом с FOLLOW, состояние в настройках (beacon_zoom_auto). - На локе автопоиск молчит: в контур не лезем. - После наведения выдержка ~3 с — дать декодеру попробовать захватиться. - Нашли там же, где уже стоит наведение (ближе 150 Гц) — не трогаем. BeaconSeedAtHz дёргает Reseed, и без этой проверки при слабом сигнале автопоиск сбрасывал бы захват по кругу, мешая декодеру сойтись. - В накопление идут только свежие кадры: таймер формы (25 Гц) быстрее анализатора (15 к/с), и повторный учёт кадра сокращал бы окно наблюдения, ради которого min-hold и заведён. В строке захвата появляется пометка AUTO — и только пока автопоиск реально работает: на локе она гаснет, чтобы не обещать действие, которого нет. Сборка ewsdr (--ws=qt6) и ewsdrd — ОК. На эфире не проверено. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
51599b2457 |
fix(beacon): контур принимал за маяк любую узкую несущую
Наводишься на соседнюю станцию — и контур честно берёт её за маяк и тащит LOError, подтягивая чужой сигнал к 10489.750. Решение принималось по CarrierLock и SNR, а CarrierLock = FCarPresent and FFreqLocked. FCarPresent — prominence сильнейшей линии в squaring-FFT: возведение в квадрат схлопывает модуляцию BPSK ±180° в одну линию на 2·fc, но такую же линию даёт любой сигнал с узкой составляющей — телеграф, пораженка, остаток несущей у цифры. FFreqLocked — Costas держит фазу, а на немодулированную несущую он садится идеально: ровный тон это BPSK без переходов. SNR считается как (E|I|)²/E[Q²], и захваченная Costas'ом несущая лежит целиком на оси I — Q пустой, SNR выходит отличный. То есть чужой сигнал не просто проходил порог, он выглядел лучше маяка. Различаем по констелляции: у настоящего BPSK точки ходят между двумя сгустками ±I (данные после свёрточного кодера равновероятны), у несущей все в одном. BeaconIQBalance считает долю меньшинства по знаку I, EMA сглаживает, порог 0.15 на взятие лока и 0.08 на сброс — гистерезис, как у SNR. Кольцо в 256 точек при 400 Bd набирается за ~0.6 с, EMA добавляет столько же: чужой сигнал отваливается примерно за секунду, а не за десяток, как ждать кадр FEC. FBeaconBal обнуляется вместе с FBeaconLocked при каждом наведении и при включении лока — после клика BPSK-ность доказывается заново, унаследовать её от прошлой цели нельзя. Скорость символов признаком служить не может: FWsym зажат в ±0.02, поэтому S.SymRate конструктивно всегда 392..408 Bd, что бы декодер ни слушал. Непрерывный цифровой сигнал с настоящей модуляцией баланс пройдёт — от него защитит только декодированный кадр AO-40. Сборка ewsdr (--ws=qt6) и ewsdrd — ОК. На эфире не проверено. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e82f573313 |
feat(beacon): окно лупы переживает перезапуск
Ширина, положение и состояние FOLLOW уезжали в дефолт при каждом открытии окна: настроил 5 кГц вокруг маяка, закрыл — в следующий раз снова 40 кГц. Положение хранится СДВИГОМ от опорной частоты маяка, а не абсолютом. Абсолют протух бы от любой правки опорной частоты, а главное — калибровка LOError копится от сеанса к сеансу и двигает шкалу, так что сохранённая абсолютная частота через неделю указывала бы уже не туда. - Settings: BeaconZoomSpanHz/OffsetHz/Follow в TGlobalSettings (ключи beacon_zoom_span/offset/follow), дефолты и чтение/запись по общему пути. - RadioController: FBcnZoom* с клампом при загрузке (span 600..200000 Гц, сдвиг ±500 кГц) — руками испорченный JSON окно не сломает. - BeaconScopeForm: RestoreZoomState в DoShow, StoreZoomState каждый тик из PollZoom. Раз состояние пишется в одном месте, оно не разъедется, каким бы путём ни менялось — колесом, drag'ом, кнопками или автоподтяжкой FOLLOW. Наведение декодера за краем восстановленного окна подтянет сам FOLLOW на первом тике, отдельной логики не потребовалось. Сборка ewsdr (--ws=qt6) и ewsdrd — ОК. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
705da75c26 |
feat(beacon): лупа наведения на маяк QO-100 в окне Beacon
На спектрограмме 576к центральный маяк — пара пикселей, ткнуть в него для
наведения декодера почти нельзя. В окне Beacon появилась карточка BEACON
TUNING: узкий кусок спектра вокруг маяка в высоком разрешении + мини-водопад,
клик по нему = BeaconSeedAtHz, ровно как клик по главному спектру. Наведение
с главной спектрограммы не тронуто.
Увеличение делает сам WDSP, а не растяжка бинов главного дисплея: второй
analyzer BCN_DISP_ID=4 на том же IQ главного тракта, со своей FFT и своим
span-clip. Клипы fscLin/fscHin считаются прямо из желаемых границ окна в
герцах, а не из зум-слайдера — анализатор умеет ассиметричный клип, поэтому
окно ставится в любое место полосы захвата, а не только вокруг центра DDC.
- WDSPEngine: SetBeaconZoom + GetBeaconZoom{Spectrum,Waterfall}. Analyzer живёт
только пока окно маяка открыто; весь доступ к disp'у под FBcnLock (SetAnalyzer
из UI, Spectrum0 из DSP-потока, GetPixels из display-потока). Кадр (1024
точки) фиксирован по размеру — ресайз окна не переармирует analyzer и не
морозит спектр на наполнение FFT-окна.
- Span-clip НЕ удешевляет FFT — она по всей полосе захвата. Отсюда потолок
262144 и своя пониженная частота кадров BCN_ZOOM_FPS=15 через ovrlp: на 60 fps
это было бы ядро под нагрузкой ради картинки, в которой ничего не меняется.
Переарм по смене sample rate, детектор/усреднение — без переарма.
- RadioController: SetBeaconZoomWindow / GetBeaconZoom* / GetCaptureWindow /
BeaconZoomBinHz. Наружу — абсолютные display-Гц, те же, в которых живут
BeaconSeedAtHz и маркеры главного спектра.
- BeaconScopeForm: клик = наведение, колесо = зум вокруг курсора, drag = пан,
двойной клик = центрировать, кнопки -/+/RESET/FOLLOW (FOLLOW подтягивает окно
только когда маркер ушёл за край, а не возит картинку постоянно). Маркеры —
теми же цветами и с той же семантикой, что DrawBeaconMarkersRaw/GL: опорная
пунктиром, центроид, три линии полосы приёма ±450 Гц, без полупрозрачной
заливки. Водопад — TWaterfallView с палитрой/гаммой из контроллера.
- Рендер по конвенции проекта: кадр лупы собирается в офскрин только по
FZoomDirty (новый кадр анализатора, сдвиг маркеров/окна, ресайз, тема), Paint
= блит + курсор. Констелляция с метриками переведена туда же: статичная
обвязка (карточки, оси, рамки и подписи плиток) кэшируется в FScopeChrome и
пересобирается лишь на ресайз/смену темы — раньше десяток RoundRect и TextOut
рисовались заново 25 раз в секунду прямо в обработчике Paint.
Лупа рисуется на CPU: GL-путь в проекте есть только у главного панафолла и
только под ключом, все прочие панели — обычные TPaintBox.
Сборка ewsdr (--ws=qt6) и ewsdrd — ОК. На железе не проверено.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
00860e5154 |
fix(slice-cat): перестройка слайса внутри полосы захвата обновляет флаг
TuneSliceInBand в быстрой ветке (цель уже внутри захваченной полосы) звал только SetSliceTarget и не слал ни одного Changed. Позицию флага UI держит сам (TickFlags -> LayoutFlags читает живой TargetHz), а частота и кромки фильтра живут в собственных полях оверлея и заливаются только из PushSliceFlagState. Итог: флаг уезжал на новую частоту, показывая старые цифры и старую полосу фильтра. Дальний QSY выглядел исправным лишь потому, что там двигалось окно DDC и приходил rfPanFreq с PushAllSliceFlagStates. Новое поле rfSliceFreq (+ FSliceFreqId как полезная нагрузка) и хелпер SliceFreqChanged: шлётся из быстрой ветки и из ветки пана 0 (rfCenterFreq флаги не перезаливает). MainForm обновляет только тронутый слайс. Тракт передачи не менялся: ActiveTXFreqHz и так берёт TargetHz слайса- источника, а SetSliceTarget сразу пушит сетевое состояние (DUC). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
b02b68aa6a |
feat(cw): терминал телеграфа — набор с клавиатуры и декодер приёма
Окно (ПКМ по CWL/CWU, F9, кнопка в настройках CW): одна лента, где принятое декодером и СВОЯ передача идут вперемешку — своё акцентным цветом. Разделять их нельзя: в QSK они перемежаются посреди фразы. Под лентой строка набора. Печать уходит в эфир ПОСИМВОЛЬНО, а не по Enter: строка показывает ровно то, что ещё не передано (очередь генератора), знаки уходят из неё по мере отправки, Backspace стирает с хвоста очереди — то, что уже звучит, вернуть нельзя. Ctrl работает манипулятором (левый точка, правый тире; у кейера прошивки лепестков нет — там это прямой ключ через бит CWX), по умолчанию выключено, чтобы Ctrl+C не уводил в эфир. Отпускание ключа ловится и на потере фокуса: иначе уход из окна с зажатым Ctrl оставил бы несущую в эфире навсегда. CWDecoder.pas: аудио → текст. Отвод берётся до громкости и мьюта — там нет программного сайдтона, зато уже отработал узкий CW-фильтр, лучшего предетектора не найти. Гёрцель гребёнкой из пяти бинов вокруг pitch (заодно показывает расстройку) → огибающая → адаптивный порог с гистерезисом → длительности → адаптивная точка → обратная таблица Морзе. Длительности живут в шагах анализа, а не в показаниях часов: разбор идёт пачками с таймера, и привязка ко времени вызова ломала бы тайминг на любой загрузке. Вылезло на тестах и учтено: пик обязан клампиться не ниже пола шума (иначе на старте порог уходит НИЖЕ шума и первым «знаком» читается собственный шум); порог дребезга берётся от текущей точки, фиксированный либо пропускает щелчки на медленной передаче, либо ест посылки на быстрой; длина посылки меряется за вычетом подтверждения дребезга, иначе скорость занижалась на 15%; расстройка запоминается только на полной амплитуде посылки, иначе индикатор пляшет. Граница честная: при вдвое неверной подсказке скорости теряется первое слово — пока не услышана настоящая точка, длина элемента неизвестна. Лента расшифровки продублирована строкой под спектром (пан 0), эхо передачи и очередь набора появились у обоих отправителей — и у локального генератора, и у кейера прошивки. ★TFlatEdit получил публичный CaretPos: он вставляет знак сам в UTF8KeyPress и гасит клавишу, поэтому OnKeyPress контрола не вызывается вовсе — из-за этого набранное «исчезало», а в эфир не уходило. Ввод перенесён на уровень формы. Проверено оффлайн: 12/20/40 WPM, расстройка, шум, слабый сигнал, цифры и знаки; плюс сквозной прогон против шести станций CW-стенда в hpsdrsim. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
25fbf10212 |
feat(cw): программный кейер — телеграф на Pluto/LibreSDR
У openHPSDR точки и тире формирует кейер в FPGA, и это остаётся путём по умолчанию: тайминги в железе, джиттер PC не влияет вовсе. У AD936x такого кейера нет вовсе, поэтому телеграфа на Pluto не было в принципе — голосовой TXA в CW не запускается, а несущую давать некому. CWKeyer.pas: TCWLocalKeyer рисует манипулированную несущую прямо в поток IQ, мимо WDSP (эталон pihpsdr cw_shape_buffer: в WDSP режим TXA_CWL — ветка SSB, да и PostGen не умеет огибающей). Длительности живут В СЭМПЛАХ, а не в миллисекундах: планировщик ОС влияет только на темп выдачи блоков, а его сглаживает FIFO бэкенда. Умеет текст, иамбик A/B и прямой ключ; TCWKeyPort читает манипулятор с модемных линий COM/USB-serial (DTR/RTS питают ключ, CTS/DSR/DCD/RI — лепестки). Оффлайн-прогон: PARIS занимает ровно 43 точки, все посылки и паузы 1/3/7 dit с точностью до сэмпла. Сессия = одна передача, а не одна посылка: PTT, реле T/R, антенна, OC и аттенюатор Pluto поднимаются один раз и держатся до конца hang — внутри манипуляция чисто цифровая. Предзаполнение FIFO задано железом: TX-поток Pluto наполняет буфер целиком (16384 пары ≈ 28 мс на 576k) и добивает нулями, чего не хватило. Сайдтон эту латентность не наследует — кольцо огибающей листается, если отставание больше 20 мс. Сдвиг несущей: у zero-IF ноль бейсбенда совпадает с частотой гетеродина, и там же его утечка — манипуляция на нуле дала бы backwave ровно на рабочей частоте. Поэтому несущая идёт на сдвиге, а гетеродин уезжает навстречу через новую единую дверь TXTuneFreqHz (все пуши TX-частоты переведены на неё). Знак (CW_TX_BB_SIGN) замерен на эфире, а не выведен. Развилка источника в UI — один список Keyer: firmware / software (PC) / off; хранится прежней парой флагов, и на железе без кейера выбор «прошивка» вырождается в программный, поэтому Pluto работает из коробки. Новое поле caps HasCWKeyer (раньше это неявно жило в HasHWMic). Попутно: у AD9361 calib_mode стоял manual_tx_quad, то есть драйвер не переигрывал калибровку TX quad (она нулит утечку гетеродина) на сменах TX LO — выставляем auto при подключении. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
827902f3e2 |
fix(xvtr): правки в трансвертере больше не травят память КВ-диапазона
При входе в слот трансвертера FCurrentBand не меняется — он продолжает указывать на тот КВ-диапазон, с которого зашли. Поэтому всё, что помнится по диапазону, из трансвертера писалось в чужую ячейку: модуляция, слот фильтра, режим и порог АРУ, CTUN. Поставил в трансвертере FM — 40 метров запомнили FM. Развилка «мы сейчас в трансвертере?» в проекте уже была, но стояла только у двух писателей из семи — у шагового аттенюатора и у зума. Их правили когда-то по симптому, отчего баг и возвращался: затыкали поле, а не класс. Заведена XvtrSlotActive, через неё идут все оставшиеся писатели. Слот трансвертера соответствующие поля уже имел (LastMode, LastFilterIdx, LastAGCMode, LastAGCTop, LastCTun) — они просто не заполнялись живыми правками, только снимком на выходе из слота. SpanHz в кэше оставлен как есть: поле помечено легаси, при восстановлении диапазона его никто не читает. Уже испорченные записи конфига правка не лечит: чтобы починить диапазон, надо встать на него, выставить нужную модуляцию и уйти — SaveCurrentBand перезапишет ячейку. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
45d10698ee |
fix(cw): запрет передачи действует и на кейер прошивки (RX-only XVTR, DoNotTx)
Аудит трансвертерного режима против дефектов этой сессии нашёл ту же болезнь, что и всё остальное: защита жила только на пути через софтовый MOX. Бэнд с DoNotTx (Alex) и RX-only слот трансвертера проверялись тремя копиями кода — в SetMOX, SetTune и SetTwoTone. С кейером в прошивке ни одна из них не исполняется: на приёмном трансвертере замыкание ключа выходило в эфир, а там на выходе обычно вход конвертера, а не антенна. Три копии сведены в TXProhibited; CWTXActive её учитывает, поэтому байт 5 обнуляется и кейер разоружается там, где передавать нельзя. Передача текста гейтится тем же условием. Вооружение сделано самовосстанавливающимся: SyncCWKeyer идемпотентен (пара сравнений плюс ранний выход внутри SetCWKeyerArmed) и вызывается ещё и из разбора HP-статуса. Разоружить кейер может заход в RX-only слот, переход на запрещённый бэнд или правка Alex; перечислять такие точки поимённо — гарантия однажды пропустить очередную, на чём уже дважды обожглись за сегодня. Остальное в трансвертере проверено и чисто: сдвиг pitch переживает XvtrTranslateTX (включая split-LO QO-100), множитель мощности слота и VHF-калибровка уже входят в CalcDriveByte и потому уезжают в байт 345, OC-выходы идут за ключом через RadioKeyed, ActivateXvtr толкает верную частоту DUC. Инвертирующие трансвертеры не поддержаны нигде в проекте — это давнее общее ограничение, не регресс телеграфа. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c6d1e409ff |
fix(cw): реле T/R и антенна за ключом прошивки + залипание HW-PTT
Отвязав FTransmitting от телеграфа, я оставил в приёмной конфигурации всё, что бэкенд считает от состояния передачи: CalcAlex0 бит 27 (реле T/R), выбор антенны (TxAnt вместо RxAnt), bypass/Ext1 и CalcOCBits, которым ключуется внешний усилитель. Прошивка гнала несущую, пока плата фильтров стояла на приём, и приёмник видел собственный передатчик — на водопаде это широкополосные полосы в моменты манипуляции. PushNetworkState отдаёт в UpdateState RadioKeyed, и он же вызывается на фронтах ключа. PTT в кадре этим не поднимается (он живёт в SendPTT), так что манипуляцией по-прежнему владеет FPGA. Выдержка RadioKeyed поднята до hang + 250 мс: по ней теперь щёлкает реле, и щёлкать оно обязано раз на передачу, а не на каждую посылку. FTuning из RadioKeyed убран — при TUN и так взведён FTransmitting, а в разборе SetTune флаги гаснут не разом. Второе: телеграфная ветка разбора статуса заканчивается Exit, и падающий фронт HW-PTT обработать было некому. Если в эфир увёл именно HW-PTT, пока источником передачи был не телеграф, а телеграф включился уже после, то передача залипала навсегда — FTransmitting взведён, Alex в TX, PTT не снимается. Подтверждено на железе: всплески появлялись после переключения передачи на слайс и оставались после возврата на главный. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0a1a1ee3af |
fix(cw): первый прогон на железе — частота, мощность, индикация, полоса
Семь дефектов, вскрытых проверкой в эфире. Общий корень у большинства: с кейером в прошивке FTransmitting не поднимается вовсе — передачей владеет FPGA, — а половина проекта считала этот флаг признаком эфира. Обрыв посылки после первого элемента. При break-in железо само поднимает T/R на каждую посылку, HPS_PTT это отражает, и разбор статуса гонял по фронту SetMOX(True/False); снятие MOX гасило CWX. Теперь в телеграфе фронт HPS_PTT не управляет передачей. Без этого и работа манипулятором пересобирала бы передающий тракт на каждой точке. Обрыв по «key hit» переведён на фронт лепестка: на плате без ключа входы могут стоять в единице, и проверка уровня убивала бы передачу сразу. Посылка уходила по центру дисплея, а не на VFO. StartRunning ставил DUC на центр DDC; до первой перестройки VFO или до первого MOX там и оставалось. Без CTUN центр совпадает с VFO, поэтому баг и дожил до телеграфа, где MOX не бывает. SetRunAndFreq получает TX-частоту, SetSliceTarget толкает кадр, когда перестраивают слайс-источник. Правило: DUC обязан стоять правильно всегда, а не только на передаче. Мощность ниже, чем на TUN: уровень (байт 345) клался в кадр только при FIsTransmitting. Введён SetCWKeyerArmed — пока кейер вооружён, уровень лежит в каждом HP-кадре. Там же пересчитывается FDriveLevel, иначе до первого касания ручки в железо уходил ноль. Индикация не показывала передачу: события rfTransmitting в телеграфе не бывает, поэтому блок рендера не выполнялся ни разу. ApplyHPStatus шлёт его сам на фронтах RadioKeyed («железо в эфире» = передача или замыкание ключа прошивкой, выдержка 8 точек и не меньше 500 мс). На RadioKeyed переведены S-метр, красная полоса главного VFO и TX-бейджи флагов вместе с красной полосой слайса на панадаптерах. Полоса на спектре рисовалась как SSB: TXSignedEdges обязана отдавать для CW голосовую боковую (через bp0 идёт тон pitch при настройке), поэтому экранные кромки развязаны в TXFilterEdgesHz — занимаемая полоса манипуляции симметрично несущей. Тот же класс, что был у ЧМ. F1..F8 молчали, пока не откроешь окно памяти: обработчик упирался в проверку лениво создаваемой формы. Текст берётся из настроек. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
53329e82e8 |
feat(cw): телеграф — FPGA-кейер, pitch, сайдтон, передача текста
CW не работал ни на приём, ни на передачу: фильтр стоял симметрично вокруг нуля (тона не было), понятия pitch не существовало, MOX в CWL отправлял в эфир голос 2.9 кГц (в WDSP TXA_CWL — ветка SSB), байт CW-опций DUC всегда был нулевым, поэтому кейер, сайдтон и break-in в прошивке молчали. Pitch сделан ОДНИМ сдвигом в движке. Кромки фильтра везде остаются относительно VFO (у CW — симметрично нулю), а гетеродин и полосу двигают три метода WDSPEngine: CWLOOffset / PushShift / PushPassband. Прямых вызовов SetRXAShiftFreq и RXASetPassband в проекте больше нет. Так таблица фильтров не перестраивается при смене pitch (в отличие от Thetis), полоска на спектре и маркер VFO верны без правок в UI, а слайсы получают свой сдвиг по своему режиму. Правила эфира: - голосовой TXA в CW не запускается вообще (гард по режиму), MOX = только PTT; несущую даёт кейер, бит CWX или тон TUN; - байт 5 собирается из настроек и перепосылается на каждой смене режима и TX-слайса — вне CW он обязан быть нулевым, иначе прошивка поднимет PTT на замыкание ключа посреди SSB; - TUN в CW: тон = pitch и встречный сдвиг DUC, несущая встаёт ровно на VFO; - приёмник на передаче в CW не глушится, иначе умолкает программный сайдтон и эфир между посылками. Программная передача текста (CWMorse): поток с абсолютными дедлайнами дёргает бит CWX в High Priority, элементы рисует прошивка. Касание манипулятора или снятие MOX обрывают передачу. Память сообщений — окно CW Messages и F1..F8 в главном окне. Оживлены CAT KS/KY/ZZKM/ZZKS/ZZKY. Настройки — вкладка Transmit, подвкладка Hardware, перед блоком FM/CTCSS. Программный сайдтон точен для передачи текста и прямого ключа; при иамбике таймингом владеет FPGA, поэтому там верен только аппаратный тон. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4e6703e590 |
fix(xvtr): TUN идёт от глобального уровня, мощность слота — множитель
При включённой опции TUN в трансвертере брал мощность слота ВМЕСТО глобального TUN Level, из-за чего ручка «Tune level» на вкладке Transmit в трансвертере была мёртвой, а настройка уходила в эфир на мощности слота (у 70cm и QO-100 — все 100 %). Теперь уровень всегда от TUN Level, а слотовая TX Power работает множителем: Pos = TUNLevel * слот/100 — та же формула, по которой в трансвертере уже масштабировался обычный drive. Смысл поля становится единым: «сколько мощности трансивера разрешено этому трансвертеру», и к настройке это относится так же, как к передаче. Галка сохранена (кому нужен неотмасштабированный TUN), но переименована в «Scale TUN by XVTR power» — прежнее название описывало снятое поведение. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
249881a579 |
fix(tx): профиль по модуляции — детерминированное правило + пробелы вызова
Три связанных правки по отзывам с эфира. 1. Активный профиль однозначно определяется парой «TX-источник + его модуляция»: ячейка таблицы заполнена — её профиль, пуста — базовый (первый в списке). Прежняя «точка возврата» (запомнить, что было активно до автоматики) ломалась в самом частом случае: заходя в FM, оператор обычно УЖЕ имел активным профиль FM с прошлого захода, точка возврата записывала его же, и выход в USB ничего не менял. Плюс она жила только в сессии и после перезапуска не работала вовсе. Поле FTXProfPreAuto удалено. Ручной выбор (дропдаун TX, список настроек, пилюля флага) дополнительно ЗАПОМИНАЕТСЯ за текущей парой — это и есть обещанное «профиль помнится по виду модуляции»: раньше в таблицу писала только пилюля, а выбор дропдауном следа не оставлял. В DMR и FMRAW автоматика активный профиль не трогает вовсе. 2. Смена диапазона и вход/выход трансвертера меняют режим главного в ApplyBandDSP (FMode := B.Mode) МИМО SetMode, поэтому автоматика там не вызывалась и при переходе КВ↔трансвертер оставался профиль прошлого режима. Зовём и оттуда. Заодно: FDSPEngine.SetMode насильно ставит FTXMode в режим главного, а SetMode контроллера следом возвращает режим источника — ApplyBandDSP этого не делала, и передача со слайса после смены диапазона уезжала в чужую модуляцию. 3. Кнопка выбора TX-профиля гаснет, когда режим ИСТОЧНИКА передачи DMR или FMRAW (в DMR передатчика нет, в FMRAW профиль обходится); открытый список закрывается. Гейт по источнику, а не по главному: с главным в DMR можно передавать со слайса. Смена режима на слайсе теперь тоже пересчитывает доступность TX-контролов — это чинит и MOX/TUN/PS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
36af031767 |
feat(tx): привязка профиля к паре «флаг + модуляция»
Одного значения на флаг мало: на слайсе с FT8 и на нём же голосом нужен разный звук, а слайс/главный меняют модуляцию на лету. Привязка стала таблицей TTXProfModeTable (режим → индекс профиля, -1 = не переключать), своя у каждого флага: MainByMode у главного (одна на устройство, общая для КВ и трансвертера) и TXProfByMode у слайса (сама разъезжается по контекстам — записи слайсов хранятся отдельно для КВ и трансвертера). Ключ — режим САМОГО ФЛАГА (FlagMode): у главного FMode, у слайса свой. Выбор в пилюле пишется в ячейку текущего режима, поэтому смена модуляции достаёт профиль, с которым в этом виде работали в прошлый раз. Применение — в трёх точках, все ДО пуша режима в WDSP (ApplyTXChainSettings внутри SetTXMode перешьёт цепь уже новыми значениями): SetTxSlice, SetMode (если передаём с главного) и SetSliceMode (если источник — этот слайс). Персист: tx_prof_modes в записи слайса, main_prof_modes в секции tx_profiles. Чтение защитное — индекс вне текущего списка профилей читается как -1. Round-trip проверен прогоном: обе таблицы переживают запись/чтение, незаданные режимы -1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
013d0406ac |
feat(tx): TX-профиль на слайс — пилюля в шапке флага
Передатчик один, профиль до сих пор был один на устройство: активный профиль звучал независимо от того, какой слайс взял передачу. С панадаптером на FT8 и вторым на FM это значит, что компрессия и EQ голосового профиля уходили в эфир на FT8 (FMRAW прикрыт сам — ApplyTXModeSettings глушит там всю обработку, а DIGU/DIGL нет). У Flex это решается дисциплиной и внешним API; делаем в радио. Привязка «источник передачи → профиль» (-1 = не переключать, звучит активный): TCtrlSlice.TXProfile для B..G, TTXProfileList.MainBind для главного (слайс A). Персист: ключ tx_profile в слайсе пана и main_bind в секции tx_profiles. SetTxSlice зовёт ApplyBoundTXProfile ДО пуша режима и цепи, иначе ApplyTXSettingsToDSP лёг бы на старый режим. Привязали источник, который передаёт прямо сейчас, — применяем сразу. Привязки — индексы, поэтому RemapTXProfileBindings: удаление профиля из середины снимает привязки на него и сдвигает те, что правее; Factory-сброс снимает все (список заменён целиком). UI: пилюля с именем профиля в первой строке флага сразу за шириной фильтра (ЛКМ — fly-out список, первая строка «As active»). Правая граница считается до SPLIT у главного и до RXM у слайса; не рисуем при пустом списке, в DMR (передачи нет) и в FMRAW (профиль там ни на что не влияет). Прокрутка списка общая с AUD — блок стрелок вынесен в DrawScrollArrows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8563c4e4cc |
feat(tx): заводские EQ-кривые ESSB, честная АЧХ в редакторе, фикс 3-полосного EQ
Ключ ко всему — семантика EQ в WDSP (wdsp/eq.c, eq_impulse + TXA.c): F[1..n] это УЗЛЫ ломаной АЧХ, между ними интерполяция по децибелам, а за крайними узлами при ctfmode=0 (так создаётся eqp у TXA, мы его не меняем) идёт кумулятивный скат (f/f0)^4 на каждый бин — фактически обрыв. Значит включённый EQ работает как второй полосовой фильтр, и верхний узел обязан лежать за верхней кромкой TX-фильтра. Settings: заводские ESSB несут свою кривую (AddEQ) — ESSB 3.5k узлы 100..3600, ESSB 5k узлы 100..5100, преамп в минус (EQ стоит до компрессора и ALC). EQ у Default/SSB DX/SSB Wide сбрасывается в заводской ноль (FlatEQ), а не наследует живой, иначе «сброс к заводским» не сбрасывал бы тембр. Сетка узлов вынесена в модульную DEF_EQ_FREQS. WDSPEngine: единая точка пуша EQ (PushTXEQProfile). Починен 3-полосный режим: раньше в WDSP уходили первые три узла 10-полосной сетки (100/200/400) и включённый EQ убивал весь голос выше 400 Гц; теперь legacy-раскладка самого WDSP (150/400/1500/6000, как SetTXAGrphEQ). EqualizerControl: кривая — точный порт eq_impulse вместо гауссовых горбов (сверено с C численно, расхождение 0.0 дБ), рисуется без клампа ±15, чтобы обрыв за крайними узлами был виден. В 3-полосном режиме ручки стоят на 150/1500/6000 и по частоте не таскаются, сетка профиля при этом цела. RadioController/SettingsForm/MainForm: кнопка Factory — пересборка заводского набора с применением Default. Без неё обновлённые заводские профили не доедут до тех, у кого секция tx_profiles уже записана: она читается, а не создаётся. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
71a97b63f0 |
feat(tx): TX-профили + вкладка Transmit на подвкладках
Профиль — именованный снимок «как я звучу», переключаемый одним кликом (идиома FlexRadio, состав по таблице TXProfile у Thetis). Граница «профиль ↔ устройство»: - в профиль: микрофонный вход (jack/boost/bias/PTT/Tip-Ring/Line-in gain), MicGain, кромки TX-фильтра, компрессор/leveler/ALC/phase rotator, EQ, AM carrier, Drive и Tune level; - у устройства остаются ATT on TX, FIR/фаза/окно фильтра (это задержка тракта, а не тембр), TUNFreq, FM/CTCSS, TX Display, TX Grid и все параметры PureSignal — смена профиля не должна перерисовывать спектр и ломать калибровку PS. Кнопки «сохранить» нет: активный профиль ведётся за живым состоянием (StoreActiveTXProfile из OnTXSettingsChange и SetDrive) — как и все остальные настройки в EWSDR, применяемые мгновенно. Settings.pas: TTXProfile/TTXProfileList, TXProfileCapture/TXProfileApply (единственные две точки, знающие состав профиля), DefaultTXProfiles (Default/SSB DX/SSB Wide/ESSB 3.5k/ESSB 5k — наследуют mic-вход и мощность от текущих настроек), Load/SaveTXProfiles (секция tx_profiles под MAC). Секции нет — заводской набор строится из текущих настроек, поэтому миграция старого конфига не меняет звук. RadioController: FTXProfiles, rfTXProfile, SelectTXProfile (снимок → DSP + перепосыл DUC Specific при смене mic-байта + SetDrive + персист), Add/Rename/Delete (последний профиль не удаляется). MainForm: кнопка профиля в свободных колонках ряда DUP/RX MUTE — блок TX не вырос по высоте; ЛКМ — список, ПКМ — Settings→Transmit→Profile. SettingsForm: страница Transmit разбита на подвкладки Profile / EQ / Hardware / Display + полоса профиля сверху; свиток 2000 px вместо этого раскладывается на четыре коротких экрана. Проверено: round-trip персиста отдельным тестом (все поля, активный индекс, усечение списка) и offscreen-снимки всех четырёх подвкладок. На железе не проверено. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
25521a2d10 |
feat(filter): произвольные кромки фильтра — таблица по режиму, редактор, drag, TX-полоска
Фильтр главного приёмника перестал быть одним числом. Теперь это таблица (Name, Low, High) на режим: 10 пресетов + VAR1/VAR2, знаковые кромки (у LSB своя строка, у USB своя), паритет с Thetis FilterForm. Слайсы независимые кромки умели и раньше — главный приёмник настраивался только пресетами. Модель (Settings/RadioController): - TFilterSlot/TFilterSet/TFilterTable + заводские дефолты из прежних FILT_*_BW и FILT_*_NAMES (переехали в Settings — из них строится таблица). - FFilterLo/FFilterHi = истина, FFilterBW производная (FilterBWFromEdges), поэтому все прежние читатели ширины (web, зум, band-cache, CAT) не тронуты. - ApplyFilterSlot — единственная точка «слот → состояние тракта». - SetFilterEdges: произвольные кромки уходят в VAR1 с автопереключением, пресеты нельзя испортить случайным движением мыши или энкодера. - Персист: секция "filters" под MAC, пишутся только режимы, отличные от заводских; секция режима сносится целиком, иначе возврат слота к заводскому оставлял бы старый ключ. - Диапазон помнит только выбранный слот: кромки живут в таблице, второго источника истины у VAR нет. UI: - 12 кнопок фильтра (2 ряда по 6), подписи из живой таблицы. - FilterPopup — поповер редактора по ПКМ на кнопке фильтра, по образцу PureSignalPopup (дочерний контрол формы, не top-level: Wayland). Слева слоты, справа Name/Low/High/Width, внизу форма полосы на знаковой оси. - Перетаскивание края полосы на спектре: ±5px, снап 10 Гц, Alt = симметрично (не Ctrl — он создаёт слайс). Отказ на передаче и когда полоса на экране уже 12px, иначе захват съедал бы клик-тюн. Отрисовка: - Полоса главного VFO рисуется по РЕАЛЬНЫМ кромкам приёмника, слайса — по его кромкам; вывод из режима и ширины остался фолбэком. Пара→ширина→пара было преобразованием с потерями: несимметричный фильтр из слайс-CAT (ZZFL/ZZFH) рисовался не там, где звучал. - На передаче полоса считается по TX-фильтру (TXSignedEdges), а не по приёмной ширине — TX-полоса от RX не зависит (модель Thetis). - CalcFilterBandXFor больше не держит свою копию таблицы знака боковой: единственная таблица теперь FilterEdgesFromBW. Управление: - CAT ZZFL/ZZFH заработали на основном порту (были заглушками). - Андромеда: энкодеры #4/#5 — независимые Filter High/Low, как на железе G2 (было: #4 ширина, #5 не реализован). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a820182716 |
fix(web): детализация спектра/водопада — честный размер кадра, поток 'W', настройка разрешения
Веб-пульт рисовал заметно грубее десктопа, и часть причин копилась давно. Клиент вообще не разбирал бинарный фрейм 0x57: водопад рисовался из кадра СПЕКТРА, а собственный поток водопада (свой детектор и усреднение wf_avg_time_ms) уходил в никуда, занимая половину трафика. Теперь водопад идёт из своего потока. Адаптер слал константу 1024 вместо реального числа точек. Разрешение анализатора = ширина панадаптера, и при узкой панели (грид из двух колонок, маленькое окно) кадр короче — хвост буфера, нули = 0 dBm, уезжал клиенту белой полосой, а частотная шкала сжималась. В демоне это обходили DAEMON_SPEC_WIDTH. Длина фрейма теперь равна числу заполненных точек; клиент и так считал N из byteLength. Децимация под потолок протокола делила «i*Count div N» — на нецелом отношении соседние группы получались по 1 и 2 точки, а смещение max-детектора зависит от размера группы: по спектру шла гребёнка ±3 дБ, в водопаде — вертикальная рябь. Заменено на целочисленный фактор (группы одинаковой ширины). Водопад терял 2 кадра из 3: анализатор даёт до 60 кадров/с, web шлёт 20 строк/с, а бралась просто «последняя». Введено двухступенчатое накопление max-hold (DSP-поток → PushState → PushLoop) по идиоме TWaterfallView.SetWaterfallData — короткие посылки CW/FT8 больше не проваливаются между строками. Отдельно исправлено в headless: PushState демона (20 Гц) и PushLoop (20 Гц) идут вразнобой, и на дрейфе фаз одна и та же строка уезжала клиенту дважды (водопад двоил и полз рывками), а на остановленном RX прокручивалась застывшая строка. Фрейм 'W' теперь отправляется только при реально пришедшем от DSP кадре (FWfAccumFresh → WfNew → FWfFresh). Спектр так не гейтится — это живая кривая. Рендер в браузере: значение бина для пикселя берётся максимумом по диапазону вместо «ближайшего» (узкие сигналы мерцали и пропадали на узком экране), при бинах реже пикселей — интерполяция; сглаживание спектра вынесено отдельным проходом по всем бинам (в цикле по пикселям часть бинов не обновлялась вовсе); строка водопада рисуется из сырых бинов, IIR ~200 мс остался только для слежения за уровнями AGC — он размазывал водопад по вертикали. Потолок точек кадра вынесен в настройку web.spec_pixels (Settings → Advanced → Web Server → Spectrum: 1024/2048/4096, дефолт 1024). Буферы кадра и WS-фреймы выросли под 4096 (WEB_SPEC_MAX, синхронно с WDSPEngine.SPECTRUM_PIXELS). В демоне эта же настройка задаёт ширину анализатора — рендера там нет, поэтому DAEMON_SPEC_WIDTH убран. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
041bc6ba7a |
feat(oc): OC Control — Open Collector выходы Penny/Alex (эталон Thetis)
Byte 1401 High Priority packet: per-band (HF 160m-6m) и per-slot (XVTR) RX/TX-маски на 7 пинов + TX pin action (MOX/TUNE/2TON и комбинации). Только openHPSDR — у Pluto/AD936x такого выхода нет, вкладка Settings скрывается на этом бэкенде. Настройки — вкладка "OC Control" в SettingsForm. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
20186ce132 |
feat(slices): аудио-устройства слайса на вкладке Slices (in/out на слот)
У каждого слайса B..G — свои Audio out / Audio in рядом с его CAT-портом.
Списки те же, что у главных комбо; пустой выбор (Default/None) = прежнее
поведение, общий выход/вход приложения.
Это устройство СЛОТА (буквы), а не шаблон:
• смена настройки применяется к слайсу, стоящему на слоте, немедленно
(SetSliceSlotAudio) — как и вся остальная форма настроек EWSDR;
• слайс, который встанет на слот позже (создание/restore из персиста),
получает то же устройство: слот сильнее аргумента AddSlice;
• выбор во вкладке AUD флага пишется обратно в настройку слота
(StoreSliceSlotAudio) — вкладка всегда показывает реальность, а не
вторую, конкурирующую истину.
Хранятся ИМЕНА PortAudio-устройств, а не индексы: порядок перечисления
между запусками плавает. Имя устройства, которого сейчас нет в системе,
не теряется — комбо добавляет его в список и держит выбранным.
ApplySliceSlotAudio зовётся на старте и на смене устройства (rfDevice):
слайсы восстанавливаются позже, слот должен знать свои звуковухи заранее.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
2d15da0462 |
feat(cat): CAT-порт на каждый слайс + Auto TX (эталон SmartSDR CAT)
Внешняя программа управляет отдельным слайсом как отдельным трансивером
через свой TCP-порт. COM-порты и 19090 остаются за главным приёмником
(слайс A) и не тронуты.
Протокол не дублируется: TCATEngine — чистый парсер поверх записи
callback'ов TCATContext, TCATTcpServer — транспорт поверх движка, поэтому
слайс-CAT = тот же движок с другим контекстом. Новый TCATSliceEndpoint
(движок + TCP-сервер на слот), массивом владеет TCATAdapter. Привязка к
СЛОТУ (буква B..G), не к Id: порт живёт, даже когда слайса нет (PS0).
Auto TX = «Auto Switch TX Slice» из SmartSDR CAT. Вся логика в одной точке
— TRadioController.RequestSliceTx, исполняется в потоке контроллера:
• пока кто-то уже в эфире (любой источник) — заявка игнорируется,
перехвата передачи нет никогда;
• без Auto TX слайс передаёт, только если уже выбран TX-источником;
• RX; снимает только СВОЮ передачу;
• TX-источник после отпускания остаётся на слайсе (как в SmartSDR).
На флаге слайса бейдж TX → AutoTX.
Частота слайса — TuneSliceInBand: свобода в пределах ВКЛЮЧЁННОГО диапазона
(трансвертер → его FreqBegin/FreqEnd, иначе band-план), другой диапазон —
отказ. Вне захваченной полосы окно DDC переезжает: доп. пан — центром на
цель (rfPanFreq → UI перекладывает флаги/шапку/зум-бар), пан 0 — центр
посередине между целью и главным VFO и только если оба влезают (общее
железное окно, главный приёмник не оглушаем).
Настройки: вкладка Slices (enable/порт 19091../Auto TX на слайс), персист
в секции cat + device-blob.
Попутно:
• ZZFL/ZZFH больше не заглушки — новые callback'и кромок фильтра
(у главного nil → прежний дефолт режима), хелпер SignedPad;
• FilterIdxFor, AGCModeToUI, BoardUtils.BandEdges;
• DSP-состояние слайса (NR/NB/SNB/ANF) зеркалится в TCtrlSlice — флаг
слайса рисовал нули и AGC главного;
• FUIReady в TMainForm.OnControllerState: адаптеры, создаваемые по ходу
FormCreate, больше не могут уронить старт рендером до постройки
виджетов (на этом падал Auto TX из настроек).
Проверено вживую: все порты 19091..19096 слушают, PS;/ZZFA;/MD;/ID;/SM0;/
ZZTX; отвечают; старт со всеми включёнными слайсами чист под gdb.
Документ — doc/CAT_SLICES.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
f425071461 |
fix(tx): ширина TX-полосы независима от RX-фильтра (модель Thetis)
Убрано RX→TX зеркало: ApplyModeFilter больше не тащит края RX-фильтра в TX (FDSPEngine.SetTXFilter удалён и из вызова, и как метод движка — был единственный вызов). Раньше TX bp0 писали два источника с разной шириной: ApplyModeFilter (RX-ширина FFilterBW) и ApplyTXChainSettings/SetTXFilterFull (TX-фильтр 200/3100) — на MOX побеждал TX-фильтр, между оверами держалась RX-ширина, ширина прыгала. Теперь TX-полосу задаёт ТОЛЬКО TX-фильтр из настроек (через TXSignedEdges), независимо от RX. RX LSB 5.0k больше не раздувает излучаемый сигнал. База под ESSB: расширение TX-фильтра в Setup сразу даёт нужную полосу передачи. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
02288292ff |
feat(pans): сетка FM-каналов и шаг тюнинга на доп. панадаптерах
Доп. паны не имеют своего FMode — режим берётся со слайсов пана:
PanHasFMSlice(PanId) = есть слайс в FM/DMR/FMRAW. По нему SyncPanZoomUI
выставляет View.FMGridStepHz = FM_STEP_HZ[FFMStepIdx], как SyncSpecViewFreq
делает для главного пана (сеттер сам гасит кэши спектра и линейки).
Тюнинг слайса привязан к той же сетке:
• колесо над слайсом/доп. паном — было зашито 12500 и только DMR/FMRAW,
теперь FM_STEP_HZ[FFMStepIdx] во всех трёх канальных режимах (гейт FFMStepOn);
• PanClickTune — там была та же зашитая 12500, теперь шаг режима целевого
слайса, как в DoSpectrumClick главного пана.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
c69d537852 |
feature(tx): слайс в FM RAW можно назначать источником передачи
TX-бейдж на флаге слайса был заблокирован и для DMR, и для FM RAW, хотя тракт FM RAW передавать умеет: SetTxSlice отбрасывает только DMR, а SetTXMode уводит FM RAW в WFM с плоской девиацией. RX-only остаётся один DMR (нет AMBE-кодера). - VfoOverlay.TxSelectionAllowed: запрет сузился до DMR. - RadioController.SetSliceMode: если режим меняют у слайса, который сейчас источник TX, передающий тракт перестраивается под новый режим. Без этого выбор FM RAW на уже выбранном TX-слайсе оставлял в эфире прежнюю модуляцию до следующего клика по бейджу. На переходах в/из FM RAW заодно пересматривается источник модуляции (внешний PCM) со сбросом кольца входа — как в главном SetMode. Заодно снята временная диагностика [MIC] (MicDbg и её вызовы) из mic-пула: зависание при выборе на слайсе входа из настроек больше не воспроизводится. Проверено на железе: TX со слайса в FM RAW работает. Подробное тестирование — следующим шагом. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
93ab03f1eb |
Merge branch 'main' into feature/slice-audio-input
# Conflicts: # ewsdr.lpi |
||
|
|
66c9fa63b9 |
fix(settings): профиль устройства уезжал в нулевой MAC при старте по AutoStart
Настройки персистятся секцией по MAC радио, но ResolveDevice брал MAC только из списка найденных дискавери. У сохранённых устройств поля MAC не было вовсе, и START по AutoStart/CONNECT без дискавери шёл с MAC 00:00:00:00:00:00 — грузился и сохранялся чужой профиль. Пользователь видел «настройки сбросились»: выключался wideband, менялся sample rate, терялись банды и паны, причём в зависимости от того, нажимал ли он перед стартом DISCOVER. - DeviceStore: у TSavedDevice поле MAC + persist в hpsdr_devices.ini (MAC=..), FindSavedByAddr/SetSavedMac (пишет ini только при изменении), MacIsZero. - RadioController.ResolveDevice: saved-ветка восстанавливает и MAC. - RadioController.EnsureDeviceMac: добор неизвестного MAC — найденное в этой сессии → ini → короткий служебный поиск в сети (700 мс, unicast на DirectIP, OnDeviceFound временно снят, чтобы не трогать список устройств в UI). - ConnectDevice: добор MAC до Connect, после успеха MAC запоминается за сохранённым устройством, так что следующий AutoStart идёт без пробника. Проверено на железе (ANAN 172.16.2.200): в hpsdr_devices.ini появился MAC=04:91:62:FD:7B:86, секция нулевого MAC больше не создаётся, старт по AutoStart и старт после DISCOVER дают один и тот же профиль. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
4aa180e7b2 |
wip(audio): выбор устройства ВВОДА на каждом слайсе + вкладки OUT/IN в AUD
Паритет с уже существующим выбором вывода: на каждом слайсе своё устройство входа (микрофон/виртуальный кабель). - RadioController: TCtrlSlice += InDeviceIndex/InDevName/AudioIn (ссылка, не владение); mic-пул FMicPool с refcount (Acquire/ReleaseMicInput) — один открытый TAudioInput на уникальное PA-устройство, смена TX-слайса не переоткрывает поток; ActiveMicInput/ActiveMicInDevName для PullSoundCardMic, Flush и DefaultMicSource; SetMainMicDevice перепривязывает ссылки слайсов. - VfoOverlay: во fly-out AUD ряд вкладок OUT | IN, событие OnAudioInDevice. - PanafallPanel: проброс списка входных устройств во флаги слайсов и главный. - Settings: персист in_dev_name. НЕ ГОТОВО: зависание GUI при выборе на слайсе того же входа, что в настройках (пул открывает поток вместо шортката на общий FAudioIn). В коде оставлена временная диагностика [MIC] в stderr. Также подозрение на замедление потока данных от радио — проверяем сравнением с main. Со мной в коммите временные тестовые программы PortAudio (other/pa*.lpr). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
d51f0fb614 |
feature(calibration): частотная калибровка опорника (ppm) + TO Error в слоте QO-100
Пункт 3 плана. Ручные подгонки остаются: ppm работает ПОД ними, а не вместо — уход тракта где-то больше, где-то меньше. - Settings: TCalibration.FreqCalPPM (JSON cal_freq_ppm, +-100 ppm, per-device). Смысл знака: «опора выше номинала на N ppm», положительное значение поднимает показываемые частоты. - RadioBackend: виртуальный SetFreqCalPPM. - HPSDRNetwork: CalFreq(Hz) = Hz / (1 + ppm) в частотном слове — все четыре точки (главный DDC, DDC панов, feedback-DDC PureSignal, DUC передачи). Тактовый генератор радио менять нечем, поэтому правим запрос. - PlutoBackend: штатный xo_correction в ad9361-phy (device-attr биндинги добавлены в IIOBindings). Номинал XO снимается при коннекте с округлением до 100 кГц, чтобы наша же коррекция с прошлой сессии не накапливалась; после записи LO переписываются, иначе правка ждала бы следующей перестройки. Одна цифра лечит RX и TX на всех диапазонах. - QO-100: TXvtrEntry.TxLOError (JSON tx_lo_error) + поле «TO Error (Hz)» на странице QO-100. Сам транспондер-offset держим стандартным (8089.5 МГц), уход тракта правим поправкой; ActiveTXFreqHz учитывает её, подсказка внизу страницы считает TX-частоту уже с ней. - UI-фиксы вкладки Calibration: длинные подсказки переведены на несколько строк (WordWrap — TLabel по умолчанию режется по краю группы), заголовки таблиц укорочены и не заезжают под кнопку «= S-meter»; RelayoutCalTab сбрасывает прокрутку страницы перед перекладкой (SetBounds в TScrollBox задаёт координаты относительно прокрученного клиента). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
a95292d2c0 |
feature(pluto): компенсация hardwaregain в уровнях + живое усиление в блоке RF AGC
На Pluto шкала dBm ничего не значила: усиление тракта 0..73 дБ входит в показания напрямую, а в режимах AGC железо крутит его само, и картинка «дышит» вместе с ним. - RadioBackend: виртуальный RxGainNowDb (TELEMETRY_NONE = регулируемого усиления нет, как у openHPSDR с его аттенюатором). - PlutoBackend: в MANUAL отдаёт FGainDb (точно, без опроса железа), в режимах AGC — hardwaregain из телеметрического потока (читается там же, где rssi, ~2 Гц, не блокируя UI). - RadioController: AttenCalDB -> FrontEndCalDB, общее понятие «регулируемое усиление входа»: openHPSDR +FAtten (аттенюатор режет), AD936x -hardwaregain (усиление поднимает). Постоянная часть тракта (LNB, кабель, конвертер) остаётся за таблицами калибровки на диапазон/трансвертер. - UI: в блоке RF AGC при режиме != MANUAL вместо надписи «AGC» — фактическое усиление («~38dB», тильда = измеренное), ползунок ездит сам. Видно, что цифра гуляет, а уровни на спектре стоят. Ограничения: FAST AGC крутит усиление быстрее опроса (2 Гц), для стабильной шкалы нужен MANUAL или SLOW; шумовая полка при смене усиления немного едет — это физика (input-referred NF), компенсацию проверять по сигналу. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
6de8c00689 |
feature(rx): плавный аттенюатор 0..31 дБ в UI, память на диапазон, компенсация уровней
Аттенюатор был только в web и только тремя ступенями (0/-10/-20), а его дБ
никуда не возвращались — при ATT 20 дБ S-метр и спектр уезжали на 20 дБ, и
свежая калибровка уровней была верна лишь при ATT=0.
- RadioController: SetAtten(Db) 0..31 (как умеет железо) вместо индекса *10.
AttenCalDB входит в SMeterCalFor и DispCalFor — стрелка, спектр, водопад и
паны компенсируются, шкала dBm остаётся абсолютной (как Thetis/piHPSDR).
У AD936x аттенюатора нет, там поправка 0 (усиление тракта — hardwaregain).
- Память на диапазон/слот трансвертера по образцу RxGainMode/RxGainDb:
TBandSettings.AttenDB (band 'atten_db') и TXvtrEntry.LastAttenDB
('last_atten_db'); восстановление и отправка в железо — в ApplyBandDSP,
сбор — в MakeBandSettings/SaveCurrentBand, вход в слот — через B.AttenDB.
SetAtten сразу кладёт значение в кэш диапазона/слота (JSON пишется общим
сохранением, а не на каждый шаг ползунка — как у drive).
- MainForm: ряд «ATT [ползунок] NNdB» в RX-блоке левой панели, отступы как у
VOL; на Pluto ряд скрыт и RX-блок ужимается (LayoutLeftPanel двигает
панели ниже). Внешние изменения приходят по rfAtten.
- Web: select → ползунок 0..31 с цифрой, attn_idx -> attn_db, cmd attn {db:}.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
366d4c9821 |
feature(calibration): офсет спектра/водопада на диапазон и на трансвертер
Вторая калибровочная таблица рядом с S-метром (паритет с Display Cal Offset в Thetis): у display-тракта своё окно/детектор, и его уровень расходится с показаниями стрелки на пару дБ, поэтому таблицы раздельные. - Settings: DispBandDB[12] / DispXvtrDB[8] (JSON cal_disp_band_i / cal_disp_xvtr_i, дефолт 0 — картинка не меняется до первой правки). - WDSPEngine: колбэк OnDispCal(PanId) — движок раз в кадр спрашивает офсет в UpdateSpectrum и прибавляет к пикселям (главный спектр/водопад в копирующем цикле, паны in-place при ненулевом офсете). На TX-дисплее не применяется. Так обошлись без хуков на каждое присваивание FCenterFreq. - RadioController: общий CalLookup(Hz, BandTbl, XvtrTbl) — на нём SMeterCalFor и DispCalFor(PanId): главный дисплей по FCenterFreq, пан по FPanFreqHz[p], т.е. кросс-банд паны получают свой офсет. Колбэк вешается в CreateEngines, поэтому работает и в GUI, и в ewsdrd; web-зеркало получает готовые кадры. - SettingsForm: группа «Spectrum / waterfall» с той же разбивкой (диапазоны + включённые трансвертеры активного железа) и кнопка «= S-meter» — копия таблицы стрелки как старт калибровки; общая раскладка сеток вынесена в LayoutCalOffsets. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
797bfc2e7c |
feature(calibration): офсет S-метра на диапазон и на трансвертер, раздельно по железу
Вместо одного офсета на всё устройство — таблицы «на диапазон» и «на слот трансвертера»; состав таблиц задаёт активное железо. - Settings: SMeterOffsetDB -> SMeterBandDB[12] / SMeterXvtrDB[8] (JSON cal_smeter_band_i / cal_smeter_xvtr_i). Легаси-ключ cal_smeter_db становится дефолтом всех слотов — показания после обновления не меняются. Разделение Pluto/openHPSDR даёт сама секция JSON: она ключуется по MAC устройства (у Pluto — synthetic MAC из serial). - Офсет убран из WDSPEngine (FSMeterCalDB): движок не знает ни диапазонов, ни трансвертеров. Теперь TRadioController.SMeterCalFor(VisibleHz) — окно включённого XVTR-слота приоритетнее диапазона, индекс диапазона по плану железа (FreqToBandIdx с IsPluto); ReadSMeterDBm для главного S-метра (MainForm + ewsdrd) и SliceSMeter по TargetHz слайса — паны N на других диапазонах считаются по своему банду, а не по главному. - SettingsForm: таблицы на 12 слотов, подписи/видимость/раскладку ставит RelayoutCalTab; SetCalibrationBackend(IsPluto) вызывается рядом с SetAntennaBackend. У openHPSDR — HF-план, у Pluto — VHF/UHF-план и скрытые группы Power/SWR detector и Supply V/A (нет Alex-моста и его телеметрии). Слоты трансвертеров показываются только включённые, с их именами. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
c645b0d1e6 |
feature(settings): вкладка Calibration — детектор мощности/КСВ, V/A, S-метр
Паритет с Thetis-Enhanced (Det. Cal. / Volts&Amps правки): - TCalibration в TGlobalSettings (JSON cal_*): модель детектора Alex-моста, per-band множитель FWD/REV 0.5..2.0, множитель напряжения, Voff/Sens датчика тока, офсет S-метра. - AlexDetToWatts: константы Thetis computeAlexFwdPower/computeRefPower по моделям (ANAN-100 / 200D / 7000DLE-Anvelina Pro 3 / 8000DLE-Orion MkII, 6m-спецкейс REV). Дефолт = прежняя формула ANAN-100 (поведение не меняется). - OnHPStatus: FWD и REV через модельную формулу x множитель банда; КСВ = (1+p)/(1-p), p=sqrt(RevW/FwdW) (Thetis) вместо отношения сырых ADC; напряжение x SupplyVoltCal; ток с калибруемыми Voff/Sens. - WDSPEngine.SMeterCalDB: офсет в GetSMeterDBm/GetSliceSMeter (главный S-метр, slice-флаги, web). - Выбор модели в комбо подставляет модельные дефолты датчика тока (Anvelina Pro 3/7000DLE: 340 мВ / 88 мВ/А — Thetis GetDefaultVoltCalibration). - Фиксы: гард FLoading при постройке вкладки (SetItemIndex стреляет OnChange на программном присвоении -> AV на nil-контролах), FPageCalib/FNavCalib в списках SelectPage (вкладка была невидимой). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7a999c95ee |
fix(puresignal): цель feedback 22 для Orion MkII + outlier-фильтр calcc
Разбор «IMD3 хуже, чем в Thetis»: юзерская плата (Orion MkII/Anvelina PRO3) при стоковой цели feedback 152 перегружает входные каскады ADC2208+кодека — фидбек сам интермодулирует, PS корректирует по искажённому наблюдению. Замеры eu2av (Thetis-Enhanced): оптимум FB≈22 при ATT≈10 дБ. - PSFBTarget: цель уровня feedback, авто по плате (board 5 → 22, иначе 152), настройка ps_fb_target (0 = авто) - авто-ATT: пороги от цели (FB > 1.5×цели / FB < 0.7×цели при ATT>0), ddB = 20*log10(FB/цель). Заодно чинит «ATT всегда 0»: старый порог >181 на этой плате никогда не достигался - PSOutlierSigmaEff → SetPSOutlierSigma (новый экспорт libwdsp): MAD- фильтр выбросов перед фитом calcc, авто σ=5.0 для Orion MkII, настройка ps_outlier_sigma (-1 = авто) - лампа FB в поповере: зоны от цели (0.7–1.3× зелёная), подпись «FB: тек/цель» Требует libwdsp с патчами eu2av (форк wdsp, коммит d27684c). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6a9aebc23c | fix(tx): enable protocol 2 CIC compensation filter |