# CAT-система EWSDR — статус реализации

Ветка разработки: `feature/cat-tx-cw`.
Дата последнего обновления: 2026-08-17.

Документ описывает текущее состояние CAT-подсистемы EWSDR: архитектуру,
полный перечень реализованных команд, осознанные заглушки и план дальнейших
работ. Поддерживается диалект Kenwood TS-2000 с расширениями `ZZ*`,
используемый совместимыми SDR-приложениями.

---

## 1. Архитектура

```
CAT-транспорты ──► TCATEngine (парсер протокола) ──► TCATContext ──► TCATAdapter ──► TRadioController
  serial (4×)         CATEngine.pas                  (callbacks)      CATAdapter.pas      (ядро)
  TCP-сервер          разбор/формат команд                            мост, маршалинг
```

| Файл | Назначение |
|---|---|
| `CATEngine.pas` (~2740 строк) | Движок протокола: разбор `<PREFIX><SUFFIX>;`, формирование ответов, реализации всех команд Kenwood + `ZZ*`. Зависит только от RTL. |
| `CATAdapter.pas` (~910 строк) | Мост к `TRadioController`. Строит `TCATContext`: геттеры читают поля контроллера напрямую (CAT-поток, read-only), сеттеры/команды маршалятся в поток контроллера через `FController.Invoke`. Владеет движком + транспортами. |
| `CATSerial.pas` (~330 строк) | Менеджер до 4 последовательных портов (виртуальные COM-пары для rigctld/N1MM/WSJT-X). |
| `CATTcp.pas` (~507 строк) | TCP-сервер (CAT-совместимый), управление клиентами по GUID (`ZZGA`/`ZZGR`). |

### 1.1 Принципы

- **CAT — равноправный клиент контроллера** наряду с `WebAdapter` и GUI. Никаких
  обращений к `MainForm`. Только `TRadioController`.
- **Потокобезопасность.** Геттеры читают поля контроллера напрямую (атомарное
  чтение в CAT-потоке). Сеттеры пишут параметр в scratch-поля
  (`FSyncFreq/FSyncInt/FSyncBool`) и зовут `FController.Invoke(@SyncXxx)` —
  GUI исполняет через `TThread.Synchronize`, демон — своей очередью.
- **`TCATContext`** — плоская запись callback'ов (`Get*`/`Set*`/`Do*`). Чтобы
  пробросить новую возможность контроллера в CAT, достаточно: (1) добавить
  callback в запись, (2) реализовать в адаптере геттер/сеттер/sync, (3)
  зарегистрировать в `TCATAdapter.Create`, (4) переписать тело команды в движке.
- **Формат команд:**
  ```
  SET:  <PREFIX><SUFFIX>;     (suffix = значение)
  GET:  <PREFIX>;             (suffix пустой)
  Resp: <PREFIX><VALUE>;
  Err:  ?;  (неверный формат)  |  E;  (ошибка выполнения)
  ```
  Расширенные `ZZ*` дублируют Kenwood-команды и добавляют то, чего в Kenwood нет.

### 1.2 Статистика покрытия (ZZ-команды)

| | Кол-во |
|---|---|
| Всего известных `ZZ*` | 315 |
| Реально реализовано в EWSDR | **~120** |
| Заглушки (отвечают валидным dummy, функции нет) | ~190 |
| Совсем не обрабатываются (вернут `?;`) | 5 |

Подписи всех 315 команд проверены по протокольному поведению (см. §2.5) — комментарию у
заглушки теперь можно верить. Оставшиеся заглушки в большинстве закрыть
**нечем**: в EWSDR физически отсутствует соответствующий тракт (см. §4).

### 1.3 Осознанные особенности реализации

Три команды имеют локальный формат, сложившийся в EWSDR.

| Команда | Базовое значение | Реализация EWSDR | Почему |
|---|---|---|---|
| `ZZBS` | код диапазона, 3 символа (`160`/`040`/`WWV`) | индекс диапазона, 2 цифры | формат заложен давно, на нём уже сидят клиенты |
| `ZZMN` | пресеты DSP-фильтров, 180 символов | имя режима по номеру | имён режимов в протоколе нет вовсе, а `ZZML` опирается на эту команду |
| `ZZST` | размер шага настройки (read-only) | индекс шага FM (00..03) | глобального шага у EWSDR нет, слот иначе мёртв; у шага FM своей команды в протоколе нет |

