Something went wrong. Try again.
Serial ports for Zig: rate, framing, flow control and modem lines, on termios2 for Linux and the DCB for Windows.
Something went wrong. Try again.
24 kB · 675 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! The backend for macOS and the BSDs — FreeBSD, NetBSD, OpenBSD and//! DragonFly — on their libc `termios`.//!//! All five inherit 4.4BSD's terminal interface, so one file covers them,//! and the few places they part company are switched on below. The useful//! inheritance is that a `speed_t` *is* the rate: `B9600` is 9600, so any//! rate can be written into `c_ospeed` by number and it is up to the driver//! whether it can produce it. FreeBSD's `uart(4)` divides down to whatever//! is asked; macOS's serial drivers refuse a rate outside the traditional//! table through `tcsetattr`, and take one through the `IOSSIOSPEED` ioctl//! instead, which `configure` falls back to.//!//! Three things differ from Linux in ways a caller can see://!//! * **No mark or space parity.** There is no `CMSPAR`, so those are//! refused as `error.UnsupportedSetting`, as one and a half stop bits//! are.//! * **On macOS, use `/dev/cu.*` rather than `/dev/tty.*`.** The `tty`//! node is the dial-in side, which waits for carrier before it lets//! anyone else in. A `Port` opens non-blocking, so it will not hang//! there, but the `cu` node is the one meant for calling out.//! * **A pseudo-terminal has modem lines, of a sort.** FreeBSD answers//! `TIOCMGET` on a pty with a fixed set of lines rather than refusing//! it, so `modem` working is no proof that a cable is attached.//!//! The structure and the constants are written out here rather than taken//! from `std.c`, whose `speed_t` is an exhaustive enum with no room for a//! rate like 31250, and whose `ioctl` takes its request as a signed `int`//! where these systems want an `unsigned long`.
const std = @import("std");const builtin = @import("builtin");const c = std.c;
const types = @import("types.zig");const Settings = types.Settings;
pub const Handle = c.fd_t;
const os = builtin.target.os.tag;const is_darwin = os.isDarwin();
comptime { switch (os) { .freebsd, .netbsd, .openbsd, .dragonfly => {}, else => if (!is_darwin) @compileError("the BSD backend does not know " ++ @tagName(os)), }}
/// `tcflag_t` and `speed_t`: an `unsigned long` on Darwin, an `unsigned int`/// on the BSDs.const Flag = if (is_darwin) c_ulong else c_uint;const Speed = Flag;
/// `struct termios` from `<sys/termios.h>`, the same shape on all five.const Termios = extern struct { iflag: Flag, oflag: Flag, cflag: Flag, lflag: Flag, cc: [20]u8, ispeed: Speed, ospeed: Speed,};
// `c_iflag`const IGNBRK: Flag = 0x1;const BRKINT: Flag = 0x2;const IGNPAR: Flag = 0x4;const PARMRK: Flag = 0x8;const INPCK: Flag = 0x10;const ISTRIP: Flag = 0x20;const INLCR: Flag = 0x40;const IGNCR: Flag = 0x80;const ICRNL: Flag = 0x100;const IXON: Flag = 0x200;const IXOFF: Flag = 0x400;const IXANY: Flag = 0x800;/// OpenBSD alone kept the upper-to-lower-case mapping.const IUCLC: Flag = if (os == .openbsd) 0x1000 else 0;const IMAXBEL: Flag = 0x2000;
// `c_oflag`const OPOST: Flag = 0x1;
// `c_cflag`const CSIZE: Flag = 0x300;const CS5: Flag = 0x000;const CS6: Flag = 0x100;const CS7: Flag = 0x200;const CS8: Flag = 0x300;const CSTOPB: Flag = 0x400;const CREAD: Flag = 0x800;const PARENB: Flag = 0x1000;const PARODD: Flag = 0x2000;const HUPCL: Flag = 0x4000;const CLOCAL: Flag = 0x8000;/// Two bits on Darwin, FreeBSD and DragonFly, `CCTS_OFLOW | CRTS_IFLOW`,/// which can be set apart; one bit on NetBSD and OpenBSD, where those two/// names are both aliases for it.const CRTSCTS: Flag = switch (os) { .netbsd, .openbsd => 0x10000, else => 0x30000,};
// `c_lflag`const ECHOE: Flag = 0x2;const ECHOK: Flag = 0x4;const ECHO: Flag = 0x8;const ECHONL: Flag = 0x10;const ISIG: Flag = 0x80;const ICANON: Flag = 0x100;const IEXTEN: Flag = 0x400;/// Not a setting but kernel state: the BSD line discipline sets it on/// switching into canonical mode, so that input already received is/// processed again, and carries it across `tcsetattr`.const PENDIN: Flag = 0x20000000;
// `c_cc` indicesconst VSTART = 12;const VSTOP = 13;const VMIN = 16;const VTIME = 17;
// `tcsetattr` actions and `tcflush` queuesconst TCSANOW = 0;const TCIFLUSH = 1;const TCOFLUSH = 2;const TCIOFLUSH = 3;
// `flock` operationsconst LOCK_EX = 2;const LOCK_NB = 4;
// The ioctls, from `<sys/ttycom.h>`, which every one of these encodes the// same way.const IOC_VOID: c_ulong = 0x20000000;const IOC_OUT: c_ulong = 0x40000000;const IOC_IN: c_ulong = 0x80000000;
fn ioc(inout: c_ulong, group: u8, num: u8, len: usize) c_ulong { return inout | ((@as(c_ulong, len) & 0x1fff) << 16) | (@as(c_ulong, group) << 8) | num;}
const TIOCEXCL = ioc(IOC_VOID, 't', 13, 0);const TIOCNXCL = ioc(IOC_VOID, 't', 14, 0);const TIOCMGET = ioc(IOC_OUT, 't', 106, @sizeOf(c_int));const TIOCMBIC = ioc(IOC_IN, 't', 107, @sizeOf(c_int));const TIOCMBIS = ioc(IOC_IN, 't', 108, @sizeOf(c_int));/// Darwin's `IOSSIOSPEED`, from `<IOKit/serial/ioss.h>`: set both rates to/// the `speed_t` pointed at, whatever it is.const IOSSIOSPEED = ioc(IOC_IN, 'T', 2, @sizeOf(Speed));
// `TIOCMGET` bits, the same as Linux's.const TIOCM_DTR = 0x002;const TIOCM_RTS = 0x004;const TIOCM_CTS = 0x020;const TIOCM_CAR = 0x040;const TIOCM_RNG = 0x080;const TIOCM_DSR = 0x100;
extern "c" fn ioctl(fd: Handle, request: c_ulong, ...) c_int;extern "c" fn tcgetattr(fd: Handle, t: *Termios) c_int;extern "c" fn tcsetattr(fd: Handle, action: c_int, t: *const Termios) c_int;extern "c" fn tcdrain(fd: Handle) c_int;extern "c" fn tcflush(fd: Handle, queue: c_int) c_int;extern "c" fn tcsendbreak(fd: Handle, duration: c_int) c_int;
/// Writes `settings` and raw mode into `t`, leaving alone what neither/// touches. Kept apart from the libc calls so that it can be tested without/// a device, for the same reason as on Linux: a pseudo-terminal does not/// keep everything it is told.fn encode(t: *Termios, settings: Settings) types.ConfigureError!void { // Raw mode, as cfmakeraw(3) describes it. t.iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IUCLC | IMAXBEL); t.oflag &= ~OPOST; t.lflag &= ~(ECHO | ECHOE | ECHOK | ECHONL | ICANON | ISIG | IEXTEN); t.cflag |= CREAD; // A read returns as soon as there is one byte, and not before. t.cc[VMIN] = 1; t.cc[VTIME] = 0;
// cfsetspeed(3) is an assignment on every one of these systems. t.ospeed = settings.baud; t.ispeed = settings.baud;
t.cflag &= ~CSIZE; t.cflag |= switch (settings.data_bits) { .five => CS5, .six => CS6, .seven => CS7, .eight => CS8, };
t.cflag &= ~(PARENB | PARODD); t.cflag |= switch (settings.parity) { .none => 0, .even => PARENB, .odd => PARENB | PARODD, .mark, .space => return error.UnsupportedSetting, }; // As on Linux: check what arrives when there is a bit to check, and let // a character that fails read as NUL. if (settings.parity == .none) { t.iflag &= ~(INPCK | IGNPAR); } else { t.iflag |= INPCK; t.iflag &= ~IGNPAR; }
switch (settings.stop_bits) { .one => t.cflag &= ~CSTOPB, .two => t.cflag |= CSTOPB, .one_and_half => return error.UnsupportedSetting, }
t.cflag &= ~CRTSCTS; t.iflag &= ~(IXON | IXOFF | IXANY); switch (settings.flow) { .none => {}, .rts_cts => t.cflag |= CRTSCTS, .xon_xoff => { t.iflag |= IXON | IXOFF; t.cc[VSTART] = 0x11; t.cc[VSTOP] = 0x13; }, .other => return error.UnsupportedSetting, }
if (settings.hang_up_on_close) t.cflag |= HUPCL else t.cflag &= ~HUPCL; if (settings.local) t.cflag |= CLOCAL else t.cflag &= ~CLOCAL;}
fn decode(t: Termios) Settings { const rtscts = t.cflag & CRTSCTS; const ixon = t.iflag & IXON != 0; const ixoff = t.iflag & IXOFF != 0; return .{ .baud = std.math.cast(u32, t.ospeed) orelse std.math.maxInt(u32), .data_bits = switch (t.cflag & CSIZE) { CS5 => .five, CS6 => .six, CS7 => .seven, else => .eight, }, .parity = if (t.cflag & PARENB == 0) .none else if (t.cflag & PARODD != 0) .odd else .even, .stop_bits = if (t.cflag & CSTOPB != 0) .two else .one, // Only one of Darwin's two hardware flow bits — CTS gating output // without RTS gating input, say — is a combination this library // does not name. .flow = if (rtscts == 0 and !ixon and !ixoff) .none else if (rtscts == CRTSCTS and !ixon and !ixoff) .rts_cts else if (rtscts == 0 and ixon and ixoff) .xon_xoff else .other, .hang_up_on_close = t.cflag & HUPCL != 0, .local = t.cflag & CLOCAL != 0, };}
pub const Port = struct { handle: Handle, /// The attributes the device had when it was opened, put back by /// `close`. original: Termios, exclusive: bool,
pub fn open(path: []const u8, settings: Settings, options: types.OpenOptions) types.OpenError!Port { var path_buf: [c.PATH_MAX]u8 = undefined; if (path.len >= path_buf.len) return error.NameTooLong; @memcpy(path_buf[0..path.len], path); path_buf[path.len] = 0;
// Non-blocking even when the caller wants blocking I/O, for the same // reason as on Linux: without CLOCAL an open waits for carrier, and // that is the dial-in `/dev/tty.*` node's whole purpose on macOS. const fd = c.open(@ptrCast(&path_buf), .{ .ACCMODE = .RDWR, .NOCTTY = true, .NONBLOCK = true, .CLOEXEC = true, }); switch (c.errno(fd)) { .SUCCESS => {}, .NOENT, .NXIO, .NODEV => return error.FileNotFound, .ACCES, .PERM, .ROFS => return error.AccessDenied, .BUSY => return error.Busy, .ISDIR => return error.NotASerialPort, .NAMETOOLONG => return error.NameTooLong, .MFILE, .NFILE, .NOMEM => return error.SystemResources, else => return error.Unexpected, } errdefer _ = c.close(fd);
var original: Termios = undefined; switch (c.errno(tcgetattr(fd, &original))) { .SUCCESS => {}, // macOS's memory devices, /dev/null among them, answer every // ioctl they do not know with ENODEV rather than ENOTTY. .NOTTY, .INVAL, .OPNOTSUPP, .NODEV => return error.NotASerialPort, .IO => return error.InputOutput, else => return error.Unexpected, }
if (options.exclusive) { // As on Linux: the advisory lock for anything that asks, and // TIOCEXCL for everything else short of root. switch (c.errno(c.flock(fd, LOCK_EX | LOCK_NB))) { .SUCCESS => {}, .AGAIN => return error.Busy, else => return error.Unexpected, } _ = ioctl(fd, TIOCEXCL); }
var port: Port = .{ .handle = fd, .original = original, .exclusive = options.exclusive }; try port.configure(settings);
if (!options.nonblocking) { const flags = c.fcntl(fd, c.F.GETFL); if (c.errno(flags) != .SUCCESS) return error.Unexpected; var o: c.O = @bitCast(@as(u32, @bitCast(flags))); o.NONBLOCK = false; if (c.errno(c.fcntl(fd, c.F.SETFL, @as(c_int, @bitCast(o)))) != .SUCCESS) return error.Unexpected; }
return port; }
/// Puts the device's original settings back and closes it. /// /// `TCSANOW` rather than the draining `TCSADRAIN`, for the reason given /// on Linux: a drain that waits on a far end that is not there never /// finishes. pub fn close(port: *Port) void { _ = tcsetattr(port.handle, TCSANOW, &port.original); if (port.exclusive) _ = ioctl(port.handle, TIOCNXCL); _ = c.close(port.handle); port.* = undefined; }
/// Applies `settings`, along with raw mode, in one `tcsetattr`, so the /// line never runs at a mixture of the old configuration and the new. /// /// On macOS, a rate the driver refuses through `tcsetattr` is tried /// again through `IOSSIOSPEED`. That takes two calls: everything else is /// applied first at the rate the port already had, and then the rate. pub fn configure(port: *Port, settings: Settings) types.ConfigureError!void { var t = try port.get(); const old_ispeed = t.ispeed; const old_ospeed = t.ospeed; try encode(&t, settings); switch (c.errno(tcsetattr(port.handle, TCSANOW, &t))) { .SUCCESS => return, .INVAL => if (!is_darwin) return error.UnsupportedBaud, .IO => return error.InputOutput, else => return error.Unexpected, }
t.ispeed = old_ispeed; t.ospeed = old_ospeed; switch (c.errno(tcsetattr(port.handle, TCSANOW, &t))) { .SUCCESS => {}, // The rate was not what it objected to after all. .INVAL => return error.UnsupportedSetting, .IO => return error.InputOutput, else => return error.Unexpected, } var speed: Speed = settings.baud; switch (c.errno(ioctl(port.handle, IOSSIOSPEED, &speed))) { .SUCCESS => {}, .INVAL, .NOTTY => return error.UnsupportedBaud, .IO => return error.InputOutput, else => return error.Unexpected, } }
/// The settings as the driver reports them, which is not necessarily what /// was asked for: a driver may round a rate, and is free to ignore what /// it does not support. pub fn current(port: *Port) types.ConfigureError!Settings { return decode(try port.get()); }
fn get(port: *Port) types.ConfigureError!Termios { var t: Termios = undefined; return switch (c.errno(tcgetattr(port.handle, &t))) { .SUCCESS => t, .IO => error.InputOutput, else => error.Unexpected, }; }
pub fn read(port: *Port, buf: []u8) types.ReadError!usize { while (true) { const rc = c.read(port.handle, buf.ptr, buf.len); return switch (c.errno(rc)) { .SUCCESS => @intCast(rc), .INTR => continue, .AGAIN => error.WouldBlock, .IO => error.InputOutput, else => error.Unexpected, }; } }
pub fn write(port: *Port, bytes: []const u8) types.WriteError!usize { while (true) { const rc = c.write(port.handle, bytes.ptr, bytes.len); return switch (c.errno(rc)) { .SUCCESS => @intCast(rc), .INTR => continue, .AGAIN => error.WouldBlock, .IO => error.InputOutput, else => error.Unexpected, }; } }
/// Waits until everything written has left the driver's buffer, which /// is not quite the same as having left the UART. pub fn drain(port: *Port) types.WriteError!void { while (true) { return switch (c.errno(tcdrain(port.handle))) { .SUCCESS => {}, .INTR => continue, .IO => error.InputOutput, else => error.Unexpected, }; } }
pub fn flush(port: *Port, queue: types.Queue) types.ConfigureError!void { const arg: c_int = switch (queue) { .input => TCIFLUSH, .output => TCOFLUSH, .both => TCIOFLUSH, }; return switch (c.errno(tcflush(port.handle, arg))) { .SUCCESS => {}, .IO => error.InputOutput, else => error.Unexpected, }; }
/// Holds the line at space for about four tenths of a second, which is /// what these systems' `tcsendbreak` does whatever duration it is given. pub fn sendBreak(port: *Port) types.WriteError!void { return switch (c.errno(tcsendbreak(port.handle, 0))) { .SUCCESS => {}, .IO => error.InputOutput, else => error.Unexpected, }; }
pub fn modem(port: *Port) types.LineError!types.Modem { var bits: c_int = 0; return switch (c.errno(ioctl(port.handle, TIOCMGET, &bits))) { .SUCCESS => .{ .dtr = bits & TIOCM_DTR != 0, .rts = bits & TIOCM_RTS != 0, .cts = bits & TIOCM_CTS != 0, .dsr = bits & TIOCM_DSR != 0, .dcd = bits & TIOCM_CAR != 0, .ri = bits & TIOCM_RNG != 0, }, .NOTTY, .INVAL, .OPNOTSUPP => error.Unsupported, .IO => error.InputOutput, else => error.Unexpected, }; }
pub fn setLine(port: *Port, line: types.Line, on: bool) types.LineError!void { const bits: c_int = switch (line) { .dtr => TIOCM_DTR, .rts => TIOCM_RTS, }; return switch (c.errno(ioctl(port.handle, if (on) TIOCMBIS else TIOCMBIC, &bits))) { .SUCCESS => {}, .NOTTY, .INVAL, .OPNOTSUPP => error.Unsupported, .IO => error.InputOutput, else => error.Unexpected, }; }};
// ---------------------------------------------------------------------------// Tests, against a pseudo-terminal, as on Linux.// ---------------------------------------------------------------------------
const testing = std.testing;
extern "c" fn posix_openpt(oflag: c_int) c_int;extern "c" fn grantpt(fd: Handle) c_int;extern "c" fn unlockpt(fd: Handle) c_int;extern "c" fn ptsname(fd: Handle) ?[*:0]const u8;
const PtyPair = struct { master: Handle, path_buf: [64]u8, path_len: usize,
fn open() !PtyPair { const flags: c.O = .{ .ACCMODE = .RDWR, .NOCTTY = true }; const master = posix_openpt(@bitCast(flags)); if (master < 0) return error.SkipZigTest; errdefer _ = c.close(master); if (grantpt(master) != 0 or unlockpt(master) != 0) return error.SkipZigTest; const name = std.mem.span(ptsname(master) orelse return error.SkipZigTest); var pair: PtyPair = .{ .master = master, .path_buf = undefined, .path_len = name.len }; if (name.len > pair.path_buf.len) return error.SkipZigTest; @memcpy(pair.path_buf[0..name.len], name); return pair; }
fn path(pair: *const PtyPair) []const u8 { return pair.path_buf[0..pair.path_len]; }
fn close(pair: *PtyPair) void { _ = c.close(pair.master); }};
test "every setting survives encoding and decoding" { const cases = [_]Settings{ .{}, .{ .baud = 9600, .data_bits = .seven, .parity = .even, .stop_bits = .two }, .{ .baud = 300, .data_bits = .five, .parity = .odd, .flow = .rts_cts, .hang_up_on_close = false }, .{ .baud = 31250, .data_bits = .six, .flow = .xon_xoff, .local = false }, .{ .baud = 4_000_000 }, }; for (cases) |want| { var t = std.mem.zeroes(Termios); try encode(&t, want); try testing.expectEqual(want, decode(t)); // Raw: nothing that would edit, echo or translate. try testing.expectEqual(@as(Flag, 0), t.lflag & (ICANON | ECHO | ISIG | IEXTEN)); try testing.expectEqual(@as(Flag, 0), t.oflag & OPOST); try testing.expectEqual(@as(Flag, 0), t.iflag & (ICRNL | INLCR | IGNCR | ISTRIP)); try testing.expectEqual(@as(u8, 1), t.cc[VMIN]); }}
test "what cannot be expressed is refused" { var t = std.mem.zeroes(Termios); try testing.expectError(error.UnsupportedSetting, encode(&t, .{ .stop_bits = .one_and_half })); try testing.expectError(error.UnsupportedSetting, encode(&t, .{ .parity = .mark })); try testing.expectError(error.UnsupportedSetting, encode(&t, .{ .parity = .space })); try testing.expectError(error.UnsupportedSetting, encode(&t, .{ .flow = .other }));}
test "half of a two-bit CRTSCTS is a combination with no name" { if (CRTSCTS == 0x10000) return error.SkipZigTest; var t = std.mem.zeroes(Termios); try encode(&t, .{}); t.cflag |= 0x10000; // CCTS_OFLOW alone try testing.expectEqual(types.Flow.other, decode(t).flow);}
test "a setting a pty keeps survives a round trip through the driver" { var pair = try PtyPair.open(); defer pair.close();
var port = try Port.open(pair.path(), .{ .baud = 9600, .stop_bits = .two, .flow = .rts_cts, .hang_up_on_close = false, }, .{}); defer port.close();
const got = try port.current(); try testing.expectEqual(@as(u32, 9600), got.baud); try testing.expectEqual(types.StopBits.two, got.stop_bits); try testing.expectEqual(types.Flow.rts_cts, got.flow); try testing.expect(!got.hang_up_on_close); try testing.expect(got.local);
try port.configure(.{ .flow = .xon_xoff }); try testing.expectEqual(types.Flow.xon_xoff, (try port.current()).flow);}
test "bytes cross in both directions untranslated" { var pair = try PtyPair.open(); defer pair.close();
var port = try Port.open(pair.path(), .{}, .{ .nonblocking = false }); defer port.close();
// A CR, an LF and a ^C: each of which a cooked tty would act on. const out = "a\rb\nc\x03"; try testing.expectEqual(out.len, try port.write(out)); var buf: [16]u8 = undefined; const n = c.read(pair.master, &buf, buf.len); try testing.expect(n > 0); try testing.expectEqualStrings(out, buf[0..@intCast(n)]);
const in = "x\ry\x04"; _ = c.write(pair.master, in, in.len); try testing.expectEqualStrings(in, buf[0..try port.read(&buf)]);}
test "a non-blocking read with nothing waiting says so" { var pair = try PtyPair.open(); defer pair.close();
var port = try Port.open(pair.path(), .{}, .{}); defer port.close(); var buf: [4]u8 = undefined; try testing.expectError(error.WouldBlock, port.read(&buf));}
test "an exclusive port refuses a second opener" { var pair = try PtyPair.open(); defer pair.close();
var port = try Port.open(pair.path(), .{}, .{}); defer port.close(); try testing.expectError(error.Busy, Port.open(pair.path(), .{}, .{}));}
test "the original settings come back on close" { var pair = try PtyPair.open(); defer pair.close();
// Read through a second descriptor on the slave, held open throughout, // rather than through the master: FreeBSD's master reports a `c_cflag` // of zero until the slave has first been opened. var path_buf: [64:0]u8 = @splat(0); @memcpy(path_buf[0..pair.path().len], pair.path()); const held = c.open(&path_buf, .{ .ACCMODE = .RDWR, .NOCTTY = true, .NONBLOCK = true }); try testing.expect(held >= 0); defer _ = c.close(held);
var before: Termios = undefined; try testing.expectEqual(@as(c_int, 0), tcgetattr(held, &before));
var port = try Port.open(pair.path(), .{ .baud = 1200, .parity = .odd }, .{}); port.close();
var after: Termios = undefined; try testing.expectEqual(@as(c_int, 0), tcgetattr(held, &after)); try testing.expectEqual(before.cflag, after.cflag); // Putting back a canonical-mode original sets PENDIN on macOS. try testing.expectEqual(before.lflag & ~PENDIN, after.lflag & ~PENDIN); try testing.expectEqual(before.ospeed, after.ospeed);}
test "what is not a terminal is refused" { try testing.expectError(error.NotASerialPort, Port.open("/dev/null", .{}, .{})); try testing.expectError(error.FileNotFound, Port.open("/dev/does-not-exist", .{}, .{}));}
test "a pty's modem lines either answer or say they are not there" { var pair = try PtyPair.open(); defer pair.close();
var port = try Port.open(pair.path(), .{}, .{}); defer port.close(); _ = port.modem() catch |err| try testing.expectEqual(error.Unsupported, err); port.setLine(.dtr, false) catch |err| try testing.expectEqual(error.Unsupported, err);}