Commit Graph
100 Commits
Author SHA1 Message Date
ew8bakandClaude Opus 5 f99443620a feat(ui): настройки показывают только то, что есть у активного железа
В TBackendCaps восемь флагов заполнялись обоими бэкендами и не читались нигде,
а в ApplyBackendCapsToUI на этом месте стояла расписка «(TODO) гейтинг полей,
которых нет у бэкенда». Из-за этого на AD936x оператору предлагали крутить то,
чего у платы нет, — и хуже всего страница PA: калибровка драйва по диапазонам
там просто ничего не делает (мощность задаётся железной аттенюацией, drive-байта
нет вовсе), но выглядела рабочей.

Теперь гейт идёт по возможностям, а не по виду устройства:
- HasPA — потолок мощности и калибровки драйва (HF + трансвертеры) против
  «TX att @100% drive»; заголовок и подзаголовок страницы следуют содержимому;
- HasHWMic — строка микрофонного разъёма (DUC Specific байт 50). Mic gain
  остаётся: он идёт в WDSP и работает с любым источником, включая звук с ПК,
  поэтому поднимается в освободившуюся строку;
- HasWideband — галки широкополосного АЦП; сам режим принудительно гасится,
  иначе сохранённый в конфиге wideband оставлял бы пустую панель без способа
  её выключить;
- HasDitherRandom — группа ADC целиком, Web Server встаёт на её место.
Вкладка QO-100 убрана на openHPSDR: TRadioController.InQO100 требует IsPluto,
то есть транспондер там недостижим. Симметрично OC Control, которого нет у
AD936x.

Измеритель: TSMeterView.HasPATelemetry. Без моста детектора FLastFwdW/FLastSWR
стоят на значениях инициализации, и «0W / SWR 1.0» на передаче — не показание,
а выдумка. Шкала рисуется пустой: деления и рамка на месте, заливки нет, ватты
на делениях не подписаны (они считаются от Max power, которого у AD936x нет).

★Отдельно — дефект раскладки, который всё это вскрыло. Перекладка страницы
настроек ломается в двух случаях, и оба случались при смене трансивера на живой
программе (Stop/Discover/Connect/Start), а не при старте:

1. Страница ПРОКРУЧЕНА. TScrollBox двигает детей, и SetBounds задаёт координаты
   относительно прокрученного клиента: всё уезжало вверх, а первая группа
   оставалась на месте — под переключателями Profile/EQ/Hardware зияла дыра.
   Лечение — сброс прокрутки первой строкой (так уже делал RelayoutCalTab).
2. Страница СПРЯТАНА. Тогда сброс не доходит до виджета, а сохранённое смещение
   возвращается при показе — и дыра та же. А попадает перекладка почти всегда
   именно на спрятанную страницу: гейты отрабатывают при открытии окна, когда
   активна одна вкладка, а до Transmit/Calibration оператор доходит потом.
   Лечение — перекладка спрятанной страницы взводит флаг и выходит, а SelectPage
   исполняет её в момент показа (RunDueRelayout). Гард стоит внутри каждой
   Relayout*, а не у вызывающих: у RelayoutCalTab их два, и второй
   (LoadXvtrSettings) приходит как раз на спрятанную страницу.

Проверено офскрин-рендером окна настроек в обе стороны (openHPSDR ↔ AD936x) для
Transmit/Power Amplifier/Calibration/Advanced, в том числе на прокрученной и
спрятанной странице. При полном железе координаты совпадают со старыми.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M37eCTZZ9aqppKWkZRxXkP
2026-08-25 10:16:31 +03:00
ew8bakandClaude Opus 5 eedaffad78 fix(ui): на месте скрытой вкладки OC Control оставалась дыра в списке настроек
У Pluto/AD936x нет Open Collector выхода, и SetOCBackend гасил пункт «OC
Control» одним Visible. Но координаты левой колонки стояли прямо в вызовах
(MakeNavButton('OC Control', 404), следом 'QO-100', 436), поэтому соседи
оставались на своих местах, и между «Transverter» и «QO-100» зиял пустой
промежуток в 32 пикселя.

Убрана сама возможность такого расхождения: раскладку считает RelayoutNav
одним проходом по списку пунктов, пропуская скрытые, а call-site больше не
называет Top. Шаги вынесены в NAV_* рядом с прочими константами формы.

Заодно закрыт тот же дефект классом выше: заголовок секции, в которой не
осталось ни одной видимой кнопки, прячется вместе с ней. Сегодня это не
проявляется (OC Control — единственный прячущийся пункт, а в секции Radio есть
и другие), но следующий такой пункт уже не оставит висеть пустую шапку.

При всех видимых пунктах новые координаты совпадают со старыми до пикселя —
на openHPSDR раскладка не изменилась.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M37eCTZZ9aqppKWkZRxXkP
2026-08-25 08:46:08 +03:00
ew8bakandClaude Opus 5 2a16304b91 Merge feature/tci-protocol: TCI 2.0 — EWSDR как сервер для логгеров, скиммеров и цифры
EWSDR встаёт на порт как ExpertSDR3: клиент (MSHV, логгер, скиммер) подключается
по WebSocket, получает при рукопожатии полный снимок состояния и дальше живёт на
уведомлениях. Приёмник TCI — это СЛОТ СЛАЙСА (B..G → 1..6), а не панадаптер:
слайс у нас и есть «приёмник» целиком, со своей модой, фильтром, АРУ и потоками.

Транспорт держится трёх правил (doc/TCI.md §1.1): отправка не блокирует, клиента
освобождает только тик-поток, Stop прокачивает Synchronize. Ими вылечены реальные
отказы — клин на остановке и смерть процесса от SIGPIPE.

Вошло:
- команды и уведомления TCI 2.0 против PDF-спеки; не реализован только
  TX_FOOTSWITCH (его шлёт сервер, разбор — в doc/TCI.md §3.1 и §4);
- бинарные потоки: IQ, аудио приёмника, TX-аудио от клиента и запись линейного
  выхода в WAV — писатель одним потоком с очередью, файл публикуется атомарно;
- KEYER: чужая манипуляция играется очередью элементов по абсолютным дедлайнам,
  а не пробросом сетевых фронтов — сеть в эфир не попадает;
- арбитраж §3.5 (захват параметра на 200 мс) для того, что клиенты действительно
  перетягивают; отказ браузерным клиентам по Origin;
- пейсинг TX-аудио: всплески своего сигнала на передаче лечатся выдачей по
  TX_CHRONO, а не общим тиком 20 мс (проверено на железе);
- CAT: TX-тракт и телеграф, единая дверь SetTXSettings, ревизия всего набора
  стендом test/cat — «нечисловое поле = индекс 0» сидело ещё в семнадцати командах;
- обрыв манипуляции CW — поколение обрыва вместо флага: пакет, пришедший следом,
  больше не отменяет уже отданный обрыв;
- платформенное: монотонные часы (на них живут дедлайны планировщика TX), сборка
  win64, своя обёртка последовательного порта — под macOS RTL-модуля Serial нет.

Стенды: test/tci (276), test/cat (50), test/platform, test/serial — на подставном
радио, без железа и без единого виджета.

На железе проверено: приём и передача с TCI-клиента, потоки, рекордер, пейсинг.
Не проверено: KEYER живым ключом по сети, прогон на живом маке. TCI пока не
заведён в граф демона ewsdrd — намеренно, см. doc/TCI.md §4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M37eCTZZ9aqppKWkZRxXkP
2026-08-25 08:26:39 +03:00
ew8bakandClaude Opus 5 f88b1615e7 fix(serial): контракт SerValid — платформенный, ноль значит разное
Ноль на Unix и на Windows означает не одно и то же, и одной проверкой тут не
обойтись. На Unix это законный дескриптор (при закрытом стандартном вводе
fpOpen отдаёт именно его), а ядро Windows нулевой хендл не выдаёт никогда —
там ноль это либо отказ RTL, либо неинициализированное поле. Внутренний путь
был безопасен и раньше (SerOpen переводит ноль RTL в SER_INVALID_HANDLE, свои
поля инициализируются им же), но публичный контракт для значения, пришедшего
извне, оказывался неверным. Теперь проверка разведена по платформам.

★Стенд научился ПРОГОНЯТЬ windows-ветку, а не только компилировать её.
Заглушка модуля Serial — чистый Паскаль, поэтому ветку можно собрать и
запустить прямо на Linux: test/platform/win_probe проверяет перевод нулевого
хендла в SER_INVALID_HANDLE, ответ SerValid и правило имени COM10+. До сих пор
у windows-пути обёртки не было никакого поведенческого покрытия вовсе.

Негативный контроль: если убрать windows-ветку из SerValid, прогон падает на
«★нулевой хендл под Windows негоден».

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bkwwyj7xVRrqnSVEseTRfV
2026-08-24 22:43:14 +03:00
ew8bakandClaude Opus 5 03afaf3083 fix(serial): ноль — законный дескриптор, а имя порта нормализуем целиком
Два замечания по обёртке, оба верные.

1. SerValid считал ноль невалидным на всех платформах. На Unix дескриптор 0 —
   обычный номер: если стандартный ввод закрыт (демон, запуск из службы),
   fpOpen отдаст именно его. Порт при этом объявлялся неоткрытым И оставался
   незакрытым — SerClose смотрит на ту же проверку. Относительно прежней
   unix-проверки `h < 0` это была регрессия. Теперь признак неудачи ровно один
   и на всех платформах — SER_INVALID_HANDLE; ноль, которым RTL под Windows
   сообщает об отказе, переводится в него внутри SerOpen (как и было).

2. SerWinDeviceName обрезала пробелы только по пути COM10+. Для ' COM3 ' и
   ' \\.\COM12 ' возвращалось исходное имя с пробелами, а CreateFile их не
   прощает: оператор, скопировавший имя с хвостовым пробелом, получал «не
   удалось открыть» на ровном месте. Теперь опознанное имя возвращается
   нормализованным; неопознанное (путь на Unix, COM3: с двоеточием) — по-прежнему
   без единого изменения, трогать чужой путь мы не вправе.

Стенд: 52 проверки. Новая часть C2 закрывает стандартный ввод, открывает порт
как дескриптор 0 и гоняет через него байты — ★негативный контроль: с прежним
`and (Handle <> 0)` она падает двумя проверками. Плюс нормализация пробелов у
короткого имени и у полной формы.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bkwwyj7xVRrqnSVEseTRfV
2026-08-24 22:31:04 +03:00
ew8bakandClaude Opus 5 53c21223a3 fix(serial): COM10 и выше не открывались под Windows
CreateFile резолвит короткое имя порта только для COM1..COM9 — начиная с
COM10 нужна форма \\.\COM10. У USB-переходников номер за десяток заезжает
легко (воткнули пару кабелей — система выдала COM11), и оператор получал
«не удалось открыть» без всякого объяснения: имя уходило в CreateFile как есть.

SerOpen под Windows пропускает имя через новую чистую SerWinDeviceName:
COM1..COM9 остаются короткими (там и так работало, менять поведение незачем),
COM10 и выше получают префикс, уже полная форма не удваивается, всё, что не
похоже на COM<число> (пути Unix, COM3: с двоеточием), не трогается вовсе.

Функция считает одинаково на всех платформах и потому проверяется стендом на
Linux — 11 проверок в test/serial (итого 47). Подсказка под полем ввода на
Windows уже называет обе формы: «COM3, \\.\COM12».

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bkwwyj7xVRrqnSVEseTRfV
2026-08-24 22:16:58 +03:00
ew8bakandClaude Opus 5 05ba39fdad feat(serial): своя обёртка последовательного порта — macOS собирается
RTL-модуль Serial собирается только для [android, linux, netbsd, openbsd,
win32, win64] — Darwin в списке нет (rtl-extra/fpmake.pp). Из-за этого под
macOS CAT по последовательному порту стоял заглушкой, а CWKeyer не собирался
вовсе.

★Скопировать чужой исходник было нельзя. Serial.SerSetParams кладёт B-константу
скорости прямо в c_cflag — идиома Linux, где это битовая маска. На BSD и macOS
B115200 это просто ЧИСЛО 115200, и в c_cflag прилетает $1C200: CLOCAL | HUPCL |
CCTS_OFLOW плюс мусор в поле CSIZE. Молча включается аппаратный CTS-flow, и
передача встаёт, если железка не держит CTS, — а ошибки никакой, порт
«открылся».

Новый SerialPort.pas:
* Unix — termios напрямую, Linux и macOS ОДНИМ путём (разница спрятана в
  константах RTL; Darwin даёт и termio, и все TIOCM_*). Скорость только через
  CFSetISpeed/CFSetOSpeed, c_cflag собирается с нуля чистой SerCFlags;
  CRTSCTS — исключительно по явной просьбе; сырой режим и VMIN/VTIME явные;
  open с O_NONBLOCK и снятием флага сразу после (часть USB-драйверов, как и
  tty-устройство на маке, висит в open до DCD).
* Windows — тонкая переадресация в RTL Serial: там он есть и в termios не лезет.
* Неуспех SerOpen на ВСЕХ платформах даёт SER_INVALID_HANDLE (RTL возвращал 0
  под Windows и −1 под Unix, из-за чего вызывающие держали свои развилки).
  Проверка — SerValid.

Точки вызова: CATSerial и CWKeyer переведены на обёртку, заглушка
CAT_SERIAL_STUB и платформенные {$IFDEF} вокруг хендлов убраны. Имена портов по
умолчанию и подсказка в настройках теперь платформенные (SerDefaultPortName /
SerPortNameHint): на маке порт зовётся /dev/cu.usbserial-XXXX, и '/dev/ttyS0'
там — заведомо неверная подсказка; осмысленного умолчания не существует,
поэтому пусто.