Плюс одно ограничение диапазона, не формата: **`ZZCD`** (hang-time break-in)
в EWSDR имеет потолок 2000 мс — столько же стоит в
настройках телеграфа и уходит в поле `HangDelay` пакета DUC Specific. Значения
выше ограничиваются общим механизмом проверки входных значений.

---

## 2. Что сделано

### 2.1 Реализованные команды Kenwood (одиночный префикс)

| Команда | Функция | Маппинг на контроллер |
|---|---|---|
| `FA` / `FB` | VFO A / B частота (11 цифр, Гц) | `Set/GetVfoA`, `Set/GetVfoB` |
| `FR` | приёмный VFO (0/1) | `Set/GetActiveVfo` |
| `FT` | передающий VFO / split | → `ZZSP` |
| `FW` / `SH` / `SL` | ширина фильтра (индекс пресета) | `Set/GetFilterIdx` |
| `MD` | режим (Kenwood-код) | `Set/GetMode` |
| `GT` | режим AGC (0=FAST…4=OFF) | `Set/GetAGCMode` |
| `AG` | громкость AF | `Set/GetVolume` |
| `PC` | мощность TX 0–100 % | `Set/GetDriveLevel` |
| `NB` | шумоподавитель (NB) | `Set/GetNBMode` |
| `NT` | автонотч (ANF) | `Set/GetANFEnabled` |
| `SM` | S-метр (шкала Kenwood 0000–0030) | `GetSMeter` |
| `IF` | составной статус (37 байт) | freq/mode/tx/split |
| `ID` | идентификатор (TS-2000 = 019) | const |
| `PS` | питание/состояние | `GetRunning` |
| `TX` / `RX` | передача / приём | `SetTransmitting` |
| `BU` / `BD` | смена диапазона ±1 | `DoBandUp/Down` |
| `UP` / `DN` / `RU` / `RD` | шаг настройки ±1 | `DoTuneUp/Down` |
| `SQ` | шумоподавитель FM (шкала 0–255) | `Set/GetSquelchLevel`+`On` |
| `CN` / `CT` | CTCSS тон / вкл | `Set/GetCTCSSTone`, `…On` |
| `OF` / `OS` | репитер: offset / направление | `Set/GetFMRptOffset`, `…Dir` |

### 2.2 Реализованные расширенные `ZZ*` (выборочно, наиболее значимые)

VFO/режим/фильтр: `ZZFA ZZFB ZZFI ZZFL ZZFH ZZFM ZZMD ZZFT ZZSP ZZVS`,
диапазоны: `ZZBS ZZBU ZZBD`,
AGC/громкость/мощность/аттенюатор: `ZZGT ZZAG ZZMA ZZPC ZZAR ZZRX`,
шумодав/нотч: `ZZNA ZZNB ZZNR ZZNS ZZNT ZZNN`,
FM: `ZZSQ ZZSO ZZTA ZZTB ZZST`, CTUN: `ZZCN`,
S-метр/телеметрия: `ZZSM ZZRV`, память: `ZZMV`,
шаги VFO: `ZZAU ZZBP ZZBM ZZSU ZZSA ZZSB`,
передача: `ZZTX`, сервис: `ZZID ZZGA ZZGR`.

### 2.3 Сделано в ветке `feature/cat-commands-expansion`

Все команды ниже **были заглушками** и стали рабочими — функционал в
`TRadioController` уже существовал, но не был проброшен в CAT.

**Группа 1 — Split TX**
- `ZZSP`, `FT` — split (TX на VFO-B), `FR` — выбор приёмного VFO,
  split-байт в составном `IF`.
- В `TRadioController` добавлен публичный метод `SetSplit(TxOnB)`
  (флаг `FSplitTxB` + `PushNetworkState` + `Changed(rfActiveVfo)`).
  TX-частоту радио берёт из `ActiveTXFreqHz` (учитывает split).

