feat(cat): CAT-порт на каждый слайс + Auto TX (эталон SmartSDR CAT)

Внешняя программа управляет отдельным слайсом как отдельным трансивером
через свой TCP-порт. COM-порты и 19090 остаются за главным приёмником
(слайс A) и не тронуты.

Протокол не дублируется: TCATEngine — чистый парсер поверх записи
callback'ов TCATContext, TCATTcpServer — транспорт поверх движка, поэтому
слайс-CAT = тот же движок с другим контекстом. Новый TCATSliceEndpoint
(движок + TCP-сервер на слот), массивом владеет TCATAdapter. Привязка к
СЛОТУ (буква B..G), не к Id: порт живёт, даже когда слайса нет (PS0).

Auto TX = «Auto Switch TX Slice» из SmartSDR CAT. Вся логика в одной точке
— TRadioController.RequestSliceTx, исполняется в потоке контроллера:
  • пока кто-то уже в эфире (любой источник) — заявка игнорируется,
    перехвата передачи нет никогда;
  • без Auto TX слайс передаёт, только если уже выбран TX-источником;
  • RX; снимает только СВОЮ передачу;
  • TX-источник после отпускания остаётся на слайсе (как в SmartSDR).
На флаге слайса бейдж TX → AutoTX.

Частота слайса — TuneSliceInBand: свобода в пределах ВКЛЮЧЁННОГО диапазона
(трансвертер → его FreqBegin/FreqEnd, иначе band-план), другой диапазон —
отказ. Вне захваченной полосы окно DDC переезжает: доп. пан — центром на
цель (rfPanFreq → UI перекладывает флаги/шапку/зум-бар), пан 0 — центр
посередине между целью и главным VFO и только если оба влезают (общее
железное окно, главный приёмник не оглушаем).

Настройки: вкладка Slices (enable/порт 19091../Auto TX на слайс), персист
в секции cat + device-blob.

Попутно:
  • ZZFL/ZZFH больше не заглушки — новые callback'и кромок фильтра
    (у главного nil → прежний дефолт режима), хелпер SignedPad;
  • FilterIdxFor, AGCModeToUI, BoardUtils.BandEdges;
  • DSP-состояние слайса (NR/NB/SNB/ANF) зеркалится в TCtrlSlice — флаг
    слайса рисовал нули и AGC главного;
  • FUIReady в TMainForm.OnControllerState: адаптеры, создаваемые по ходу
    FormCreate, больше не могут уронить старт рендером до постройки
    виджетов (на этом падал Auto TX из настроек).

