5 Commits
Author SHA1 Message Date
ew8bak 8237f03930 docs: add GPL license and source headers 2026-08-25 21:40:58 +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
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 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 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