Новый стенд test/serial (36 проверок) — на псевдотерминале: открытие, termios
обратным чтением, ★«CRTSCTS сам не включился», ★«в c_cflag нет ничего, кроме
наших флагов», формат через чистую SerCFlags (обратным чтением его не
проверить: pty восьмибитно-чистый и молча меняет CS7 на CS8), обмен, срок
таймаута, CR не превращается в CR+LF, эха нет, и сквозной прогон CAT через
обёртку. Негативный контроль: с `or Cardinal(BitsPerSec)` в SerSetParams (та
самая идиома, что ломает RTL на BSD) проверка c_cflag падает — «1CAB0 против
8B0».

test/platform расширен: теперь сим-компилирует и SerialPort, для его
Windows-ветки добавлена заглушка модуля Serial с теми же сигнатурами.

Модемные линии (DTR/RTS/CTS/DSR/CD/RI) на pty не проверяются — ioctl TIOCM*
там не работает; это остаётся на живой порт.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bkwwyj7xVRrqnSVEseTRfV
2026-08-24 22:09:44 +03:00
ew8bakandClaude Opus 5 f2ecd63fe1 fix(platform): win64-сборка падала на GetEnvironmentVariable
Модуль Windows стоит в uses ПОСЛЕ SysUtils, поэтому его трёхпараметрический
GetEnvironmentVariable(PChar;PChar;DWORD) перекрывает однопараметрический из
SysUtils, и три вызова в PlatformUtils падали с «wrong number of parameters».
Лечение — явная квалификация SysUtils.GetEnvironmentVariable.

★Сломалось это не сейчас: unit Windows появился в uses ещё в d209697 (вместе с
QueryPerformanceCounter для монотонных часов), и с тех пор под win64 не
собиралось вовсе — просто сборочная машина туда не заходила. GetAppCfgDir с
%APPDATA% живёт с e9f4bf1 и до d209697 работал.

Новый стенд test/platform. Кросс-RTL обычно не установлен, поэтому ветки
{$IFDEF WINDOWS} и {$IFDEF DARWIN} на Linux не компилируются ВООБЩЕ, и ошибка в
них всплывает только на сборочной машине. Стенд переписывает копию
PlatformUtils.pas так, чтобы платформенные условия читались как свои
(-dSIMWIN / -dSIMMAC), и компилирует без линковки (-Cn): системных функций тут
нет, но синтаксис, типы и — главное — разрешение имён проверяются
по-настоящему. Для Windows подкладывается заглушка unit Windows, объявляющая
ровно те имена, которыми настоящий перекрывает SysUtils; на ней и держится вся
проверка.

★Негативный контроль: на коде до правки стенд выдаёт РОВНО те же три ошибки
(строки 210, 271, 273), что и сборочная машина.

Чего он не делает: живых вызовов системных счётчиков — их правильность
доказывает только прогон на самой платформе.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bkwwyj7xVRrqnSVEseTRfV
2026-08-24 21:31:59 +03:00
ew8bakandClaude Opus 5 fe3d862d00 fix(dsp): канал TXA жил на два потока без разделения
TX-DSP поток зовёт fexchange0(TXA_CHAN) через буферы FTXIn/FTXOut, и ровно туда
же на каждом T/R лезет SetTXRun из потока контроллера. Гейт FTXActive их не
разделял: поток проверяет его СНАРУЖИ, а внутрь блока входит позже. Три дефекта
одной природы:

* деструктор звал SetTXRun(False), который поток НЕ останавливает — он лишь
  опускает гейт и тут же сам сливает канал своими fexchange0, то есть лезет в
  TXA одновременно с ещё живым потоком, прямо перед сносом объекта. Теперь
  Destroy сразу зовёт Close: тот опускает гейт, будит поток, дожидается WaitFor
  и лишь потом закрывает каналы. Комментарий «останавливаем TX поток» врал;
* слив на отпускании PTT шёл без всякой синхронизации — окно гонки примерно
  1-1.5% на каждый T/R (блок ~150 мкс против периода 10.7 мс). Появился FTXLock
  по идиоме RX-стороны (FMainGated + FMainLock): поток берёт лок вокруг
  ProcessTXBlock и ПЕРЕЧИТЫВАЕТ гейт под ним, оба слива в SetTXRun идут под тем
  же локом. Ждать контроллеру не дольше одного блока TXA;
* ветка ЗАПУСКА оставалась открытой: SetMOX не отсекает MOX=True поверх уже
  идущей передачи (прислать повтор вправе CAT, web и TCI), а SetTXRun(True)
  обнулял индексы mic-кольца и дёргал SetChannelState мимо лока. ★Худший исход
  не теоретический: TX-поток дописывает свой локальный tail поверх обнулённого,
  кольцо выглядит почти полным СТАРЫХ сэмплов, и они уходят в эфир пачкой.
  Теперь `if Run and FTXActive then Exit` плюс весь старт одной транзакцией под
  FTXLock, FTXActive публикуется последним.

★Перенастройку TXA в ChangeSampleRate под лок НЕ берём: там SetChannelState с
dmode=1 ждёт фейда, а фейд прокручивают fexchange0 самого потока — под локом это
клин на ~106 мс (тот самый старый фриз TX→RX). Синхронизация там своя:
остановка канала. Второй известный кросс-поточный доступ к TXA — pscc из
сетевого потока (PureSignal), так же устроено в Thetis.

Попутно закрыт use-after-free: TX-поток создаёт SetTXRun, которому открытый
движок не нужен, а гасил его только код ЗА гейтом `if not FInitialized then
Exit` в Close — на неоткрытом движке поток переживал Free объекта. Ловилось не
там, где сделано: следующий TThread.Create в процессе падал с AV (в стенде — на
секции часов после пейсинга, раньше — на третьем TRadioController в FreeEngines).
Остановка потока перенесена в начало Close, до гейта.

Стенд: порядок секций теперь намеренный — часы идут ПОСЛЕ тяжёлых частей и
служат сторожем починки потока (упадёт снова — увидим там). Новая проверка
повторного MOX с ★негативным контролем: без гейта «было 3000, стало 0».
Для неё в движке появилось диагностическое свойство TXMicFill. 276/276,
test/cat 50/50, GUI (--ws=qt6) и демон собираются.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bkwwyj7xVRrqnSVEseTRfV
2026-08-24 21:20:38 +03:00
ew8bakandClaude Opus 5 edc19fa882 fix(platform): монотонные часы переполнялись на Windows и были стенными на macOS
Два дефекта в источнике времени, на котором стоят абсолютные дедлайны пейсинга
TX: планировщик TX_CHRONO и отправитель DUC ведут по нему сетку, и скачок часов
для них означает не потерю точности, а остановку выдачи.

* Windows: (V * 1000000) div QPCFreq переполняет Int64 на 9.223e12 тиках — при
  типовой для Windows 8+ частоте QPC 10 МГц это 10.7 суток аптайма, дальше
  разрыв повторяется каждые 21.35 суток (2^64/10^6). Между разрывами функция
  линейна, поэтому страдает не «всякая машина старше N суток», а сессия,
  пережившая сам момент скачка: время уходит в большой минус, NextDueUs
  становится недостижим, маркеры TX_CHRONO прекращаются до перезапуска, долг
  TCI уходит в минус, а проверки вида `FTxLastRxUs > 0` перестают срабатывать.
  ★Мест было ДВА: PlatformUtils.MonotonicUs и локальная ClockUs в потоке
  отправителя DUC (win-ветка HPSDRNetwork) — вторая пейсит ВСЕ передачи на
  Windows, не только TCI: WaitUntil на переполненной разности выходит сразу,
  пакеты уходят без выдержки, очередь опустошается пачкой и сохнет.
  Лечение — общая TicksToUs через частное и остаток (остаток меньше частоты,
  произведение не переполняется; потолок отодвинулся на сотни тысяч лет).
* macOS: MonotonicUs проваливалась в общий Unix-{$ELSE} с fpgettimeofday, то
  есть на СТЕННЫЕ часы. Шаг NTP назад — планировщик замирает до недостижимого
  срока, вперёд — выдаёт пачку и начисляет фиктивный долг. Взят
  mach_absolute_time + mach_timebase_info: монотонен и есть на любой версии
  (clock_gettime на macOS только с 10.12), пересчёт тоже через частное/остаток —
  на Apple Silicon база 125/3.

Публикация состояния часов: всё держится в ОДНОМ слове и поднимается в
initialization, до старта любых потоков. Пара «значение + флаг» на слабой
модели памяти (Windows ARM, Apple Silicon) позволяла читателю увидеть флаг
раньше значения, получить частоту 0 и вернуть время 0 — единичный ноль в
монотонных часах есть прыжок на десятки лет назад. Windows: QPCFreq, где 0 —
не выбран, >0 — частота, -1 — QPC непригоден (тогда GetTickCount64, чтобы часы
хотя бы шли). Darwin: MachBase = numer shl 32 or denom.

Стенд, секция H: совпадение со старой формулой ниже порога, ★негативный
контроль на пороге (старая даёт -922337203685 мкс, новая +922337203685),
монотонность через прежний разрыв, 100 суток на частотах 10/3.579545/24/1 МГц,
год при 24 МГц, и контракт «один источник на все потоки» — четыре потока,
~850 тыс. чтений, ни нулей, ни хода назад. Итого 274/274, test/cat 50/50.

★Чего стенд не покрывает: win- и darwin-ветки здесь не компилируются вовсе
(кросс-RTL не установлен). Их тела проверялись вырезкой в пробную программу с
подставным API — синтаксис и арифметика сходятся, живой вызов системного
счётчика остаётся за прогоном на той платформе.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bkwwyj7xVRrqnSVEseTRfV
2026-08-24 21:17:57 +03:00
ew8bakandClaude Opus 5 867da5921e chore(tx): детектор голодания снят — всплесков не видно, вернём при нужде
Прибор из b173b7f отработал своё на этом заходе: всплески не появляются, и
держать его в тракте нет причин. Снято ровно то, что тот коммит поставил:
юнит TxHealth.pas, замеры простоев в отправителе DUC (обе ветки), публикация
контекста пейсинга из планировщика TCI, счёт бита HPS_FIFO_EMPTY, вызовы
TxHealthService в таймере UI и цикле демона, индикатор «·сухо N» в статусной
строке и секция G стенда.

Вернуть — реверсом этого коммита; опора детектора и правило его устройства
(в тракте передачи ничего, кроме записи в память) записаны в
test/hpsdr/README.md, чтобы не восстанавливать рассуждение заново.

Проверено: test/tci 262/262, test/cat 50/50, GUI (--ws=qt6) и демон собираются.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bkwwyj7xVRrqnSVEseTRfV
2026-08-24 19:25:57 +03:00
ew8bakandClaude Opus 5 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 (лечение — fa38fa7). Инструмент чинил дефект собой.

Журнал ~/.config/ewsdr/txhealth.log, строка на передачу: «чисто» либо «ОПАСНО
(первое на N мс)» плюс подробности с контекстом пейсинга. В статусной строке
UI — «·сухо N» за сеанс, чтобы не помнить, какая посылка была плохой.

Стенд: секция G в test/tci (порог подушки, сводка, момент от фронта PTT,
контекст рядом с событием, молчание вне передачи) — 276/276. Диск стенд не
трогает намеренно: берёт сводки через TxHealthTake/TxHealthDetail, мимо
TxHealthService, иначе писал бы в настоящий журнал пользователя.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bkwwyj7xVRrqnSVEseTRfV
2026-08-24 19:23:13 +03:00
ew8bakandClaude Opus 5 fa38fa7f75 fix(tci): бухгалтерия пейсинга переезжала из прошлой передачи в следующую
Посреди передачи из MSHV рядом с сигналом вставала вторая несущая — и не
изредка, а с первых миллисекунд посылки. Это голая несущая гетеродина DUC:
очередь пересыхает, модуляции нет.

Цепочка. Конец посылки клиент делает командой trx:N,false. SetMOX(False)
снимает TCIMicActive внутри Invoke, а FTxClient обнуляется ПОСЛЕ возврата из
него, в HandleTrx. Опустить FTxRunning умел только тик планировщика — и только
пока видел живого FTxClient с уже снятым TCIMicActive. Окно = хвост SetMOX,
пара миллисекунд против шага тика в полкванта: тик попадал в него через раз.

Промах означал, что следующая передача идёт МИМО TxResetAccounting и наследует
FTxOwed, FTxInFlight, FTxSatSinceUs и оценку задержки. Долг сразу у потолка,
в полёте чужие кредиты прошлой посылки, условие выдачи не выполняется — маркеры
не идут вовсе, пока не сработает сторож (выдержка до 250 мс). Нулевой pre-roll
кончается раньше, очередь DUC сохнет, радио отдаёт несущую.

Лечение — инвариант «нет клиента, нет и идущей передачи», как в ClearTxClient
(он его соблюдал, а HandleTrx нет):
* FTxRunning := False рядом с FTxClient := Client — первый тик передачи всегда
  начинает с TxResetAccounting, каким бы ни был исход гонки;
* то же в обоих местах, где источник снимается;
* то же в выходе PushTxChrono по C = nil — страховка от будущих путей.

Аванс (FTxLeadKeep, FTxGapUs) не трогаем: он намеренно переживает передачу,
на нём стоит pre-roll следующего PTT.

Стенд: новая фаза «повторы» в TestTxPacing — шесть циклов PTT на одном клиенте,
проверка флага сразу по подтверждению trx:0,false и счёт маркеров в первые
200 мс каждой передачи. Негативный контроль на старом коде: флаг протухал 6/6,
маркеров 4 вместо 22. Стенд 262/262, test/cat 50/50, GUI и демон собираются.