**Группа 2 — FM Squelch**
- `SQ` (шкала Kenwood 0–255), `ZZSQ` (0–100 %), `ZZSO` (вкл/выкл).
- Маппинг: `SetFMSquelch` / `SetFMSquelchLevel`. Уровень>0 включает
  шумоподавитель.

**Группа 3 — FM CTCSS**
- `CT`/`ZZTA` (вкл/выкл), `CN` (1-based тон 01–38), `ZZTB` (0-based 00–37).
- Маппинг: `SetFMCTCSS` / `SetFMCTCSSTone`, таблица `CTCSS_TONES[38]`.

**Группа 4 — FM-репитер**
- `OF` (offset, 9 цифр Гц), `OS` (направление).
- Трансляция направлений Kenwood (0=simplex,1=+,2=−) ↔ внутренние
  `RPT_NONE/MINUS/PLUS`. Маппинг: `SetFMRpt` / `SetFMRptOffset`.

**Группа 5 — FM tuning step**
- `ZZST` (индекс 00–03: 6.25/12.5/20/25 кГц). Маппинг: `SetFMStep`.

**Доп. команды (вторая итерация)**
| Команда | Что | Примечание |
|---|---|---|
| `ZZSO` | Squelch on/off | завершает группу squelch |
| `ZZNT` | ANF on/off | раньше ошибочно был алиасом NB |
| `ZZCN` | CTUN (click-tune) RX1 | новый callback `Get/SetCTun` → `SetCTun` |
| `ZZAR` | RX1 AGC-T / RF gain (`±nnn`) | новый callback `Get/SetAGCTop` → `SetAGCTop` (кламп 20–120 dB) |
| `ZZAU`/`ZZBP` | сдвиг VFO A/B вверх на шаг `nn` (00–14) | **исправлен баг**: были заалиашены на `ZZFA`/`ZZAP` |
| `ZZSU` | шаг активного VFO вверх | `DoTuneUp` |

Шкала шагов `ZZAU/ZZBP` задаётся функцией `StepIdxToHz`:
1 Гц, 10, 25, 50, 100, 250, 500, 1к, 5к, 9к, 10к, 100к, 250к, 500к, 1 МГц.

**Новые callback'и в `TCATContext`** (этой ветки): split, squelch on/level,
CTCSS on/tone, FM repeater dir/offset, FM step, AGC-T, CTUN.

### 2.4 Сделано в ветке `feature/cat-tx-cw`

К этому моменту в EWSDR появились TX-профили со всей «звуковой» цепью и
телеграф (кейер, сайдтон, break-in) — команды, которые §4 когда-то объявил
незакрываемыми, стали рабочими.

**TX-тракт** (правки уезжают в активный TX-профиль):

| Команда | Что | Маппинг |
|---|---|---|
| `MG` / `ZZMG` | усиление микрофона | `MicGainDB`; `MG` — шкала Kenwood 100:70, `ZZMG` — дБ со знаком |
| `MO` / `ZZMO` | кнопка MON | self-monitor даунлинка: `SetRxMuteOnTx` (инверсия) |
| `ZZTM` | громкость монитора | `SetTXMonVolume` (новый метод контроллера) |
| `ZZTL` / `ZZTH` | кромки TX-фильтра, Гц | `FilterLow` / `FilterHigh` |
| `PR` / `ZZPK` | речевой компрессор вкл/выкл | `CompressorOn` |
| `ZZPL` | усиление компрессора, дБ | `CompressorGain` (0..20) |
| `ZZET` | кнопка TXEQ | `EQOn` |
| `ZZEB` | значения TX-эквалайзера | `EQNumBands` + `EQGains` (поле длиной 36 символов). Полос бывает только 3 или 10 — движок (`PushTXEQProfile`) и редактор знают ровно эти два случая, остальное отвергается и не выдаётся |
| `ZZTO` | мощность настройки | `TUNLevel` |
| `ZZTU` | кнопка TUN | `SetTune` |
| `ZZUT` | двухтональник 2TON | `SetTwoTone` |
| `ZZLI` | кнопка PS-A | `SetPureSignal` |
| `ZZUS` | одиночная калибровка PS | `PSSingleCal` (write-only) |
| `ZZTP` | активный TX-профиль | `SelectTXProfile` / `TXProfileCount` |
| `ZZFD` | девиация передачи (2.5/5 кГц) | `TTXSettings.FMDeviation` |

