mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +00:00
RTL-модуль Serial собирается только для [android, linux, netbsd, openbsd,
win32, win64] — Darwin в списке нет (rtl-extra/fpmake.pp). Из-за этого под
macOS CAT по последовательному порту стоял заглушкой, а CWKeyer не собирался
вовсе.
★Скопировать чужой исходник было нельзя. Serial.SerSetParams кладёт B-константу
скорости прямо в c_cflag — идиома Linux, где это битовая маска. На BSD и macOS
B115200 это просто ЧИСЛО 115200, и в c_cflag прилетает $1C200: CLOCAL | HUPCL |
CCTS_OFLOW плюс мусор в поле CSIZE. Молча включается аппаратный CTS-flow, и
передача встаёт, если железка не держит CTS, — а ошибки никакой, порт
«открылся».
Новый SerialPort.pas:
* Unix — termios напрямую, Linux и macOS ОДНИМ путём (разница спрятана в
константах RTL; Darwin даёт и termio, и все TIOCM_*). Скорость только через
CFSetISpeed/CFSetOSpeed, c_cflag собирается с нуля чистой SerCFlags;
CRTSCTS — исключительно по явной просьбе; сырой режим и VMIN/VTIME явные;
open с O_NONBLOCK и снятием флага сразу после (часть USB-драйверов, как и
tty-устройство на маке, висит в open до DCD).
* Windows — тонкая переадресация в RTL Serial: там он есть и в termios не лезет.
* Неуспех SerOpen на ВСЕХ платформах даёт SER_INVALID_HANDLE (RTL возвращал 0
под Windows и −1 под Unix, из-за чего вызывающие держали свои развилки).
Проверка — SerValid.
Точки вызова: CATSerial и CWKeyer переведены на обёртку, заглушка
CAT_SERIAL_STUB и платформенные {$IFDEF} вокруг хендлов убраны. Имена портов по
умолчанию и подсказка в настройках теперь платформенные (SerDefaultPortName /
SerPortNameHint): на маке порт зовётся /dev/cu.usbserial-XXXX, и '/dev/ttyS0'
там — заведомо неверная подсказка; осмысленного умолчания не существует,
поэтому пусто.
Новый стенд test/serial (36 проверок) — на псевдотерминале: открытие, termios
обратным чтением, ★«CRTSCTS сам не включился», ★«в c_cflag нет ничего, кроме
наших флагов», формат через чистую SerCFlags (обратным чтением его не
проверить: pty восьмибитно-чистый и молча меняет CS7 на CS8), обмен, срок
таймаута, CR не превращается в CR+LF, эха нет, и сквозной прогон CAT через
обёртку. Негативный контроль: с `or Cardinal(BitsPerSec)` в SerSetParams (та
самая идиома, что ломает RTL на BSD) проверка c_cflag падает — «1CAB0 против
8B0».
test/platform расширен: теперь сим-компилирует и SerialPort, для его
Windows-ветки добавлена заглушка модуля Serial с теми же сигнатурами.
Модемные линии (DTR/RTS/CTS/DSR/CD/RI) на pty не проверяются — ioctl TIOCM*
там не работает; это остаётся на живой порт.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bkwwyj7xVRrqnSVEseTRfV
309 lines
9.3 KiB
ObjectPascal
309 lines
9.3 KiB
ObjectPascal
unit CATSerial;
|
|
{
|
|
CATSerial.pas — CAT через последовательные порты (до 4 штук).
|
|
|
|
Каждый порт запускает отдельный поток-слушатель, который:
|
|
1. Читает байты из COM/ttyS порта
|
|
2. Накапливает их в буфер до символа ';'
|
|
3. Передаёт команду в TCATEngine.Parse()
|
|
4. Отправляет ответ обратно в порт
|
|
|
|
Платформы (имя порта вводится вручную, подсказка — SerPortNameHint):
|
|
• Linux : /dev/ttyS0..3, /dev/ttyUSB0, /dev/ttyACM0
|
|
• macOS : /dev/cu.usbserial-XXXX
|
|
• Windows: COM1..COM4
|
|
|
|
Зависимости: CATEngine, SerialPort (наша обёртка; RTL-модуль Serial под macOS
|
|
не собирается вовсе — см. шапку SerialPort.pas).
|
|
}
|
|
{$IFDEF FPC}
|
|
{$MODE Delphi}
|
|
{$ENDIF}
|
|
|
|
interface
|
|
|
|
uses
|
|
Classes, SysUtils,
|
|
SerialPort,
|
|
SyncObjs,
|
|
CATEngine;
|
|
|
|
const
|
|
CAT_SERIAL_MAX_PORTS = 4;
|
|
CAT_SERIAL_BUF_SIZE = 256;
|
|
|
|
type
|
|
TCATSerialParity = (cspNone, cspOdd, cspEven);
|
|
|
|
TCATSerialConfig = record
|
|
Enabled: Boolean;
|
|
PortName: string; // e.g. '/dev/ttyS0' or 'COM1'
|
|
BaudRate: Integer; // 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200
|
|
DataBits: Integer; // 7 or 8
|
|
StopBits: Integer; // 1 or 2
|
|
Parity: TCATSerialParity;
|
|
Andromeda: Boolean; // порт — передняя панель Andromeda/G2 (push ZZZI наружу)
|
|
end;
|
|
|
|
TCATSerialPort = class;
|
|
|
|
{ TCATSerialThread — per-port listener thread }
|
|
TCATSerialThread = class(TThread)
|
|
private
|
|
FOwner: TCATSerialPort;
|
|
FHandle: TSerialHandle;
|
|
FBuf: string;
|
|
procedure ProcessBuffer;
|
|
protected
|
|
procedure Execute; override;
|
|
public
|
|
constructor Create(AOwner: TCATSerialPort);
|
|
end;
|
|
|
|
{ TCATSerialPort — one serial CAT port }
|
|
TCATSerialPort = class
|
|
private
|
|
FConfig: TCATSerialConfig;
|
|
FEngine: TCATEngine;
|
|
FThread: TCATSerialThread;
|
|
FHandle: TSerialHandle;
|
|
FLock: TCriticalSection;
|
|
FActive: Boolean;
|
|
FLastError: string;
|
|
function OpenPort: Boolean; // синхронно, из Start — чтобы отказ был виден сразу
|
|
procedure ClosePort;
|
|
public
|
|
constructor Create(AEngine: TCATEngine; const ACfg: TCATSerialConfig);
|
|
destructor Destroy; override;
|
|
procedure Start;
|
|
procedure Stop;
|
|
procedure SendStr(const S: string);
|
|
property Handle: TSerialHandle read FHandle write FHandle;
|
|
property Config: TCATSerialConfig read FConfig;
|
|
property Engine: TCATEngine read FEngine;
|
|
property Active: Boolean read FActive;
|
|
// Причина, по которой порт не поднялся (пусто, если поднялся). Раньше отказ
|
|
// SerOpen нигде не оседал, а порт продолжал числиться активным.
|
|
property LastError: string read FLastError;
|
|
end;
|
|
|
|
{ TCATSerialManager — manages up to 4 serial ports }
|
|
TCATSerialManager = class
|
|
private
|
|
FPorts: array[0..CAT_SERIAL_MAX_PORTS-1] of TCATSerialPort;
|
|
FEngine: TCATEngine;
|
|
FAndromedaPort: TCATSerialPort; // первый активный порт с Andromeda=true (или nil)
|
|
public
|
|
constructor Create(AEngine: TCATEngine);
|
|
destructor Destroy; override;
|
|
procedure ApplyConfig(const Configs: array of TCATSerialConfig);
|
|
procedure StopAll;
|
|
function ActiveCount: Integer;
|
|
// Передняя панель: есть ли активный Andromeda-порт и отправка строки в него.
|
|
function HasAndromeda: Boolean;
|
|
procedure SendToAndromeda(const S: string); // сигнатура совместима с TAndromedaSend
|
|
end;
|
|
|
|
implementation
|
|
|
|
{ ── TCATSerialThread ──────────────────────────────────────────────────────── }
|
|
|
|
constructor TCATSerialThread.Create(AOwner: TCATSerialPort);
|
|
begin
|
|
FOwner := AOwner;
|
|
FBuf := '';
|
|
FreeOnTerminate := False;
|
|
inherited Create(True);
|
|
end;
|
|
|
|
procedure TCATSerialThread.ProcessBuffer;
|
|
var
|
|
tpos: Integer;
|
|
cmd, resp: string;
|
|
begin
|
|
repeat
|
|
tpos := Pos(';', FBuf);
|
|
if tpos = 0 then Break;
|
|
cmd := Copy(FBuf, 1, tpos);
|
|
Delete(FBuf, 1, tpos);
|
|
resp := FOwner.Engine.Parse(cmd);
|
|
if resp <> '' then
|
|
FOwner.SendStr(resp);
|
|
until tpos = 0;
|
|
// Guard against runaway buffers (no terminator seen in 256+ chars)
|
|
if Length(FBuf) > CAT_SERIAL_BUF_SIZE then FBuf := '';
|
|
end;
|
|
|
|
procedure TCATSerialThread.Execute;
|
|
// Порт уже открыт владельцем (TCATSerialPort.Start) — поток только читает.
|
|
// Закрывает тоже владелец, в Stop, после WaitFor.
|
|
var
|
|
ch: Byte;
|
|
n: LongInt;
|
|
begin
|
|
FHandle := FOwner.Handle;
|
|
while not Terminated do begin
|
|
n := SerReadTimeout(FHandle, ch, 50);
|
|
if n = 1 then begin
|
|
FBuf := FBuf + Chr(ch);
|
|
if ch = Ord(';') then ProcessBuffer;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
{ ── TCATSerialPort ────────────────────────────────────────────────────────── }
|
|
|
|
constructor TCATSerialPort.Create(AEngine: TCATEngine; const ACfg: TCATSerialConfig);
|
|
begin
|
|
inherited Create;
|
|
FEngine := AEngine;
|
|
FConfig := ACfg;
|
|
FHandle := SER_INVALID_HANDLE;
|
|
FLock := TCriticalSection.Create;
|
|
FActive := False;
|
|
end;
|
|
|
|
destructor TCATSerialPort.Destroy;
|
|
begin
|
|
Stop;
|
|
FLock.Free;
|
|
inherited;
|
|
end;
|
|
|
|
function TCATSerialPort.OpenPort: Boolean;
|
|
var
|
|
h: TSerialHandle;
|
|
par: TParityType;
|
|
begin
|
|
Result := False;
|
|
h := SerOpen(FConfig.PortName);
|
|
if not SerValid(h) then
|
|
begin
|
|
FLastError := 'не удалось открыть ' + FConfig.PortName;
|
|
Exit;
|
|
end;
|
|
|
|
case FConfig.Parity of
|
|
cspOdd: par := OddParity;
|
|
cspEven: par := EvenParity;
|
|
else par := NoneParity;
|
|
end;
|
|
|
|
SerSetParams(h, FConfig.BaudRate, FConfig.DataBits, par, FConfig.StopBits, []);
|
|
FHandle := h;
|
|
FLastError := '';
|
|
Result := True;
|
|
end;
|
|
|
|
procedure TCATSerialPort.ClosePort;
|
|
begin
|
|
if SerValid(FHandle) then SerClose(FHandle);
|
|
FHandle := SER_INVALID_HANDLE;
|
|
end;
|
|
|
|
procedure TCATSerialPort.Start;
|
|
// Порт открываем ЗДЕСЬ, а не в потоке: иначе отказ SerOpen оставался внутри
|
|
// потока, а порт всё это время числился активным — и в ActiveCount, и в UI.
|
|
begin
|
|
if FActive or not FConfig.Enabled then Exit;
|
|
if not OpenPort then Exit; // FActive остаётся False, причина — в LastError
|
|
FActive := True;
|
|
FThread := TCATSerialThread.Create(Self);
|
|
FThread.Start;
|
|
end;
|
|
|
|
procedure TCATSerialPort.Stop;
|
|
begin
|
|
if not FActive then Exit;
|
|
FActive := False;
|
|
if Assigned(FThread) then begin
|
|
FThread.Terminate;
|
|
FThread.WaitFor;
|
|
FreeAndNil(FThread);
|
|
end;
|
|
ClosePort;
|
|
end;
|
|
|
|
procedure TCATSerialPort.SendStr(const S: string);
|
|
var buf: AnsiString;
|
|
begin
|
|
if not SerValid(FHandle) then Exit;
|
|
FLock.Acquire;
|
|
try
|
|
buf := AnsiString(S);
|
|
if Length(buf) > 0 then
|
|
SerWrite(FHandle, buf[1], Length(buf));
|
|
except
|
|
// ignore write errors (port may have been closed)
|
|
end;
|
|
FLock.Release;
|
|
end;
|
|
|
|
{ ── TCATSerialManager ─────────────────────────────────────────────────────── }
|
|
|
|
constructor TCATSerialManager.Create(AEngine: TCATEngine);
|
|
var i: Integer;
|
|
begin
|
|
inherited Create;
|
|
FEngine := AEngine;
|
|
FAndromedaPort := nil;
|
|
for i := 0 to CAT_SERIAL_MAX_PORTS-1 do
|
|
FPorts[i] := nil;
|
|
end;
|
|
|
|
destructor TCATSerialManager.Destroy;
|
|
begin
|
|
StopAll;
|
|
inherited;
|
|
end;
|
|
|
|
procedure TCATSerialManager.ApplyConfig(const Configs: array of TCATSerialConfig);
|
|
var
|
|
i: Integer;
|
|
cnt: Integer;
|
|
begin
|
|
StopAll;
|
|
cnt := Length(Configs);
|
|
if cnt > CAT_SERIAL_MAX_PORTS then cnt := CAT_SERIAL_MAX_PORTS;
|
|
for i := 0 to cnt-1 do begin
|
|
FPorts[i] := TCATSerialPort.Create(FEngine, Configs[i]);
|
|
if Configs[i].Enabled then FPorts[i].Start;
|
|
// Панель назначаем только на РЕАЛЬНО поднявшийся порт: раньше хватало галки
|
|
// в настройках, и HasAndromeda рапортовал о панели, которой нет.
|
|
if FPorts[i].Active and Configs[i].Andromeda and (FAndromedaPort = nil) then
|
|
FAndromedaPort := FPorts[i];
|
|
end;
|
|
end;
|
|
|
|
function TCATSerialManager.HasAndromeda: Boolean;
|
|
begin
|
|
Result := Assigned(FAndromedaPort);
|
|
end;
|
|
|
|
procedure TCATSerialManager.SendToAndromeda(const S: string);
|
|
begin
|
|
if Assigned(FAndromedaPort) then FAndromedaPort.SendStr(S);
|
|
end;
|
|
|
|
procedure TCATSerialManager.StopAll;
|
|
var i: Integer;
|
|
begin
|
|
FAndromedaPort := nil; // указывает в FPorts[] — обнулить ДО освобождения
|
|
for i := 0 to CAT_SERIAL_MAX_PORTS-1 do begin
|
|
if Assigned(FPorts[i]) then begin
|
|
FPorts[i].Stop;
|
|
FreeAndNil(FPorts[i]);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function TCATSerialManager.ActiveCount: Integer;
|
|
var i: Integer;
|
|
begin
|
|
Result := 0;
|
|
for i := 0 to CAT_SERIAL_MAX_PORTS-1 do
|
|
if Assigned(FPorts[i]) and FPorts[i].Active then Inc(Result);
|
|
end;
|
|
|
|
end.
|