Два дефекта в источнике времени, на котором стоят абсолютные дедлайны пейсинга
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
Стенд TCI
Прогон:
test/tci/run.sh
Собирает tcitest.pas и запускает его; код возврата ненулевой, если хоть одна
проверка провалена. Внешних библиотек стенд не требует — WebSocket-клиент в нём
написан на сыром сокете. libwdsp нужна только последней части (сквозной
прогон через движок); без неё эта часть сообщает о пропуске, а остальные идут
как обычно.
Что проверяется — в шапке tcitest.pas; чего стенд не покрывает —
в doc/TCI.md, §5.
Две грабли, из-за которых стенд собирается именно так (обе стоили по часу):
-Mobjfpcобязателен. С-Mdelphiв командной строке выключаются вложенные комментарии, и{$MODE Delphi}внутри шапкиWebUtils.pasзакрывает комментарий раньше времени — компиляция падает на «illegal character».- Каталог
.ppu— свой, с именем неlib, и путь абсолютный. Относительноеlib/<cpu>-<os>компилятор ищет и относительно-Fu, то есть находит units GUI-сборки в корне проекта, а тамPlatformUtilsсобран с LCL: стенд падает на линковке сundefined reference to TC_$FORMS_$$_SCREEN. По той же причине сборка идёт с-dHEADLESS, как у демона.
Каталоги units/ и bin/ — выход сборки, в git не попадают.
Проверка на живом эфире без передачи
python3 test/tci/live_rx_test.py --host 127.0.0.1 --port 40001
Наглядный UI-прогон с автоматическим возвратом к исходным значениям:
python3 -u test/tci/live_rx_test.py --visible --commands-only --dwell 1.5
Стенд проверяет receive-only команды, IQ, аудио демодулятора и
line-out для всех обнаруженных приёмников. Он никогда не посылает
START, STOP, установку TRX/TUNE, TX-аудио и CW-текст. Если радио
уже на передаче, прогон аварийно прекращается.
Стенд «как MSHV»
test/tci/mshv_sim.py --rx 0 # в MSHV это «TCI Client rx1»
test/tci/mshv_sim.py --rx 1 --audio # «TCI Client rx2»
mshv_sim.py повторяет логику TCI-клиента MSHV по его исходникам — включая обе
особенности, из-за которых он ведёт себя не так, как наш стенд:
- фильтр по номеру приёмника —
if (ls2.at(0)!=tci_trx) continue;: строка, адресованная не его приёмнику, для MSHV не существует (и то же самое для бинарных блоков, по полюreceiverзаголовка); - инициализация — один запрос и один ответ: он шлёт
vfo:<rx>,0;раз в 500 мс, максимум пять раз, и ждётvfo:<rx>,0,<частота>. Не дождался — «Error To Initialize TCI Server». Других причин у этой ошибки нет.
Поэтому стенд отвечает на вопрос «почему MSHV не подключается» одной строкой, а
заодно ловит то, что иначе проявляется молча: без tx_enable со своим
номером приёмника у MSHV не работает PTT (set_ptt начинается с
if (!tci_tx_enable) return;).
По умолчанию стенд безопасен на приём: ни TRX, ни TUNE, ни START/STOP,
ни TX-аудио он не шлёт. Передачу включает явный --tx.