★Отдельной тестовой процедурой это сделать нельзя: третий TRadioController в
процессе даёт AV в FreeEngines (состояние WDSP), стенд падает до проверок.
Фаза живёт внутри TestTxPacing и стартует после потерь ответов — то есть с
заведомо отравленной бухгалтерией.

★Почему дефект вылез только сейчас: TraceDump('tx') из снятых зондов стоял в
конце SetMOX(False) и писал до 16 тысяч строк — десятки миллисекунд ровно в
этом окне, и тик гарантированно успевал. Прогоны шли с EWSDR_TXTRACE=1, так
что инструмент собой же и прикрывал гонку.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bkwwyj7xVRrqnSVEseTRfV
2026-08-24 18:45:55 +03:00
ew8bakandClaude Opus 5 166fc65fc8 chore(tx): сняты зонды трассировки — пейсинг проверен на железе
Всплесков на передаче нет, инструмент своё отработал и больше не нужен в
горячем пути. Снято ровно то, что ставил d209697, и ничего сверх:

* удалён юнит TxTrace.pas (кольцо + сброс дампа + TraceInit);
* TCIAdapter: точки съёма маркеров и TX-аудио, поля FTxTraceLast/FTxTraceAudio;
* HPSDRNetwork: DUC-DRY/DUC-RUN и переменная DryFrom (жила только для них),
  вместе с ними ушли из uses TxTrace и PlatformUtils — их добавлял тот же
  коммит, MonotonicUs в этом файле больше нигде не встречается;
* RadioController: PTT-метка в SetMOX и сброс дампа на снятии PTT;
* TraceInit из ewsdr.lpr и ewsdrd.lpr.

Каждая удалённая строка стояла под `if TraceOn` — вычислений, от которых
зависит тракт, в них не было. ★PlatformUtils.MonotonicUs НЕ тронут: на нём
абсолютные дедлайны планировщика TX, это не отладка.

Скрипт разбора test/hpsdr/txtrace_scan.py оставлен, формат дампа не менялся;
в README стенда записан рецепт возврата зондов из d209697.

Проверено: lazbuild --ws=qt6 и build-ewsdrd.sh собираются, test/tci 260/260,
test/cat 50/50 — те же числа, что и с зондами.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bkwwyj7xVRrqnSVEseTRfV
2026-08-24 17:52:23 +03:00
ew8bakandClaude Opus 5 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
2026-08-23 22:06:10 +03:00
ew8bakandClaude Opus 5 ced8581ada fix(hpsdr): TX-антенна тонула в Alex1, OC-пины били через один, SO_EXCLUSIVEADDRUSE
Три дефекта, найденные сверкой с прошивкой (Anvelina_PROIII, board_type=5)
и с Thetis.

1. CalcAlex1 жёстко ставил бит 24 (ANT1) с комментарием «ADC1 дефолт».
   Старшие 16 бит Alex1 — не фильтры ADC1, а TX-СЛОВО Alex: прошивка кладёт
   байты 1428-1429 в Alex_Tx_data и на PTT подменяет им верхнюю половину
   Alex-слова целиком (Orion.v:2346, Alex_Tx_data_ok = Alex_Tx_data[10:8] —
   те же ANT1/2/3). Захардкоженный ANT1 делал условие истинным и затирал
   антенну, посчитанную в CalcAlex0: оператор с TX на ANT2/ANT3 передавал
   в ANT1. Thetis делает ровно то, что теперь делаем мы —
   netInterface.c:489 чистит биты 24-26 и ставит _TXANT_1/2/3 из tx_ant.

2. Байт 1401 (Open Collector) уходил без сдвига. Бит 0 не разведён, пины
   сидят на битах 1..7 (Orion.v:913, USEROUT0..6 = Open_Collector[1..7]) —
   та же раскладка, что у C2[7:1] в протоколе 1. Каждый включённый пин
   срабатывал на соседнем выходе, седьмой не срабатывал никогда.
   CalcOCBits остаётся в логических координатах (бит i = пин i+1),
   сдвиг делает SendFullHP на границе с проводом.

3. SO_EXCLUSIVEADDRUSE = not 5 ($FFFFFFFA) вместо ~SO_REUSEADDR = not 4
   ($FFFFFFFB) — как и написано в собственном комментарии. Опции с таким
   номером нет: setsockopt возвращал ошибку (её результат не проверяется),
   и порт оставался перехватываемым, ровно от чего строка и спасала.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 16:25:47 +03:00
ew8bakandClaude Opus 5 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>
2026-08-20 16:07:25 +03:00
ew8bakandClaude Opus 5 3a5dced17c fix(tci): счётчик клиентских потоков переживал неудачный старт — Stop висел вечно
AcceptLoop делал InterLockedIncrement(FThreadCount) и следом создавал поток
безо всякой защиты. Не родился поток (память, лимит потоков в системе) —
исключение уходило из AcceptLoop и TTCIAcceptThread.Execute, accept-поток умирал
при FRunning = True, а счётчик оставался ≥ 1 навсегда. Ждут его на шаге 4
TTCIServer.Stop БЕЗ таймаута (и намеренно: следом освобождаются и клиенты, и сам
сервер), так что программа зависала и на выходе, и на любой смене настроек TCI.

Инкремент остался до старта — переносить его после Start нельзя, там гонка с
ThreadDone уходящего потока. Спавн обёрнут в try/except: счётчик возвращается
назад, сокет гасится Kill, клиент метится FClosed и достаётся тик-потоку
(ReapClients), как при обычном отключении. Объект потока уносит себя сам —
упавший конструктор сразу, поднявшийся по FreeOnTerminate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 16:06:53 +03:00
ew8bakandClaude Opus 5 6e4ba1c1c5 fix(tci): клик по DX-споту на доп. пане слал номер ПАНА вместо номера приёмника
Приёмником TCI в этой ветке стал слот СЛАЙСА (TCI_MAX_RX = 1 + MAX_SLICES,
«панадаптером приёмник быть перестал»), а PanNSpectrumMouseDown отдавал в
rx_clicked_on_spot P.PanId. Клик по пану 1 уезжал как «rx_clicked_on_spot:1,…»,
то есть адресовал слайс в слоте 0 — возможно, стоящий на ГЛАВНОМ пане, — а то и
приёмник, которого нет вовсе: клиенты, фильтрующие по номеру, такое уведомление
просто теряют. Вызов rx0 с главного пана был и остаётся верным.

Теперь номер берётся у слайса, который на спот и увели: DXTuneSpotOnPan стал
функцией и возвращает его, TTCIAdapter.RxOfSlice переводит id в номер TCI.

Заодно два следствия того же места:

  * Не увели ничего (спот вне полосы захвата зумнутого пана, слоты кончились) —
    молчим. Иначе уведомление уходило под номером 0, то есть от имени главного
    приёмника, который никуда не двигался, и логгер открывал карточку QSO с QSY
    на частоту, где радио не стоит.

  * AddSliceAtFreqPan стал функцией и честно сообщает, что получилось. При
    неудаче (нет свободных слотов, частота вне захвата) он не трогает
    FActiveSliceId, а DXTuneSpotOnPan читал именно его — и уводил на спот ЧУЖОЙ
    слайс, вообще говоря с другого пана, вместе с SetSliceModeBW и
    PushSliceFlagState не на том пане.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 16:06:53 +03:00
ew8bakandClaude Opus 5 11d8e11ba5 fix(tci): выключение TUNE не забывало хозяина эфира — уход клиента рубил чужую передачу
Задний фронт передачи ловился только по rfTransmitting, а выключение TUN гасит
эфир изнутри SetTune: там SetMOX(False) шлёт rfTransmitting, пока FTuning ещё
True (флаг сбрасывается строкой ниже, см. RadioController.SetTune), поэтому
TxNow оставался True и ForgetTxOwner не звался. Следующий за этим
Changed(rfTuning) обработчик игнорировал вовсе.

Итог: клиент сделал «tune:0,true» и стал хозяином эфира, оператор выключил TUN
кнопкой (тогда сброс в DispatchCommand не срабатывает — команды-то не было),
позже нажал PTT сам, а когда сокет клиента закрылся, HandleDisconnect →
StopTxOf всё ещё видел его хозяином и снимал ОПЕРАТОРСКУЮ передачу. Ровно то,
что комментарий у ForgetTxOwner обещает не допускать.

Теперь фронт ловится и по rfTuning. На стенде — сценарий целиком: клиент
поднимает TUN, оператор гасит его сам и встаёт в эфир, уход клиента передачу не
трогает (негативный контроль: с прежним условием проверка падает).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 16:06:21 +03:00
ew8bakandClaude Opus 5 4b715c8b03 fix(tci): смена настроек TCI освобождала клиентов, не сняв тапы DSP
ApplySettings гасил сервер в порядке Stop → StopAllStreams → SetTaps(False), а
TTCIServer.Stop на шаге 5 освобождает всех клиентов. Тапы аудио и IQ в этот
момент ещё стояли, и DSP-поток, войдя в OnAudioTap между возвратом из Stop и
захватом FStreamLock в StopAllStreams, шёл по FStreams в FeedAudio → EmitFull →
FClient.SendBin, то есть брал FOutLock уже освобождённого объекта. Достаточно
было сменить порт или снять галку «Enable» в Settings → CAT, пока у клиента жив
AUDIO_START или IQ_START.

Порядок приведён к тому, что и так был в Destroy: ПЕРВЫМИ снимаем тапы (их
снятие ждёт выхода DSP-потока из вызова), потом останавливаем сервер, потом
гасим потоки — их объекты ссылаются на клиентов, поэтому они последние.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 16:05:29 +03:00
ew8bakandClaude Opus 5 b62369ec6b fix(tci): callsign_send слал размноженный позывной, tci_error — неэкранированное имя
Два расхождения со спекой, оба в том, что уходит клиенту.

callsign_send (§3.2.2). Команда должна нести «финальный вариант позывного,
переданного в эфир», а несла его вместе с повторами: для «cw_msg:0,_,RA6LH$2,
599 004;» уезжало «callsign_send:RA6LH RA6LH;», потому что переменная Call к
этому моменту уже была затёрта развёрнутым текстом сообщения. Теперь позывной
запоминается ДО развёртки. Заодно: подтверждение уходит АВТОРУ сообщения, а не
broadcast'ом (у остальных клиентов своих сообщений нет), и через TCIEscape —
позывной пришёл от клиента, и символы ^ ~ * после снятия экранирования
превращались в сырые : , ; прямо посреди кадра.

Момент отправки остаётся расхождением, теперь честно описанным в doc/TCI.md:
документ шлёт команду по факту окончания передачи позывного, а наш передатчик
текста моментов внутри очереди не отмечает. Доотправка позывного (cw_msg:arg1;)
всё равно не поддержана, так что финальный вариант известен уже в момент
постановки в очередь и позже не изменится.

tci_error. Имя команды возвращается клиенту как ПЕРВЫЙ АРГУМЕНТ, то есть обязано
экранироваться, — и общий обработчик (HandleCommand) это делает, а пятнадцать
точечных отказов пропускали его сырым. Имя приходит от клиента, TCIParse режет
его только по ':', так что «foo,bar:1;» возвращался как «tci_error:foo,bar,bad
receiver;» — три аргумента вместо двух.

Заодно в doc/TCI.md: правило 3 §1.1 теперь говорит про ВСЕ потоки сервера
(accept и тик тоже, общий JoinPumped), а не только про клиентские, и счётчик
проверок стенда приведён к нынешним 244.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 11:21:58 +03:00
ew8bakandClaude Opus 5 4081ea0e57 fix(cat): нечисловое поле = индекс 0 ещё в семнадцати командах, ZZGT и ZZBE
Всё найдено новым стендом (test/cat), все правки им же и закрыты.

Нечисловое поле. Правку «TryStrToInt вместо StrToIntDef(s, 0)» однажды получили
пять команд (ZZAU, ZZBP, ZZBM, ZZBS, ZZFI), а остальные остались как были — в
том числе КЕНВУДОВСКИЕ ДВОЙНИКИ тех же самых величин, ходящие в те же сеттеры:
FWxxxx;, SHxx; и SLxx; ставили фильтр 0 (тот же индекс, что чинили у ZZFI),
AG0xxx; и SQ0xxx; — громкость и порог шумоподавителя в ноль, GTxxx; — АРУ в
FAST, PCxxx; и ZZPCxxx; — мощность в ноль. Всего семнадцать команд: CN, FW, GT,
NB, PC, SH/SL, AG, SQ и ZZAG, ZZAR, ZZNA, ZZNB, ZZNR, ZZPC, ZZSQ, ZZST, ZZTB.
Разбор везде приведён к идиоме ZZFL/ZZFH: не число — ошибка формата.

Не тронуты три места, где мягкий разбор безвреден или намеренный: FR сам
сверяет поле с '0'/'1' до преобразования; MD и ZZMD от нечислового получают 0, а
установка идёт от 1, то есть ничего не делают; ZZOS трактует мусор как симплекс
по эталону (default в String2OffsetDirection), и клиенты на это рассчитывают.

ZZGT. Опрос отвечал тремя цифрами (как кенвудовская GT), а установка принимала
ровно один символ: клиент, прочитавший «ZZGT000;» и написавший его назад,
получал «?;» — а читать значение и писать его обратно умеет любой логгер.
Принимаются обе ширины, поле разбирается строго.

ZZBE. Формы были перевёрнуты: опрос «ZZBE;» отвечал «?;», а установка
«ZZBE01;» возвращала данные ('1'), причём саму установку никто не исполнял.
Вся семья «сдвиг VFO на nn шагов» (ZZAD ZZAE ZZAF ZZBF ZZSG ZZSH) — однострочные
заглушки в таблице, и документ числит ZZBE среди нереализованных; приведено к
ним.

