# CAT дополнительных слайсов (Slice CAT)

Ветка: `feature/slice-cat`. Дата: 2026-07-25.

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

Каждый порт привязан к определённому слайсу. Флаг **Auto TX** разрешает
команде передачи автоматически назначить этот слайс источником TX; без флага
источник выбирается оператором вручную.

---

## 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 **остаётся** на этом слайсе.
Бейдж на флаге это показывает.

`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`, `Audio out`, `Audio in`.

### 4.1 Аудио слота

`Audio out`/`Audio in` — это **устройство слота (буквы)**, а не шаблон:

* смена настройки применяется немедленно к слайсу, который стоит на слоте
  сейчас (`TRadioController.SetSliceSlotAudio`);
* слайс, который встанет на слот позже (создание/восстановление из персиста),
  получает то же устройство — оно сильнее того, что попросил вызывающий
  `AddSlice`;
* выбор во вкладке `AUD` флага слайса пишется обратно в настройку слота
  (`SetSliceDevice`/`SetSliceInDevice` → `MainForm.StoreSliceSlotAudio`), так
  что вкладка всегда показывает реальное устройство — одна истина, два места
  правки;
* пусто (`Default (app output)` / `None (app input)`) — как раньше: общий выход
  приложения и общий вход.

Хранятся **имена** PortAudio-устройств, а не индексы: порядок перечисления между
запусками плавает. Имя отсутствующего сейчас устройства (кабель не поднят) не
теряется — комбо добавляет его в список и держит выбранным.

* Хранятся вместе с остальным 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`), **на эфире и на экране
не проверено**.
