diff --git a/CATEngine.pas b/CATEngine.pas index 3bedc2a..e47478e 100644 --- a/CATEngine.pas +++ b/CATEngine.pas @@ -321,7 +321,6 @@ type function ZZBA: string; function ZZBD: string; function ZZBP(const s: string): string; - function ZZBE(const s: string): string; function ZZBI(const s: string): string; function ZZBM(const s: string): string; function ZZBS(const s: string): string; @@ -870,8 +869,8 @@ function TCATEngine.CmdAG(const s: string): string; var v: Integer; begin if Length(s) = 4 then begin - v := Round(StrToIntDef(Copy(s,2,3), 0) / 2.55); - v := EnsureRange(v, 0, 100); + if not TryStrToInt(Copy(s, 2, 3), v) then Exit(CAT_ERROR); + v := EnsureRange(Round(v / 2.55), 0, 100); if Assigned(FCtx.SetVolume) then FCtx.SetVolume(v); Result := ''; end else if Length(s) = 1 then begin @@ -904,7 +903,8 @@ function TCATEngine.CmdCN(const s: string): string; var idx: Integer; begin if Length(s) = 2 then begin - idx := EnsureRange(StrToIntDef(s, 1) - 1, 0, 37); + if not TryStrToInt(s, idx) then Exit(CAT_ERROR); + idx := EnsureRange(idx - 1, 0, 37); if Assigned(FCtx.SetCTCSSTone) then FCtx.SetCTCSSTone(idx); Result := ''; end else if Length(s) = 0 then @@ -979,7 +979,8 @@ function TCATEngine.CmdFW(const s: string): string; var idx: Integer; begin if Length(s) = 4 then begin - idx := EnsureRange(StrToIntDef(s, 0), 0, 9); + if not TryStrToInt(s, idx) then Exit(CAT_ERROR); + idx := EnsureRange(idx, 0, 9); if Assigned(FCtx.SetFilterIdx) then FCtx.SetFilterIdx(idx); Result := ''; end else if Length(s) = 0 then @@ -990,9 +991,11 @@ end; function TCATEngine.CmdGT(const s: string): string; // GT: AGC mode (3 digits) — 0=FAST,1=MED,2=SLOW,3=LONG,4=OFF +var m: Integer; begin if Length(s) = 3 then begin - if Assigned(FCtx.SetAGCMode) then FCtx.SetAGCMode(EnsureRange(StrToIntDef(s,0),0,4)); + if not TryStrToInt(s, m) then Exit(CAT_ERROR); + if Assigned(FCtx.SetAGCMode) then FCtx.SetAGCMode(EnsureRange(m, 0, 4)); Result := ''; end else if Length(s) = 0 then Result := Pad(SafeGetAGCMode, 3) @@ -1112,10 +1115,11 @@ end; function TCATEngine.CmdNB(const s: string): string; // NB: noise blanker on/off → maps to NBMode +var v: Integer; begin if Length(s) = 1 then begin - if Assigned(FCtx.SetNBMode) then - FCtx.SetNBMode(StrToIntDef(s, 0)); + if not TryStrToInt(s, v) then Exit(CAT_ERROR); + if Assigned(FCtx.SetNBMode) then FCtx.SetNBMode(v); Result := ''; end else if Length(s) = 0 then Result := IntToStr(EnsureRange(SafeGetNBMode, 0, 1)) @@ -1194,7 +1198,8 @@ function TCATEngine.CmdPC(const s: string): string; var v: Integer; begin if Length(s) = 3 then begin - v := EnsureRange(StrToIntDef(s,0), 0, 100); + if not TryStrToInt(s, v) then Exit(CAT_ERROR); + v := EnsureRange(v, 0, 100); if Assigned(FCtx.SetDriveLevel) then FCtx.SetDriveLevel(v); Result := ''; end else if Length(s) = 0 then @@ -1268,7 +1273,8 @@ function TCATEngine.CmdSH(const s: string): string; var idx: Integer; begin if Length(s) = 2 then begin - idx := EnsureRange(StrToIntDef(s, 0), 0, 9); + if not TryStrToInt(s, idx) then Exit(CAT_ERROR); + idx := EnsureRange(idx, 0, 9); if Assigned(FCtx.SetFilterIdx) then FCtx.SetFilterIdx(idx); Result := ''; end else if Length(s) = 0 then @@ -1304,7 +1310,8 @@ function TCATEngine.CmdSQ(const s: string): string; var lvl255, lvl100: Integer; begin if Length(s) = 4 then begin - lvl255 := EnsureRange(StrToIntDef(Copy(s, 2, 3), 0), 0, 255); + if not TryStrToInt(Copy(s, 2, 3), lvl255) then Exit(CAT_ERROR); + lvl255 := EnsureRange(lvl255, 0, 255); lvl100 := Round(lvl255 * 100 / 255); if Assigned(FCtx.SetSquelchLevel) then FCtx.SetSquelchLevel(lvl100); if Assigned(FCtx.SetSquelchOn) then FCtx.SetSquelchOn(lvl100 > 0); @@ -1362,7 +1369,7 @@ begin else if ext = 'BA' then ans := ZZBA else if ext = 'BB' then ans := '' // STUB: bandswitch RX2 вверх — RX2 нет else if ext = 'BD' then begin ZZBD; ans := ''; end - else if ext = 'BE' then ans := ZZBE(s) + else if ext = 'BE' then ans := '' // STUB: VFO B вниз на nn пресетных шагов — см. ZZAE else if ext = 'BF' then ans := '' // STUB: VFO B вверх на nn пресетных шагов — см. ZZAE else if ext = 'BG' then ans := '' // STUB: группа диапазонов HF/VHF — не разделяем else if ext = 'BI' then ans := ZZBI(s) @@ -1701,7 +1708,8 @@ function TCATEngine.ZZAG(const s: string): string; var v: Integer; begin if Length(s) = 3 then begin - v := EnsureRange(StrToIntDef(s, 0), 0, 100); + if not TryStrToInt(s, v) then Exit(CAT_ERROR); + v := EnsureRange(v, 0, 100); if Assigned(FCtx.SetVolume) then FCtx.SetVolume(v); Result := ''; end else if Length(s) = 0 then @@ -1731,7 +1739,7 @@ function TCATEngine.ZZAR(const s: string): string; var v: Integer; begin if Length(s) = 4 then begin - v := StrToIntDef(s, 0); // StrToIntDef понимает ведущий +/- + if not TryStrToInt(s, v) then Exit(CAT_ERROR); // Val понимает ведущий +/- if Assigned(FCtx.SetAGCTop) then FCtx.SetAGCTop(v); Result := ''; end else if Length(s) = 0 then begin @@ -1796,15 +1804,6 @@ begin Result := ''; end; -function TCATEngine.ZZBE(const s: string): string; -// ZZBE: VFO B вниз на nn пресетных шагов — STUB: глобального размера шага у -// EWSDR нет (см. ZZAC). Шаг по индексу — это ZZBM/ZZBP. -begin - if Length(s) = 3 then Result := '' - else if Length(s) = 2 then Result := '1' - else Result := CAT_ERROR; -end; - function TCATEngine.ZZBI(const s: string): string; // ZZBI: кнопка BIN (бинауральный приём) — STUB: такого режима нет. // (Индекс фильтра — это ZZFI; раньше он жил здесь.) @@ -1942,7 +1941,9 @@ begin begin FillChar(G, SizeOf(G), 0); if Assigned(FCtx.GetTXEQ) then nb := FCtx.GetTXEQ(G) else nb := 0; - nb := EnsureRange(nb, 0, High(G)); + // Отдаём ровно то, что примем обратно (см. разбор SET ниже): сетка бывает + // только трёх- или десятиполосной, а движок всё, что не 3, играет как 10. + if nb <> 3 then nb := 10; ans := Pad(nb, 3); for i := 0 to nb do begin @@ -2089,17 +2090,18 @@ begin end; function TCATEngine.ZZGT(const s: string): string; -// ZZGT: AGC mode (1 char: 0=FAST,1=MED,2=SLOW,3=LONG,4=OFF) → 3-digit response +// ZZGT: режим АРУ (0=FAST,1=MED,2=SLOW,3=LONG,4=OFF). Ответ — три цифры, как у +// кенвудовской GT, и SET обязан принимать ту же ширину: клиент, прочитавший +// «ZZGT000;», пишет назад ровно это, и односимвольная форма отдавала ему «?;». +// Однобуквенную форму оставляем — на ней уже сидят клиенты. var m: Integer; begin - if Length(s) = 1 then begin - m := EnsureRange(StrToIntDef(s, 0), 0, 4); - if Assigned(FCtx.SetAGCMode) then FCtx.SetAGCMode(m); - Result := ''; - end else if Length(s) = 0 then - Result := Pad(SafeGetAGCMode, 3) - else - Result := CAT_ERROR; + if Length(s) = 0 then Exit(Pad(SafeGetAGCMode, 3)); + if (Length(s) <> 1) and (Length(s) <> 3) then Exit(CAT_ERROR); + if not TryStrToInt(s, m) then Exit(CAT_ERROR); + m := EnsureRange(m, 0, 4); + if Assigned(FCtx.SetAGCMode) then FCtx.SetAGCMode(m); + Result := ''; end; function TCATEngine.ZZID: string; @@ -2245,7 +2247,7 @@ function TCATEngine.ZZNA(const s: string): string; var v: Integer; begin if Length(s) = 1 then begin - v := StrToIntDef(s, 0); + if not TryStrToInt(s, v) then Exit(CAT_ERROR); if Assigned(FCtx.SetNBMode) then FCtx.SetNBMode(v); Result := ''; end else if Length(s) = 0 then @@ -2259,7 +2261,8 @@ function TCATEngine.ZZNB(const s: string): string; var v: Integer; begin if Length(s) = 1 then begin - v := EnsureRange(StrToIntDef(s,0),0,4); + if not TryStrToInt(s, v) then Exit(CAT_ERROR); + v := EnsureRange(v, 0, 4); if Assigned(FCtx.SetNBMode) then FCtx.SetNBMode(v); Result := ''; end else if Length(s) = 0 then @@ -2287,7 +2290,8 @@ function TCATEngine.ZZNR(const s: string): string; var v: Integer; begin if Length(s) = 1 then begin - v := EnsureRange(StrToIntDef(s,0),0,4); + if not TryStrToInt(s, v) then Exit(CAT_ERROR); + v := EnsureRange(v, 0, 4); if Assigned(FCtx.SetNRMode) then FCtx.SetNRMode(v); Result := ''; end else if Length(s) = 0 then @@ -2343,7 +2347,8 @@ function TCATEngine.ZZPC(const s: string): string; var v: Integer; begin if Length(s) = 3 then begin - v := EnsureRange(StrToIntDef(s,0),0,100); + if not TryStrToInt(s, v) then Exit(CAT_ERROR); + v := EnsureRange(v, 0, 100); if Assigned(FCtx.SetDriveLevel) then FCtx.SetDriveLevel(v); Result := ''; end else if Length(s) = 0 then @@ -2491,7 +2496,8 @@ function TCATEngine.ZZSQ(const s: string): string; var v: Integer; begin if Length(s) = 3 then begin - v := EnsureRange(StrToIntDef(s, 0), 0, 100); + if not TryStrToInt(s, v) then Exit(CAT_ERROR); + v := EnsureRange(v, 0, 100); if Assigned(FCtx.SetSquelchLevel) then FCtx.SetSquelchLevel(v); if Assigned(FCtx.SetSquelchOn) then FCtx.SetSquelchOn(v > 0); Result := ''; @@ -2510,9 +2516,11 @@ end; function TCATEngine.ZZST(const s: string): string; // ZZST: FM tuning step index (2 digits): 00=6.25k, 01=12.5k, 02=20k, 03=25k. +var v: Integer; begin if Length(s) = 2 then begin - if Assigned(FCtx.SetFMStep) then FCtx.SetFMStep(EnsureRange(StrToIntDef(s, 0), 0, 3)); + if not TryStrToInt(s, v) then Exit(CAT_ERROR); + if Assigned(FCtx.SetFMStep) then FCtx.SetFMStep(EnsureRange(v, 0, 3)); Result := ''; end else if Length(s) = 0 then Result := Pad(EnsureRange(SafeGetFMStep, 0, 3), 2) @@ -2536,9 +2544,11 @@ end; function TCATEngine.ZZTB(const s: string): string; // ZZTB: CTCSS tone index (2 digits, 0-based 00..37 в таблице CTCSS_TONES). +var v: Integer; begin if Length(s) = 2 then begin - if Assigned(FCtx.SetCTCSSTone) then FCtx.SetCTCSSTone(EnsureRange(StrToIntDef(s, 0), 0, 37)); + if not TryStrToInt(s, v) then Exit(CAT_ERROR); + if Assigned(FCtx.SetCTCSSTone) then FCtx.SetCTCSSTone(EnsureRange(v, 0, 37)); Result := ''; end else if Length(s) = 0 then Result := Pad(EnsureRange(SafeGetCTCSSTone, 0, 37), 2) diff --git a/doc/CAT_STATUS.md b/doc/CAT_STATUS.md index 95a59dd..3e9948f 100644 --- a/doc/CAT_STATUS.md +++ b/doc/CAT_STATUS.md @@ -176,7 +176,7 @@ CTCSS on/tone, FM repeater dir/offset, FM step, AGC-T, CTUN. | `PR` / `ZZPK` | речевой компрессор вкл/выкл | `CompressorOn` | | `ZZPL` | усиление компрессора, дБ | `CompressorGain` (0..20) | | `ZZET` | кнопка TXEQ | `EQOn` | -| `ZZEB` | значения TX-эквалайзера | `EQNumBands` + `EQGains` (формат Thetis, 36 символов) | +| `ZZEB` | значения TX-эквалайзера | `EQNumBands` + `EQGains` (формат Thetis, 36 символов). Полос бывает только 3 или 10 — движок (`PushTXEQProfile`) и редактор знают ровно эти два случая, остальное отвергается и не выдаётся | | `ZZTO` | мощность настройки | `TUNLevel` | | `ZZTU` | кнопка TUN | `SetTune` | | `ZZUT` | двухтональник 2TON | `SetTwoTone` | @@ -243,7 +243,7 @@ CW: по CAT прилетает что угодно, а контроллер п функция живёт на самом деле): `ZZAA`, `ZZVG` (громкость — `ZZAG`) · `ZZOA` (антенна RX1) · `ZZAP` (мощность — `ZZPC`) · `ZZDN` (мощность — `ZZPC`; сам `ZZDN` = низ водопада) · `ZZAC` (диапазон — `ZZBS`) · `ZZBI` (фильтр — `ZZFI`) -· `ZZMB` (режим — `ZZMD`) · `ZZVA` (обмен VFO — `ZZVS`) · `ZZPD`, `ZZPO` +· `ZZMB` (режим — `ZZMD`) · `ZZVA` (обмен VFO — `ZZVS`) · ★`ZZVB` (усиление приёма в VAC; **опрос** копировал VFO A в VFO B, то есть терял сплит оператора, — тело переписали позже комментария) · `ZZPD`, `ZZPO` (мощность и питание — `ZZPC`/`ZZPS`) · `ZZQM` (режим — `ZZMD`) · `ZZSR` (опорный уровень спектра) · `ZZRD`/`ZZRU` (RIT; шаг настройки — `ZZSA`/`ZZSB`). @@ -273,8 +273,11 @@ Thetis, у нас есть, ширины полей совпадают с `CATSt | `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`, список режимов) мог уехать обрезанным, и молча: усечение здесь не ошибка. Теперь дописываем остаток в цикле | +| `CATTcp.SendStr` | один `send` на ответ. TCP не обязан отдать весь буфер за раз — длинный ответ (`IF`, `ZZEB`, список режимов) мог уехать обрезанным, и молча: усечение здесь не ошибка. Теперь дописываем остаток в цикле. ★И пишем через `WebUtils.SockSend`, а не голым `fpSend`: в нём `MSG_NOSIGNAL`, без которого запись в закрытый клиентом сокет приходит как `SIGPIPE` и убивает процесс целиком (обработчика сигнала в дереве нет, а цикл дозаписи умножает число попыток) | | `CATSerial` | порт помечался активным ДО `SerOpen`; при отказе он навсегда оставался «работающим» в `ActiveCount` и UI, а причина нигде не оседала. Открытие переехало из потока в `TCATSerialPort.Start` (синхронно), появилось свойство `LastError`, поток теперь только читает, а закрывает владелец в `Stop`. Заодно Andromeda-порт назначается только на реально поднявшийся порт | Формат `ZZOT` сверен отдельно: эталон читает 9 цифр как МГц с шестью знаками @@ -331,7 +334,7 @@ Thetis, у нас есть, ширины полей совпадают с `CATSt | Область | Команды (примеры) | Причина | |---|---|---| | **Второй приёмник 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** | `ZZVC ZZVD ZZVF ZZVI ZZVK ZZVM ZZVO ZZVQ ZZVR ZZVT ZZVU ZZVV ZZVX ZZVY ZZVZ ZZYA ZZYB` | нет виртуального аудиокабеля | +| **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` — это усреднение спектра) | нет диверсити-приёма | @@ -357,6 +360,26 @@ Thetis, у нас есть, ширины полей совпадают с `CATSt --- +## 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. **Сверить формат** в `CATStructs.xml` + `CATCommands.cs` (Thetis). @@ -369,3 +392,6 @@ Thetis, у нас есть, ширины полей совпадают с `CATSt 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`, стенд скажет об этом сразу.