ZZEB. Выдача клампится туда же, куда и приём: сетка эквалайзера бывает только
трёх- или десятиполосной. Иначе опрос отдавал число полос, которое разбор той же
команды отвергал.

Стенд: 50 проверок, все зелёные; на коде до этого коммита падают 19.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 11:21:38 +03:00
ew8bakandClaude Opus 5 d6a7dd96c5 test(cat): стенд разбора команд — свойства всего набора, а не список
Команд в CATEngine больше трёхсот, и проверять их поимённо бессмысленно: список
отстанет от кода в первую же неделю. Поэтому стенд ходит по кодам перебором
AA..ZZ (незнакомый движок отсеет сам, ответив «?;») и проверяет свойства,
которые обязаны выполняться на всём наборе сразу.

Круговой прогон: то, что команда отдала на опрос, она обязана принять обратно.
Это ловит расхождение ширин GET и SET — клиент, который читает значение и пишет
его назад, обычная идиома логгеров. Read-only статусы (IF, ID, ZZIF, ZZID)
исключены списком: их ответ — составной снимок, обратно его не пишут ни Kenwood,
ни Thetis.

Мусор в поле: разбор не имеет права упасть ни на одном аргументе (сборка с
-Criot: диапазоны, переполнения, приведения типов) и обязан отвечать либо ничем,
либо «?;»/«E;», либо ОДНИМ корректным кадром — лишняя ';' внутри ответа для
клиента означает две команды вместо одной, и дальше он читает мусор. Отдельным
прогоном на чистом радио: нечисловое поле не имеет права ничего перестроить.
Числовые формы в этот прогон не входят намеренно — «ZZFA00000000000;»
синтаксически законна, и решать её судьбу должен контроллер, а не разбор.

Радио подставное (TFakeRadio): ни движка, ни железа, но геттеры и сеттеры
настоящие, поэтому видно не «команда ответила», а ЧТО она изменила.

Плюс точечные регрессии на дефекты, которые уже случались: заглушка, молча
правившая радио (ZZVB копировал VFO A в VFO B прямо на опросе), ошибочные
алиасы, нечисловое поле как индекс 0, ширины полей.

★-B в обоих стендах (и в TCI тоже). Не «для чистоты»: fpc сверяет .ppu с
исходником по времени с точностью до секунды, и правка, попавшая в ту же
секунду, что и прошлая сборка, молча не подхватывается — стенд выносит вердикт
о СТАРОМ коде. Это хуже, чем не запускаться вовсе, и ловится обычно на
негативном контроле, когда исходник меняют туда-обратно (на нём и поймалось).
Полная пересборка графа стенда занимает около секунды.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 11:21:14 +03:00
ew8bakandClaude Opus 5 210f3e6457 fix(cw): обрыв манипуляции — поколение, а не флаг: пакет следом его отменял
Один дефект в двух местах: и TCWElemPlayer (чужая манипуляция по TCI), и
TCWSender (передача текста — F1..F8, набор в терминале, CAT KY) сбрасывали
FAbortReq прямо в Enqueue. Поток замечает обрыв только на очередном срезе Hold,
то есть в пределах 5 мс; всё, что прилетело в это окно, снимало флаг, и уже
отданный AbortPlay/AbortSending пропадал бесследно.

Чем это кончается в эфире: обрыв существует ровно затем, чтобы касание
манипулятора, снятие MOX или уход из телеграфа прекратили программную
манипуляцию (как «key hit» в Thetis/pihpsdr). Проглоченный обрыв означает, что
оператор взял ключ в руки, а софт продолжает манипулировать вместе с ним.

Источники, кормящие Enqueue, ровно такие, чтобы в 5 мс попадать: клиент TCI
шлёт элементы KEYER десятками в секунду; терминал отдаёт набор В ЭФИР ПО
СИМВОЛУ на нажатие клавиши; поле текста KY — 25 знаков, поэтому длинное
сообщение логгер шлёт несколькими командами подряд. У передачи текста цена
выше: AbortSending чистит FPending, но взятое сообщение живёт в локальной
переменной потока, и доигрывается ВЕСЬ его остаток — замер на стенде дал 620 мс
(остаток 720-мс тире на 5 WPM), у KEYER — 742 мс.

Лечение общее: вместо флага счётчик поколений. AbortPlay/AbortSending его
инкрементируют, Enqueue не трогает вовсе, поток несёт своё поколение (Take,
Aborted, Hold, SendChar) и принимает новое только сам — когда отпустил ключ и
бросил очередь. У TCWSender есть точка получше: текст и поколение берутся одним
заходом под лок (TakeText(out Gen)), а это закрывает заодно и вторую точку
сброса — ту, что стояла в начале Execute. Добор по ходу передачи стал
TakeMore(Gen): после обрыва не берём ничего, и набранное ПОСЛЕ него не теряется
вместе с брошенным, а уходит следующим сообщением со своим поколением.

Стенд: 244 проверки (было 238). В C2 — регрессия на KEYER, новая часть C3 на
передачу текста (раньше TCWSender стендом не покрывался вовсе): длительность
точки по скорости, старт сообщения, обрыв с пакетом следом, и что после обрыва
передача снова идёт. Обе регрессии проверены негативным контролем — на старом
CWMorse.pas они падают с теми самыми 742 и 620 мс.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 10:44:52 +03:00
ew8bakandClaude Opus 5 6aa4db3cb8 fix(tci): Stop прокачивает Synchronize и на accept/тик-потоках, а не только на клиентских
Шаг 4 остановки сервера намеренно ждёт клиентские потоки с прокачкой
CheckSynchronize — Stop зовут из потока контроллера, и клиентский поток может
как раз висеть на FController.Invoke, то есть на TThread.Synchronize к этому
самому потоку. Шаги 2 и 3 при этом делали глухой WaitFor, хотя тик-поток
ходит той же дорогой: ReapClients → Disconnected → HandleDisconnect →
StopTxOf → Invoke.

Проверка CanInvoke у адаптера от этого не спасает: она читает Stopping ДО
входа в Synchronize, а FStopping поднимается в начале Stop — значит клиент,
отвалившийся ровно в момент снятия галки «Enable TCI server» (или закрытия
приложения), успевает проскочить в это окно. Дальше тик-поток стоит в
Synchronize, главный — в FTickThread.WaitFor, и таймаута ни у того, ни у
другого нет: приложение висит намертво.

Общий JoinPumped: ждём Finished, прокачивая очередь (Sleep, если зовут не из
главного потока), и только потом WaitFor + FreeAndNil. Finished в FPC
взводится ПОСЛЕ DoTerminate, поэтому финальный WaitFor уже не может застать
чужой Synchronize и не блокирует.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 10:44:28 +03:00
ew8bakandClaude Opus 5 fe6fb9e292 fix(cat): ZZVB копировал VFO при опросе, KY писал в сокет без MSG_NOSIGNAL
Три замечания ревью в CAT-слое.

ZZVB. Команда была переписана под заглушку VAC (усиление приёма в
виртуальном кабеле, которого у нас нет), но заменили только комментарий —
тело осталось прежним, от старого ошибочного алиаса «обмен VFO». А ветка с
пустым суффиксом — это ФОРМА ОПРОСА: логгер, который просто спрашивает
«ZZVB;», молча затирал VFO B частотой VFO A, то есть терял сплит оператора и
даже не получал в ответ ошибку. Соседи по этому же диффу (ZZVA, ZZVG, ZZVS,
ZZAA, ZZAP, ZZBI, ZZBM, ZZDN, ZZMA, ZZMV, ZZQM, ZZOA, ZZPO, ZZSR) тело
получили, ZZVB — нет. Теперь это строчная заглушка в таблице, рядом с ZZVC и
ZZVD, которые про тот же несуществующий VAC.

ZZEB. Число полос эквалайзера принималось любое из 0..10 и уходило прямо в
TTXSettings.EQNumBands, то есть в СОХРАНЯЕМЫЙ TX-профиль. Потребители знают
ровно два случая: WDSPEngine.PushTXEQProfile ветвится на «3», редактор в
настройках — на 3 и 10. «ZZEB000…;» записывал ноль полос, всё прочее тихо
играло по полной 11-узловой кривой с чужой подписью. Принимаем только 3 и 10.

CATTcp.SendStr. Писал в сокет голым fpSend/send с флагами 0. В этой же ветке
WebUtils.SockSend получил MSG_NOSIGNAL ровно потому, что запись в закрытый
клиентом сокет иначе приходит как SIGPIPE, а он по умолчанию убивает процесс
целиком; обработчика сигнала в дереве нет. CAT про это забыли, а добавленный
здесь же цикл дозаписи расширил окно: длинный ответ (IF, ZZEB, список
режимов) уходит теперь несколькими send, и каждый может застать клиента уже
ушедшим. Пишем через WebUtils.SockSend — заодно ушла платформенная развилка.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 10:44:14 +03:00
ew8bakandClaude Opus 5 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>
2026-08-19 22:46:37 +03:00
ew8bakandClaude Opus 5 66892fc7e0 fix(tci): пара «клиент+приёмник» только по факту передачи; перебор имён .part
[P1] FTxClient/FTxRx писались ДО Invoke, и команда, которая ничего не сделала,
всё равно их перебивала. Клиент, уже передающий с приёмника 1, шлёт
trx:0,true,tci — передатчик занят, SyncSetTRX не делает ничего (Started=False),
а маркеры ИДУЩЕЙ передачи с этого мига уходят под номером 0. MSHV такие блоки
отбрасывает (network.cpp:231), то есть передача просто замолкает. Теперь пара
назначается после Invoke и только при Started — тем же признаком, по которому
назначается хозяин эфира. Тот же гейт закрывает близнеца: trx:<N>,true без
',tci' поверх своей же передачи больше не снимает источник модуляции. Снятие
(',false') работает как прежде.

[P2] Имя временного файла (pid + счётчик) уникально внутри процесса, но не
между запусками: «.part», оставшийся от прошлой жизни (публиковать было
нечем), плюс повторно выданный системой pid дают EEXIST на создании — и
задание пропадало молча. TCICreateTempNear перебирает до 64 имён, но только
пока ошибка — «имя занято»: нет прав или каталога перебором не лечится.

Стенд test/tci: 219 проверок (было 217). Новое — занятое имя «.part» записи не
теряет (стенд занимает ровно то имя, которое возьмёт писатель) и пустая
команда TRX не меняет номер приёмника в маркерах. Негативный контроль на обе
правки. Попутно в тесте поправлены два комментария, описывавшие прежнюю
реализацию публикации.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 22:02:40 +03:00
ew8bakandClaude Opus 5 ecd42c32f8 fix(tci): передача с слайса — маркер TX_CHRONO под номером клиента, фронт rfTransmitting
Живой прогон с MSHV на втором слайсе (openHPSDR): приём в порядке, эфир по
trx:1,true,tci поднимается, а звук от клиента не доходит. Два независимых
дефекта, оба видны только на приёмнике > 0 — на rx0 передача работала, потому
стенд их и не ловил.

1. Маркеры TX_CHRONO уходили с receiver = 0 жёстко. MSHV шлёт TX-аудио ТОЛЬКО
   в ответ на маркер и фильтрует все входящие бинарные блоки по номеру
   приёмника первой же строкой обработчика (network.cpp: `if
   (pStream->receiver != tci_trx) return;`, ветка TxChrono там же и собирает
   блок). У клиента на слайсе tci_trx = 1, так что маркеры отбрасывались
   целиком. Теперь вместе с клиентом-модулятором запоминается номер приёмника
   из его же TRX (FTxRx), и маркеры идут под ним.

2. Changed(rfTransmitting) из SetTxSlice стирал TCIMicRequested. Контроллер
   шлёт это поле и просто как «перерисуй TX-бейджи», а адаптер понимал любой
   такой сигнал при FTransmitting = false как «передача кончилась». Приходил
   он посередине нашей же команды: SyncSetTRX ставит просьбу → RequestSliceTx
   → SetTxSlice → Changed → просьба стёрта → SetMOX выбирает микрофон уже без
   неё. В эфир шёл микрофон оператора (тишина), а TX-аудио клиента
   отбрасывалось — TCIMicActive не поднят. Теперь ловится фронт «было → стало»
   (FLastTxOn), а не всякое уведомление.

Стенд test/tci: 217 проверок (было 214), все зелёные. Новое — часть E, слайс
как приёмник 1: по trx:1,true,tci модуляция из TCI взята, маркеры TX_CHRONO
идут и названы номером 1. Негативный контроль разделён: каждая правка краснит
свою проверку.

Проверено на железе: MSHV на втором слайсе передаёт.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 21:47:30 +03:00
ew8bakandClaude Opus 5 3dc7035f9e fix(tci): писатель WAV — один поток с очередью, файл публикуется атомарно
SAVE запускал поток на каждую запись с FreeOnTerminate: его никто не держал
и никто не ждал. Замерено отдельным процессом — при штатном выходе сразу
после сохранения от ожидаемых 100000044 байт на диске оставалось 40960, а
заголовок заявлял полную длину; на медленном каталоге идущие подряд SAVE
плодили сотни потоков, чья память стеков в 128-МБ бюджет не входила.

Теперь писатель один и принадлежит адаптеру (лениво на первом SAVE), очередь
ограничена TCI_RECORD_MAX_JOBS = 16 (сверх — клиенту writer busy и возврат
резерва), а деструктор адаптера гасит его через TCIStopWriter: Close →
WaitDrained (без срока) → Free. Срока здесь нет намеренно: поток, стоящий в
write/fsync, изнутри процесса не останавливается (Terminate не указ, Free
обязан WaitFor, бросить живой TThread нельзя — он ходит в общий бюджет),
поэтому срок не ограничивал выход, а только терял подтверждённые клиенту
записи. Ограниченный выход = писатель отдельным процессом, одним TThread не
делается; это записано в коде и в доке.

