mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +00:00
feature/tci-protocol
534
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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
|
||
|
|
f2ecd63fe1 |
fix(platform): win64-сборка падала на GetEnvironmentVariable
Модуль Windows стоит в uses ПОСЛЕ SysUtils, поэтому его трёхпараметрический GetEnvironmentVariable(PChar;PChar;DWORD) перекрывает однопараметрический из SysUtils, и три вызова в PlatformUtils падали с «wrong number of parameters». Лечение — явная квалификация SysUtils.GetEnvironmentVariable. ★Сломалось это не сейчас: unit Windows появился в uses ещё в |
||
|
|
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 |
||
|
|
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
|
||
|
|
867da5921e |
chore(tx): детектор голодания снят — всплесков не видно, вернём при нужде
Прибор из
|
||
|
|
b173b7f542 |
feat(tx): тихий детектор голодания очереди TX
Всплеск, рождённый нашим трактом, невозможен без одного события: очередь DUC
простаивает дольше подушки отправителя (DUC_FIFO_THROTTLE = 2000 отсчётов при
192 кГц = 10.42 мс). Съём 2026-08-21 это показал прямо: шесть всплесков — шесть
простоев 10.3-23.5 мс, а паузы 8 мс и короче не дали ни одного. Значит счётчик
таких простоев — полный детектор «наш/не наш», и оператору больше не нужно
называть интервал на глаз.
Новый юнит TxHealth.pas. Включён всегда: переменной окружения нет намеренно —
прибор, который надо не забыть включить, к моменту дефекта выключен.
* поток отправителя DUC мерит каждый простой очереди на передаче — одно чтение
монотонных часов на ПЕРЕХОД, ни файлов, ни локов (обе ветки, win и unix);
* планировщик TCI публикует рядом состояние пейсинга (аванс, долг, в полёте,
квант, шаг, период пачек клиента) — простыми записями, без лока: числа
диагностические, а лок в потоке отправителя DUC недопустим для тайминга;
* сетевой поток считает бит HPS_FIFO_EMPTY из HP-статуса радио — поле ГЛУБИНЫ
FIFO на этой прошивке статично (256) и веры ему нет, а бит мы не читали вовсе;
* ★журнал пишет ТОЛЬКО потребитель — таймер UI (100 мс) и цикл демона через
TxHealthService. В тракте передачи диска нет по построению.
Последнее — правило, оплаченное регрессом: прошлый прибор (TxTrace) сбрасывал
дамп синхронно в хвосте SetMOX, и эти десятки миллисекунд закрывали гонку в
пейсинге TCI (лечение —
|
||
|
|
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
|
||
|
|
166fc65fc8 |
chore(tx): сняты зонды трассировки — пейсинг проверен на железе
Всплесков на передаче нет, инструмент своё отработал и больше не нужен в горячем пути. Снято ровно то, что ставил |
||
|
|
d2096976df |
fix(tci): всплески на передаче — TX-аудио просилось общим тиком 20 мс
Посреди передачи из MSHV на водопаде появлялись всплески своего сигнала. Цепочка: очередь DUC пустеет дольше подушки отправителя (DUC_FIFO_THROTTLE = 2000 отсчётов = 10.4 мс) → FIFO радио сохнет → модуляция обрывается → в эфире остаётся голая несущая на частоте гетеродина DUC, в стороне от тона ровно на звуковой сдвиг. Доказано pcap-съёмом: шесть всплесков в дампе — ровно столько, сколько видел оператор, и каждый стоит за паузой 10.3-23.5 мс, а паузы 8 мс и короче не дали ни одного. Виноват не клиент и не блокировка UI, а зернистость НАШЕГО запроса. Слой первый: PushTxChrono жил на общем тике сервера 20 мс, а просил блок клиента целиком (2048 отсчётов = 42.7 мс) — маркер выходил через два или три тика, то есть через 40 или 60 мс. Слой второй: MSHV отвечает пачками по 4-5 блоков раз в ~44 мс (STREAM_C = 4096 при 96 кГц), и мелкий квант этого не лечит — нужен запас не меньше пачки. Сделано: * квант запроса = один блок TXA (512 отсчётов движка), а не блок клиента; * свой поток-планировщик TxTickLoop с АБСОЛЮТНЫМИ дедлайнами (опоздание одного пробуждения не сдвигает сетку); общий тик маркеров больше не шлёт; * бухгалтерия Owed/InFlight в кадрах на канал, гасится по k до интерполятора; потолок долга обязан быть выше окна в полёте (TCI_TX_OWED_HEADROOM_Q), иначе связывающим становится он и подача падает до 58% реального времени при полностью исправном клиенте; * SendBinNow: маркеры пишутся в сокет напрямую под FWriteLock, минуя очередь (та выпускается лишь на пробуждении потока клиента, TCI_POLL_MS = 20 мс — вдвое больше кванта, и подача снова рвалась); * FReapLock: планировщик TX — новый поток, а правило «клиента освобождает только тик-поток» держалось на том, что им же он и пользуется; * аванс под зернистость клиента: измеряется по ПЕРИОДУ между пачками (размер пачки зависит от того, сколько мы запросили ⇒ положительная обратная связь), переживает конец передачи, умеет уменьшаться по выдержке TCI_TX_LEAD_DOWN_MS, потолок TCI_TX_LEAD_MAX_MS; * старт передачи: KickTxTick будит планировщика на фронте PTT, TxPreWarm шлёт один маркер ДО SetMOX (41 мс раздумий клиента накладываются на нашу же подготовку тракта) под гейтом «передатчик свободен и чужого источника нет», PrimeDUCIQ для источника TCI растянут до Max(6096, аванс×4) — путь микрофона радио, CW и web не затронут; * посев аванса TCI_TX_LEAD_DEF_MS = 50 мс, пока про клиента ничего не известно: обучение к первому осушению физически не успевает. Монотонные часы одного источника для всех потоков — PlatformUtils.MonotonicUs (абсолютные дедлайны не терпят часов, способных прыгнуть от NTP). На железе: опасных осушений посреди передачи НОЛЬ (было 12 за 11 с), четыре передачи из пяти вообще без единого, включая старт; прогон 15:21 чист везде, в том числе на первой передаче после подключения. Всплесков оператор больше не видит. Приборы: TxTrace.pas (EWSDR_TXTRACE=1) и стенд test/hpsdr — кольцевой tcpdump capture.sh, разбор дампа pcap_tx_scan.py, разбор трассы txtrace_scan.py. Стенд test/tci: часть F «Пейсинг TX», 260 проверок, провалов нет; живой клиент с рампой, RTT и потерями — test/tci/tx_chrono_bench.py. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gmVQnna1i4EbSZ2VGm6KD |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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 (её нет с
|
||
|
|
7aae0fdfcd |
fix(tci): SAVE отдаёт писателю сами куски записи, а не сплошную копию
Потолок 128 МБ всё ещё пробивался примерно вдвое на пике: Take собирал
линейную копию ДО освобождения FChunks, поэтому при полном бюджете рядом
жили ~128 МБ кусков и ~128 МБ копии, а счётчик показывал 128. Передача
резерва писателю (
|
||
|
|
9b77809a73 |
fix(tci,cat): резерв бюджета переезжает писателю; строгий разбор полей ZZ-команд
Три замечания по |
||
|
|
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>
|
||
|
|
adbb8d02ac |
fix(tci): Stream.length — вещественные отсчёты всего блока, а не на канал
MSHV не декодировал FT4 по TCI (по виртуальному кабелю — декодировал).
Регрессия из
|
||
|
|
bb48f5d3fa |
feat(tci): приёмник = слот слайса; TRX/TUNE адресуют передатчик; стенд «как MSHV»
Модель приёмников переделана: приёмник TCI — это СЛОТ СЛАЙСА, а не панадаптер.
rx0 — главный тракт (каналы A/B = VFO A/B), rx N — слайс слота N−1, то есть
буквы B..G с флага, на каком бы пане он ни стоял. Панорама стала свойством
приёмника: от неё берутся DDS (у слайса только чтение) и поток IQ.
Почему: у Pluto панорама ровно одна (MaxPans = 1), и второй приёмник там
существует ТОЛЬКО как слайс главного пана — при нумерации по панам он был
недоступен вовсе, а TRX_COUNT навсегда равнялся единице. С другого конца —
клиенты: у MSHV в настройках всего «TCI Client rx1/rx2», то есть приёмники 0 и
1, третьего номера ввести некуда. Со слотами правило «первый созданный слайс =
приёмник 1» держится на любом железе: на openHPSDR слайс второго пана и на
Pluto слайс главного одинаково занимают слот B. Номер совпадает с буквой на
экране и с портом слайс-CAT. Цена: панорама без слайсов из TCI пропала, а
второй слайс пана перестал быть «каналом B» и стал своим приёмником — у канала
B в протоколе только частота, IF и громкость, у приёмника же всё.
TRX/TUNE раньше игнорировали arg1 (номер передатчика) целиком: клиент доп.
приёмника уводил в эфир слайс ОПЕРАТОРА — чужая частота, а с кросс-бандовым
мультислайс-TX и чужой диапазон, с чужими антенной и фильтрами; trx:9,true жал
PTT. Теперь номер разбирается и проверяется, приёмник N > 0 идёт через
RequestSliceTx (та же дверь, что у CAT-порта слайса: «в эфире только один» и
Auto TX), у контроллера появился параметр Tune для TUN тем же путём. Чужую
передачу не трогаем вовсе — ни источник модуляции, ни тон: SetMOX(True) поверх
идущей передачи не выходит рано, а заново выбирает микрофон. Хозяином эфира
клиент становится, только если передача началась именно от его команды, и
решает это Sync-метод в потоке контроллера (снимок «шла ли передача», взятый в
потоке клиента, врал: между разбором и исполнением влезает PTT оператора).
Разбор исходников MSHV (он фильтрует ВСЕ строки и бинарные блоки по номеру
приёмника) дал ещё три правки:
* ответ на TRX/TUNE адресуется номером АВТОРА, состояние в нём — «в эфире
именно твой слайс»; в рассылку идёт номер реально передающего;
* tx_enable рассылается каждому живому приёмнику со своим номером и входит в
картину нового приёмника — без этого у MSHV молча мёртвая PTT
(set_ptt начинается с `if (!tci_tx_enable) return;`);
* про несуществующий приёмник молчим целиком (LiveRx), в том числе на чтение:
ответ «vfo:1,0,0» MSHV принимал бы за конец инициализации.
Попутно, вне TCI: SendDUCSpecificFromSettings трогала FNetwork без Assigned, а
зовут её по любому PTT/TUN (SetMOX → SyncCWKeyer → она) — до подключения
устройства это была Access violation, у TCI её глотал обработчик команды.
Стенды: новый test/tci/mshv_sim.py — точная копия логики клиента MSHV, отвечает
на вопрос «почему он не подключается» одной строкой (на живом приложении
воспроизвёл ошибку инициализации для rx2 до правки). tcitest — 158/158, в
сквозном прогоне добавлено создание слайса на главном пане: он становится
приёмником 1, отвечает на vfo:1,0, слушается командой, отдаёт аудио с
receiver = 1 и замолкает после удаления.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
dc6f996e70 |
fix(tci): дефекты живого прогона — length аудио, маршруты тапов, MOX, рекордер, EOF сокета
Восемь дефектов, найденных прогоном настоящего TCI-клиента (три приёмника: NFM, DIGU, FMRAW) и его отчётом. 1. UI доп. панорам не перерисовывался: rfSliceState рассылался, но ветки в MainForm.OnControllerState не было (частоту несёт отдельный rfSliceFreq). 2. Stream.length у аудио — сэмплы НА КАНАЛ (§4.3), у IQ — вещественные отсчёты (§3.4: комплексных = length/channels). Было ×каналы везде, у стерео получалось вдвое больше. Развилка в TCIFillHeader + разбор TX-аудио в HandleBinary. 3+4. Дыры в маршрутах аудио движка: demod-тап звался только для DMR/FMRAW (у DIGU не было RX_AUDIO), а пост-громкостный — только для нецифровых (у FMRAW не было LINEOUT). Плюс мьют слайса больше не убивает RX_AUDIO: движку сообщают SetAudioTapsActive. 5. Клиент, поставивший TRX, уходил — MOX оставался. FTrxOwner + StopTxOf; TCIMicRequested снимается и по окончании любой передачи. 6. Гонка снятия IQ-тапа: SetIQTap(nil) возвращался раньше, чем DSP-поток выходил из вызова. FIQTapLock (порядок FSliceLock → FIQTapLock). 7. Рекордер был кольцом «последние N секунд», а §4.3 говорит про МАКСИМАЛЬНОЕ время записи с удалением по истечении. Переделан в линейный буфер с окном по часам от START. 8. TCIServer.HandleClient считал recv = 0 таймаутом: ноль — это EOF, errno при нём не трогается и несёт EAGAIN от прошлого истёкшего TCI_POLL_MS. Обычный TCP-разрыв без close-кадра не освобождал слот до остановки сервера, и после нескольких аварийных отключений новые клиенты упирались в TCI_MAX_CLIENTS. Теперь R = 0 рвёт связь безусловно, errno спрашивается только при R < 0. Попутно: MainForm.RecreateDSPEngine (смена sample rate до START) терял внутренние колбэки контроллера — введён AttachEngineCallbacks. Стенд test/tci заведён в репозиторий (run.sh, 126/126 зелёных, включая сквозной прогон через живой WDSP), доп. проверки на оба пути отключения клиента. doc/TCI.md приведена в соответствие: правило про recv = 0 в §1.1, единицы Stream.length, линейный буфер рекордера. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
de0830fd19 |
feat(tci): этап 2 — бинарные потоки IQ/аудио, TX-аудио и запись линейного выхода
Реализованы все четыре потока §3.4 и рекордер:
• RX_AUDIO_STREAM — тап ДО громкости и мьюта (OnDemodAudioReady): скиммеру
и цифре нужен звук приёмника, а не то, что осталось после ручки;
• LINEOUT_STREAM — тап ПОСЛЕ (OnAudioReady), то есть что слышно;
• IQ_STREAM — тап сырого IQ в движке, ОДИН вызов на накопленный блок;
• TX_AUDIO_STREAM + TX_CHRONO — TRX:0,true,tci берёт модуляцию из потока
клиента (флаг TCIMicRequested впереди web в SetMOX), маркеры времени идут
из тика по часам, аудио клиента разворачивается в 48 кГц моно в тот же
ринг, что и web-микрофон;
• LINE_OUT_RECORDER_* — кольцо int16 на приёмник, WAV пишет отдельный поток.
Тапы аудио в контроллере многоадресные (AddAudioTap): слушают, ничего не
забирая, в отличие от OnAudioConsume, которым владеет web. Блоки нарезает и
раскладывает по кольцам клиентов сам DSP-поток, в сокет пишет поток клиента —
та же дисциплина, что у команд. Порядок локов везде FSliceLock → FStreamLock.
У очереди команд и кольца блоков разная политика переполнения: команду терять
нельзя, блок потока — можно (теряется самый старый).
Пересчёт частоты многоступенчатый (TCIStreams). Одноступенчатый FIR на верхнем
пресете Pluto (5760 кГц, коэффициент 120) упирался в потолок отводов и давал
завал 1.3 дБ в полосе при подавлении зеркала 16 дБ — то есть поток IQ с
мусором. Теперь коэффициент раскладывается на множители, спецификацию фильтра
каждой ступени задаёт ИТОГОВАЯ полоса, а свёртка идёт со сложением
симметричных пар: −83 дБ на любом коэффициенте, ≈10% ядра на 5.76 МГц.
Согласование частот с железом: из пресетов Pluto 576 и 960 кГц на 384 не
делятся, поэтому отдаём наибольшую ЗАКОННУЮ частоту, делящую источник нацело
(576/960 → 192 кГц). Ответ на IQ_SAMPLERATE называет достижимое, а не просьбу
клиента, и переобъявляется без запроса при смене rate и устройства.
Приёмный буфер соединения 4 → 32 КБ: блок TX-аудио это 64 байта заголовка плюс
data[16384], а кадр крупнее буфера не собирается никогда.
Настройки TCI переехали из Advanced на вкладку CAT, справа от TCP CAT Server:
это такой же канал внешнего управления трансивером.
Стенд (scratchpad, tcitest.pas): 112 проверок, все зелёные — включая сквозной
прогон через живой WDSP (синтетический IQ → блоки RX-аудио и IQ у настоящего
WS-клиента, и обратно TX-аудио клиента → блоки TX-IQ).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
4165cbe9a5 |
fix(tci): ревизия — потоки, валидация, арбитраж и синхронизация клиентов
Разбор семи проходов ревью ветки. Ниже — по сути, а не по списку. Потоки. Сетевые потоки больше не читают модель контроллера напрямую. Слайсы снимаются в потоке контроллера (RefreshSlices → FSliceSnap, на событиях rfSliceFreq/rfSliceState/rfDevice/…), железо — тоже (RefreshDev → TTCIDevSnap: имя платы, границы, число панов, HasTX). Копия TCtrlSlice из чужого потока портила счётчик ссылок managed-строк, а BackendCaps и BoardDisplayName смотрят в FNetwork, который UI освобождает на смене устройства. По той же причине ActiveTXFreqHz переведён на GetSliceView. Sync-методы читают живую таблицу: они уже в потоке контроллера. Жизненный цикл. Stop ждёт выхода клиентских потоков БЕЗ таймаута, прокачивая очередь Synchronize: выйти по таймауту нельзя — следом освобождаются и клиенты, и сам сервер. OnDisconnect зовётся и при остановке (иначе захваты параметров ушедших клиентов доживали до следующего запуска). Отправка переехала на поток самого клиента (recv с TCI_POLL_MS): общий поток задерживал всех на таймаут записи в один медленный сокет. WebUtils.SockSend шлёт с MSG_NOSIGNAL — SIGPIPE убивал headless-процесс. Транспорт. Слот протокола выдаётся только после Upgrade, а сокет до него живёт по таймауту handshake: восемь молчащих соединений закрывали дверь настоящим клиентам. Handshake с заголовком Origin получает 403 — авторизации в TCI нет, и без этого открытая вкладка браузера дотягивалась до TRX и VFO. Заголовки разбираются построчно, текстовые кадры проверяются на UTF-8, close длиной один байт отвергается, на close отвечаем close. Валидация. Все установки ходят через TCITryArg* — «vfo^0~0~abc» больше не превращается в честный ноль. Частота проверяется дважды: в потоке клиента по снимку и в SyncSetVfo/SyncSetCenter по живым границам (устройство успевают сменить между разбором и исполнением). Границы теперь из ОДНОГО источника (FreqLimits поверх VisibleFreqBounds) — тот же, что уходит в VFO_LIMITS; сами VFO_LIMITS переобъявляются при смене железа, и их кэш ведётся независимо от того, подключён ли кто-то. Слайс двигается только TuneSliceInBand, как у CAT: прямой SetSliceTarget уводил TX-слайс в DUC на чужой диапазон без антенн и фильтров. Параметры потоков сверяются со списками спецификации, а IQ_START и прочие запуски честно отвечают ошибкой вместо молчания. Синхронизация клиентов (§3.5). Появился захват параметра на 200 мс: два логгера больше не перетягивают частоту. Пачка инициализации уходит под FClientLock — изменение между строкой снимка и READY терялось навсегда. Глобальные величины (tune_drive, cw_macros_*, split_enable, mon_volume) рассылаются всем, а правки оператора приходят событиями: rfTXProfile, rfActiveVfo, rfMonVolume и новый rfCWSettings. Создание и удаление слайса рассылается по rfDevice (сравнение расстановки), у живого пана без слайсов канал A показывает центр — иначе клиент навсегда оставался с частотой удалённого слайса. Прочее. SliceFreqChanged переехал внутрь SetSliceTarget — один путь для мыши, CAT и TCI (перетаскивание флага мимо клиентов проходило молча). VOLUME и MON_VOLUME развели: SetVolume правит АКТИВНУЮ громкость, поэтому команда на DUP-передаче уезжала в монитор — добавлен адресный SetRxVolume. Настройки сохраняются только после успешного применения, при отказе поднимается прежний слушатель. Время спота — UTC. Подписки на измерители читаются и пишутся под локом клиента. Проверено стендом (сырой WS-клиент + живой TRadioController без железа): 73 проверки, включая изоляцию медленного клиента, остановку под Synchronize, арбитраж до и после 200 мс, отбраковку по живым границам и переобъявление VFO_LIMITS. На реальном железе и с реальным клиентом по-прежнему не гонялось. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
84c9e60b93 |
fix(tci): жизненный цикл, синхронизация слайсов и WebSocket по RFC
Разбор ревью ветки. Критичное — четыре отказа жизненного цикла и один пробел синхронизации. Use-after-free стора спотов: FTCIAdapter освобождается ДО FDXStore. Команда SPOT/SPOT_DELETE, пришедшая между их гибелью, обращалась к освобождённой памяти. Bind-адрес: TCIParseIPv4 стал строгим (out + Boolean, ровно четыре октета 0..255). Кривой адрес — отказ поднимать сокет, а не молчаливый INADDR_ANY: авторизации в TCI нет. В UI порт и адрес применяются по уходу фокуса и по Close, а не на каждую букву — набор «127.0.0.1» по дороге проходил через «127.0.0.» и открывал порт наружу. Остановка при висящем Synchronize: флаг Stopping (адаптер не начинает новых Invoke), прокачка CheckSynchronize в цикле ожидания Stop и запрет освобождать клиента, чей поток не вышел. Владение переделано: клиента освобождает только тик-поток (ReapClients), клиентский лишь помечает себя закрытым. Отправка больше не блокирует вызывающего: Send/Broadcast кладут строку в очередь клиента, в сокет пишет тик-поток вне общего лока, склеивая очередь в общие кадры. Медленный клиент морозил UI на таймаут отправки за каждое движение ручки VFO; теперь он просто вылетает. Слайсы: в контроллере появилось rfSliceState (нагрузка — FSliceFreqId), его шлют сами сеттеры слайса; SyncSetVfo зовёт SliceFreqChanged, как CAT. Адаптер разворачивает Id в пару (приёмник, канал) и рассылает состояние именно этого канала, а не канала 0 каждого пана. WebSocket по RFC 6455: маска обязательна, FIN/continuation собираются, RSV и незнакомые opcode рвут соединение, control-кадры ≤125 и только целиком, 64-битная длина не сворачивается в отрицательный Integer, пустой Sec-WebSocket-Key получает 400. Хвост пакета handshake больше не выбрасывается — первая команда не теряется. Клиент после исключения в разборе не остаётся висеть в массиве. Ещё: DSP и squelch доп. приёмников читаются и пишутся из TCtrlSlice (парные сеттеры сохраняли соседние поля значениями главного тракта); параметры потоков — в TTCIClient, они клиентские по спецификации; эхо под своим локом; ApplySettings возвращает результат, отказ старта виден оператору; инициализация объявляет только существующие каналы; SET_IN_FOCUS реализован через OnFocusRequest. Осознанно не сделано и записано в doc/TCI.md §3.1: AGC_GAIN для приёмников >0 (AGC-T один на тракт), цвет спота, KEYER, TX_FOOTSWITCH, арбитраж нескольких клиентов. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
82f0e4f771 |
feat(tci): TCI 2.0 — EWSDR как сервер (команды и уведомления)
Протокол Expert Electronics поверх WebSocket, порт 40001. EWSDR слушает, клиенты — логгеры, скиммеры, цифровые программы. - TCIProtocol.pas — чистый слой протокола: разбор/сборка `имя:арг;`, экранирование `^ ~ *`, словарь видов связи, пересчёт громкости в дБ. - TCIServer.pas — WS-сервер поверх WebUtils/WsClient: accept-поток, поток на клиента, HTTP-Upgrade, фреймы, рассылка, тик 20 мс. - TCIAdapter.pas — мост к TRadioController по схеме CAT: геттеры читают поля напрямую, сеттеры через Invoke, уведомления через AddStateListener. Маппинг: приёмник TCI = панадаптер, канал A/B = VFO A/B (пан 0) либо первый/второй слайс (паны 1..). Попутно: TRadioController.RemoveStateListener (адаптер умирает раньше контроллера) и TDXSpotStore.RemoveCall (spot_delete). Настройки — секция "tci" в settings.json (умолчание: выключено, 127.0.0.1, так как авторизации в протоколе нет) и вкладка Advanced → TCI Server. Бинарные потоки (IQ/аудио) — этап 2. Статус, таблица команд и список осознанных эхо-заглушек — в doc/TCI.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
92c01fa2c6 | Merge feature/cat-tx-cw: CAT-команды TX-тракта и телеграфа, ревизия алиасов и подписей, починка транспортов | ||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
f89f202a34 | Merge feature/dxcluster-spots: споты DX-кластера на панадаптерах, мода по бэндплану, кнопка DX в RX | ||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
13b46f0b7d |
ui(dxcluster): системный шрифт вместо захардкоженного 'Courier New'
По проекту гарнитуры не задаём — работает системный шрифт ( |
||
|
|
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-кластера (
|
||
|
|
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> |
||
|
|
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> |