Files
ewsdr/SerialPort.pas
ew8bakandClaude Opus 5 f88b1615e7 fix(serial): контракт SerValid — платформенный, ноль значит разное
Ноль на Unix и на Windows означает не одно и то же, и одной проверкой тут не
обойтись. На Unix это законный дескриптор (при закрытом стандартном вводе
fpOpen отдаёт именно его), а ядро Windows нулевой хендл не выдаёт никогда —
там ноль это либо отказ RTL, либо неинициализированное поле. Внутренний путь
был безопасен и раньше (SerOpen переводит ноль RTL в SER_INVALID_HANDLE, свои
поля инициализируются им же), но публичный контракт для значения, пришедшего
извне, оказывался неверным. Теперь проверка разведена по платформам.

★Стенд научился ПРОГОНЯТЬ windows-ветку, а не только компилировать её.
Заглушка модуля Serial — чистый Паскаль, поэтому ветку можно собрать и
запустить прямо на Linux: test/platform/win_probe проверяет перевод нулевого
хендла в SER_INVALID_HANDLE, ответ SerValid и правило имени COM10+. До сих пор
у windows-пути обёртки не было никакого поведенческого покрытия вовсе.

Негативный контроль: если убрать windows-ветку из SerValid, прогон падает на
«★нулевой хендл под Windows негоден».

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bkwwyj7xVRrqnSVEseTRfV
2026-08-24 22:43:14 +03:00