Результат записи больше не игнорируется: FileWrite возвращает число байт и
при ошибке даёт 0/-1 без исключения, поэтому на полном диске файл спокойно
дописывался до конца огрызком. TCIWriteAll — цикл с проверкой каждого вызова,
плюс FileFlush перед публикацией (на ext4 с отложенным размещением ENOSPC
приходит именно там).

Файл появляется под целевым именем целиком или не появляется вовсе: данные
пишутся во временный файл рядом (эксклюзивно, не по симлинку), а публикует
их TCIPublishFile — renameat2(RENAME_NOREPLACE) напрямую через Do_SysCall,
если его нет — link + unlink, если нет и ссылок (FAT/exFAT, часть CIFS/SMB и
FUSE) — отказ с сохранением данных в .part. FileExists + rename не делается
нигде: это тот самый TOCTOU. На Windows — MoveFileW без REPLACE_EXISTING.

doc/TCI.md: §2.5 переписан (писатель-очередь, остановка, публикация); заодно
исправлено устаревшее описание склейки кусков в Take (её нет с 7aae0fd).

Стенд test/tci: 214 проверок (было 199), все зелёные. Новое — временный файл
убирается после удачи, неудачная запись не оставляет ни файла, ни .part, за
всё время записи 32 МБ целевое имя ни разу не видно незаконченным, отказ
сверх потолка заданий, после Close заданий не берут, TCIStopWriter дожидается
и самой записи, и хвоста очереди за ней. Все новые гарантии прогнаны
негативным контролем; путь link проверен сборкой с выключенным renameat2,
сам renameat2 — под strace.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 19:40:31 +03:00
ew8bakandClaude Opus 5 7aae0fdfcd fix(tci): SAVE отдаёт писателю сами куски записи, а не сплошную копию
Потолок 128 МБ всё ещё пробивался примерно вдвое на пике: Take собирал
линейную копию ДО освобождения FChunks, поэтому при полном бюджете рядом
жили ~128 МБ кусков и ~128 МБ копии, а счётчик показывал 128. Передача
резерва писателю (9b77809) закрывала учёт, но не сам пик.

Копии больше нет вовсе. Take отдаёт куски КАК ЕСТЬ — новый TTCIRecTake
(Chunks + Chunk + Count + Reserved), — и вместе с ними уезжает их место в
бюджете целиком. TTCIWavWriter пишет куски в файл подряд: в WAV сэмплы и
так лежат встык, а хвост последнего куска за Count просто не наш. Место
отпускается в ReleaseData, ровно один раз на любом пути выхода Execute.

Продовый путь теперь не выделяет под запись ни одного лишнего байта:
TTCIPcm остался только внутри куска. Склейка нужна одному стенду, чтобы
проверять порядок и уровень, — она и живёт в стенде (FlatTake).

Стенд 199/199: 2.5 с записи отдаются ТРЕМЯ кусками по секунде (свёрнутая
копия дала бы один), счёт бюджета при Take не меняется, резерва хватает
на отданные куски, склеенные куски дают непрерывный звук, писатель
возвращает резерв по окончании. Проверено, что копирующая реализация Take
краснит четыре проверки. GUI (--ws=qt6) и демон зелёные.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 13:18:20 +03:00
ew8bakandClaude Opus 5 9b77809a73 fix(tci,cat): резерв бюджета переезжает писателю; строгий разбор полей ZZ-команд
Три замечания по 79132f1.

1. [P2] Лимит памяти рекордеров обходился при SAVE. Take собирал сплошную
   копию записи, а DropData тут же возвращал исходные куски в бюджет —
   копия жила дальше в асинхронном TTCIWavWriter уже неучтённой. Один SAVE
   поднимал настоящее потребление примерно вдвое, а на медленном или
   зависшем сетевом каталоге очередь writer-потоков и их буферов росла
   мимо потолка вовсе. Теперь копия НАСЛЕДУЕТ резерв кусков, из которых
   собрана: Take отдаёт его out-параметром Reserved, писатель держит до
   конца записи и отпускает в ReleaseData — ровно один раз, сколько бы
   путей выхода ни было у Execute. Новых денег у бюджета копия не берёт,
   так что потолок теперь считает и очередь сохранений тоже.

2. [P2] Пять ZZ-команд проверяли длину поля, но не содержимое:
   StrToIntDef(s, 0) превращал любую нечисловую пару символов в индекс 0.
   ZZBSxx; переключал диапазон на нулевой вместо ?;, ZZBMxx; и
   ZZAUxx;/ZZBPxx; двигали VFO, ZZFIxx; выбирал фильтр 0. Разбор приведён
   к идиоме ZZFL/ZZFH: TryStrToInt, иначе ошибка формата.

3. [P3] Сообщение об отказе запуска TCI звало в Settings → Advanced, а
   настройки там уже не живут — вкладка CAT (переезд был в de0830f).

Стенд 194/194: Take отдаёт резерв размером с копию, после Take занят ровно
он, писатель возвращает его по окончании. Без фикса первая проверка
краснеет. GUI (--ws=qt6) и демон зелёные. doc/TCI.md §2.5 и сводка стенда,
doc/CAT_STATUS.md — таблица разбора.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 12:50:21 +03:00
ew8bakandClaude Opus 5 79132f133c fix(tci): рекордер линейного выхода — память под потолком, файл только в своём каталоге
Два дефекта уровня P1 в LINE_OUT_RECORDER_*. Авторизации в протоколе нет
(§3.1), bind наружу разрешён — значит «сколько стоит одна строка из сети»
это вопрос живучести процесса, а не аккуратности.

1. Неограниченное выделение памяти. CmdRecorder проверял только ValidRx
   (номер в потолке), а конструктор выделял буфер целиком: 300 с × 48 кГц
   × 2 канала × int16 = 57.6 МБ на команду, приёмников 1+MAX_SLICES=7, то
   есть 403 МБ семью строками. У мёртвого приёмника Feed не зовут — значит
   срок записи никто не проверял; уход клиента рекордеры не трогал вовсе;
   DropDeadRxStreams бежит только на rfDevice/rfConnected и смене карты
   слайсов, в покое не срабатывает. Память жила до остановки сервера.

   Лечение тремя замками:
   - память набирается кусками по секунде, START не стоит ни байта;
     куски не перевыделяются (никакого realloc в DSP-потоке) и склеиваются
     один раз в Take — уже после того, как рекордер вынут из таблицы;
   - общий бюджет TCI_RECORD_MAX_BYTES (128 МБ) на все рекордеры сразу,
     спрашивается на каждый кусок; отказ не рушит запись, набранное
     остаётся сохраняемым;
   - освобождение по трём событиям: START требует живого приёмника
     (RxActive, ответ receiver is not running), тик сервера подметает
     истёкшие окна по часам (SweepRecorders — окно закрывается от START
     и без единого блока звука), уход клиента забирает его записи
     (DropClientRecorders; рекордер живёт на приёмнике, но платит за него
     тот, кто нажал START).

2. Перезапись произвольного файла. Путь из сети уходил в fmCreate почти
   как пришёл — вместе с '..' и абсолютными путями. Теперь TCIRecordPath
   берёт из строки ТОЛЬКО имя файла, каталог — настроенный tci.record_dir
   (пусто = <каталог конфигурации>/records). Каталог из просьбы
   отбрасывается молча: полный путь на сервере клиенту всё равно
   бесполезен, файл ложится не на его машину. Имя валидируется (пусто,
   '.', '..', управляющие, ':', длиннее 120, расширение не .wav →
   bad file name); после ExtractFileName выйти за каталог нечем. Файл
   создаётся эксклюзивно (TCICreateNewFile: O_EXCL|O_NOFOLLOW на Unix,
   CREATE_NEW на Windows) — ни перезаписи, ни симлинка, без окна между
   FileExists и созданием; клиенту заранее file exists.

Попутно: MainForm.ApplyTCISettings собирал TTCISettings по полям с
чистого листа — новое поле RecordDir обнулялось бы при каждом применении
вкладки CAT. В uses TCIStreams Windows стоит первым намеренно: иначе его
TCriticalSection перекрыл бы SyncObjs (та же грабля, что в DX-кластере).

Стенд 189/189 (было 169): 11 проверок имени файла (/etc/passwd.wav,
../../.., D|\rec\a.wav), бюджет (START не выделяет, растёт кусками,
потолок, отказ не рушит запись, срок истекает без Feed), существующий WAV
не перезаписывается, START на мёртвом приёмнике и с чужим номером, и
сквозная проверка в части E — клиент стартует запись, набирает память
живым звуком через WDSP, рвёт TCP, бюджет возвращается к нулю. Без фиксов
новые проверки краснеют. GUI (--ws=qt6) и демон зелёные.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 12:30:36 +03:00
ew8bakandClaude Opus 5 adbb8d02ac fix(tci): Stream.length — вещественные отсчёты всего блока, а не на канал
MSHV не декодировал FT4 по TCI (по виртуальному кабелю — декодировал).
Регрессия из dc6f996: там length аудио был переведён на «сэмплы на канал»
по формулировке §4.3, а живые клиенты считают по нему БАЙТЫ блока
(network.cpp: `int cr2 = pStream->length*bit_s;` с шагом `chan*bit_s`,
на передаче `quint32 cr3 = pStream->length*bit_s;`). При Channels=2
(умолчание MSHV) клиент разбирал половину каждого блока: звук с дырами
50%, водопад шире и грязнее, декодер разваливался. В моно дефекта не
видно — единицы совпадают, поэтому первый прогон стенда увёл в сторону.

Правка верна и по документу, а не только по клиенту: §3.4 после IQ
(«количество вещественных отсчётов… комплексных = length/channels»)
говорит «аудиопоток приёмника ПОЛНОСТЬЮ ПОВТОРЯЕТ IQ поток» и
перечисляет ровно три отличия — каналы, формат сэмплов, число сэмплов в
пакете. Единиц length среди них нет, поле в struct Stream одно.
Развилка по типу потока была вычитана из воздуха.

§4.3 путает две величины, и её формулировка верна лишь для моно:
AUDIO_STREAM_SAMPLES — кадры НА КАНАЛ, Stream.length — отсчёты ВСЕГО
блока. Что arg1 считает кадры, видно из самой §4.3 дважды: минимум
512/256/128/100 на 48/24/12/8 кГц даёт обещанные «не меньше 10 мс»
только при счёте на канал, и потолок data[16384] = 2048 × 2 × float32.
TX_CHRONO замыкает круг: клиент шлёт столько отсчётов, сколько названо
в length маркера, и возвращает то же число обратно.

Размер блока не менялся (2048@48к = 42.7 мс). Гипотеза про клиппинг
тапа RX_AUDIO проверена замером и снята: пик 0.115.

Стенд: проверки length переписаны на инвариант «байт = length × размер
отсчёта»; новый test/tci/ft4_bench.py снимает поток TCI и PipeWire-
источник одновременно, режет на нарезки FT4 по общим часам и гоняет
через настоящий jt9 --ft4 (блок разбирает КАК MSHV — этим и поймал).
Проверено вживую: MSHV декодирует FT4 по TCI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 12:26:29 +03:00
ew8bakandClaude Opus 5 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>
2026-08-18 22:10:02 +03:00
ew8bakandClaude Opus 5 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>
2026-08-18 18:10:29 +03:00
ew8bakandClaude Opus 5 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>
2026-08-18 12:36:50 +03:00
ew8bakandClaude Opus 5 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>
2026-08-17 22:53:32 +03:00
ew8bakandClaude Opus 5 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>
2026-08-17 16:49:31 +03:00
ew8bakandClaude Opus 5 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>
2026-08-17 16:22:49 +03:00
ew8bak 92c01fa2c6 Merge feature/cat-tx-cw: CAT-команды TX-тракта и телеграфа, ревизия алиасов и подписей, починка транспортов 2026-08-17 13:15:14 +03:00
ew8bakandClaude Opus 5 20bf3c1f39 fix(cat): транспорты — короткая запись в TCP и молчаливый отказ serial-порта
CATTcp.SendStr делал один send на ответ. TCP не обязан отдавать весь буфер за
раз, а усечение здесь не ошибка — длинный ответ (IF, ZZEB, список режимов) мог
уехать обрезанным, и молча. Дописываем остаток в цикле.

CATSerial помечал порт активным ДО SerOpen, а открытие шло внутри потока: при
отказе порт навсегда оставался «работающим» в ActiveCount и UI, а причина
нигде не оседала. Открытие переехало в TCATSerialPort.Start и делается
синхронно, так что отказ виден сразу — FActive остаётся False, причина в новом
свойстве LastError. Поток теперь только читает, закрывает владелец в Stop.

Заодно Andromeda-порт назначался по галке в настройках, без оглядки на то,
поднялся ли порт: HasAndromeda рапортовал о панели, которой нет.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 13:13:46 +03:00
ew8bakandClaude Opus 5 27a8d1d5cf feat(cat): TX-тракт и телеграф; ревизия алиасов, подписей и валидации
Со времён прошлой правки CAT в EWSDR появились TX-профили со всей «звуковой»
цепью и телеграф — команды, которые doc/CAT_STATUS.md объявлял незакрываемыми,
стали рабочими.

TX-тракт: MG/ZZMG (усиление микрофона), MO/ZZMO + ZZTM (монитор передачи и его
уровень), ZZTL/ZZTH (кромки TX-фильтра), PR/ZZPK + ZZPL (речевой компрессор),
ZZET + ZZEB (эквалайзер), ZZTO + ZZTU (мощность и кнопка настройки), ZZUT
(2TON), ZZLI + ZZUS (PureSignal), ZZTP (выбор профиля), ZZFD (девиация).

