fix(tci,cat): резерв бюджета переезжает писателю; строгий разбор полей ZZ-команд

Три замечания по 79132f1.

1. [P2] Лимит памяти рекордеров обходился при SAVE. Take собирал сплошную
   копию записи, а DropData тут же возвращал исходные куски в бюджет —
   копия жила дальше в асинхронном TTCIWavWriter уже неучтённой. Один SAVE
   поднимал настоящее потребление примерно вдвое, а на медленном или
   зависшем сетевом каталоге очередь writer-потоков и их буферов росла
   мимо потолка вовсе. Теперь копия НАСЛЕДУЕТ резерв кусков, из которых
   собрана: Take отдаёт его out-параметром Reserved, писатель держит до
   конца записи и отпускает в ReleaseData — ровно один раз, сколько бы
   путей выхода ни было у Execute. Новых денег у бюджета копия не берёт,
   так что потолок теперь считает и очередь сохранений тоже.

2. [P2] Пять ZZ-команд проверяли длину поля, но не содержимое:
   StrToIntDef(s, 0) превращал любую нечисловую пару символов в индекс 0.
   ZZBSxx; переключал диапазон на нулевой вместо ?;, ZZBMxx; и
   ZZAUxx;/ZZBPxx; двигали VFO, ZZFIxx; выбирал фильтр 0. Разбор приведён
   к идиоме ZZFL/ZZFH: TryStrToInt, иначе ошибка формата.

3. [P3] Сообщение об отказе запуска TCI звало в Settings → Advanced, а
   настройки там уже не живут — вкладка CAT (переезд был в de0830f).

Стенд 194/194: Take отдаёт резерв размером с копию, после Take занят ровно
он, писатель возвращает его по окончании. Без фикса первая проверка
краснеет. GUI (--ws=qt6) и демон зелёные. doc/TCI.md §2.5 и сводка стенда,
doc/CAT_STATUS.md — таблица разбора.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 12:50:21 +03:00
co-authored by Claude Opus 5
parent 79132f133c
commit 9b77809a73
7 changed files with 119 additions and 31 deletions
+17 -5
View File
@@ -1743,10 +1743,13 @@ end;
function TCATEngine.ZZAU(const s: string): string;
// ZZAU: сдвиг VFO A вверх на один шаг с индексом nn (00..14, см. StepIdxToHz).
// ★Нечисловое поле — ошибка формата: StrToIntDef молча дал бы индекс 0, то
// есть «ZZAUxx;» двигал бы VFO вместо честного «?;» (как в ZZFL/ZZFH).
var idx: Integer;
begin
if Length(s) = 2 then begin
idx := EnsureRange(StrToIntDef(s, 0), 0, 14);
if not TryStrToInt(s, idx) then Exit(CAT_ERROR);
idx := EnsureRange(idx, 0, 14);
if Assigned(FCtx.SetVfoA) then FCtx.SetVfoA(SafeGetVfoA + StepIdxToHz(idx));
Result := '';
end else
@@ -1755,10 +1758,12 @@ end;
function TCATEngine.ZZBP(const s: string): string;
// ZZBP: сдвиг VFO B вверх на один шаг с индексом nn (00..14, см. StepIdxToHz).
// Разбор поля — как у ZZAU: нечисловое значит ошибку, а не нулевой индекс.
var idx: Integer;
begin
if Length(s) = 2 then begin
idx := EnsureRange(StrToIntDef(s, 0), 0, 14);
if not TryStrToInt(s, idx) then Exit(CAT_ERROR);
idx := EnsureRange(idx, 0, 14);
if Assigned(FCtx.SetVfoB) then FCtx.SetVfoB(SafeGetVfoB + StepIdxToHz(idx));
Result := '';
end else
@@ -1811,10 +1816,12 @@ end;
function TCATEngine.ZZBM(const s: string): string;
// ZZBM: сдвиг VFO B ВНИЗ на один шаг с индексом nn (00..14) — пара к ZZBP.
// (Режим — это ZZMD; раньше ZZBM был ошибочно заалиашен на него.)
// Разбор поля — как у ZZAU: нечисловое значит ошибку, а не нулевой индекс.
var idx: Integer;
begin
if Length(s) = 2 then begin
idx := EnsureRange(StrToIntDef(s, 0), 0, 14);
if not TryStrToInt(s, idx) then Exit(CAT_ERROR);
idx := EnsureRange(idx, 0, 14);
if Assigned(FCtx.SetVfoB) then FCtx.SetVfoB(SafeGetVfoB - StepIdxToHz(idx));
Result := '';
end else
@@ -1825,10 +1832,13 @@ function TCATEngine.ZZBS(const s: string): string;
// ZZBS: выбор диапазона. ⚠ Отклонение от Thetis: там поле трёхсимвольное и
// несёт КОД диапазона ("160"/"040"/"WWV"), у нас — двузначный ИНДЕКС в списке
// диапазонов EWSDR. Менять поздно: на этом формате уже сидят клиенты.
// ★Нечисловое поле — ошибка формата: StrToIntDef переключал бы на диапазон 0,
// то есть уводил бы оператора с рабочего бэнда по опечатке клиента.
var idx: Integer;
begin
if Length(s) = 2 then begin
idx := EnsureRange(StrToIntDef(s, 0), 0, 10);
if not TryStrToInt(s, idx) then Exit(CAT_ERROR);
idx := EnsureRange(idx, 0, 10);
if Assigned(FCtx.DoBandByIndex) then FCtx.DoBandByIndex(idx);
Result := '';
end else if Length(s) = 0 then
@@ -2000,10 +2010,12 @@ end;
function TCATEngine.ZZFI(const s: string): string;
// ZZFI: индекс текущего фильтра (2 цифры).
// Разбор поля — как у ZZFL/ZZFH: нечисловое значит ошибку, а не фильтр 0.
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