From 07155241b9cdd66dde11d327ed1204bb091d51c2 Mon Sep 17 00:00:00 2001 From: mochalins <117967760+mochalins@users.noreply.github.com> Date: Sun, 15 Sep 2024 17:41:38 +0900 Subject: [PATCH] feat: Initial untested windows/posix Still needs tests, check read behavior, add iterate/flush/poll --- README.md | 1 + src/backend/posix.zig | 88 ++++++++++++ src/backend/windows.zig | 289 ++++++++++++++++++++++++++++++++++++++++ src/serialport.zig | 103 ++++++++++++++ 4 files changed, 481 insertions(+) create mode 100644 README.md create mode 100644 src/backend/posix.zig create mode 100644 src/backend/windows.zig diff --git a/README.md b/README.md new file mode 100644 index 0000000..08e5945 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# serialport diff --git a/src/backend/posix.zig b/src/backend/posix.zig new file mode 100644 index 0000000..41429bc --- /dev/null +++ b/src/backend/posix.zig @@ -0,0 +1,88 @@ +const std = @import("std"); +const serialport = @import("../serialport.zig"); + +pub const Port = struct { + name: []const u8, + file: ?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(self: *@This()) !void { + if (self.file != null) return; + self.file = try std.fs.cwd().openFile(self.name, .{ + .mode = .read_write, + .allow_ctty = false, + }); + } + + pub fn configure(self: *@This(), config: serialport.Config) !void { + if (self.file == null) return; + + var settings = try std.posix.tcgetattr(self.file.?.handle); + + settings.iflag = .{}; + settings.iflag.INPCK = config.parity != .none; + settings.iflag.IXON = config.handshake == .software; + settings.iflag.IXOFF = config.handshake == .software; + + settings.cflag = .{}; + settings.cflag.CREAD = true; + settings.cflag.CSTOPB = config.stop_bits == .two; + settings.cflag.CSIZE = @enumFromInt(@intFromEnum(config.word_size)); + settings.cflag.CRTSCTS = config.handshake == .hardware; + settings.cflag.PARENB = config.parity != .none; + switch (config.parity) { + .none, .even => {}, + .odd => settings.cflag.PARODD = true, + .mark => { + settings.cflag.PARODD = true; + settings.cflag.CMSPAR = true; + }, + .space => { + settings.cflag.PARODD = false; + settings.cflag.CMSPAR = true; + }, + } + + 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; + // Inter-byte timeout before read returns. + settings.cc[@intFromEnum(std.posix.V.TIME)] = 0; + settings.cc[@intFromEnum(std.posix.V.START)] = 0x11; + settings.cc[@intFromEnum(std.posix.V.STOP)] = 0x13; + + try std.posix.tcsetattr(self.file.?.handle, .NOW, settings); + } + + pub fn close(self: *@This()) void { + if (self.file) |f| { + f.close(); + self.file = null; + } + } + + pub fn reader(self: @This()) ?Reader { + return (self.file orelse return null).reader(); + } + + pub fn writer(self: @This()) ?Writer { + return (self.file orelse return null).writer(); + } + + pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void { + if (self.file) |f| { + f.close(); + self.file = null; + } + allocator.free(self.name); + self.* = undefined; + } +}; diff --git a/src/backend/windows.zig b/src/backend/windows.zig new file mode 100644 index 0000000..5e004a0 --- /dev/null +++ b/src/backend/windows.zig @@ -0,0 +1,289 @@ +const std = @import("std"); +const serialport = @import("../serialport.zig"); +const windows = std.os.windows; + +/// Windows baud rate table, sourced from Microsoft `DCB` documentation in +/// `win32`'s `winbase.h`. Non-exhaustive enum to allow for custom baud rate +/// values. +pub const BaudRate = enum(windows.DWORD) { + B110 = 110, + B300 = 300, + B600 = 600, + B1200 = 1200, + B2400 = 2400, + B9600 = 9600, + B14400 = 14400, + B19200 = 19200, + B38400 = 38400, + B57600 = 57600, + B115200 = 115200, + B128000 = 128000, + B256000 = 256000, + _, +}; + +pub const Port = struct { + display_name: []const u8, + file_name: []const u8, + file: ?std.fs.File, + + pub const ReadError = + windows.ReadFileError || + windows.OpenError || + windows.WaitForSingleObjectError; + pub const Reader = std.io.GenericReader( + ReadContext, + ReadError, + readFn, + ); + pub const WriteError = + windows.WriteFileError || + windows.OpenError || + windows.WaitForSingleObjectError; + pub const Writer = std.io.GenericWriter( + WriteContext, + WriteError, + writeFn, + ); + + pub fn open(self: *@This()) !void { + if (self.file != null) return; + + const path_w = try windows.sliceToPrefixedFileW( + std.fs.cwd().fd, + self.file_name, + ); + self.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 (self.file.?.handle == windows.INVALID_HANDLE_VALUE) { + self.file = null; + switch (windows.GetLastError()) { + windows.Win32Error.FILE_NOT_FOUND => { + return error.FileNotFound; + }, + else => |e| return windows.unexpectedError(e), + } + } + } + + pub fn configure(self: *@This(), config: serialport.Config) !void { + if (self.file == null) return; + + var dcb: DCB = std.mem.zeroes(DCB); + dcb.DCBlength = @sizeOf(DCB); + + if (GetCommState(self.file.?, &dcb) == 0) + return windows.unexpectedError(windows.GetLastError()); + + dcb.BaudRate = config.baud_rate; + dcb.flags = .{ + .Parity = config.parity != .none, + .OutxCtsFlow = config.handshake == .hardware, + .OutX = config.handshake == .software, + .InX = config.handshake == .software, + .RtsControl = config.handshake == .hardware, + }; + dcb.ByteSize = 5 + @intFromEnum(config.word_size); + dcb.Parity = @intFromEnum(config.parity); + dcb.StopBits = if (config.stop_bits == .two) 2 else 0; + dcb.XonChar = 0x11; + dcb.XoffChar = 0x13; + + if (SetCommState(self.file.?, &dcb) == 0) { + return windows.unexpectedError(windows.GetLastError()); + } + } + + pub fn close(self: *@This()) void { + if (self.file) |f| { + f.close(); + self.file = null; + } + } + + pub fn reader(self: @This()) ?Reader { + return .{ + .context = self.file orelse return null, + }; + } + + pub fn writer(self: @This()) ?Writer { + return .{ + .context = self.file orelse return null, + }; + } + + pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void { + if (self.file) |f| { + f.close(); + self.file = null; + } + allocator.free(self.display_name); + allocator.free(self.file_name); + self.* = undefined; + } + + const ReadContext = std.fs.File; + fn readFn(context: ReadContext, buffer: []u8) ReadError!usize { + var overlapped: windows.OVERLAPPED = .{ + .Internal = 0, + .InternalHigh = 0, + .DUMMYUNIONNAME = .{ + .DUMMYSTRUCTNAME = .{ + .Offset = 0, + .OffsetHigh = 0, + }, + }, + .hEvent = try windows.CreateEventEx( + null, + "", + windows.CREATE_EVENT_MANUAL_RESET, + windows.EVENT_ALL_ACCESS, + ), + }; + + const want_read_count: windows.DWORD = @min( + @as(windows.DWORD, std.math.maxInt(windows.DWORD)), + buffer.len, + ); + var amt_read: windows.DWORD = undefined; + if (try windows.kernel32.ReadFile( + context.handle, + buffer.ptr, + want_read_count, + &amt_read, + &overlapped, + ) == 0) { + switch (windows.GetLastError()) { + windows.Win32Error.IO_PENDING => { + try windows.WaitForSingleObject( + overlapped.hEvent.?, + windows.INFINITE, + ); + const read_amount = try windows.GetOverlappedResult( + context.handle, + &overlapped, + true, + ); + return read_amount; + }, + else => |e| return windows.unexpectedError(e), + } + } + return amt_read; + } + + const WriteContext = std.fs.File; + fn writeFn(context: WriteContext, bytes: []const u8) WriteError!usize { + var bytes_written: windows.DWORD = undefined; + var overlapped: windows.OVERLAPPED = .{ + .Internal = 0, + .InternalHigh = 0, + .DUMMYUNIONNAME = .{ + .DUMMYSTRUCTNAME = .{ + .Offset = 0, + .OffsetHigh = 0, + }, + }, + .hEvent = try windows.CreateEventEx( + null, + "", + windows.CREATE_EVENT_MANUAL_RESET, + windows.EVENT_ALL_ACCESS, + ), + }; + defer windows.CloseHandle(overlapped.hEvent.?); + const adjusted_len = + std.math.cast(u32, bytes.len) orelse std.math.maxInt(u32); + + if (windows.kernel32.WriteFile( + context.handle, + bytes.ptr, + adjusted_len, + &bytes_written, + &overlapped, + ) == 0) { + switch (windows.GetLastError()) { + .INVALID_USER_BUFFER => return error.SystemResources, + .NOT_ENOUGH_MEMORY => return error.SystemResources, + .OPERATION_ABORTED => return error.OperationAborted, + .NOT_ENOUGH_QUOTA => return error.SystemResources, + .IO_PENDING => { + try windows.WaitForSingleObject( + overlapped.hEvent.?, + windows.INFINITE, + ); + const amount_written = try windows.GetOverlappedResult( + context.handle, + &overlapped, + true, + ); + return amount_written; + }, + .BROKEN_PIPE => return error.BrokenPipe, + .INVALID_HANDLE => return error.NotOpenForWriting, + .LOCK_VIOLATION => return error.LockViolation, + .NETNAME_DELETED => return error.ConnectionResetByPeer, + else => |e| return windows.unexpectedError(e), + } + } + return adjusted_len; + } +}; + +/// Windows control settings for a serial communications device, sourced from +/// Microsoft `DCB` documentation in `win32`'s `winbase.h`. +const DCB = extern struct { + DCBlength: windows.DWORD, + BaudRate: BaudRate, + flags: Flags, + Reserved: windows.WORD, + XonLim: windows.WORD, + XoffLim: windows.WORD, + ByteSize: windows.BYTE, + Parity: windows.BYTE, + StopBits: windows.BYTE, + XonChar: u8, + XoffChar: u8, + ErrorChar: u8, + EofChar: u8, + EvtChar: u8, + Reserved1: windows.WORD, + + const Flags = packed struct(windows.DWORD) { + Binary: bool = true, + Parity: bool = false, + OutxCtsFlow: bool = false, + OutxDsrFlow: bool = false, + DtrControl: u2 = 1, + DsrSensitivity: bool = false, + TXContinueOnXoff: bool = false, + OutX: bool = false, + InX: bool = false, + ErrorChar: bool = false, + Null: bool = false, + RtsControl: bool = 0, + _unused: u1 = 0, + AbortOnError: bool = false, + _: u17 = 0, + }; +}; + +extern "kernel32" fn SetCommState( + hFile: windows.HANDLE, + lpDCB: *DCB, +) callconv(windows.WINAPI) windows.BOOL; + +extern "kernel32" fn GetCommState( + hFile: windows.HANDLE, + lpDCB: *DCB, +) callconv(windows.WINAPI) windows.BOOL; diff --git a/src/serialport.zig b/src/serialport.zig index e69de29..449229c 100644 --- a/src/serialport.zig +++ b/src/serialport.zig @@ -0,0 +1,103 @@ +const builtin = @import("builtin"); +const std = @import("std"); + +pub const Config = struct { + baud_rate: BaudRate, + parity: Parity = .none, + stop_bits: StopBits = .one, + word_size: WordSize = .eight, + handshake: Handshake = .none, + + pub const BaudRate = if (@hasDecl(backend, "BaudRate")) + backend.BaudRate + else if (@TypeOf(std.posix.speed_t) != void) + std.posix.speed_t + else + @compileError("unsupported backend/OS"); + + pub const Parity = enum(u3) { + /// No parity bit is used. + none, + /// Parity bit is `0` when an odd number of bits is set in the data. + odd, + /// Parity bit is `0` when an even number of bits is set in the data. + even, + /// Parity bit is always `1`. + mark, + /// Parity bit is always `0`. + space, + }; + + pub const StopBits = enum(u1) { + /// Length of stop bits is one bit. + one, + /// Length of stop bits is two bits. + two, + }; + + pub const WordSize = enum(u2) { + /// There are five data bits per word. + five, + /// There are six data bits per word. + six, + /// There are seven data bits per word. + seven, + /// There are eight data bits per word. + eight, + }; + + pub const Handshake = enum(u2) { + /// No handshake is used. + none, + /// XON-XOFF software handshake is used. + software, + /// Hardware handshake with RTS (RFR) / CTS is used. + hardware, + }; +}; + +pub const Port = backend.Port; + +pub const ManagedPort = struct { + allocator: std.mem.Allocator, + port: Port, + + pub const Reader = Port.Reader; + pub const ReadError = Port.ReadError; + pub const Writer = Port.Writer; + pub const WriteError = Port.WriteError; + + pub fn open(self: *@This()) !void { + return self.port.open(); + } + + pub fn configure(self: *@This(), config: Config) !void { + return self.port.configure(config); + } + + pub fn close(self: *@This()) void { + self.port.close(); + } + + pub fn reader(self: @This()) ?Reader { + return self.port.reader(); + } + + pub fn writer(self: @This()) ?Writer { + return self.port.writer(); + } + + pub fn deinit(self: *@This()) void { + self.port.deinit(self.allocator); + self.* = undefined; + } +}; + +const backend = switch (builtin.os.tag) { + .windows => @import("backend/windows.zig"), + else => @import("backend/posix.zig"), +}; + +test { + std.testing.refAllDeclsRecursive(@This()); +}