469 lines
18 KiB
ObjectPascal
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
unit SerialPort;
{
SerialPort.pas — последовательный порт: один API на все три платформы.
ЗАЧЕМ СВОЙ. Модуль `Serial` из RTL собирается только для
[android, linux, netbsd, openbsd, win32, win64] — Darwin в списке нет
(rtl-extra/fpmake.pp), поэтому под macOS проект не собирался вовсе, а CAT и
телеграфный ключ приходилось глушить заглушкой.
★И скопировать чужой исходник было нельзя. `Serial.SerSetParams` кладёт
B-константу скорости прямо в c_cflag — идиома Linux, где это битовая маска.
На BSD и macOS `B115200` это просто ЧИСЛО 115200, и в c_cflag прилетает
$1C200, то есть CLOCAL | HUPCL | CCTS_OFLOW плюс мусор в поле CSIZE:
молча включается аппаратный CTS-flow, и передача встаёт, если железка не
держит CTS. Ошибки при этом никакой — порт «открылся». Поэтому скорость
ставим единственным переносимым способом: CFSetISpeed/CFSetOSpeed, а c_cflag
собираем с нуля.
Устройство:
• Unix (Linux и macOS одним путём) — termios напрямую, без развилок по ОС:
разница между ними спрятана в константах RTL. Модемные линии — ioctl
TIOCM*, они есть и там, и там.
• Windows — тонкая переадресация в RTL `Serial` (там он собирается и работает).
Имена и сигнатуры намеренно повторяют RTL-модуль: точки вызова (CATSerial,
CWKeyer) меняют только строку uses. ★Отличие ровно одно, и оно к лучшему:
неуспех SerOpen на ВСЕХ платформах даёт SER_INVALID_HANDLE (RTL возвращал 0
на Windows и 1 на Unix, из-за чего вызывающие держали свои развилки).
Проверять результат следует через SerValid.
}
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
interface
type
{$IFDEF WINDOWS}
TSerialHandle = THandle;
{$ELSE}
TSerialHandle = LongInt;
{$ENDIF}
TParityType = (NoneParity, OddParity, EvenParity);
TSerialFlag = (RtsCtsFlowControl);
TSerialFlags = set of TSerialFlag;
const
// На Windows THandle(-1) — это и есть INVALID_HANDLE_VALUE.
SER_INVALID_HANDLE = TSerialHandle(-1);
{ Открыть порт. Имя — как его пишет система:
Linux /dev/ttyS0, /dev/ttyUSB0, /dev/ttyACM0
macOS /dev/cu.usbserial-XXXX ★именно cu, а не tty: tty-устройство
блокируется на open до появления DCD
Windows COM1, \\.\COM12 для номеров больше девяти
Неуспех — SER_INVALID_HANDLE. }
function SerOpen(const DeviceName: string): TSerialHandle;
procedure SerClose(Handle: TSerialHandle);
{ Годен ли хендл (одна проверка вместо платформенных развилок у вызывающих). }
function SerValid(Handle: TSerialHandle): Boolean;
procedure SerSetParams(Handle: TSerialHandle; BitsPerSec: LongInt;
ByteSize: Integer; Parity: TParityType; StopBits: Integer;
Flags: TSerialFlags);
function SerRead(Handle: TSerialHandle; var Buffer; Count: LongInt): LongInt;
function SerWrite(Handle: TSerialHandle; const Buffer; Count: LongInt): LongInt;
{ Ждать ОДИН байт не дольше TimeOutMs. Результат — сколько прочитано (0 или 1). }
function SerReadTimeout(Handle: TSerialHandle; var Buffer;
TimeOutMs: LongInt): LongInt;
procedure SerSetDTR(Handle: TSerialHandle; State: Boolean);
procedure SerSetRTS(Handle: TSerialHandle; State: Boolean);
function SerGetCTS(Handle: TSerialHandle): Boolean;
function SerGetDSR(Handle: TSerialHandle): Boolean;
function SerGetCD(Handle: TSerialHandle): Boolean;
function SerGetRI(Handle: TSerialHandle): Boolean;
{$IFNDEF WINDOWS}
{ Флаги c_cflag для заданного формата. Вынесено в интерфейс РАДИ СТЕНДА:
псевдотерминал хранит не всё, что ему ставят (CS7 и чётность он молча
заменяет на CS8 без чётности — pty восьмибитно-чистый), поэтому проверять
сборку флагов обратным чтением termios нельзя, а проверять надо. }
function SerCFlags(ByteSize: Integer; Parity: TParityType; StopBits: Integer;
Flags: TSerialFlags): Cardinal;
{$ENDIF}
{ Имя устройства в той форме, которую понимает CreateFile под Windows.
★COM10 и выше открываются ТОЛЬКО как \\.\COM10: короткое имя система
резолвит лишь для COM1..COM9, а у USB-переходников номер за десяток заезжает
легко. Функция чистая и считает одинаково на всех платформах (ради стенда),
а зовёт её только Windows-ветка SerOpen: на Unix имя порта — это путь, и
трогать его нечего. }
function SerWinDeviceName(const Name: string): string;
{ Имя порта по умолчанию для настроек и подсказок в UI: у каждой системы своё,
и '/dev/ttyS0' на маке — заведомо неверная подсказка. Index 0..N. }
function SerDefaultPortName(Index: Integer): string;
{ Пример имени для подсказки под полем ввода. }
function SerPortNameHint: string;
implementation
uses
SysUtils
{$IFDEF WINDOWS}
, Serial // RTL: на Windows он есть и работает
{$ELSE}
, BaseUnix, termio, Unix
{$ENDIF};
{$IFDEF WINDOWS}
// ── Windows: переадресация в RTL ───────────────────────────────────────────
// Своей реализации здесь не нужно: RTL-модуль под Windows собирается и в
// битовые поля termios не лезет — там свой DCB. Приводим только к нашим типам
// и к единому «неуспех = SER_INVALID_HANDLE».
function RtlParity(P: TParityType): Serial.TParityType;
begin
case P of
OddParity: Result := Serial.OddParity;
EvenParity: Result := Serial.EvenParity;
else
Result := Serial.NoneParity;
end;
end;
function RtlFlags(F: TSerialFlags): Serial.TSerialFlags;
begin
Result := [];
if RtsCtsFlowControl in F then Include(Result, Serial.RtsCtsFlowControl);
end;
function SerOpen(const DeviceName: string): TSerialHandle;
begin
Result := Serial.SerOpen(SerWinDeviceName(DeviceName));
// ★RTL под Windows отдаёт 0 при неудаче, под Unix −1. Приводим к одному.
if Result = 0 then Result := SER_INVALID_HANDLE;
end;
procedure SerClose(Handle: TSerialHandle);
begin
if SerValid(Handle) then Serial.SerClose(Handle);
end;
procedure SerSetParams(Handle: TSerialHandle; BitsPerSec: LongInt;
ByteSize: Integer; Parity: TParityType; StopBits: Integer;
Flags: TSerialFlags);
begin
Serial.SerSetParams(Handle, BitsPerSec, ByteSize, RtlParity(Parity),
StopBits, RtlFlags(Flags));
end;
function SerRead(Handle: TSerialHandle; var Buffer; Count: LongInt): LongInt;
begin
Result := Serial.SerRead(Handle, Buffer, Count);
end;
function SerWrite(Handle: TSerialHandle; const Buffer; Count: LongInt): LongInt;
begin
Result := Serial.SerWrite(Handle, Buffer, Count);
end;
function SerReadTimeout(Handle: TSerialHandle; var Buffer;
TimeOutMs: LongInt): LongInt;
begin
Result := Serial.SerReadTimeout(Handle, Buffer, TimeOutMs);
end;
procedure SerSetDTR(Handle: TSerialHandle; State: Boolean);
begin
Serial.SerSetDTR(Handle, State);
end;
procedure SerSetRTS(Handle: TSerialHandle; State: Boolean);
begin
Serial.SerSetRTS(Handle, State);
end;
function SerGetCTS(Handle: TSerialHandle): Boolean;
begin
Result := Serial.SerGetCTS(Handle);
end;
function SerGetDSR(Handle: TSerialHandle): Boolean;
begin
Result := Serial.SerGetDSR(Handle);
end;
function SerGetCD(Handle: TSerialHandle): Boolean;
begin
Result := Serial.SerGetCD(Handle);
end;
function SerGetRI(Handle: TSerialHandle): Boolean;
begin
Result := Serial.SerGetRI(Handle);
end;
{$ELSE}
// ── Unix: termios напрямую, один путь для Linux и macOS ────────────────────
function BaudConst(BitsPerSec: LongInt): Cardinal;
// Скорость → константа termios. На Linux это битовая маска, на BSD/macOS —
// само число; обе прячутся за одним именем, поэтому развилки по ОС не нужно.
begin
case BitsPerSec of
50: Result := B50;
75: Result := B75;
110: Result := B110;
134: Result := B134;
150: Result := B150;
200: Result := B200;
300: Result := B300;
600: Result := B600;
1200: Result := B1200;
1800: Result := B1800;
2400: Result := B2400;
4800: Result := B4800;
9600: Result := B9600;
19200: Result := B19200;
38400: Result := B38400;
57600: Result := B57600;
115200: Result := B115200;
230400: Result := B230400;
else
Result := B9600;
end;
end;
function SerCFlags(ByteSize: Integer; Parity: TParityType; StopBits: Integer;
Flags: TSerialFlags): Cardinal;
// ★Только флаги — никаких скоростей (см. шапку модуля). CLOCAL = «модемных
// линий не ждём», без него open и read на кабеле без DCD ведут себя по-разному
// на разных системах.
begin
Result := CREAD or CLOCAL;
case ByteSize of
5: Result := Result or CS5;
6: Result := Result or CS6;
7: Result := Result or CS7;
else
Result := Result or CS8;
end;
case Parity of
OddParity: Result := Result or PARENB or PARODD;
EvenParity: Result := Result or PARENB;
end;
if StopBits = 2 then Result := Result or CSTOPB;
// Аппаратный поток — ТОЛЬКО по явной просьбе. Случайно включённый CRTSCTS
// вешает передачу намертво на кабеле без CTS.
if RtsCtsFlowControl in Flags then Result := Result or CRTSCTS;
end;
function SerOpen(const DeviceName: string): TSerialHandle;
var
Fl: LongInt;
begin
// ★O_NONBLOCK на открытии обязателен: часть USB-драйверов (и любое tty-, а не
// cu-устройство на macOS) висит в open до поднятия DCD. Сразу после открытия
// флаг снимаем — дальше нам нужны обычные блокирующие чтение и запись, а
// ожидание делает fpSelect в SerReadTimeout.
Result := fpOpen(DeviceName, O_RDWR or O_NOCTTY or O_NONBLOCK);
if Result < 0 then Exit(SER_INVALID_HANDLE);
Fl := fpFcntl(Result, F_GetFl, 0);
if Fl >= 0 then fpFcntl(Result, F_SetFl, Fl and (not O_NONBLOCK));
end;
procedure SerClose(Handle: TSerialHandle);
begin
if SerValid(Handle) then fpClose(Handle);
end;
procedure SerSetParams(Handle: TSerialHandle; BitsPerSec: LongInt;
ByteSize: Integer; Parity: TParityType; StopBits: Integer;
Flags: TSerialFlags);
var
Tios: termios;
Speed: Cardinal;
begin
if not SerValid(Handle) then Exit;
FillChar(Tios, SizeOf(Tios), 0);
Tios.c_cflag := SerCFlags(ByteSize, Parity, StopBits, Flags);
// Сырой режим: ни канонической обработки строк, ни эха, ни трансляции
// символов на входе и выходе. CAT — двоичный поток с ';' в роли конца
// команды, любая «помощь» терминала его портит.
Tios.c_iflag := 0;
Tios.c_oflag := 0;
Tios.c_lflag := 0;
Tios.c_cc[VMIN] := 0; // read возвращает то, что есть, и не ждёт
Tios.c_cc[VTIME] := 0;
Speed := BaudConst(BitsPerSec);
CFSetISpeed(Tios, Speed);
CFSetOSpeed(Tios, Speed);
TCFlush(Handle, TCIOFLUSH);
TCSetAttr(Handle, TCSANOW, Tios);
end;
function SerRead(Handle: TSerialHandle; var Buffer; Count: LongInt): LongInt;
begin
if not SerValid(Handle) then Exit(0);
Result := fpRead(Handle, Buffer, Count);
if Result < 0 then Result := 0;
end;
function SerWrite(Handle: TSerialHandle; const Buffer; Count: LongInt): LongInt;
begin
if not SerValid(Handle) then Exit(0);
Result := fpWrite(Handle, Buffer, Count);
if Result < 0 then Result := 0;
end;
function SerReadTimeout(Handle: TSerialHandle; var Buffer;
TimeOutMs: LongInt): LongInt;
var
FDS: TFDSet;
TV: TimeVal;
N: LongInt;
begin
Result := 0;
if not SerValid(Handle) then Exit;
fpFD_Zero(FDS);
fpFD_Set(Handle, FDS);
TV.tv_sec := TimeOutMs div 1000;
TV.tv_usec := (TimeOutMs mod 1000) * 1000;
N := fpSelect(Handle + 1, @FDS, nil, nil, @TV);
if N <= 0 then Exit; // таймаут или ошибка — байта нет
Result := fpRead(Handle, Buffer, 1);
if Result < 0 then Result := 0;
end;
procedure SetModemBit(Handle: TSerialHandle; Bit: Cardinal; State: Boolean);
var
Bits: Cardinal;
begin
if not SerValid(Handle) then Exit;
Bits := Bit;
if State then
fpIOCtl(Handle, TIOCMBIS, @Bits)
else
fpIOCtl(Handle, TIOCMBIC, @Bits);
end;
function ModemBitSet(Handle: TSerialHandle; Bit: Cardinal): Boolean;
var
Bits: Cardinal;
begin
Result := False;
if not SerValid(Handle) then Exit;
Bits := 0;
if fpIOCtl(Handle, TIOCMGET, @Bits) < 0 then Exit;
Result := (Bits and Bit) <> 0;
end;
procedure SerSetDTR(Handle: TSerialHandle; State: Boolean);
begin
SetModemBit(Handle, TIOCM_DTR, State);
end;
procedure SerSetRTS(Handle: TSerialHandle; State: Boolean);
begin
SetModemBit(Handle, TIOCM_RTS, State);
end;
function SerGetCTS(Handle: TSerialHandle): Boolean;
begin
Result := ModemBitSet(Handle, TIOCM_CTS);
end;
function SerGetDSR(Handle: TSerialHandle): Boolean;
begin
Result := ModemBitSet(Handle, TIOCM_DSR);
end;
function SerGetCD(Handle: TSerialHandle): Boolean;
begin
Result := ModemBitSet(Handle, TIOCM_CAR);
end;
function SerGetRI(Handle: TSerialHandle): Boolean;
begin
Result := ModemBitSet(Handle, TIOCM_RNG);
end;
{$ENDIF}
// ── Общее для всех платформ ────────────────────────────────────────────────
function SerWinDeviceName(const Name: string): string;
var
S: string;
i, N: Integer;
begin
// ★Имя, которое не опознано как COM-порт, возвращаем БЕЗ изменений: это может
// быть путь, и обрезать у него что-либо мы не вправе. А вот опознанное имя
// отдаём нормализованным — пробелы вокруг ' COM3 ' CreateFile не прощает, и
// оператор, скопировавший имя из документации с хвостовым пробелом, получал
// бы «не удалось открыть» на ровном месте.
Result := Name;
S := Trim(Name);
if Length(S) < 4 then Exit;
// Уже полная форма — только снимаем пробелы.
if Copy(S, 1, 4) = '\\.\' then Exit(S);
if UpperCase(Copy(S, 1, 3)) <> 'COM' then Exit;
N := 0;
for i := 4 to Length(S) do
begin
if (S[i] < '0') or (S[i] > '9') then Exit; // COM3:, COM-что-то — не трогаем
N := N * 10 + (Ord(S[i]) - Ord('0'));
if N > 999 then Exit;
end;
// COM1..COM9 открываются и коротким именем — префикс им не нужен.
if N >= 10 then
Result := '\\.\' + S
else
Result := S;
end;
function SerValid(Handle: TSerialHandle): Boolean;
// ★Ноль значит РАЗНОЕ на разных системах, и одной проверкой тут не обойтись.
begin
{$IFDEF WINDOWS}
// Ядро Windows нулевой хендл не выдаёт никогда: ноль здесь — либо отказ RTL
// (SerOpen переводит его в SER_INVALID_HANDLE), либо просто неинициализированное
// поле. Отвергаем, чтобы контракт был верен и для значения, пришедшего извне.
Result := (Handle <> SER_INVALID_HANDLE) and (Handle <> 0);
{$ELSE}
// А на Unix ноль ЗАКОНЕН: при закрытом стандартном вводе (демон, запуск из
// службы) fpOpen отдаст именно его. Объявить такой порт ошибкой значит не
// только потерять рабочий порт, но и не закрыть дескриптор — SerClose
// смотрит сюда же. Признак неудачи один: SER_INVALID_HANDLE.
Result := Handle <> SER_INVALID_HANDLE;
{$ENDIF}
end;
function SerDefaultPortName(Index: Integer): string;
begin
{$IF DEFINED(WINDOWS)}
Result := 'COM' + IntToStr(Index + 1);
{$ELSEIF DEFINED(DARWIN)}
// ★На маке имя порта зависит от переходника (/dev/cu.usbserial-A1B2C3), так
// что осмысленного умолчания не существует — пустая строка честнее наугад
// придуманной. Формат подсказывает SerPortNameHint в окне настроек.
Result := '';
{$ELSE}
Result := '/dev/ttyS' + IntToStr(Index);
{$ENDIF}
end;
function SerPortNameHint: string;
begin
{$IF DEFINED(WINDOWS)}
Result := 'COM3, \\.\COM12';
{$ELSEIF DEFINED(DARWIN)}
Result := '/dev/cu.usbserial-XXXX';
{$ELSE}
Result := '/dev/ttyUSB0, /dev/ttyS0';
{$ENDIF}
end;
end.