Телеграф (были проброшены только KS/KY/ZZKS/ZZKY): ZZCS скорость, ZZCL тон,
ZZCI иамбик, ZZCB/ZZCD break-in и hang-time, ZZCM сайдтон.

Ревизия подписей. Комментарии у заглушек писались по буквам кода, а не по
Thetis, и врали примерно в 150 местах. Хуже: часть РАБОТАЮЩИХ команд была
привязана не к своей функции — внешний софт получал осмысленный, но неверный
ответ, что хуже честной заглушки. Всё сверено с CATCommands.cs:

  ZZMA  режим        -> кнопка MUT            ZZNN  заглушка -> SNB
  ZZRX  переход в RX -> аттенюатор RX1        ZZNS  SNB      -> кнопка NR2
  ZZRV  версия ПО    -> напряжение питания    ZZVS  алиас ZZSP -> операции с VFO
  ZZMV  индекс режима-> счётчик памяти        ZZBM  режим    -> VFO B вниз на nn
  ZZKM  режим кейера -> запуск CW-макроса     ZZFT  всегда A -> TX-частота (split)
  ZZFI/ZZBS  были алиасами -> сами держат фильтр и диапазон

Ошибочные алиасы на громкость/мощность сняты (ZZAA ZZVG ZZOA ZZAP ZZDN ZZAC
ZZBI ZZMB ZZVA ZZPD ZZPO ZZQM ZZSR ZZRD ZZRU) — теперь честные заглушки с
указанием, где функция живёт на самом деле.

Кенвудовские команды сверены отдельно, покрытие полное (все 40 из Thetis),
ширины полей совпадают с CATStructs.xml. Исправлено: SM отдавал 4 цифры вместо
5; RD/RU перестраивали VFO, хотя это RIT; KY не срезал набивку поля пробелами
и гнал её в эфир паузами; CT принимала любой символ и «CT9;» молча гасил тон;
OF/OS несли реализацию сами, а ZZOT/ZZOS были заглушками — клиент Thetis
обращается как раз к ZZ* и не получал ничего.

Валидация. Команды без параметров не проверяли суффикс: «TXanything;» доходил
до CmdTX и ПОДНИМАЛ ПЕРЕДАЧУ (так же RX UP DN BD BU QI RC ID IF) — эталон
отбраковывает лишний суффикс в парсере, у нас теперь список в IsParamless.
ZZTX поднимал передачу на любом значении кроме нуля («2=TUNE» — выдумка).
ZZFL/ZZFH принимали поле любой длины от 4 символов и через StrToIntDef молча
схлопывали кромку в ноль. ZZMG принимал 1-2 символа. KY/ZZKY не ограничивали
текст 25 символами, и очередь передачи могла расти произвольно.

Осознанные отклонения от эталона сведены в отдельную таблицу документа:
ZZBS (индекс диапазона вместо кода), ZZMN (имя режима вместо пресетов
фильтров), ZZST (шаг FM вместо размера шага настройки), ZZCD (потолок 2000 мс —
наш предел, он же в поле HangDelay пакета DUC Specific).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 13:13:33 +03:00
ew8bakandClaude Opus 5 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>
2026-08-17 13:12:10 +03:00
ew8bak f89f202a34 Merge feature/dxcluster-spots: споты DX-кластера на панадаптерах, мода по бэндплану, кнопка DX в RX 2026-08-17 11:29:04 +03:00
ew8bakandClaude Opus 5 6ed1be3dc7 fix(dxcluster): резолв имени, гейт логина, TTL и мелочи UI по итогам ревизии
Сеть и резолв (DXClusterClient):
- Резолв ушёл в отдельный поток: системный резолвер блокирующий и не
  прерывается, а сокета в этот момент ещё нет — Stop из UI-потока висел на
  DNS-таймауте. Сессия ждёт квантами по 200 мс, просыпаясь на FStopEvent;
  Stop теперь отрабатывает за 100-200 мс в любой фазе.
- Реестр запросов: по одному резолверу на имя, не больше DX_MAX_RESOLVERS.
  Не дождавшись, сессия оставляет запрос в реестре и на следующей попытке
  цепляется к нему же (иначе зависший резолвер плодил бы вечные потоки).
  Слотов несколько, чтобы смена адреса работала поверх зависшего прежнего;
  литеральный IP разбирается до реестра — ввод адреса руками обязан работать
  всегда. Запись живёт по счётчику ссылок, исключение в резолвере не
  оставляет слот занятым.
- getaddrinfo вместо netdb.ResolveHostByName: тот ходит в DNS сам и
  /etc/hosts не читает вовсе (getent находит localhost, ResolveHostByName —
  нет), т.е. локальный алиас кластера не работал. Заодно реентерабельно.
- WSAStartup перенесён в initialization, WSACleanup убран: Stop не ждёт
  резолвер, а тот может сидеть в gethostbyname.

Логин (DXClusterClient):
- Команды пользователя больше не уходят в незавершённый логин: очередь
  разбирается только после post-login, SendCommand говорит в лог, что
  команда ждёт.
- ONLINE не по таймеру, а по существу: LoginSettled требует ответа сервера
  после учётки (с потолком молчания), пароль ждёт своего приглашения.
  Подтверждение ставится ПОСЛЕ разбора куска, а не на приход байтов —
  иначе исход логина зависел от границ TCP-пакетов.
- Приглашения и отказы: строгий детектор (текст, заканчивающийся
  двоеточием) и для отправки пароля, и для вердикта — по вхождению слова
  пароль улетал командой от строки приветствия. Повтор приглашения пароля
  или позывного = отказ авторизации (dxsError, без реконнекта); опоздавшее
  приглашение после слепой отправки позывного отказом не считается.
  Эхо уже отвеченного приглашения гасится окном в одну строку.
- Ошибка отправки post-login рвёт сессию, а не только цикл команд;
  неотправленная очередь возвращается на следующее соединение.

UI и данные:
- QSY по споту крутит активный VFO, а не всегда A (MainForm).
- TTL спотов чистится тиком независимо от видимости оверлея (DXSpotStore
  .Purge + ServiceDXCluster).
- Выделение в окне списка держится по позывному И частоте: один позывной
  живёт на разных диапазонах (DXClusterForm).
- Кнопка DX правит и отложенную копию настроек, иначе debounce SETUP
  возвращал прежнее состояние подписей (MainForm).

Проверено на фейковом кластере: приглашения с CRLF и без, границы
TCP-пакетов, отказ по паролю и по позывному, опоздавшее приглашение,
ловушки ложного срабатывания.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 22:00:41 +03:00
ew8bakandClaude Opus 5 eb4af61fad ui(spectrum): полоса главного фильтра — как у слайсов, со своим оттенком
Главный фильтр рисовался иначе, чем слайсовые полосы, причём в CPU-пути
сильнее, чем в GL.

1. Порядок отрисовки. Кромки, несущая и её треугольник у главного шли ПОСЛЕ
   кривой спектра, у слайсов — до неё, а в GL-вьюхе весь блок фильтров идёт
   перед DrawSpectrumCurve. Из-за этого на CPU главная полоса единственная
   лезла поверх сигнала и выглядела жирнее и ярче слайсовых при тех же цвете и
   толщине линий. Блок перенесён к слайсам, до кривой: порядок кадра теперь
   одинаков у обоих путей.

2. Заливка. Была НЕПРОЗРАЧНОЙ, рядом с просвечивающими слайсовыми смотрелась
   плотным блоком. Теперь та же полупрозрачность, что у слайсов; прозрачность
   вынесена в общие SPEC_BAND_ALPHA/SPEC_BAND_ALPHA_TX (VfoOverlay, рядом с
   палитрой слайсов) — раньше это были четыре магических числа по двум вьюхам.
   Осиротевшая FillBandRaw удалена.

3. Цвет. С полупрозрачностью полоса перестала читаться: SpecFilter (34,78,106)
   почти совпадает с нижним цветом фонового градиента спектра (31,79,108), и
   заливка давала не оттенок, а затемнение — над низом градиента выходило
   ровно (32,79,108), то есть фон. Полосе дан свой цвет темы SpecFilterBand:
   тёплый янтарь RGB(255,150,40) на тёмной (фон сине-стальной, тёплое на нём
   читается) и насыщённый зелёный RGB(60,150,60) на светлой. SpecFilter
   остался подложкой подписей AGC — иначе перекрасились бы и они.

4. Слайс B перекрашен из оранжевого в васильковый RGB(97,118,255): с янтарной
   полосой главного фильтра оранжевый стал неразличим. Оттенок выбран по
   свободному месту в круге: заняты 0°(TX), 31°(полоса), 55°(E), 132°(A),
   190°(C), 195°(G), 276°(F), 308°(D); самый широкий промежуток 195..276°,
   середина ≈236°. Палитра одна на всё, поэтому вместе с полосой и несущей
   перекрашиваются буква слайса на спектре и бейдж во флаге VfoOverlay.

Сборка ewsdr (--ws=qt6) и ewsdrd — ОК. На экране не проверено: нужен эфир со
слайсами; цвета правятся одной строкой (SpecFilterBand в AppTheme,
SLICE_COLORS в VfoOverlay).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 23:14:59 +03:00
ew8bakandClaude Opus 5 d0c82ade73 ui(spectrum): вертикаль несущей не перечёркивает букву полосы фильтра
Буква слайса стоит по центру полосы фильтра у самого верха, и вертикаль
несущей шла туда же — от Y=0. В SSB/CW это не мешало (несущая лежит у кромки
полосы, буква — по центру), а в модуляциях с несущей (AM/FM/DSB) центр полосы
и есть несущая, и линия шла прямо по букве.

Одна функция на оба вида — CarrierTopY (CPU) / CarrierTopYGL (GL): смотрит,
попадает ли вертикаль под глиф, и если да, отдаёт Y ПОД буквой. Решение по
факту пересечения, а не по списку «модуляций с несущей»: список пришлось бы
вести и сопровождать, а в SSB зазор и так выходит нулевым сам собой.

- Слайсы: DrawSliceFilterLinesRaw и DrawSliceFilterMarkers.
- Главный VFO: вместе с линией вниз уезжает и её «шляпка» — треугольник в CPU
  (у RawTriangleDown появился параметр верхней координаты) и метка в GL. Без
  этого они остались бы поверх буквы и зазор не помог бы.
- Полоса TX при split сверяется с той же буквой — она нарисована на RX-полосе.
- В GL буква главного флага теперь рисуется ПОСЛЕ линий, как в CPU: порядок
  наложения у путей должен совпадать.

Кегль буквы вынесен в BAND_LETTER_FONT: рисование и расчёт зазора обязаны
брать его из одного места, иначе зазор разъедется с глифом.

Метрика проверена замером (Qt6, offscreen): глиф 8 bold = 7x12 px ⇒ вертикаль
стартует с Y=14, по горизонтали зазор ±6 px от центра полосы. Правится
константами CLEAR_X/CLEAR_Y рядом с функцией.

Сборка ewsdr (--ws=qt6) и ewsdrd — ОК. На экране не проверено: нужен эфир с
AM/FM и слайсами.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 19:21:08 +03:00
ew8bakandClaude Opus 5 13b46f0b7d ui(dxcluster): системный шрифт вместо захардкоженного 'Courier New'
По проекту гарнитуры не задаём — работает системный шрифт (907c2c0 выпилил
'Courier New' из flat-контролов, 19babef из статус-бара). Новый код DX-кластера
это правило нарушал в трёх местах, все три введены в a2fdc3a.

- DXSpotOverlay: подписи спотов на спектре рисуются системным шрифтом —
  гарнитура убрана совсем. Моноширинность там ни к чему: позывные не в
  колонках, ширина чипа и так меряется по TextWidth. Размер по-прежнему задан
  через Font.Height от высоты ряда — иначе подпись не влезала бы при DPI.
- DXClusterForm: список спотов и лог соединения ОСТАЮТСЯ моноширинными
  осознанно (колонки частота/позывной/мода/UTC/спотер выровнены пробелами в
  FormatSpotLine, пропорциональный шрифт их развалит), но имя теперь
  платформенное — MONO_FONT = Consolas/Monospace, как в CWTerminalForm и в
  полосе CW у PanafallPanel. 'Courier New' на Linux не существует вовсе,
  fontconfig молча подменял его.

Font.Size/Font.Style не трогал: их проект оставляет и после перехода на
системный шрифт (в StatusBar размер сохранён).

Дифы всех четырёх коммитов ветки проверены по Font.Name — других мест, где
DX-код задаёт гарнитуру, нет. Сборка ewsdr (--ws=qt6) — ОК.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 18:43:36 +03:00
ew8bakandClaude Opus 5 84f710476f fix(dxcluster): сборка под win64 — Windows.pas перекрывал TCriticalSection
Сборщик под win64 падал в конструкторе клиента кластера: «identifier idents no
member Create». Windows.pas объявляет TCriticalSection как ЗАПИСЬ (алиас
TRTLCriticalSection), а в uses он стоял ПОСЛЕ SyncObjs — и поле FLock, и
TCriticalSection.Create разрешались в запись. На Linux эта ветка uses не
компилируется вовсе, поэтому баг жил с первого коммита DX-кластера (a2fdc3a)
и всплыл на первой же сборке под Windows.

Windows.pas убран из uses совсем: сокетам он не нужен, всё даёт WinSock2 —
так же сделано в WsClient (там тоже FLock: TCriticalSection) и HPSDRNetwork.
Причина подтверждена минимальным репро, дающим дословно тот же текст ошибки.