Проверено вживую: все порты 19091..19096 слушают, PS;/ZZFA;/MD;/ID;/SM0;/
ZZTX; отвечают; старт со всеми включёнными слайсами чист под gdb.
Документ — doc/CAT_SLICES.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 13:56:36 +03:00
co-authored by Claude Opus 5
parent 8611898ad1
commit 2d15da0462
13 changed files with 1357 additions and 34 deletions
+160
View File
@@ -0,0 +1,160 @@
# CAT дополнительных слайсов (Slice CAT)
Ветка: `feature/slice-cat`. Дата: 2026-07-25.
Внешняя программа (WSJT-X, FLDIGI, лог) управляет **отдельным слайсом** EWSDR
как отдельным трансивером — через свой TCP-порт. COM-порты и основной TCP-порт
(19090) остаются за главным приёмником (слайс A) и не тронуты.
Эталон поведения — **SmartSDR CAT** (FlexRadio): порт привязан к слайсу
(«VFO A Slice»), а флаг **Auto TX** — это их «Auto Switch TX Slice»:
> When enabled Auto Switch TX Slice automatically switches the Transmit Slice to
> the VFO A Slice selected above when a CAT TX command is sent on the com port…
> If this option is not enabled, the operator will have to manually designate the
> Transmit Slice.
---
## 1. Архитектура
Протокол **не дублируется**. `TCATEngine` — чистый парсер/формировщик поверх
плоской записи callback'ов `TCATContext`, `TCATTcpServer` — транспорт поверх
движка. Значит «CAT слайса» = **тот же движок с другим контекстом**:
```
TCATAdapter ──┬── главный контекст ──── TCATEngine ── serial ×4 + TCP 19090 (слайс A)
├── TCATSliceEndpoint ─── TCATEngine ── TCP 19091 (слайс B)
├── TCATSliceEndpoint ─── TCATEngine ── TCP 19092 (слайс C)
└── … по слоту на букву (B..G, потолок = WDSPEngine.MAX_SLICES)
```
| Файл | Что добавлено |
|---|---|
| `CATSliceEndpoint.pas` (новый) | `TCATSliceEndpoint` — контекст поверх `FSlices[слот]`, свой движок + свой TCP-сервер. Константа `CAT_SLICE_SLOTS`. |
| `CATAdapter.pas` | Владеет массивом эндпоинтов; `ApplySliceSettings` поднимает/опускает порты и кладёт Auto TX в контроллер. |
| `CATEngine.pas` | `ZZFL`/`ZZFH` перестали быть заглушками: новые callback'и `Get/SetFilterLow/High` (у главного nil → прежний дефолт режима). Хелпер `SignedPad`. |
| `RadioController.pas` | `RequestSliceTx`, `FSliceAutoTx[слот]`, слот-хелперы, `FilterIdxFor`, `AGCModeToUI`, зеркало DSP-состояния слайса в `TCtrlSlice`. |
| `Settings.pas` | `CATSliceEnabled/CATSlicePort/CATSliceAutoTX[0..5]`, персист в корневой секции `cat` и в device-blob. |
| `SettingsForm.pas` | Вкладка **Slices**. |
| `VfoOverlay.pas`, `PanafallPanel.pas` | Бейдж `AutoTX` вместо `TX` на флаге слайса. |
**Привязка к слоту, а не к Id слайса.** Слот 0..5 = буква B..G. Слайс на слоте
создаётся/удаляется в любой момент, порт при этом остаётся поднятым: пока слайса
нет, эндпоинт отвечает «радио выключено» (`PS0;`) и игнорирует команды. Настройка
(порт, Auto TX) переживает удаление слайса.
**Потокобезопасность** — как у главного адаптера: геттеры читают состояние в
CAT-потоке, сеттеры кладут параметр под локом эндпоинта и маршалятся в поток
контроллера через `Invoke` (GUI = `TThread.Synchronize`).
---
## 2. Auto TX и защита передачи
Вся логика — в **одной** точке: `TRadioController.RequestSliceTx(SliceId, On_)`,
исполняется в потоке контроллера, поэтому проверка «уже в эфире» и смена
TX-источника атомарны относительно GUI.
`TX;` / `ZZTX1;` на порту слайса:
1. Слайса на слоте нет → игнор.
2. **Кто-то уже передаёт** (другой слайс, главный VFO, кнопка MOX/TUNE,
аппаратная PTT) → игнор. Перехвата эфира нет никогда — передаёт всегда
только один слайс.
3. Режим слайса `DMR` → игнор (AMBE-передатчика нет).
4. Слайс не выбран TX-источником:
* **Auto TX включён** → `SetTxSlice(слайс)`, затем `SetMOX(True)`;
* **Auto TX выключен** → игнор (оператор назначает источник руками —
бейдж `TX` на флаге слайса).
5. Слайс уже TX-источник → просто `SetMOX(True)`.
`RX;` / `ZZTX0;`: снимает передачу **только свою** — если TX-источник именно этот
слайс. `RX;` на порту слайса C не обрывает эфир слайса B. TUNE снимается тоже.
Выбор TX-источника после отпускания PTT **остаётся** на этом слайсе (как в
SmartSDR: Transmit Slice переключён, а не одолжен). Бейдж на флаге это показывает.
`GetTransmitting` (для `TX`/`IF`/`ZZTX`) на порту слайса = «передаёт **этот**
слайс». Чужой эфир виден как приём — иначе управляющее ПО решило бы, что его PTT
прошла.
---
## 3. Что поддерживает CAT-порт слайса
| Команды | Маппинг на слайс |
|---|---|
| `FA` `ZZFA` (VFO A) | `TuneSliceInBand` — свободно в пределах включённого диапазона, см. §3.1 |
| `FB` `ZZFB` (VFO B) | теневое значение: читается/пишется, эфир не двигает (второго VFO у слайса нет) |
| `MD` `ZZMD` `ZZMA` | `SetSliceMode` |
| `FW` `SH` `SL` `ZZFI` (индекс фильтра) | `FilterIdxFor`/`FilterBWFor``SetSliceModeBW` |
| `ZZFL` `ZZFH` (кромки, Гц) | `FilterLo/FilterHi``SetSliceFilter` |
| `GT` `ZZGT` (AGC) | `AGC``SetSliceAGCMode` (`AGCModeToUI`/`FromUI`) |
| `AG` `ZZAG` (громкость) | `Volume``SetSliceVolume` |
| `ZZNR` `ZZNB` `ZZNS` `ZZNT` (DSP) | `SetSliceDSP` (состояние зеркалится в `TCtrlSlice`) |
| `SQ` `ZZSQ` `ZZSO` (squelch) | `FMSQOn/FMSQLevel``SetSliceFMSquelch` |
| `SM` `ZZSM` | `SliceSMeter` |
| `TX` `RX` `ZZTX` | `RequestSliceTx` (см. §2) |
| `PS` | слайс существует **и** радио запущено |
| `IF` `ID` | составной статус/идентификатор по данным слайса |
| `UP` `DN` `RU` `RD` | шаг ±10 Гц по частоте слайса |
| `PC` `ZZPC` (мощность), `ZZBS` (диапазон, read) | глобальные — передатчик один на всё радио |
### 3.1 Частота слайса: свобода в пределах диапазона
`TRadioController.TuneSliceInBand(Id, TargetHz)` — единственная точка перестройки
слайса из CAT:
1. **Другой диапазон → отказ.** Границы берутся у включённого сейчас диапазона:
активный трансвертер → его `FreqBegin/FreqEnd` (2 м = 144…146 МГц), иначе
band-план бэкенда (`BoardUtils.BandEdges`). Стоим на 2 м, программа просит
145.500 → 144.174 — можно; 7.074 МГц — команда отбрасывается. Смена диапазона
остаётся решением оператора.
2. Цель внутри захваченной полосы (`SliceFitsCapture`) → просто `SetSliceTarget`.
3. Цель внутри диапазона, но вне окна DDC → **окно переезжает центром на цель**:
* слайс на дополнительном пане (свой DDC) — свободно (`SetPanDDCFreq` +
`Changed(rfPanFreq)`, UI перекладывает флаги/шапку/зум-бар);
* слайс на пане 0 — окно общее с главным приёмником, поэтому центр ставится
ПОСЕРЕДИНЕ между целью и активным VFO: так в окно попадают оба. Если они
дальше, чем окно (`|цель VFO| > 0.95·полоса`), команда отбрасывается —
перестраивать главный приёмник ради слайса нельзя.
Практический вывод: чтобы внешняя программа гуляла по всему диапазону, слайс
лучше держать на своём пане (дополнительный DDC). На пане 0 её свобода —
в пределах общего окна вокруг главного VFO.
**Осознанно не проброшено** (движок ответит валидной заглушкой): split/`FR`/`FT`
(второго VFO нет), смена диапазона `BU`/`BD`/`ZZBS`-set (диапазон — свойство пана,
на котором живёт слайс), CTUN, AGC-T, CTCSS/репитер/FM-шаг (настройки главного
тракта), Andromeda (панель одна и висит на главном).
---
## 4. Настройки
Вкладка **Slices**: на каждый слайс B..G — `Enable CAT TCP server`, `Port`
(по умолчанию 19091, 19092, …) и `Auto TX`.
* Хранятся вместе с остальным CAT: корневая секция `cat` (`slice_en_N`,
`slice_port_N`, `slice_autotx_N`) + device-blob (`cat_slice_*`), не привязаны
к MAC — доступны до подключения железа.
* Порт, совпадающий с основным TCP-портом или с портом слайса меньшей буквы,
**не поднимается** (иначе второй `bind` просто упал бы молча).
* Правка одной строки не рвёт живые сессии остальных: эндпоинт перезапускается
только при реальном изменении своего enable/порта.
---
## 5. Проверка
```
# слайс B на 19091
printf 'ZZFA;' | nc -q1 127.0.0.1 19091 # частота слайса
printf 'ZZMD;' | nc -q1 127.0.0.1 19091 # режим
printf 'TX;' | nc -q1 127.0.0.1 19091 # PTT (нужен Auto TX либо выбранный TX-слайс)
printf 'RX;' | nc -q1 127.0.0.1 19091
```
Статус: собрано (`lazbuild --ws=qt6` + `build-ewsdrd.sh`), **на эфире и на экране
не проверено**.
+4
View File
@@ -188,6 +188,10 @@ CTCSS on/tone, FM repeater dir/offset, FM step, AGC-T, CTUN.
## 5. Транспорты
> CAT **дополнительных слайсов** (свой TCP-порт на слайс B..G + Auto TX) вынесен
> в отдельный документ: [`CAT_SLICES.md`](CAT_SLICES.md). Тот же движок, другой
> контекст; транспорты ниже принадлежат главному приёмнику (слайс A).
- **Serial** (`CATSerial.pas`): до 4 портов, параметры (порт/скорость/биты/
чётность/стоп) из `TGlobalSettings.CATSerial*`, применяются
`TCATAdapter.ApplySettings`. На текущей платформе сборки может быть