**Телеграф** (раньше были проброшены только `KS`/`KY`/`ZZKM`/`ZZKS`/`ZZKY`):

| Команда | Что | Маппинг (`TCWSettings`) |
|---|---|---|
| `ZZCS` | скорость кейера, WPM (2 разряда) | `.Speed` — та же величина, что у `KS` |
| `ZZCL` | тон приёма/сайдтона, Гц | `.Pitch` |
| `ZZCI` | иамбик вкл/выкл | `.KeyerMode` (вкл выбирает iambic B, если стоял прямой ключ) |
| `ZZCB` | break-in прошивки | `.BreakIn` |
| `ZZCD` | hang-time break-in, мс | `.HangTimeMS` |
| `ZZCM` | сайдтон; поле `monitor disable` использует инвертированное значение | `.SidetoneSW` или `.SidetoneHW` — тот, что отвечает за текущий источник манипуляции |

**Новое в контроллере.** Правка TX-настроек получила одну публичную дверь —
`SetTXSettings(const T; Notify)`, по образцу `SetCWSettings`: применение к WDSP,
перепосыл DUC Specific, живой TUN, запись в активный профиль и персист теперь
там, а не в обработчике MainForm (тот делегирует с `Notify=False`, иначе
`rfTXProfile` перезагрузил бы вкладку Transmit под руками у правящего).
Добавлен `SetTXMonVolume` — громкость self-monitor'а адресно, вне TX-контекста.

**Новое в движке.** Хелперы `OnOff` и `NumField` — разбор двухпозиционных и
числовых полей в одном месте, чтобы тела команд остались про смысл. Границы
значений (`MIC_GAIN_MIN`… в шапке юнита) повторяют контролы вкладок Transmit и
CW: по CAT прилетает что угодно, а контроллер принимает запись целиком и уже не
знает, кто её правил.

> Команды слайсов (`CATSliceEndpoint`) этих callback'ов не получают: TX-тракт
> один на радио, а профиль слайса переключается своим путём
> (`SetSliceTXProfile`). Все новые команды nil-безопасны и на слайс-порту
> отвечают дефолтом.

### 2.5 Ревизия алиасов и подписей

Подписи писались по буквам кода, а не по CAT, и врали примерно в 150
местах. Хуже: часть **работающих** команд была привязана не к своей функции —
внешний софт получал осмысленный, но неверный ответ, что хуже честной
заглушки. Все подписи проверены и исправлены.

**Команда делала не своё дело — теперь делает своё:**

| Команда | Было | Стало |
|---|---|---|
| `ZZMA` | режим (алиас `ZZMD`) | кнопка **MUT** → `SetMute` |
| `ZZRX` | переход на приём | **шаговый аттенюатор RX1** 0..31 дБ → `SetAtten` |
| `ZZRV` | версия ПО | **напряжение питания** PA (телеметрия) |
| `ZZMV` | индекс текущего режима | **число записей** памяти каналов |
| `ZZNN` | заглушка «ручной нотч» | **SNB** (спектральный бланкер) |
| `ZZNS` | SNB | кнопка **NR2** (режим NR 2) |
| `ZZVS` | алиас `ZZSP` (split) | **операции с парой VFO**: `0`=A→B, `1`=B→A, `2`=обмен, `3`=IF→VFO |
| `ZZBM` | режим (алиас `ZZMD`) | **VFO B вниз** на nn шагов (пара к `ZZBP`) |
| `ZZFI` | алиас `ZZBI` | сам **индекс фильтра** |
| `ZZBS` | алиас `ZZAC` | сам **выбор диапазона** |
| `ZZKM` | режим кейера | запуск **CW-макроса** (ячейка памяти сообщений) |
| `ZZFT` | всегда VFO A | **TX-частота** с учётом split |
| `ZZTX` | любое значение ≠ `0` поднимало передачу («2=TUNE» — выдумка) | строго `0`/`1`, иначе `?;` и передача НЕ включается (настройка — `ZZTU`) |