Сверено по исходнику winsock2.pp (FPC 3.2.2), чтобы следующая итерация
сборщика не встала на следующем имени: connect/socket/select/getsockopt/
setsockopt/FD_SET/FD_ZERO/WSAStartup/WSACleanup/inet_addr/gethostbyname/htons,
типы TWSAData/TSockAddrIn/TFDSet/TTimeVal/PHostEnt и константы INADDR_NONE/
SOL_SOCKET/SO_ERROR/SO_KEEPALIVE/WSAE* — все на месте. Указательная форма
connect(@Addr, …) тоже корректна: в winsock2 есть обе перегрузки.

Заодно FNV-хэш подписи набора спотов обёрнут в {$Q-}{$R-}: он живёт
переполнением, а проверки правятся в .lpi (в Release их сейчас нет).
Порядок «Windows после SyncObjs» проверен по всему проекту — больше нигде.

Сборка ewsdr (--ws=qt6) и ewsdrd — ОК, тест бэндплана зелёный. Win64 проверит
сборщик.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 18:32:59 +03:00
ew8bakandClaude Opus 5 d12d8239da feat(dxcluster): мода спота по бэндплану, когда комментарий молчит
В CW и SSB спотеры сплошь и рядом не пишут моду, и такой спот оставался
dxmUnknown: ни фильтр по модам его не видел, ни QSY по нему моду не ставил.
Теперь порядок такой: сначала комментарий (как было), и только если он молчит —
участок бэндплана.

DXSpotStore.DXModeFromFreq — таблица участков, читается сверху вниз, первое
попадание выигрывает. Узкие «водопои» цифры стоят РАНЬШЕ широких сегментов:
FT8 на 7074 живёт посреди телефонного участка R1, FT4 на 21140 — посреди
15-метрового, и без такого порядка они утонули бы в SSB.

★ Границы — по IARU Region 1 (наш регион), а споты прилетают со всего мира,
поэтому там, где регионы расходятся, мода НЕ выводится вовсе: пропуск честнее
ошибки. Отсюда дырки 1843-1850 (R1 телефон против R2 CW), 60 м (канальный),
маячные щели, 2 м/70 см кроме FT8-окон. Два спорных куска всё же отданы R1:
3570-3600 — цифре (у R2 это ещё CW, но споты там почти сплошь FT8/RTTY) и
7053-7300 — телефону (у R2 ниже 7125 данные).

QO-100 (даунлинк 10489.5-10490.0) — отдельной веткой по плану AMSAT-DL, теми же
границами, что рисует BandPlanOverlay: CW, NB/DIGI, две SSB-зоны. Маяки и
mixed modes не гадаем.

Угаданное помечено (TDXSpot.ModeGuessed) и в окне списка выводится с '?' —
'CW?' против 'CW': спотер моду не называл, а на границах участков таблица
может ошибаться, поэтому разницу видно. На спектре в чипе только позывной —
там ничего не изменилось.

Проверено консольным тестом на этих же юнитах: 25 частот + 6 полных строк
кластера (комментарий перебивает таблицу; 7012.5 → CW guessed, 7145 → SSB
guessed, 10489.680 → SSB guessed; 1846, 60 м и 144.300 остаются без моды).
Сборка ewsdr (--ws=qt6) и ewsdrd — ОК.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 18:13:28 +03:00
ew8bakandClaude Opus 5 17fa965aec feat(dxcluster): споты на всех панадаптерах + кнопка DX в блоке RX
Подписи спотов рисовались только на главном пане. Теперь их видит каждый пан,
в чьё окно попадает частота спота.

Оверлей — ЭКЗЕМПЛЯР НА ПАН, а не один общий: кэш полосы подписей ключуется
центром/спаном/шириной, а у панов они свои — общий оверлей пересобирался бы на
каждый пан каждый кадр, то есть ровно то, ради чего кэш и заводился. Живёт в
TPanafallPanel (AttachDXSpots/DetachDXSpots/DXOverlay, Owner=панель), база
спотов по-прежнему одна на всех. FDXSpotOverlay в MainForm стал алиасом на
оверлей пана 0 — как FSpecView для FPan.View: настройки, тема и тик затухания
идут общим циклом по FPans, и главный пан перестал быть особым случаем (иначе
тумблер DX гасил бы подписи только на нём).

★ Version стора глобальна, а окно у пана своё, поэтому в EnsureRendered на
смену версии сначала берётся снимок окна и сверяется его подпись (FNV-1a по
позывному, частоте, моде и метке времени). Совпала — ни пересборки битмапа, ни
перезаливки GL-текстуры: спот, севший на чужой диапазон, до этого пана не
доходит. Без этого цена пересборки множилась бы на число панов. Снимок берётся
один раз и переиспользуется раскладкой — стор второй раз не дёргаем.

Клик по подписи на пане N = QSY слайса ЭТОГО пана (активного, если он здесь,
иначе первого; нет ни одного — создаём в точке) с модой по комментарию
кластера и полосой пресета этой моды. Своего VFO у панов N нет, а ретюнить их
DDC под спот нельзя — увезло бы весь пан. Мода спота → режим вынесена в общую
DXSpotRadioMode для обоих путей.

Низкий пан (грид): полоса подписей и штрихи пропускаются целиком — раньше в
GL-пути полоса легла бы поверх спектра, а штрихи пошли бы снизу вверх.

Кнопка DX переехала из тулбара в блок RX левой панели, сразу после CTUN (ряд
стал четырёхколоночным: CTUN | DX | Channel | BEACON): споты — часть приёмного
вида, а не глобальная команда уровня DISCOVER/START/SETUP. Заодно снят расчёт
SafeLeft, резервировавший под неё место в шапке.

Подписи по умолчанию ВЫКЛЮЧЕНЫ (show_spots: дефолт и фолбэк чтения были True),
состояние переживает перезапуск. Клик по кнопке подливает состояние в открытый
SETUP: там своя копия конфига, и первая же правка любого поля страницы вернула
бы подписи обратно.

fix: Stop клиента кластера больше не держит главный поток. Он делал Terminate +
WaitFor, а поток в этот момент сидит в блокирующем recv и просыпается лишь по
своему кванту (до 1 с), на застрявшем send — до SO_SNDTIMEO. Всё это время
окно висело на выходе и на переподключении после правки настроек. Теперь Stop
рвёт живой сокет SockShutdown (хэндл — зеркало под тем же локом, поток снимает
его ДО close, поэтому shutdown по закрытому fd невозможен), а FormDestroy зовёт
неблокирующий RequestStop первой строкой: поток доживает параллельно с
разборкой радио, и join в конце уже никого не ждёт.

Сборка ewsdr (--ws=qt6) и ewsdrd — ОК. На железе не проверено: доп. паны
требуют подключённого радио.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 18:00:08 +03:00
ew8bakandClaude Opus 5 a2fdc3a741 feat(dxcluster): споты DX-кластера на панадаптере
Telnet-клиент DX-кластера, база спотов и подписи позывных прямо на спектре
на своих частотах. Разложено на три слоя, как бэндплан и лупа маяка.

DXClusterClient.pas — один рабочий поток: резолв, неблокирующий connect с
select квантами по 200 мс (Stop не ждёт таймаут соединения), логин позывным,
чтение строк, реконнект с backoff. Приглашения логина И пароля ловятся в
незавершённом хвосте буфера — типичный telnet-prompt приходит без CR/LF.
Ошибки recv отличаются от таймаута кванта (EAGAIN/EINTR/WSAETIMEDOUT), иначе
на ECONNRESET поток крутился бы в пустом цикле вместо реконнекта. Отправка
дописывает частичный send. LCL-free.

DXSpotStore.pas — потокобезопасная база: дедуп по позывному, TTL, потолок
записей, монотонный Version. Единственная точка обмена потока с UI: никакого
Synchronize, UI сам замечает правки по Version, как оверлеи — по ключам кэша.

DXSpotOverlay.pas — рендер по модели BandPlanOverlay/VfoOverlay: кэшируется
только полоса подписей (W × BandH) в key-color битмап, пересборка строго по
dirty-ключу, на кадр — один keyed-композит. Штрихи от полосы до низа спектра
рисует вызывающая сторона теми же примитивами, что и прочие маркеры: CPU —
RawVLine внутри RawBegin/RawEnd, GL — DrawLine по готовому списку X/цвет.
GL берёт тот же битмап текстурой и заливает её только при смене RenderVersion.
Пересекающиеся подписи раскладываются лесенкой, цвет гаснет с возрастом,
свой позывной выделен; палитра парная под тёмную и светлую тему.

DXClusterForm.pas — окно списка: споты, лог соединения, строка команды
кластеру (диалект set/filter у всех свой — не угадываем). Двойной клик или
Enter = QSY. Данные тянутся поллингом по Version/LogVersion.

Интеграция: кнопка DX в тулбаре (ЛКМ — подписи на спектре, ПКМ — окно),
клик по подписи спота = QSY с автовыбором моды по комментарию кластера,
страница SETUP → DX Cluster с персистом в секции "dxcluster". Правки SETUP
прилетают посимвольно, поэтому запись конфига, TTL стора и переподключение
откладываются до паузы в наборе — иначе набор позывного стоил бы шесть
реконнектов, а промежуточный TTL «3» необратимо выбросил бы споты.

Частота спота кладётся как есть и сравнивается с GetViewWindow: на QO-100
кластеры постят downlink 10489.xxx, что совпадает со шкалой пана само собой.

Побочно в общих юнитах: WebUtils.SockSetRcvTimeout, FlatMemo.OnChange,
FlatListBox.OnKeyDown, BlendBitmapKey вынесен в interface VfoOverlay (одна
копия дворд-блендера на проект).

Проверено на локальном фейковом кластере: логин и пароль по prompt без CR/LF,
разбор спотов (включая QO-100), уход в RETRY по RST, Stop за 200 мс на
висящем connect. На железе рендер не проверялся.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 15:47:26 +03:00
ew8bak ab848404b1 Merge feature/beacon-zoom-panel: лупа наведения на маяк QO-100
Окно Beacon получило карточку BEACON TUNING: узкий кусок спектра вокруг маяка в
высоком разрешении плюс мини-водопад. На спектрограмме 576к маяк занимает пару
пикселей и ткнуть в него для наведения декодера почти нельзя — здесь то же
наведение делается по широкому следу. Наведение с главной спектрограммы не
тронуто.

Увеличение делает сам WDSP: второй analyzer на том же IQ главного тракта, со
своей FFT и ассиметричным span-clip, посчитанным прямо из границ окна в герцах.

Вошло:
- лупа с кликом-наведением, зумом колесом, панорамированием и FOLLOW;
- автопоиск маяка (AUTO) по min-hold — критерий «непрерывный след», а не
  «самый громкий»;
- состояние захвата прямо в лупе: полоса приёма меняет начертание на локе,
  рядом SNR и расхождение с опорной частотой;
- окно лупы переживает перезапуск (сдвиг от опорной, а не абсолют);
- контур больше не принимает за маяк голую несущую — проверка BPSK по
  констелляции;
- взведённый клик снимается при любом наведении, не только мышью;
- попутно: пропадавшая расстройка тона в статусе терминала телеграфа ('%+d'
  в Format Object Pascal не работает).

На эфире проверено частично: наведение и картинка — да, автопоиск и новый
критерий лока — нет.
2026-08-14 13:03:27 +03:00
ew8bakandClaude Opus 5 c30c23a64b fix(beacon): взведённый клик снимается при любом наведении, не только мышью
FBeaconArming гасился только в обработчике клика по спектру. Навёл AUTO из окна
лупы — флаг остался взведён, и следующий случайный ЛКМ по панораме утаскивал
маяк на пустое место. Те же грабли были для наведения из web и CAT.

Снимаем в обработчике rfBeaconLock, как только BeaconDecodeFreqHz стал
ненулевым — кто навёл, роли не играет. FSpectrumDirty просим только в этот
момент: маркеры и так едут вместе с кадрами, а на стопе спектр статичен, дёргать
перерисовку 10 раз в секунду незачем.

Сборка ewsdr (--ws=qt6) — ОК.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 13:01:42 +03:00
ew8bakandClaude Opus 5 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>
2026-08-14 13:01:26 +03:00
ew8bakandClaude Opus 5 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>
2026-08-14 12:59:46 +03:00
ew8bakandClaude Opus 5 504265f8ad fix(cw): в статусе терминала пропадала расстройка тона
Format(', tone %+d Hz', ...) печатал ', tone d Hz' — без значения. Флага '+' в
Format Object Pascal нет, синтаксис здесь %[индекс:][-][ширина][.точность]тип;
встретив '%+', FPC съедает процент с плюсом и копирует остаток спецификатора
как обычный текст. Знак ставим руками, отрицательные %d печатает со своим
минусом сам.

Найдено попутно — тот же промах был в новом коде лупы маяка.

Сборка ewsdr (--ws=qt6) — ОК.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 12:13:33 +03:00
ew8bakandClaude Opus 5 a5daf33cc9 feat(beacon): состояние захвата видно прямо в лупе
Чтобы понять «попал или нет», приходилось переводить взгляд на плитки метрик
под графиком. Теперь это читается в самой лупе:

- Полоса приёма показывает захват начертанием: в поиске три линии пунктирные,
  на локе — сплошные и стянуты сверху скобой. Новых цветов не добавлено, та же
  семантика $0020D0FF, что и на главном спектре.
- Строка захвата под частотой центра: LOCK · 14.2 dB (зелёная) / SEARCH (янтарь)
  / BEACON OFF. Состояние берётся из BeaconState — это гистерезисный
  FBeaconLocked, которому доверяет сам контур коррекции, а не мгновенный
  CarrierLock: тот мерцал бы на границе.
