Skip to content

Commit

Permalink
refactor: Make backends directly usable
Browse files Browse the repository at this point in the history
mochalins committed Sep 23, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 7c06560 commit 8f540c2
Showing 5 changed files with 388 additions and 374 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@

## Todo

- [ ] Refactor backends to expose public API best suited for OS, then use these
- [x] Refactor backends to expose public API best suited for OS, then use these
APIs in platform-agnostic API implementation
- [ ] Offer both blocking and non-blocking reads/writes
- [ ] Port descriptions and information
300 changes: 141 additions & 159 deletions src/backend/linux.zig
Original file line number Diff line number Diff line change
@@ -27,59 +27,63 @@ pub const BaudRate = b: {
break :b @Type(std.builtin.Type{ .@"enum" = baud_rate_ti });
};

pub const PortImpl = struct {
file: std.fs.File,
orig_termios: ?linux.termios = null,
};

pub const ReadError = std.fs.File.ReadError;
pub const Reader = std.fs.File.Reader;
pub const WriteError = std.fs.File.WriteError;
pub const Writer = std.fs.File.Writer;

pub fn open(path: []const u8) !PortImpl {
return .{
.file = try std.fs.cwd().openFile(path, .{
.mode = .read_write,
.allow_ctty = false,
}),
};
pub fn open(path: []const u8) !std.fs.File {
return try std.fs.cwd().openFile(path, .{
.mode = .read_write,
.allow_ctty = false,
});
}

pub fn close(port: *PortImpl) void {
if (port.orig_termios) |orig_termios| {
std.posix.tcsetattr(port.file.handle, .NOW, orig_termios) catch {};
}
port.orig_termios = null;
port.file.close();
port.* = undefined;
}
/// Configure serial port. Returns original `termios` settings on success.
pub fn configure(
port: std.fs.File,
config: serialport.Config,
) !linux.termios {
var settings = try std.posix.tcgetattr(port.handle);
const orig_termios = settings;

fn configureParity(
termios: *linux.termios,
parity: serialport.Config.Parity,
) void {
termios.cflag.PARENB = parity != .none;
termios.cflag.PARODD = parity == .odd or parity == .mark;
termios.cflag.CMSPAR = parity == .mark or parity == .space;
// `cfmakeraw`
settings.iflag.IGNBRK = false;
settings.iflag.BRKINT = false;
settings.iflag.PARMRK = false;
settings.iflag.ISTRIP = false;
settings.iflag.INLCR = false;
settings.iflag.IGNCR = false;
settings.iflag.ICRNL = false;
settings.iflag.IXON = false;

termios.iflag.INPCK = parity != .none;
termios.iflag.IGNPAR = parity == .none;
}
settings.oflag.OPOST = false;

fn configureFlowControl(
termios: *linux.termios,
flow_control: serialport.Config.FlowControl,
) void {
termios.cflag.CLOCAL = flow_control == .none;
termios.cflag.CRTSCTS = flow_control == .hardware;
settings.lflag.ECHO = false;
settings.lflag.ECHONL = false;
settings.lflag.ICANON = false;
settings.lflag.ISIG = false;
settings.lflag.IEXTEN = false;

termios.iflag.IXANY = flow_control == .software;
termios.iflag.IXON = flow_control == .software;
termios.iflag.IXOFF = flow_control == .software;
settings.cflag.CREAD = true;
settings.cflag.CSTOPB = config.stop_bits == .two;
settings.cflag.CSIZE = @enumFromInt(@intFromEnum(config.data_bits));

configureBaudRate(&settings, config.baud_rate, config.input_baud_rate);
configureParity(&settings, config.parity);
configureFlowControl(&settings, config.flow_control);

// Minimum arrived bytes before read returns.
settings.cc[@intFromEnum(linux.V.MIN)] = 0;
// Inter-byte timeout before read returns.
settings.cc[@intFromEnum(linux.V.TIME)] = 0;
settings.cc[@intFromEnum(linux.V.START)] = 0x11;
settings.cc[@intFromEnum(linux.V.STOP)] = 0x13;

try std.posix.tcsetattr(port.handle, .NOW, settings);
return orig_termios;
}

pub fn configure(port: *PortImpl, config: serialport.Config) !void {
fn configureBaudRate(
termios: *linux.termios,
baud_rate: BaudRate,
input_baud_rate: ?BaudRate,
) void {
const CBAUD: u32 = switch (comptime builtin.target.cpu.arch) {
.powerpc, .powerpcle, .powerpc64, .powerpc64le => 0x000000FF,
else => 0x0000100F,
@@ -94,93 +98,72 @@ pub fn configure(port: *PortImpl, config: serialport.Config) !void {
};
const IBSHIFT = 16;

const custom_output_baud: bool = std.enums.tagName(
serialport.Config.BaudRate,
config.baud_rate,
) == null;
const custom_input_baud: bool = if (config.input_baud_rate) |ibr|
std.enums.tagName(serialport.Config.BaudRate, ibr) == null
const custom_out: bool = std.enums.tagName(BaudRate, baud_rate) == null;
const custom_in: bool = if (input_baud_rate) |ibr|
std.enums.tagName(BaudRate, ibr) == null
else
custom_output_baud;
custom_out;

var output_baud_bits = @intFromEnum(config.baud_rate);
var input_baud_bits = @intFromEnum(
if (config.input_baud_rate) |ibr| ibr else config.baud_rate,
);
var out_bits = @intFromEnum(baud_rate);
var in_bits = @intFromEnum(if (input_baud_rate) |ibr| ibr else baud_rate);

inline for (@typeInfo(serialport.Config.BaudRate).@"enum".fields) |field| {
if (output_baud_bits == field.value) {
output_baud_bits =
@intFromEnum(@field(linux.speed_t, field.name));
inline for (@typeInfo(BaudRate).@"enum".fields) |field| {
if (out_bits == field.value) {
out_bits = @intFromEnum(@field(linux.speed_t, field.name));
}
if (input_baud_bits == field.value) {
input_baud_bits =
@intFromEnum(@field(linux.speed_t, field.name));
if (in_bits == field.value) {
in_bits = @intFromEnum(@field(linux.speed_t, field.name));
}
}

var settings = try std.posix.tcgetattr(port.file.handle);
const orig_termios = settings;

// `cfmakeraw`
settings.iflag.IGNBRK = false;
settings.iflag.BRKINT = false;
settings.iflag.PARMRK = false;
settings.iflag.ISTRIP = false;
settings.iflag.INLCR = false;
settings.iflag.IGNCR = false;
settings.iflag.ICRNL = false;
settings.iflag.IXON = false;

settings.oflag.OPOST = false;

settings.lflag.ECHO = false;
settings.lflag.ECHONL = false;
settings.lflag.ICANON = false;
settings.lflag.ISIG = false;
settings.lflag.IEXTEN = false;

var cflag: u32 = @bitCast(settings.cflag);
var cflag: u32 = @bitCast(termios.cflag);

// Set CBAUD and CIBAUD in cflag.
cflag &= ~CBAUD;
cflag &= ~CIBAUD;
if (custom_output_baud) {
if (custom_out) {
cflag |= BOTHER;
} else {
cflag |= output_baud_bits;
cflag |= out_bits;
}
if (custom_input_baud) {
if (custom_in) {
cflag |= BOTHER << IBSHIFT;
} else {
cflag |= input_baud_bits << IBSHIFT;
cflag |= in_bits << IBSHIFT;
}
termios.cflag = @bitCast(cflag);

settings.cflag = @bitCast(cflag);
settings.cflag.CREAD = true;
settings.cflag.CSTOPB = config.stop_bits == .two;
settings.cflag.CSIZE = @enumFromInt(@intFromEnum(config.data_bits));
const ospeed: *u32 = @ptrCast(&termios.ospeed);
ospeed.* = out_bits;
const ispeed: *u32 = @ptrCast(&termios.ispeed);
ispeed.* = in_bits;
}

configureParity(&settings, config.parity);
configureFlowControl(&settings, config.flow_control);
fn configureParity(
termios: *linux.termios,
parity: serialport.Config.Parity,
) void {
termios.cflag.PARENB = parity != .none;
termios.cflag.PARODD = parity == .odd or parity == .mark;
termios.cflag.CMSPAR = parity == .mark or parity == .space;

const ospeed: *u32 = @ptrCast(&settings.ospeed);
ospeed.* = output_baud_bits;
const ispeed: *u32 = @ptrCast(&settings.ispeed);
ispeed.* = input_baud_bits;
termios.iflag.INPCK = parity != .none;
termios.iflag.IGNPAR = parity == .none;
}

// Minimum arrived bytes before read returns.
settings.cc[@intFromEnum(linux.V.MIN)] = 0;
// Inter-byte timeout before read returns.
settings.cc[@intFromEnum(linux.V.TIME)] = 0;
settings.cc[@intFromEnum(linux.V.START)] = 0x11;
settings.cc[@intFromEnum(linux.V.STOP)] = 0x13;
fn configureFlowControl(
termios: *linux.termios,
flow_control: serialport.Config.FlowControl,
) void {
termios.cflag.CLOCAL = flow_control == .none;
termios.cflag.CRTSCTS = flow_control == .hardware;

try std.posix.tcsetattr(port.file.handle, .NOW, settings);
port.orig_termios = orig_termios;
termios.iflag.IXANY = flow_control == .software;
termios.iflag.IXON = flow_control == .software;
termios.iflag.IXOFF = flow_control == .software;
}

pub fn flush(port: PortImpl, options: serialport.Port.FlushOptions) !void {
pub fn flush(port: std.fs.File, options: serialport.FlushOptions) !void {
if (!options.input and !options.output) return;

const TCIFLUSH = 0;
@@ -190,7 +173,7 @@ pub fn flush(port: PortImpl, options: serialport.Port.FlushOptions) !void {

const result = linux.syscall3(
.ioctl,
@bitCast(@as(isize, @intCast(port.file.handle))),
@bitCast(@as(isize, @intCast(port.handle))),
TCFLSH,
if (options.input and options.output)
TCIOFLUSH
@@ -207,14 +190,12 @@ pub fn flush(port: PortImpl, options: serialport.Port.FlushOptions) !void {
};
}

pub fn poll(port: PortImpl) !bool {
var pollfds: [1]linux.pollfd = .{
.{
.fd = port.file.handle,
.events = linux.POLL.IN,
.revents = undefined,
},
};
pub fn poll(port: std.fs.File) !bool {
var pollfds: [1]linux.pollfd = .{.{
.fd = port.handle,
.events = linux.POLL.IN,
.revents = undefined,
}};
if (linux.poll(&pollfds, 1, 0) == 0) return false;

if (pollfds[0].revents & linux.POLL.IN == 0) return false;
@@ -225,16 +206,8 @@ pub fn poll(port: PortImpl) !bool {
return true;
}

pub fn reader(port: PortImpl) Reader {
return port.file.reader();
}

pub fn writer(port: PortImpl) Writer {
return port.file.writer();
}

pub fn iterate() !IteratorImpl {
var result: IteratorImpl = .{
pub fn iterate() !Iterator {
var result: Iterator = .{
.dir = std.fs.cwd().openDir(
"/dev/serial/by-id",
.{ .iterate = true },
@@ -250,16 +223,16 @@ pub fn iterate() !IteratorImpl {
return result;
}

pub const IteratorImpl = struct {
pub const Iterator = struct {
dir: ?std.fs.Dir,
iterator: std.fs.Dir.Iterator,
name_buffer: [256]u8 = undefined,
path_buffer: [std.fs.max_path_bytes]u8 = undefined,

pub fn next(self: *@This()) !?serialport.Iterator.Stub {
pub fn next(self: *@This()) !?serialport.Stub {
if (self.dir == null) return null;

var result: serialport.Iterator.Stub = undefined;
var result: serialport.Stub = undefined;
while (try self.iterator.next()) |entry| {
if (entry.kind != .sym_link) continue;
@memcpy(self.name_buffer[0..entry.name.len], entry.name);
@@ -284,7 +257,10 @@ pub const IteratorImpl = struct {
}
};

fn openVirtualPorts(master_port: *PortImpl, slave_port: *PortImpl) !void {
fn openVirtualPorts(
master_port: *std.fs.File,
slave_port: *std.fs.File,
) !void {
const c = @cImport({
@cDefine("_XOPEN_SOURCE", "700");
@cInclude("stdlib.h");
@@ -293,13 +269,13 @@ fn openVirtualPorts(master_port: *PortImpl, slave_port: *PortImpl) !void {
});

master_port.* = try open("/dev/ptmx");
errdefer close(master_port);
errdefer master_port.close();

if (c.grantpt(master_port.file.handle) < 0 or
c.unlockpt(master_port.file.handle) < 0)
if (c.grantpt(master_port.handle) < 0 or
c.unlockpt(master_port.handle) < 0)
return error.MasterPseudoTerminalSetupError;

const slave_name = c.ptsname(master_port.file.handle) orelse
const slave_name = c.ptsname(master_port.handle) orelse
return error.SlavePseudoTerminalSetupError;
const slave_name_len = std.mem.len(slave_name);
if (slave_name_len == 0)
@@ -309,22 +285,24 @@ fn openVirtualPorts(master_port: *PortImpl, slave_port: *PortImpl) !void {
}

test "software flow control" {
var master: PortImpl = undefined;
var slave: PortImpl = undefined;
var master: std.fs.File = undefined;
var slave: std.fs.File = undefined;
try openVirtualPorts(&master, &slave);
defer close(&master);
defer close(&slave);
defer master.close();
defer slave.close();

const config: serialport.Config = .{
.baud_rate = .B230400,
.flow_control = .software,
};

try configure(&master, config);
try configure(&slave, config);
const orig_master = try configure(master, config);
defer std.posix.tcsetattr(master.handle, .NOW, orig_master) catch {};
const orig_slave = try configure(slave, config);
defer std.posix.tcsetattr(slave.handle, .NOW, orig_slave) catch {};

const writer_m = writer(master);
const reader_s = reader(slave);
const writer_m = master.writer();
const reader_s = slave.reader();

try std.testing.expectEqual(false, try poll(slave));
try std.testing.expectEqual(12, try writer_m.write("test message"));
@@ -353,18 +331,20 @@ test "software flow control" {
}

test {
var master: PortImpl = undefined;
var slave: PortImpl = undefined;
var master: std.fs.File = undefined;
var slave: std.fs.File = undefined;
try openVirtualPorts(&master, &slave);
defer close(&master);
defer close(&slave);
defer master.close();
defer slave.close();

const config: serialport.Config = .{ .baud_rate = .B115200 };
try configure(&master, config);
try configure(&slave, config);
const orig_master = try configure(master, config);
defer std.posix.tcsetattr(master.handle, .NOW, orig_master) catch {};
const orig_slave = try configure(slave, config);
defer std.posix.tcsetattr(slave.handle, .NOW, orig_slave) catch {};

const writer_m = writer(master);
const reader_s = reader(slave);
const writer_m = master.writer();
const reader_s = slave.reader();

try std.testing.expectEqual(false, try poll(slave));
try std.testing.expectEqual(12, try writer_m.write("test message"));
@@ -393,18 +373,20 @@ test {
}

test "custom baud rate" {
var master: PortImpl = undefined;
var slave: PortImpl = undefined;
var master: std.fs.File = undefined;
var slave: std.fs.File = undefined;
try openVirtualPorts(&master, &slave);
defer close(&master);
defer close(&slave);
defer master.close();
defer slave.close();

const config: serialport.Config = .{ .baud_rate = @enumFromInt(7667) };
try configure(&master, config);
try configure(&slave, config);
const orig_master = try configure(master, config);
defer std.posix.tcsetattr(master.handle, .NOW, orig_master) catch {};
const orig_slave = try configure(slave, config);
defer std.posix.tcsetattr(slave.handle, .NOW, orig_slave) catch {};

const writer_m = writer(master);
const reader_s = reader(slave);
const writer_m = master.writer();
const reader_s = slave.reader();

try std.testing.expectEqual(false, try poll(slave));
try std.testing.expectEqual(12, try writer_m.write("test message"));
116 changes: 60 additions & 56 deletions src/backend/macos.zig
Original file line number Diff line number Diff line change
@@ -4,60 +4,47 @@ const c = @cImport({
@cInclude("termios.h");
});

pub const PortImpl = std.fs.File;
pub const ReadError = std.fs.File.ReadError;
pub const Reader = std.fs.File.Reader;
pub const WriteError = std.fs.File.WriteError;
pub const Writer = std.fs.File.Writer;

pub fn open(path: []const u8) !PortImpl {
pub fn open(path: []const u8) !std.fs.File {
return try std.fs.cwd().openFile(path, .{
.mode = .read_write,
.allow_ctty = false,
});
}

pub fn close(port: PortImpl) void {
port.close();
}
pub fn configure(
port: std.fs.File,
config: serialport.Config,
) !std.posix.termios {
if (config.parity == .mark or config.parity == .space)
return error.ParityMarkSpaceUnsupported;

pub fn configure(port: PortImpl, config: serialport.Config) !void {
var settings = try std.posix.tcgetattr(port.handle);
const orig_termios = settings;

c.cfmakeraw(&settings);

if (config.input_baud_rate) |ibr| {
switch (std.posix.errno(c.cfsetospeed(&settings, config.baud_rate))) {
.SUCCESS => {},
else => |err| std.posix.unexpectedErrno(err),
}
switch (std.posix.errno(c.cfsetispeed(&settings, ibr))) {
.SUCCESS => {},
else => |err| std.posix.unexpectedErrno(err),
}
} else {
switch (std.posix.errno(c.cfsetspeed(&settings, config.baud_rate))) {
.SUCCESS => {},
else => |err| std.posix.unexpectedErrno(err),
}
}

if (config.input_baud_rate != null) return error.InputBaudRateUnsupported;

settings.iflag = .{};
settings.iflag.INPCK = config.parity != .none;
settings.iflag.IXON = config.flow_control == .software;
settings.iflag.IXOFF = config.flow_control == .software;
configureParity(&settings, config.parity);
configureFlowControl(&settings, config.flow_control);

settings.cflag = @bitCast(@intFromEnum(config.baud_rate));
settings.cflag.CREAD = true;
settings.cflag.CSTOPB = config.stop_bits == .two;
settings.cflag.CSIZE = @enumFromInt(@intFromEnum(config.data_bits));
if (config.flow_control == .hardware) {
settings.cflag.CCTS_OFLOW = true;
settings.cflag.CRTS_IFLOW = true;
} else {
settings.cflag.CLOCAL = true;
}

settings.cflag.PARENB = config.parity != .none;
switch (config.parity) {
.none, .even => {},
.odd => settings.cflag.PARODD = true,
.mark => {
return error.ParityMarkSpaceUnsupported;
},
.space => {
return error.ParityMarkSpaceUnsupported;
},
}

settings.oflag = .{};
settings.lflag = .{};
settings.ispeed = config.baud_rate;
settings.ospeed = config.baud_rate;

// Minimum arrived bytes before read returns.
settings.cc[@intFromEnum(std.posix.V.MIN)] = 0;
@@ -67,9 +54,34 @@ pub fn configure(port: PortImpl, config: serialport.Config) !void {
settings.cc[@intFromEnum(std.posix.V.STOP)] = 0x13;

try std.posix.tcsetattr(port.handle, .NOW, settings);
return orig_termios;
}

fn configureParity(
termios: *std.posix.termios,
parity: serialport.Config.Parity,
) void {
termios.cflag.PARENB = parity != .none;
termios.cflag.PARODD = parity == .odd;

termios.iflag.INPCK = parity != .none;
termios.iflag.IGNPAR = parity == .none;
}

fn configureFlowControl(
termios: *std.posix.termios,
flow_control: serialport.Config.FlowControl,
) void {
termios.cflag.CLOCAL = flow_control == .none;
termios.cflag.CCTS_OFLOW = flow_control == .hardware;
termios.cflag.CRTS_IFLOW = flow_control == .hardware;

termios.iflag.IXANY = flow_control == .software;
termios.iflag.IXON = flow_control == .software;
termios.iflag.IXOFF = flow_control == .software;
}

pub fn flush(port: PortImpl, options: serialport.Port.FlushOptions) !void {
pub fn flush(port: std.fs.File, options: serialport.FlushOptions) !void {
if (!options.input and !options.output) return;
const result = c.tcflush(
port.handle,
@@ -88,7 +100,7 @@ pub fn flush(port: PortImpl, options: serialport.Port.FlushOptions) !void {
};
}

pub fn poll(port: PortImpl) !bool {
pub fn poll(port: std.fs.File) !bool {
var pollfds: [1]std.posix.pollfd = .{
.{
.fd = port.handle,
@@ -105,31 +117,23 @@ pub fn poll(port: PortImpl) !bool {
return true;
}

pub fn reader(port: PortImpl) Reader {
return port.reader();
}

pub fn writer(port: PortImpl) Writer {
return port.writer();
}

pub fn iterate() !IteratorImpl {
var result: IteratorImpl = .{
pub fn iterate() !Iterator {
var result: Iterator = .{
.dir = try std.fs.cwd().openDir("/dev", .{ .iterate = true }),
.iterator = undefined,
};
result.iterator = result.dir.iterate();
return result;
}

pub const IteratorImpl = struct {
pub const Iterator = struct {
dir: std.fs.Dir,
iterator: std.fs.Dir.Iterator,
name_buffer: [256]u8 = undefined,
path_buffer: [std.fs.max_path_bytes]u8 = undefined,

pub fn next(self: *@This()) !?serialport.Iterator.Stub {
var result: serialport.Iterator.Stub = undefined;
pub fn next(self: *@This()) !?serialport.Stub {
var result: serialport.Stub = undefined;
while (try self.iterator.next()) |entry| {
if (entry.kind != .file) continue;
if (entry.name.len < 4) continue;
86 changes: 37 additions & 49 deletions src/backend/windows.zig
Original file line number Diff line number Diff line change
@@ -22,11 +22,6 @@ pub const BaudRate = enum(windows.DWORD) {
_,
};

pub const PortImpl = struct {
file: std.fs.File,
poll_overlapped: ?windows.OVERLAPPED = null,
};

pub const ReadError =
windows.ReadFileError ||
windows.OpenError ||
@@ -48,45 +43,38 @@ pub const Writer = std.io.GenericWriter(
writeFn,
);

pub fn open(path: []const u8) !PortImpl {
pub fn open(path: []const u8) !std.fs.File {
const path_w = try windows.sliceToPrefixedFileW(std.fs.cwd().fd, path);
var result: PortImpl = .{
.file = .{
.handle = windows.kernel32.CreateFileW(
path_w.span(),
windows.GENERIC_READ | windows.GENERIC_WRITE,
0,
null,
windows.OPEN_EXISTING,
windows.FILE_FLAG_OVERLAPPED,
null,
),
},
const result: std.fs.File = .{
.handle = windows.kernel32.CreateFileW(
path_w.span(),
windows.GENERIC_READ | windows.GENERIC_WRITE,
0,
null,
windows.OPEN_EXISTING,
windows.FILE_FLAG_OVERLAPPED,
null,
),
};
if (result.file.handle == windows.INVALID_HANDLE_VALUE) {
if (result.handle == windows.INVALID_HANDLE_VALUE) {
switch (windows.GetLastError()) {
windows.Win32Error.FILE_NOT_FOUND => {
return error.FileNotFound;
},
else => |e| return windows.unexpectedError(e),
}
}
errdefer result.close();
return result;
}

pub fn close(port: *PortImpl) void {
port.file.close();
port.poll_overlapped = null;
}

pub fn configure(port: *const PortImpl, config: serialport.Config) !void {
pub fn configure(port: std.fs.File, config: serialport.Config) !void {
var dcb: DCB = std.mem.zeroes(DCB);
dcb.DCBlength = @sizeOf(DCB);

if (config.input_baud_rate != null) return error.InputBaudRateUnsupported;
if (config.input_baud_rate != null)
return error.InputBaudRateUnsupported;

if (GetCommState(port.file.handle, &dcb) == 0)
if (GetCommState(port.handle, &dcb) == 0)
return windows.unexpectedError(windows.GetLastError());

dcb.BaudRate = config.baud_rate;
@@ -103,10 +91,10 @@ pub fn configure(port: *const PortImpl, config: serialport.Config) !void {
dcb.XonChar = 0x11;
dcb.XoffChar = 0x13;

if (SetCommState(port.file.handle, &dcb) == 0) {
if (SetCommState(port.handle, &dcb) == 0) {
return windows.unexpectedError(windows.GetLastError());
}
if (SetCommMask(port.file.handle, .{ .RXCHAR = true }) == 0) {
if (SetCommMask(port.handle, .{ .RXCHAR = true }) == 0) {
return windows.unexpectedError(windows.GetLastError());
}
const timeouts: CommTimeouts = .{
@@ -116,39 +104,39 @@ pub fn configure(port: *const PortImpl, config: serialport.Config) !void {
.WriteTotalTimeoutMultiplier = 0,
.WriteTotalTimeoutConstant = 0,
};
if (SetCommTimeouts(port.file.handle, &timeouts) == 0) {
if (SetCommTimeouts(port.handle, &timeouts) == 0) {
return windows.unexpectedError(windows.GetLastError());
}
}

pub fn flush(port: *const PortImpl, options: serialport.Port.FlushOptions) !void {
pub fn flush(port: std.fs.File, options: serialport.FlushOptions) !void {
if (!options.input and !options.output) return;
if (PurgeComm(port.file.handle, .{
if (PurgeComm(port.handle, .{
.PURGE_TXCLEAR = options.output,
.PURGE_RXCLEAR = options.input,
}) == 0) {
return windows.unexpectedError(windows.GetLastError());
}
}

pub fn poll(port: *PortImpl) !bool {
pub fn poll(port: std.fs.File, overlapped_: *?windows.OVERLAPPED) !bool {
var comstat: ComStat = undefined;
if (ClearCommError(port.file.handle, null, &comstat) == 0) {
if (ClearCommError(port.handle, null, &comstat) == 0) {
return windows.unexpectedError(windows.GetLastError());
}
if (comstat.cbInQue > 0) return true;

var events: EventMask = undefined;
if (port.poll_overlapped) |*overlapped| {
if (overlapped_.*) |*overlapped| {
if (windows.GetOverlappedResult(
port.file.handle,
port.handle,
overlapped,
false,
) catch |e| switch (e) {
error.WouldBlock => return false,
else => return e,
} != 0) {
port.poll_overlapped = null;
overlapped_.* = null;
return true;
} else {
switch (windows.GetLastError()) {
@@ -157,7 +145,7 @@ pub fn poll(port: *PortImpl) !bool {
}
}
} else {
port.poll_overlapped = .{
overlapped_.* = .{
.Internal = 0,
.InternalHigh = 0,
.DUMMYUNIONNAME = .{
@@ -173,7 +161,7 @@ pub fn poll(port: *PortImpl) !bool {
windows.EVENT_ALL_ACCESS,
),
};
if (WaitCommEvent(port.file.handle, &events, &port.poll_overlapped.?) == 0) {
if (WaitCommEvent(port.handle, &events, &overlapped_.*.?) == 0) {
switch (windows.GetLastError()) {
windows.Win32Error.IO_PENDING => return false,
else => |e| return windows.unexpectedError(e),
@@ -183,15 +171,15 @@ pub fn poll(port: *PortImpl) !bool {
}
}

pub fn reader(port: *const PortImpl) Reader {
return .{ .context = port.file };
pub fn reader(port: std.fs.File) Reader {
return .{ .context = port };
}

pub fn writer(port: *const PortImpl) Writer {
return .{ .context = port.file };
pub fn writer(port: std.fs.File) Writer {
return .{ .context = port };
}

pub fn iterate() !IteratorImpl {
pub fn iterate() !Iterator {
const HKEY_LOCAL_MACHINE = @as(windows.HKEY, @ptrFromInt(0x80000002));
const KEY_READ = 0x20019;

@@ -228,7 +216,7 @@ pub fn iterate() !IteratorImpl {
'\\',
};

var result: IteratorImpl = .{ .key = undefined };
var result: Iterator = .{ .key = undefined };
if (windows.advapi32.RegOpenKeyExW(
HKEY_LOCAL_MACHINE,
&w_str,
@@ -242,13 +230,13 @@ pub fn iterate() !IteratorImpl {
return result;
}

pub const IteratorImpl = struct {
pub const Iterator = struct {
key: windows.HKEY,
index: windows.DWORD = 0,
name_buffer: [16]u8 = undefined,
path_buffer: [16]u8 = undefined,

pub fn next(self: *@This()) !?serialport.Iterator.Stub {
pub fn next(self: *@This()) !?serialport.Stub {
defer self.index += 1;

var name_size: windows.DWORD = 256;
@@ -265,7 +253,7 @@ pub const IteratorImpl = struct {
&self.name_buffer,
&data_size,
)) {
0 => serialport.Iterator.Stub{
0 => serialport.Stub{
.name = self.name_buffer[0 .. data_size - 1],
.path = try std.fmt.bufPrint(
&self.path_buffer,
258 changes: 149 additions & 109 deletions src/serialport.zig
Original file line number Diff line number Diff line change
@@ -1,117 +1,146 @@
const serialport = @This();
const builtin = @import("builtin");
const std = @import("std");

const serialport = @This();
pub const linux = @import("backend/linux.zig");
pub const macos = @import("backend/macos.zig");
pub const windows = @import("backend/windows.zig");

pub fn iterate() !Iterator {
return .{
._impl = try backend.iterate(),
};
switch (builtin.target.os.tag) {
.linux, .macos => return backend.iterate(),
else => return .{
._impl = try backend.iterate(),
},
}
}

pub fn open(file_path: []const u8) !Port {
return .{ ._impl = try backend.open(file_path) };
return switch (builtin.target.os.tag) {
.linux, .macos, .windows => .{ ._impl = .{
.file = try backend.open(file_path),
} },
else => @compileError("unsupported OS"),
};
}

pub const Port = struct {
_impl: backend.PortImpl,
const PortImpl = switch (builtin.target.os.tag) {
.linux, .macos => struct {
file: std.fs.File,
orig_termios: ?std.posix.termios = null,
},
.windows => struct {
file: std.fs.File,
poll_overlapped: ?std.os.windows.OVERLAPPED = null,
},
else => @compileError("unsupported OS"),
};

pub const Reader = backend.Reader;
pub const ReadError = backend.ReadError;
pub const Writer = backend.Writer;
pub const WriteError = backend.WriteError;
pub const Port = struct {
_impl: PortImpl,

pub const FlushOptions = struct {
input: bool = false,
output: bool = false,
pub const Reader = switch (builtin.target.os.tag) {
.linux, .macos => std.fs.File.Reader,
.windows => windows.Reader,
else => @compileError("unsupported OS"),
};

const close_ptr = switch (@typeInfo(
@typeInfo(@TypeOf(backend.close)).@"fn".params[0].type.?,
)) {
.pointer => true,
.@"struct" => false,
else => @compileError("invalid function signature"),
pub const ReadError = switch (builtin.target.os.tag) {
.linux, .macos => std.fs.File.ReadError,
.windows => windows.ReadError,
else => @compileError("unsupported OS"),
};
pub const Writer = switch (builtin.target.os.tag) {
.linux, .macos => std.fs.File.Writer,
.windows => windows.Writer,
else => @compileError("unsupported OS"),
};
pub const WriteError = switch (builtin.target.os.tag) {
.linux, .macos => std.fs.File.WriteError,
.windows => windows.WriteError,
else => @compileError("unsupported OS"),
};

pub fn close(self: *@This()) void {
backend.close(if (comptime close_ptr) &self._impl else self._impl);
switch (comptime builtin.target.os.tag) {
.linux, .macos => {
if (self._impl.orig_termios) |orig_termios| {
std.posix.tcsetattr(
self._impl.file.handle,
.NOW,
orig_termios,
) catch {};
}
self._impl.file.close();
},
.windows => {
self._impl.file.close();
},
else => @compileError("unsupported OS"),
}
self.* = undefined;
}

const configure_ptr = switch (@typeInfo(
@typeInfo(@TypeOf(backend.configure)).@"fn".params[0].type.?,
)) {
.pointer => true,
.@"struct" => false,
else => @compileError("invalid function signature"),
};
pub fn configure(
self: if (configure_ptr) *@This() else @This(),
config: Config,
) !void {
return backend.configure(
if (comptime configure_ptr) &self._impl else self._impl,
config,
);
pub fn configure(self: *@This(), config: Config) !void {
switch (comptime builtin.target.os.tag) {
.linux, .macos => {
const termios = try backend.configure(
self._impl.file,
config,
);
// Only save original termios once so that reconfiguration
// will not overwrite original termios.
if (self._impl.orig_termios == null) {
self._impl.orig_termios = termios;
}
},
.windows => try windows.configure(self._impl.file, config),
else => @compileError("unsupported OS"),
}
}

const flush_ptr = switch (@typeInfo(
@typeInfo(@TypeOf(backend.flush)).@"fn".params[0].type.?,
)) {
.pointer => true,
.@"struct" => false,
else => @compileError("invalid function signature"),
};
pub fn flush(
self: if (flush_ptr) *@This() else @This(),
options: FlushOptions,
) !void {
return backend.flush(
if (comptime flush_ptr) &self._impl else self._impl,
options,
);
pub fn flush(self: *@This(), options: FlushOptions) !void {
switch (comptime builtin.target.os.tag) {
.linux, .macos, .windows => try backend.flush(
self._impl.file,
options,
),
else => @compileError("unsupported OS"),
}
}

const poll_ptr = switch (@typeInfo(
@typeInfo(@TypeOf(backend.poll)).@"fn".params[0].type.?,
)) {
.pointer => true,
.@"struct" => false,
else => @compileError("invalid function signature"),
};
pub fn poll(self: if (poll_ptr) *@This() else @This()) !bool {
return backend.poll(
if (comptime poll_ptr) &self._impl else self._impl,
);
pub fn poll(self: *@This()) !bool {
switch (comptime builtin.target.os.tag) {
.linux, .macos => return backend.poll(self._impl.file),
.windows => return windows.poll(
self._impl.file,
&self._impl.poll_overlapped,
),
else => @compileError("unsupported OS"),
}
}

const reader_ptr = switch (@typeInfo(
@typeInfo(@TypeOf(backend.reader)).@"fn".params[0].type.?,
)) {
.pointer => true,
.@"struct" => false,
else => @compileError("invalid function signature"),
};
pub fn reader(self: if (reader_ptr) *@This() else @This()) Reader {
return backend.reader(
if (comptime reader_ptr) &self._impl else self._impl,
);
pub fn reader(self: @This()) Reader {
switch (comptime builtin.target.os.tag) {
.linux, .macos => return self._impl.file.reader(),
.windows => return windows.reader(self._impl.file),
else => @compileError("unsupported OS"),
}
}

const writer_ptr = switch (@typeInfo(
@typeInfo(@TypeOf(backend.writer)).@"fn".params[0].type.?,
)) {
.pointer => true,
.@"struct" => false,
else => @compileError("invalid function signature"),
};
pub fn writer(self: if (writer_ptr) *@This() else @This()) Writer {
return backend.writer(
if (comptime writer_ptr) &self._impl else self._impl,
);
pub fn writer(self: @This()) Writer {
switch (comptime builtin.target.os.tag) {
.linux, .macos => return self._impl.file.writer(),
.windows => return windows.writer(self._impl.file),
else => @compileError("unsupported OS"),
}
}
};

pub const FlushOptions = struct {
input: bool = false,
output: bool = false,
};

pub const Config = struct {
/// Baud rate. Used as both output and input baud rate, unless an input
/// baud is separately provided.
@@ -178,38 +207,49 @@ pub const Config = struct {
};
};

pub const Iterator = struct {
_impl: backend.IteratorImpl,
/// Serial port stub that contains minimal information necessary to
/// identify and open a serial port. Stubs may be dependent on its source
/// iterator, and are not guaranteed to stay valid after iterator state
/// is changed.
pub const Stub = struct {
name: []const u8,
path: []const u8,

pub fn open(self: @This()) !Port {
return serialport.open(self.path);
}
};

/// Serial port stub that contains minimal information necessary to
/// identify and open a serial port. Stubs may be dependent on its source
/// iterator, and are not guaranteed to stay valid after iterator state
/// is changed.
pub const Stub = struct {
name: []const u8,
path: []const u8,
pub const Iterator = switch (builtin.target.os.tag) {
.linux => linux.Iterator,
.macos => macos.Iterator,
else => struct {
_impl: backend.IteratorImpl,

pub fn open(self: @This()) !Port {
return serialport.open(self.path);
pub fn next(self: *@This()) !?Stub {
return self._impl.next();
}
};

pub fn next(self: *@This()) !?Stub {
return self._impl.next();
}

pub fn deinit(self: *@This()) void {
self._impl.deinit();
}
pub fn deinit(self: *@This()) void {
self._impl.deinit();
}
},
};

const backend = switch (builtin.os.tag) {
.windows => @import("backend/windows.zig"),
.macos => @import("backend/macos.zig"),
.linux => @import("backend/linux.zig"),
const backend = switch (builtin.target.os.tag) {
.windows => windows,
.macos => macos,
.linux => linux,
else => @compileError("unsupported OS"),
};

test {
std.testing.refAllDeclsRecursive(@This());
std.testing.refAllDeclsRecursive(Port);
std.testing.refAllDeclsRecursive(Iterator);
std.testing.refAllDeclsRecursive(Stub);

switch (builtin.target.os.tag) {
.linux => std.testing.refAllDeclsRecursive(linux),
else => std.testing.refAllDeclsRecursive(builtin),
}
}

0 comments on commit 8f540c2

Please sign in to comment.