Files
ewsdr/test/serial/serialtest.pas
T

423 lines
19 KiB
ObjectPascal
Raw 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.
{
Copyright (C)
2026 - Uladzimir Karpenka, EW8BAK
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
}
program serialtest;
{
Стенд обёртки последовательного порта (SerialPort.pas) на псевдотерминале.
Зачем именно pty. Настоящего COM-порта на сборочной машине нет, а проверить
надо ровно то, на чём обжигается перенос: как обёртка НАСТРАИВАЕТ порт.
Псевдотерминал — тот же tty с той же дисциплиной линии, поэтому termios
проверяется всерьёз: скорость, размер символа, чётность, стоп-биты и — самое
важное — что аппаратный поток CRTSCTS НЕ включился сам собой. Именно этим
грешит `Serial` из RTL на BSD/macOS: там B-константы это числа, а он кладёт
их в c_cflag, и в поле прилетает CCTS_OFLOW (см. шапку SerialPort.pas).
Чего pty НЕ проверяет: модемные линии DTR/RTS/CTS/DSR/CD/RI — ioctl TIOCM* на
псевдотерминале не работает. Это остаётся на живой порт с железкой.
}
{$MODE Delphi}
uses
cthreads, SysUtils, BaseUnix, termio, Unix,
SerialPort, CATEngine, CATSerial;
var
Passed, Failed: Integer;
procedure Check(const Name: string; Cond: Boolean; const Extra: string = '');
begin
if Cond then
begin
Inc(Passed);
WriteLn(' ok ', Name);
end
else
begin
Inc(Failed);
if Extra <> '' then
WriteLn(' FAIL ', Name, ' ', Extra)
else
WriteLn(' FAIL ', Name);
end;
end;
// ── Псевдотерминал ──────────────────────────────────────────────────────────
function posix_openpt(Flags: cint): cint; cdecl; external 'c' name 'posix_openpt';
function grantpt(Fd: cint): cint; cdecl; external 'c' name 'grantpt';
function unlockpt(Fd: cint): cint; cdecl; external 'c' name 'unlockpt';
function ptsname(Fd: cint): PChar; cdecl; external 'c' name 'ptsname';
function OpenPty(out Master: cint; out SlaveName: string): Boolean;
var P: PChar;
begin
Result := False;
SlaveName := '';
Master := posix_openpt(O_RDWR or O_NOCTTY);
if Master < 0 then Exit;
if (grantpt(Master) <> 0) or (unlockpt(Master) <> 0) then
begin
fpClose(Master);
Exit;
end;
P := ptsname(Master);
if P = nil then
begin
fpClose(Master);
Exit;
end;
SlaveName := string(P);
Result := True;
end;
function MasterWrite(Master: cint; const S: AnsiString): Boolean;
begin
Result := (Length(S) > 0) and (fpWrite(Master, S[1], Length(S)) = Length(S));
end;
function MasterReadStr(Master: cint; TimeoutMs: Integer): AnsiString;
// Собирает всё, что мастер отдаёт, пока не замолчит на TimeoutMs.
var
FDS: TFDSet;
TV: TimeVal;
Buf: array[0..255] of Byte;
N, i: LongInt;
begin
Result := '';
repeat
fpFD_Zero(FDS);
fpFD_Set(Master, FDS);
TV.tv_sec := TimeoutMs div 1000;
TV.tv_usec := (TimeoutMs mod 1000) * 1000;
if fpSelect(Master + 1, @FDS, nil, nil, @TV) <= 0 then Break;
N := fpRead(Master, Buf[0], SizeOf(Buf));
if N <= 0 then Break;
for i := 0 to N - 1 do Result := Result + Chr(Buf[i]);
until False;
end;
// ── A. Открытие ─────────────────────────────────────────────────────────────
procedure TestOpen;
var
H, Master: cint;
Slave: string;
begin
WriteLn('A. Открытие порта');
H := SerOpen('/dev/does-not-exist-ewsdr');
Check('несуществующий порт не открывается', not SerValid(H));
Check('неуспех даёт SER_INVALID_HANDLE, а не ноль',
H = SER_INVALID_HANDLE, IntToStr(H));
if not OpenPty(Master, Slave) then
begin
Check('псевдотерминал создан', False);
Exit;
end;
H := SerOpen(Slave);
Check('порт открывается', SerValid(H), Slave);
SerClose(H);
Check('после закрытия хендл больше не годится', not SerValid(SER_INVALID_HANDLE));
fpClose(Master);
end;
// ── A2. Имя устройства для Windows ─────────────────────────────────────────
procedure TestWinName;
// ★COM10 и выше CreateFile открывает ТОЛЬКО в форме \\.\COM10 — короткое имя
// система резолвит лишь для COM1..COM9, а у USB-переходников номер за десяток
// заезжает легко. Функция чистая, поэтому проверяется и на Linux.
begin
WriteLn('A2. Имя устройства для Windows (COM10 и выше)');
Check('COM1 остаётся коротким', SerWinDeviceName('COM1') = 'COM1');
Check('COM9 остаётся коротким', SerWinDeviceName('COM9') = 'COM9');
Check('★COM10 получает префикс',
SerWinDeviceName('COM10') = '\\.\COM10', SerWinDeviceName('COM10'));
Check('COM123 получает префикс',
SerWinDeviceName('COM123') = '\\.\COM123', SerWinDeviceName('COM123'));
Check('строчные буквы тоже узнаются',
SerWinDeviceName('com12') = '\\.\com12', SerWinDeviceName('com12'));
Check('пробелы вокруг имени не мешают',
SerWinDeviceName(' COM12 ') = '\\.\COM12', SerWinDeviceName(' COM12 '));
// ★Пробелы CreateFile не прощает и коротким именам тоже: опознанное имя
// отдаём нормализованным, а не «как ввели».
Check('пробелы снимаются и у короткого имени',
SerWinDeviceName(' COM3 ') = 'COM3', '[' + SerWinDeviceName(' COM3 ') + ']');
Check('пробелы снимаются и у полной формы',
SerWinDeviceName(' \\.\COM12 ') = '\\.\COM12',
'[' + SerWinDeviceName(' \\.\COM12 ') + ']');
Check('уже полная форма не удваивается',
SerWinDeviceName('\\.\COM12') = '\\.\COM12');
Check('путь Unix не трогаем',
SerWinDeviceName('/dev/ttyUSB0') = '/dev/ttyUSB0');
Check('/dev/cu.* не трогаем',
SerWinDeviceName('/dev/cu.usbserial-A1') = '/dev/cu.usbserial-A1');
Check('нечисловой хвост не трогаем', SerWinDeviceName('COM3:') = 'COM3:');
Check('пустое имя не ломает', SerWinDeviceName('') = '');
end;
// ── B. Настройка порта ──────────────────────────────────────────────────────
procedure TestParams;
var
H, Master: cint;
Slave: string;
T: termios;
function Attrs: Boolean;
begin
FillChar(T, SizeOf(T), 0);
Result := TCGetAttr(H, T) = 0;
end;
begin
WriteLn('B. Настройка порта (termios читается обратно)');
if not OpenPty(Master, Slave) then
begin
Check('псевдотерминал создан', False);
Exit;
end;
H := SerOpen(Slave);
if not SerValid(H) then
begin
Check('порт открыт', False);
fpClose(Master);
Exit;
end;
SerSetParams(H, 115200, 8, NoneParity, 1, []);
if not Attrs then
Check('termios читается', False)
else
begin
Check('скорость встала (115200)',
(T.c_cflag and CBAUD) = B115200, IntToStr(T.c_cflag and CBAUD));
Check('символ 8 бит', (T.c_cflag and CSIZE) = CS8);
Check('чётности нет', (T.c_cflag and PARENB) = 0);
Check('один стоп-бит', (T.c_cflag and CSTOPB) = 0);
Check('приём и CLOCAL подняты',
(T.c_cflag and CREAD) <> 0, IntToStr(T.c_cflag));
// ★Главная проверка всего стенда. Аппаратный поток обязан быть выключен,
// пока его не просили: на кабеле без CTS включённый CRTSCTS вешает
// передачу намертво, а ошибки при этом никакой. Именно это и делает
// RTL-модуль на BSD/macOS, кладя скорость в c_cflag.
Check('★аппаратный поток НЕ включился сам',
(T.c_cflag and CRTSCTS) = 0, IntToStr(T.c_cflag and CRTSCTS));
// ★И ещё жёстче: кроме битов скорости в c_cflag не должно быть НИЧЕГО,
// кроме наших флагов. Ровно этим и ломается RTL-модуль на BSD/macOS: там
// B-константа это число, и оно ложится в c_cflag посторонними битами.
Check('★в c_cflag только наши флаги, скорость в него не попала',
(T.c_cflag and (not CBAUD)) = SerCFlags(8, NoneParity, 1, []),
Format('%x против %x', [T.c_cflag and (not CBAUD),
SerCFlags(8, NoneParity, 1, [])]));
Check('сырой режим: канонической обработки нет', (T.c_lflag and ICANON) = 0);
Check('сырой режим: эха нет', (T.c_lflag and ECHO) = 0);
Check('сырой режим: перевода строк на выходе нет', (T.c_oflag and OPOST) = 0);
end;
// Позитивный контроль к предыдущей проверке: попросили — значит включился.
SerSetParams(H, 9600, 8, NoneParity, 1, [RtsCtsFlowControl]);
if Attrs then
Check('аппаратный поток включается по просьбе',
(T.c_cflag and CRTSCTS) <> 0);
SerSetParams(H, 4800, 7, EvenParity, 2, []);
if Attrs then
begin
Check('скорость 4800', (T.c_cflag and CBAUD) = B4800);
Check('два стоп-бита', (T.c_cflag and CSTOPB) <> 0);
end;
// ★Размер символа и чётность обратным чтением НЕ проверяются: pty
// восьмибитно-чистый, ядро молча заменяет CS7 на CS8 и снимает PARENB
// (проверено отдельной пробой: ставим CS7|PARENB|PARODD — читаем CS8 без
// PARENB). Поэтому сборка флагов вынесена в чистую SerCFlags, и проверяем
// именно её — на живом порту в c_cflag уходит ровно это значение.
Check('формат: 8N1', SerCFlags(8, NoneParity, 1, []) =
Cardinal(CREAD or CLOCAL or CS8));
Check('формат: 7 бит', (SerCFlags(7, NoneParity, 1, []) and CSIZE) = CS7);
Check('формат: 6 бит', (SerCFlags(6, NoneParity, 1, []) and CSIZE) = CS6);
Check('формат: 5 бит', (SerCFlags(5, NoneParity, 1, []) and CSIZE) = CS5);
Check('формат: чётная чётность',
(SerCFlags(8, EvenParity, 1, []) and (PARENB or PARODD)) = PARENB);
Check('формат: нечётная чётность',
(SerCFlags(8, OddParity, 1, []) and (PARENB or PARODD)) =
Cardinal(PARENB or PARODD));
Check('формат: два стоп-бита',
(SerCFlags(8, NoneParity, 2, []) and CSTOPB) <> 0);
Check('★формат: без просьбы аппаратного потока нет',
(SerCFlags(8, NoneParity, 1, []) and CRTSCTS) = 0);
Check('формат: по просьбе аппаратный поток есть',
(SerCFlags(8, NoneParity, 1, [RtsCtsFlowControl]) and CRTSCTS) <> 0);
SerClose(H);
fpClose(Master);
end;
// ── C. Обмен ────────────────────────────────────────────────────────────────
procedure TestIO;
var
H, Master: cint;
Slave, Got: string;
S: AnsiString;
B: Byte;
N: LongInt;
T0, Dt: QWord;
begin
WriteLn('C. Обмен и таймаут');
if not OpenPty(Master, Slave) then
begin
Check('псевдотерминал создан', False);
Exit;
end;
H := SerOpen(Slave);
SerSetParams(H, 115200, 8, NoneParity, 1, []);
S := 'FA00014200000;';
Check('запись прошла целиком',
SerWrite(H, S[1], Length(S)) = Length(S));
Got := MasterReadStr(Master, 200);
Check('на другом конце то же самое', Got = S, Got);
// ★Сырой режим на выходе: CR обязан дойти как CR. Если бы остался ONLCR,
// терминал подменил бы его на CR+LF, и CAT-ответы приезжали бы битыми.
S := #13;
SerWrite(H, S[1], 1);
Got := MasterReadStr(Master, 200);
Check('★CR не превращается в CR+LF', Got = #13, IntToStr(Length(Got)));
MasterWrite(Master, 'Z');
N := SerReadTimeout(H, B, 500);
Check('чтение с таймаутом отдаёт байт', (N = 1) and (B = Ord('Z')),
Format('n=%d b=%d', [N, B]));
// Эхо выключено: то, что мы прочитали, не должно вернуться мастеру.
Got := MasterReadStr(Master, 100);
Check('★эха нет: принятое не возвращается отправителю', Got = '', Got);
T0 := GetTickCount64;
N := SerReadTimeout(H, B, 150);
Dt := GetTickCount64 - T0;
Check('таймаут возвращает 0', N = 0, IntToStr(N));
Check('таймаут выдерживает срок и не висит',
(Dt >= 120) and (Dt < 900), Format('%d мс', [Dt]));
SerClose(H);
fpClose(Master);
end;
// ── C2. Законный дескриптор 0 ──────────────────────────────────────────────
procedure TestFdZero;
// ★Регрессия, которую легко внести и невозможно заметить на рабочем столе: на
// Unix дескриптор 0 — обычный номер, и если стандартный ввод закрыт (демон,
// запуск из службы), порт откроется именно как 0. Проверка вида «хендл ноль =
// ошибка» тогда не только теряет рабочий порт, но и оставляет дескриптор
// незакрытым: SerClose смотрит на ту же проверку.
var
Master, H: cint;
Slave: string;
B: Byte;
N: LongInt;
begin
WriteLn('C2. Дескриптор 0 — законный порт (стандартный ввод закрыт)');
if not OpenPty(Master, Slave) then
begin
Check('псевдотерминал создан', False);
Exit;
end;
fpClose(0); // освобождаем нулевой дескриптор
H := SerOpen(Slave);
if H <> 0 then
WriteLn(' .. дескриптор 0 занял не порт (', H, ') — проверка ослаблена')
else
Check('порт открылся как дескриптор 0', True);
Check('★нулевой дескриптор считается годным', SerValid(H), IntToStr(H));
SerSetParams(H, 9600, 8, NoneParity, 1, []);
MasterWrite(Master, 'X');
N := SerReadTimeout(H, B, 500);
Check('через него идут данные', (N = 1) and (B = Ord('X')),
Format('n=%d b=%d', [N, B]));
SerClose(H);
fpClose(Master);
// Возвращаем стандартный ввод на место: иначе следующий открытый файл снова
// сядет на нулевой дескриптор, и это уже будет случайностью, а не проверкой.
fpOpen('/dev/null', O_RDONLY);
end;
// ── D. Сквозной прогон: CAT поверх обёртки ─────────────────────────────────
procedure TestCatThrough;
// Байты на мастере → поток CATSerial → разбор в TCATEngine → ответ обратно.
// Контекст радио пустой намеренно: команда ID отвечает константой и ни одного
// колбэка не трогает — стенду нужен транспорт, а не радио (его проверяет
// test/cat).
var
Master: cint;
Slave, Got: string;
Ctx: TCATContext;
Eng: TCATEngine;
Cfg: TCATSerialConfig;
Port: TCATSerialPort;
begin
WriteLn('D. Сквозной прогон: CAT через обёртку');
if not OpenPty(Master, Slave) then
begin
Check('псевдотерминал создан', False);
Exit;
end;
FillChar(Ctx, SizeOf(Ctx), 0);
Eng := TCATEngine.Create(Ctx);
FillChar(Cfg, SizeOf(Cfg), 0);
Cfg.Enabled := True;
Cfg.PortName := Slave;
Cfg.BaudRate := 9600;
Cfg.DataBits := 8;
Cfg.StopBits := 1;
Cfg.Parity := cspNone;
Port := TCATSerialPort.Create(Eng, Cfg);
try
Port.Start;
Check('порт CAT поднялся', Port.Active, Port.LastError);
MasterWrite(Master, 'ID;');
Got := MasterReadStr(Master, 1000);
Check('на команду пришёл ответ', Copy(Got, 1, 2) = 'ID', Got);
Check('ответ закрыт точкой с запятой',
(Length(Got) > 0) and (Got[Length(Got)] = ';'), Got);
finally
Port.Stop;
Port.Free;
Eng.Free;
fpClose(Master);
end;
end;
begin
Passed := 0;
Failed := 0;
WriteLn('=== Стенд последовательного порта (SerialPort.pas на pty) ===');
TestOpen;
TestWinName;
TestParams;
TestIO;
TestFdZero;
TestCatThrough;
WriteLn;
WriteLn(Format('Итого: %d проверок, провалено %d', [Passed + Failed, Failed]));
if Failed > 0 then Halt(1);
end.