- Δ от опорной рядом: расхождение измеренной частоты маяка с опорной, то есть
  расстояние между оранжевым и зелёным маркерами числом. Это невязка, которую
  лок стекает в LOError; в пределах дедбэнда (20 Гц) — зелёная, иначе янтарная.

Знак у Δ ставится вручную: флага '+' в Format Object Pascal нет (это printf'изм),
'%+.0f' молча съедается и в строку попадает голый хвост спецификатора — на
экране было «Δ .0f Hz» вместо значения.

Состояние и SNR снимаются раз в тик в PollZoom и кладутся в поля: копия
TBeaconScope в отрисовку не лезет, а SNR сравнивается с допуском 0.2 дБ —
иначе дрожание в сотых долях дБ пересобирало бы кадр каждый тик и dirty-флаг
потерял бы смысл.

Сборка ewsdr (--ws=qt6) — ОК.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 12:13:33 +03:00
ew8bakandClaude Opus 5 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>
2026-08-14 12:03:13 +03:00
ew8bakandClaude Opus 5 ac216fa332 fix(beacon): трасса лупы не рисовалась до первого клика по маяку
Перерисовку спектра лупы гейтил не тот сигнал: возврат GetBeaconZoomSpectrum
(«в кэше лежит непрочитанный кадр») игнорировался — функция звалась как
процедура, — а FZoomDirty ставился только по смене границ окна и по сдвигу
маркеров. Пока никто не панорамирует, границы не меняются, а до клика все три
маркерные частоты нулевые и неподвижные: кадр собирался один раз и застывал.

Клик задаёт наведение декодера, дальше BeaconDecodeFreqHz непрерывно едет за
несущей в ServiceBeaconLock — сравнение с FLastDecHz начинает срабатывать
каждый тик, и спектр «оживает». Водопад работал с самого начала: он всегда шёл
по своему честному флагу свежести, хотя слой тот же самый и анализатор один.

Теперь свежесть кадра — основной триггер (трасса идёт 15/с, как и даёт
анализатор); проверки окна и маркеров оставлены как дополнительные, они дают
отклик на зум/пан не дожидаясь следующего кадра.

Сборка ewsdr (--ws=qt6) — ОК.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 11:54:13 +03:00
ew8bakandClaude Opus 5 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>
2026-08-14 11:42:25 +03:00
ew8bakandClaude Opus 5 b0a93ddb14 Merge feature/cw: телеграф (кейер прошивки + программный), терминал и декодер CW
Телеграф на openHPSDR P2 и Pluto/LibreSDR: кейер в FPGA (тайминги в железе) и
программный кейер для бэкендов без него, pitch как ЕДИНЫЙ сдвиг гетеродина в
движке, сайдтон, CWX/F1..F8, CAT KY/KS/ZZKM, терминал с набором с клавиатуры и
декодером приёма. Голос в CW заблокирован, реле T/R и антенна ходят за ключом
прошивки, запрет передачи (RX-only XVTR, DoNotTx) действует и на кейер FPGA.

Попутно: окно повторов Specific-пакетов только со старта (e87ca6b), правки в
трансвертере больше не травят память КВ-диапазона (827902f), перестройка
слайса по CAT внутри полосы захвата обновляет флаг (00860e5).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 23:47:57 +03:00
ew8bakandClaude Opus 5 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>
2026-08-12 23:46:05 +03:00
ew8bakandClaude Opus 5 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>
2026-08-10 21:28:48 +03:00
ew8bakandClaude Opus 5 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>
2026-08-10 17:38:54 +03:00
ew8bakandClaude Opus 5 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>
2026-08-10 14:33:22 +03:00
ew8bakandClaude Opus 5 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>
2026-08-10 14:09:05 +03:00
ew8bakandClaude Opus 5 e87ca6b186 fix(hpsdr): окно повторов Specific-пакетов — только со старта, не на каждую пере-сборку
RebuildDDCSpecific сбрасывал FResendCount, а этим счётчиком keepalive-поток
управляет окном повторов: пока он меньше 100, каждые 500 мс переотправляются
DDC Specific и DUC Specific. Окно задумано как стартовый костыль — порты
1025/1026 на железе открываются только после Run=1.

Сброс из пере-сборки делал окно рецидивирующим: любая переконфигурация на
живой сессии заводила ещё десять пере-латчей DDC Specific в течение пяти
секунд. Вызывают её вход и выход PureSignal из передачи (то есть каждый MOX,
когда PS вооружён), добавление и удаление панадаптера, смена sample rate.
Пакет конфигурирует все DDC разом, поэтому пере-латч на ходу трогает и
главный приёмник — этой же операцией когда-то был получен мусорный поток на
доп. панах. С PS и 2TON выходило по двадцать пере-латчей на передачу, что
похоже на давний открытый пункт про всплески водопада при 2TON.

Счётчик сбрасывается теперь только по Run=1. Размен: повторы заодно
маскировали потерю UDP-пакета с конфигом посреди сессии, и переконфигурация
стала одним пакетом.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 13:55:19 +03:00
ew8bakandClaude Opus 5 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>
2026-08-10 13:54:58 +03:00
ew8bakandClaude Opus 5 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>
2026-08-10 13:14:38 +03:00
ew8bakandClaude Opus 5 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>
2026-08-10 12:20:56 +03:00
ew8bak 0ec120c2d5 Merge feature/tx-profiles: TX-профили, подвкладки Transmit, заводские EQ ESSB, привязка профиля к «флаг + модуляция» + пласт UI/DPI 2026-08-09 22:06:15 +03:00
ew8bakandClaude Opus 5 6195c80ce3 fix(tx): полоса ЧМ на передаче — занимаемая полоса, а не кромки bp0
TXFilterEdgesHz была тонкой обёрткой над TXSignedEdges, то есть отдавала
в отрисовку кромки аудио-полосовика перед модулятором. У SSB/CW/DIGI/AM
это то же самое, что полоса вокруг несущей, а у ЧМ — нет: FM RAW рисовался
как (20..7000), WFM как (20..15000), то есть полоса уезжала вверх от
несущей и визуально «переключалась на USB».

Теперь у экранных кромок своя семантика — Карсон ±(девиация + верхний срез
аудио) для FM/WFM/FM RAW, остальные режимы как были. TXSignedEdges и
SetTXABandpassFreqs не тронуты: bp0 остаётся портом Thetis, в эфире ничего
не меняется. Попутно 20.0/7000.0 из трёх мест FM RAW сведены в
RAW_AF_LOW/RAW_AF_HIGH.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 19:09:31 +03:00
ew8bak 7c9064659b ui(main): align left panel sliders 2026-08-05 16:51:43 +03:00
ew8bak 19babef4d7 ui(status): use system font 2026-08-05 16:11:57 +03:00
ew8bak a0f4b044db ui(spectrum): theme sample rate overlay 2026-08-05 16:07:25 +03:00
ew8bak b70daa9dbf ui(main): fix DPI layout and theme switching 2026-08-05 15:55:03 +03:00
ew8bak 998cb3f0fe ui(beacon): add flat memo with overlay scroll 2026-08-05 15:06:48 +03:00
ew8bak 0396eeed49 ui(channels): redesign editor and fix DPI scaling 2026-08-05 14:50:15 +03:00
ew8bak 5120d8d21d ui(beacon): redesign decoder diagnostics form 2026-08-05 14:35:39 +03:00
ew8bak 960f221179 ui(settings): replace native vertical scrollbar 2026-08-05 14:10:20 +03:00
ew8bak 1d04973d6f fix(discovery): use system fonts and correct DPI scaling 2026-08-05 13:49:23 +03:00
ew8bak b7b9119339 feat(ui): add hover overlay scroll for left panel 2026-08-05 13:44:12 +03:00
ew8bak 94bb15bbaf fix(ui): scale Pluto left-panel relayout 2026-08-05 13:24:54 +03:00
ew8bak 907c2c05c3 ui: use system font in flat controls 2026-08-05 13:20:03 +03:00
ew8bak bd057a7849 fix(settings): prevent duplicate DPI scaling 2026-08-05 12:42:56 +03:00
ew8bak 9047dc586d ui(settings): align cards and spacing 2026-08-04 21:02:15 +03:00
ew8bak adf5c5016e ui(settings): refine transmit and hardware layouts 2026-08-04 20:47:55 +03:00
ew8bak 612bc73687 ui(settings): add responsive page layout 2026-08-04 20:36:17 +03:00
ew8bak 304e574592 ui(settings): normalize layout and system fonts 2026-08-04 20:23:33 +03:00
ew8bakandClaude Opus 5 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>
2026-08-04 19:40:59 +03:00
ew8bakandClaude Opus 5 199d0ffa85 ui(tx): треугольник пилюли профиля выровнен по надписи
Треугольник центрировался по прямоугольнику пилюли, а текст — по TextHeight,
который включает выносные элементы вниз. В подписи одни заглавные, они сидят
в верхней части бокса, поэтому треугольник визуально проваливался: по пикселям
текст занимал строки 7..13, треугольник 11..15.

Считаем от метрики самой надписи (TxtY + TextHeight div 2 - 2), а не от бокса,
чтобы выравнивание пережило смену шрифта или кегля. После правки текст 7..13,
треугольник 8..12.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 19:30:54 +03:00
ew8bakandClaude Opus 5 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>
2026-08-04 19:23:49 +03:00
ew8bakandClaude Opus 5 82df4dfb8c feat(tx): заводской профиль FM — плотная модуляция с компрессией
Шестой заводской профиль: компрессор 8 дБ + своя кривая EQ. Для FM компрессия
работает иначе, чем для SSB: девиация ограничена жёстко, громкость даёт
средний уровень, а не пики. По цепи TXA (wdsp/TXA.c:xtxa) eqp → leveler → bp0
→ compressor → ALC идут ДО xfmmod, то есть профиль на FM-звук влияет целиком.

Кривая намеренно не задирает верх: предыскажение (xemphp) по умолчанию стоит
вторым вариантом — после ALC, прямо перед модулятором, — поэтому компрессор
его не удержит и лишний буст ушёл бы в перемодуляцию на свистящих. Снизу срез
бубнения, лёгкий подъём разборчивости 1.4..2.3 кГц, спад к 3.4 кГц. Крайние
узлы 200 и 3400 взяты с запасом вокруг AF-полосы модулятора 300..3000, чтобы
обрыв EQ не попал внутрь неё (проверено расчётом по формуле eq.c).

Полоса 300/3000 в профиле для самого FM косметическая — края bp0 движок
считает по Карсону; она сыграет, только если привязать профиль к голосовому
режиму. Девиация, AF-срез и позиция предыскажения остаются свойствами
аппарата (вкладка Hardware) и профилем не переключаются.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 18:47:22 +03:00
ew8bakandClaude Opus 5 0ba808e886 fix(tx): Factory возвращает заводской звук, а не снимок живых настроек
«Default» строился через TXProfileCapture(живые настройки), поэтому сброс,
сделанный на профиле ESSB 5k, оставлял в нём High cut 5000 — то есть Factory
не сбрасывал ровно то, ради чего его жмут. Правило «заводское значит
заводское» было применено только к EQ (FlatEQ), теперь ко всему тембру.

База заводских профилей — DefaultTX, поверх копируется только живая обвязка
микрофона: разъём и режим (Mic In/Line In, boost, bias, PTT, Tip/Ring),
Line In gain и Mic gain. Их сброс сорвал бы настройку микшера. Мощность тоже
остаётся живой — скачок drive ушёл бы в усилитель. Всё остальное (полоса,
компрессор, leveler, ALC, phase rotator, EQ, AM carrier, TUN level) заводское.

Текст подтверждения и подсказка обещали, что полоса и динамика останутся как
есть, — переписаны под новое поведение.

Проверено прогоном: с живыми ESSB 5k + компрессор 12 дБ + Line In + mic gain
22.5 заводской Default выходит 200/3100, comp off, EQ ровный, а Line In,
mic gain и drive сохраняются.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 18:34:36 +03:00
ew8bakandClaude Opus 5 f320ef1bcf fix(tx): полоса передающего слайса на доп. панах рисовалась по RX-ширине
CalcSliceBandX берёт кромки TX из FTXEdge* ТОЙ вьюхи, где живёт слайс, а
PushFilterEdgesToView отдавала их только FSpecView (пан 0). У панов N поля
оставались нулями, ветка TX не срабатывала, и слайс на передаче показывал
свою RX-полосу независимо от профиля и TX-фильтра.

Кромки TX уходят во вьюхи всех панов, кадр каждого помечается грязным.
RX-кромки главного туда не отдаём: на панах N главный VFO не рисуется.

Подтверждено на железе.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 18:20:52 +03:00
ew8bakandClaude Opus 5 8f95795bbb ui(tx): вкладка Transmit целиком на английском + сетка группы Microphone
Страница была единственной с русскими подписями — переведены подсказки всех
четырёх подвкладок, надпись «Profile drive: N %» и диалоги профиля (New /
Rename / Delete / Factory / лимит). Строковых литералов с кириллицей в
SettingsForm больше нет.

Группа Microphone приведена к сетке страницы: подписи в колонке PAD, поля в
колонке CX (комбобокс Mic jack и спин Mic gain теперь в одной колонке),
Connector переехал во вторую пару колонок к Mic gain — на не-Orion он скрыт и
дыры не оставляет, поэтому отдельная третья строка не нужна (176 → 134 px).
Тумблеры boost/bias/PTT расставлены с равным шагом; пара «Line In gain»
занимает место чекбокса boost и заканчивается до «Mic bias».

Английские подписи длиннее русских — все проверены рендером формы offscreen,
в группы вписываются.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 18:08:59 +03:00
ew8bakandClaude Opus 5 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>
2026-08-04 17:58:46 +03:00