**Ошибочные алиасы сняты — теперь честные заглушки** (в скобках — где эта
функция живёт на самом деле): `ZZAA`, `ZZVG` (громкость — `ZZAG`) · `ZZOA`
(антенна RX1) · `ZZAP` (мощность — `ZZPC`) · `ZZDN` (мощность — `ZZPC`; сам
`ZZDN` = низ водопада) · `ZZAC` (диапазон — `ZZBS`) · `ZZBI` (фильтр — `ZZFI`)
· `ZZMB` (режим — `ZZMD`) · `ZZVA` (обмен VFO — `ZZVS`) · ★`ZZVB` (усиление приёма в VAC; **опрос** копировал VFO A в VFO B, то есть терял сплит оператора, — тело переписали позже комментария) · `ZZPD`, `ZZPO`
(мощность и питание — `ZZPC`/`ZZPS`) · `ZZQM` (режим — `ZZMD`) · `ZZSR`
(опорный уровень спектра) · `ZZRD`/`ZZRU` (RIT; шаг настройки — `ZZSA`/`ZZSB`).

> Что заметят внешние клиенты: `ZZRX;` больше не уводит в приём (для этого есть
> `ZZTX0;` и кенвудовская `RX;`), `ZZVA;` больше не меняет VFO местами (теперь
> `ZZVS0;`), `ZZKM` не выставляет режим кейера (иамбик вкл/выкл — `ZZCI`),
> `RD`/`RU` больше не перестраивают VFO (шаг — `UP`/`DN`).
> Во всех случаях старое поведение было отсебятиной.

**Кенвудовские команды.** Сверены отдельно: реализованы все 40 команд,
необходимых для совместимости, и проверена ширина полей. Исправлено:

| Команда | Что было не так |
|---|---|
| `SM` | ответ был 4 цифры вместо **5** (`SM0015;` → `SM00019;`) — парсеры логгеров ждут пять; заодно принимаем селектор `2` |
| `RD` / `RU` | перестраивали VFO, хотя это **RIT** (его у нас нет) — стали заглушками, принимающими 5-значный аргумент |
| `KY` | поле текста у Kenwood фиксированной ширины и добито пробелами — хвост уходил в эфир словесными паузами, теперь срезается |
| `IF` | в комментарии значилось «37 байт», реальная и правильная длина ответа — **35** |
| `SH` / `SL` | обе крутят один и тот же индекс пресета; раздельных «сторон» у главного приёмника нет — оговорка добавлена в код |
| `CT` | принимала любой символ: `CT9;` молча ГАСИЛ тон вместо ответа `?;`. Теперь это обёртка над `ZZTA`, а сама `ZZTA` проверяет значение строго |
| `OF` / `OS` | несли реализацию сами, а `ZZOT`/`ZZOS` были заглушками — клиент расширенных команд не получал ничего. Реализация переехала в `ZZOT`/`ZZOS`, кенвудовские стали обёртками. Заодно `OF` с нечисловым полем больше не обнуляет сдвиг молча |

**Разбор и транспорты.** Отдельный проход по валидации и вводу-выводу:

| Где | Что было не так |
|---|---|
| `TCATEngine.Parse` | команды без параметров не проверяли суффикс: `TXanything;` доходил до `CmdTX` и **поднимал передачу**; так же вели себя `RX UP DN BD BU QI RC ID IF`. Теперь лишний суффикс отбраковывается в парсере, а список безаргументных команд хранится в `IsParamless` |
| `ZZFL` / `ZZFH` | принимали поле любой длины от 4 символов и гнали его через `StrToIntDef`: `ZZFLabcd;` молча схлопывал кромку в ноль. Поле фиксированное — ровно 5 символов со знаком, разбор строгий |
| `ZZAU` `ZZBP` `ZZBM` `ZZBS` `ZZFI` | длину поля проверяли, а содержимое — нет: `StrToIntDef(s, 0)` превращал любую нечисловую пару символов в индекс 0. То есть `ZZBSxx;` **переключал диапазон** на нулевой вместо `?;`, `ZZBMxx;` и `ZZAUxx;`/`ZZBPxx;` двигали VFO, а `ZZFIxx;` выбирал фильтр 0. Разбор приведён к идиоме `ZZFL`/`ZZFH`: `TryStrToInt`, иначе ошибка формата |
| `CN` `FW` `GT` `NB` `PC` `SH`/`SL` `AG` `SQ`, `ZZAG` `ZZAR` `ZZNA` `ZZNB` `ZZNR` `ZZPC` `ZZSQ` `ZZST` `ZZTB` | ★тот же дефект, найденный сплошным прогоном (`test/cat`): правку получили только пять команд выше, а у остальных `StrToIntDef` остался — в том числе у **кенвудовских двойников уже исправленных величин**. `FWxxxx;`/`SHxx;`/`SLxx;` ставили фильтр 0 (тот же индекс, что `ZZFI`), `AG0xxx;` и `SQ0xxx;` — громкость и порог в ноль, `GTxxx;` — АРУ в FAST, `PCxxx;`/`ZZPCxxx;` — мощность в ноль. Разбор везде строгий. Не тронуты `FR` (сам сверяет `0`/`1`), `MD`/`ZZMD` (нечисловое даёт 0, а установка идёт от 1) и `ZZOS`, где некорректное значение намеренно трактуется как симплекс |
| `ZZGT` | опрос отвечал тремя цифрами (как кенвудовская `GT`), а установка принимала ровно один символ: клиент, прочитавший `ZZGT000;` и написавший его назад, получал `?;`. Теперь принимаются обе ширины |
| `ZZBE` | формы были перевёрнуты: опрос `ZZBE;` отвечал `?;`, а установка `ZZBE01;` возвращала данные (`'1'`). Вся семья «сдвиг VFO на nn шагов» (`ZZAD ZZAE ZZAF ZZBF ZZSG ZZSH`) — однострочные заглушки, `ZZBE` приведён к ним |
| `KY` / `ZZKY` | текст не ограничивался; поле у Kenwood фиксированное, 25 символов. Длиннее — `?;`: очередь передачи не должна расти произвольно, иначе один пакет уводит станцию в эфир на неопределённое время |
| `CATTcp.SendStr` | один `send` на ответ. TCP не обязан отдать весь буфер за раз — длинный ответ (`IF`, `ZZEB`, список режимов) мог уехать обрезанным, и молча: усечение здесь не ошибка. Теперь дописываем остаток в цикле. ★И пишем через `WebUtils.SockSend`, а не голым `fpSend`: в нём `MSG_NOSIGNAL`, без которого запись в закрытый клиентом сокет приходит как `SIGPIPE` и убивает процесс целиком (обработчика сигнала в дереве нет, а цикл дозаписи умножает число попыток) |
| `CATSerial` | порт помечался активным ДО `SerOpen`; при отказе он навсегда оставался «работающим» в `ActiveCount` и UI, а причина нигде не оседала. Открытие переехало из потока в `TCATSerialPort.Start` (синхронно), появилось свойство `LastError`, поток теперь только читает, а закрывает владелец в `Stop`. Заодно Andromeda-порт назначается только на реально поднявшийся порт |

Формат `ZZOT` проверен отдельно: 9 цифр кодируют МГц с шестью знаками после
запятой, что численно соответствует целому значению в герцах. Некорректное
значение `ZZOS` намеренно трактуется как симплекс для совместимости клиентов.

Формат ответа на `ZZ*` — `ZZ` + код + **эхо суффикса запроса** + значение.
Эхо суффикса важно для `ZZSM0;`, где запрос несёт селектор приёмника.

---

## 3. Что нужно сделать (актуально / есть чем закрыть)

Эти команды — заглушки, но соответствующая функция в EWSDR **уже есть** или
легко добавляется. Приоритетные кандидаты:

| Команда(ы) | Что | Чем закрыть | Сложность |
|---|---|---|---|
| `ZZVL`, `ZZUX`/`ZZUY` | блокировка VFO (общая и раздельная) | `SetVfoLock` | низкая |
| `ZZSW`, `ZZZW` | обмен VFO A/B | `VfoSwap` | низкая |
| `ZZAD ZZAE ZZAF ZZBE ZZBF`, `ZZSG`/`ZZSH` | сдвиг VFO на nn шагов | `StepIdxToHz` + `DoTuneUp/Down`, как у `ZZAU`/`ZZBP` | низкая |
| `ZZCU` | загрузка CPU (read) | телеметрия процесса | низкая |
| `ZZSF` | фильтр «центр + ширина» одной командой | `SetFilterEdges` | низкая |
| `ZZSG`/`ZZSH`, `ZZSD` | шаг VFO B и колеса — по одному шагу | те же `SetVfoB`/`DoTune*` | низкая |
| `ZZMY ZZMZ ZZMW ZZMX ZZQR ZZQS ZZQM` | запись/чтение памяти каналов (счётчик `ZZMV` уже есть) | `ChannelStore` / `ChannelController` | средняя |
| `ZZDN`/`ZZDO`, `ZZDP`/`ZZDQ`/`ZZDR` | низ/верх водопада, сетка спектра | `FSpecRefLevel`/`FSpecRange`, per-pan дисплей | средняя |
| `ZZPD`/`ZZPE`, `ZZPY`/`ZZPZ` | пан и зум дисплея | `SetPanZoomPan` | средняя |
| `ZZSN`, `ZZZM`, `ZZZV` | серийник, модель, версия железа | `BoardUtils` | низкая |
| `ZZXN` / `ZZXV` | составные статусы RX1/VFO | сборка из готовых геттеров | средняя |
| `ZZOC`/`ZZOD`/`ZZOE` | антенны | `SetAnt936x` (порты AD936x) | средняя |
| `ZZDX` / `ZZDY` | кнопка DX и порог спотов | флаг `ShowSpots` живёт в `MainForm` — сперва поднять в контроллер | средняя |
| `ZZAS` | RX2 AGC-T | — только при появлении RX2 | — |

> Прежде чем реализовывать новую команду, проверяй ширину, знак и смысл поля
> по документации протокола и поведению совместимых клиентов. Также проверяй
> **смысл**: подпись у заглушки в диспетчере может врать (см. §1.2).

### 3.1 Совсем не обрабатываются (вернут `?;`)

`ZZCU` (CPU), `ZZDY` (DX threshold), `ZZQA`/`ZZQB` (Quick Play/Rec),
`ZZVF` (VAC stereo). Из них реалистично добавить только `ZZCU` и `ZZDY`.

---

## 4. Осознанные заглушки (закрывать нечем)

Команды отвечают валидным dummy-значением для совместимости с управляющим ПО
(rigctld, N1MM, WSJT-X, Log4OM), но функции в EWSDR нет. Это ~70 % всех заглушек.

| Область | Команды (примеры) | Причина |
|---|---|---|
| **Второй приёмник RX2 / sub-RX** | `ZZAB ZZAS ZZBA ZZBB ZZBT ZZMB ZZOB ZZRS ZZSV ZZSX ZZNC ZZND ZZNF ZZNO ZZNU ZZNW ZZFJ ZZFR ZZFS ZZGU ZZLE ZZLF ZZPB ZZRY ZZXO` | один приёмник |
| **VAC / VAC2** | `ZZVA ZZVB ZZVC ZZVD ZZVF ZZVI ZZVK ZZVM ZZVO ZZVQ ZZVR ZZVT ZZVU ZZVV ZZVX ZZVY ZZVZ ZZYA ZZYB` | нет виртуального аудиокабеля |
| **RTTY / DIGI-офсеты** | `ZZRA ZZRB ZZRH ZZRL` | нет RTTY-движка |
| **RIT / XIT** | `RT XT RC`, `ZZRF ZZRT ZZXC ZZXD ZZXF ZZXS ZZXU` | нет тракта смещения приёма (CTUN — не замена) |
| **Diversity** | `ZZDB ZZDC ZZDD ZZDE ZZDF ZZDG ZZDH` (но не `ZZDA` — это усреднение спектра) | нет диверсити-приёма |
| **Компандер, noise gate, APF, VOX** | `ZZCP ZZCT`, `ZZGE ZZGL`, `ZZAA ZZAB ZZAT ZZAY`, `ZZVE ZZXH` | таких блоков в тракте нет (речевой компрессор — есть, это `ZZPK`/`ZZPL`) |
| **Вендорские команды микшера и служебной шины** | `ZZWA…ZZWS`, `ZZWT…ZZWW`, `ZZFV ZZFW ZZFX ZZFY` | неподдерживаемое оборудование |
| **ATU (Aries/Ganymede)** | `ZZOV ZZOW ZZOX ZZOZ ZZZA` | нет тюнера |
| **DSP-буферы, JSON-команды** | `ZZHA ZZHR ZZHT ZZHU ZZHV ZZHW ZZHX`, `ZZJP ZZJQ ZZJR ZZJS` | внутренние крутилки CAT |

---

## 5. Транспорты

> CAT **дополнительных слайсов** (свой TCP-порт на слайс B..G + Auto TX) вынесен
> в отдельный документ: [`CAT_SLICES.md`](CAT_SLICES.md). Тот же движок, другой
> контекст; транспорты ниже принадлежат главному приёмнику (слайс A).

- **Serial** (`CATSerial.pas`): до 4 портов, параметры (порт/скорость/биты/
  чётность/стоп) из `TGlobalSettings.CATSerial*`, применяются
  `TCATAdapter.ApplySettings`. На текущей платформе сборки может быть
  `{$DEFINE CAT_SERIAL_STUB}` (см. начало файла) — проверить перед релизом.
- **TCP** (`CATTcp.pas`): сервер на `CATTcpPort`, включается флагом
  `CATTcpEnabled`. Клиенты идентифицируются GUID через `ZZGA`/`ZZGR`.

---

## 5.1 Стенд

`test/cat/run.sh` — сборка и прогон `test/cat/cattest.pas` (железа и движка не
требует, команды ходят по подставному радио). Проверяет не список команд, а
свойства всего набора, и по кодам ходит перебором `AA`..`ZZ`, поэтому новая
команда попадает под проверку сама:

- **круговой прогон** — ответ на опрос обязан приниматься обратно (так нашлась
  разница ширин у `ZZGT`);
- **мусор в поле** — разбор не падает (сборка с `-Criot`), отвечает одним
  корректным кадром, а нечисловое поле ничего не перестраивает (так нашлись
  `FW`/`SH`/`SL`/`AG`/`SQ`/`GT` и десяток `ZZ`-двойников);
- **точечные регрессии** — заглушки, которые молча правили радио.

Подробности — `test/cat/README.md`. После правки любой команды прогон
обязателен: он дешёвый (секунды) и ловит ровно тот класс дефектов, которым эта
подсистема болеет.

---

## 6. Как добавить новую CAT-команду (чек-лист)

1. **Сверить формат** с документацией протокола и тестами совместимости.
2. Если нужна новая возможность контроллера — есть ли публичный метод/поле
   в `TRadioController`? Если нет — добавить (по образцу `SetSplit`).
3. В `CATEngine.pas`: добавить callback(и) в `TCATContext`, `SafeGet*`-хелпер
   для GET, переписать тело команды (`CmdXX` или `ZZXX`).
4. В `CATAdapter.pas`: геттер (чтение `FController.F*`), сеттер + `SyncXX`
   (через `Invoke`), регистрация в `TCATAdapter.Create`.
5. Если команда `ZZ*` ещё не в диспетчере — добавить ветку `else if ext = '..'`.
6. Сборка-проверка: `lazbuild -B --ws=qt6 ewsdr.lpr` (GUI-граф включает CAT;
   headless-демон `ewsdrd` CAT **не** содержит).
7. Прогнать стенд: `test/cat/run.sh`. Новая команда попадёт в круговой прогон и
   в прогон мусором автоматически — если ширины GET и SET разошлись или поле
   разбирается через `StrToIntDef`, стенд скажет об этом сразу.
