├── .gitignore ├── LICENCE ├── README.md ├── build.zig ├── build.zig.zon ├── examples ├── echo.zig ├── list.zig └── list_port_info.zig └── src └── serial.zig /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache 2 | .zig-cache 3 | zig-out 4 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Felix Queißner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zig Serial Port Library 2 | 3 | Library for configuring and listing serial ports. 4 | 5 | ## Features 6 | 7 | - Basic serial port configuration 8 | - Baud Rate 9 | - Parity (none, even, odd, mark, space) 10 | - Stop Bits (one, two) 11 | - Handshake (none, hardware, software) 12 | - Byte Size (5, 6, 7, 8) 13 | - Flush serial port send/receive buffers 14 | - List available serial ports 15 | - API: supports Windows, Linux and Mac 16 | 17 | ## Example 18 | 19 | ```zig 20 | // Port configuration. 21 | // Serial ports are just files, \\.\COM1 for COM1 on Windows: 22 | var serial = try std.fs.cwd().openFile("\\\\.\\COM1", .{ .mode = .read_write }) ; 23 | defer serial.close(); 24 | 25 | try zig_serial.configureSerialPort(serial, zig_serial.SerialConfig{ 26 | .baud_rate = 19200, 27 | .word_size = 8, 28 | .parity = .none, 29 | .stop_bits = .one, 30 | .handshake = .none, 31 | }); 32 | ``` 33 | 34 | ## Usage 35 | 36 | ### Library integration 37 | 38 | Integrate the library in your project via the Zig package manager: 39 | 40 | - add `serial` to your `.zig.zon` file by providing the URL to the archive of a tag or specific commit of the library 41 | - to update the hash, run `zig fetch --save [URL/to/tag/or/commit.tar.gz]` 42 | 43 | ### Running tests 44 | 45 | The `build.zig` file contains a test step that can be called with `zig build test`. Note that this requires a serial port to be available on the system; 46 | 47 | - Linux: `/dev/ttyUSB0` 48 | - Mac: `/dev/cu.usbmodem101` 49 | - Windows: `COM3` 50 | 51 | ### Building the examples 52 | 53 | You can build the examples from the `./examples` directory by calling `zig build examples`. Binaries will be generated in `./zig-out/bin` by default. 54 | 55 | - Note that the `list_port_info` example currently only works on Windows 56 | 57 | ### Building the documentation 58 | 59 | You can generate the documentation by running `zig build docs`. 60 | After that you can browse it by: 61 | 62 | 1. starting the web server. For example, by running `python -m http.server 8000 zig-out/docs` 63 | 2. reading the docs from your browser at `http://127.0.0.1:8000` 64 | 65 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const log = std.log.scoped(.serial_lib__build); 3 | 4 | const example_files = [_][]const u8{ 5 | "echo", 6 | "list", 7 | "list_port_info", 8 | }; 9 | 10 | pub fn build(b: *std.Build) void { 11 | const optimize = b.standardOptimizeOption(.{}); 12 | const target = b.standardTargetOptions(.{}); 13 | 14 | const serial_mod = b.addModule("serial", .{ 15 | .root_source_file = b.path("src/serial.zig"), 16 | }); 17 | 18 | const unit_tests = b.addTest(.{ 19 | .root_source_file = b.path("src/serial.zig"), 20 | .target = target, 21 | .optimize = optimize, 22 | }); 23 | const run_unit_tests = b.addRunArtifact(unit_tests); 24 | const test_step = b.step("test", "Run unit tests"); 25 | test_step.dependOn(&run_unit_tests.step); 26 | 27 | const example_step = b.step("examples", "Build examples"); 28 | { 29 | for (example_files) |example_name| { 30 | const example = b.addExecutable(.{ 31 | .name = example_name, 32 | .root_source_file = b.path( 33 | b.fmt("examples/{s}.zig", .{example_name}), 34 | ), 35 | .target = target, 36 | .optimize = optimize, 37 | }); 38 | 39 | // port info only works on Windows! 40 | // TODO: Linux and MacOS port info support 41 | example.root_module.addImport("serial", serial_mod); 42 | const install_example = b.addInstallArtifact(example, .{}); 43 | example_step.dependOn(&example.step); 44 | example_step.dependOn(&install_example.step); 45 | } 46 | } 47 | const docs_step = b.step("docs", "Emit documentation"); 48 | 49 | const docs_install = b.addInstallDirectory(.{ 50 | .install_dir = .prefix, 51 | .install_subdir = "docs", 52 | .source_dir = unit_tests.getEmittedDocs(), 53 | }); 54 | docs_step.dependOn(&docs_install.step); 55 | } 56 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .serial, 3 | .version = "0.0.1", 4 | .minimum_zig_version = "0.14.0", 5 | .fingerprint = 0xd374c9dccc91873e, 6 | .paths = .{ 7 | "src", // all files in src directory 8 | "README.md", 9 | "LICENSE", 10 | "build.zig", 11 | "build.zig.zon", 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /examples/echo.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zig_serial = @import("serial"); 3 | 4 | pub fn main() !u8 { 5 | const port_name = if (@import("builtin").os.tag == .windows) "\\\\.\\COM1" else "/dev/ttyUSB0"; 6 | 7 | var serial = std.fs.cwd().openFile(port_name, .{ .mode = .read_write }) catch |err| switch (err) { 8 | error.FileNotFound => { 9 | std.debug.print("Invalid config: the serial port '{s}' does not exist.\n", .{port_name}); 10 | return 1; 11 | }, 12 | else => return err, 13 | }; 14 | defer serial.close(); 15 | 16 | try zig_serial.configureSerialPort(serial, zig_serial.SerialConfig{ 17 | .baud_rate = 115200, 18 | .word_size = .eight, 19 | .parity = .none, 20 | .stop_bits = .one, 21 | .handshake = .none, 22 | }); 23 | 24 | try serial.writer().writeAll("Hello, World!\r\n"); 25 | 26 | while (true) { 27 | const b = try serial.reader().readByte(); 28 | try serial.writer().writeByte(b); 29 | } 30 | 31 | return 0; 32 | } 33 | -------------------------------------------------------------------------------- /examples/list.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zig_serial = @import("serial"); 3 | 4 | pub fn main() !u8 { 5 | var iterator = try zig_serial.list(); 6 | defer iterator.deinit(); 7 | 8 | while (try iterator.next()) |port| { 9 | std.debug.print("path={s},\tname={s},\tdriver={s}\n", .{ port.file_name, port.display_name, port.driver orelse "" }); 10 | } 11 | 12 | return 0; 13 | } 14 | -------------------------------------------------------------------------------- /examples/list_port_info.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zig_serial = @import("serial"); 3 | 4 | pub fn main() !u8 { 5 | var iterator = try zig_serial.list_info(); 6 | defer iterator.deinit(); 7 | 8 | while (try iterator.next()) |info| { 9 | std.debug.print("\nPort name: {s}\n", .{info.port_name}); 10 | std.debug.print(" - System location: {s}\n", .{info.system_location}); 11 | std.debug.print(" - Friendly name: {s}\n", .{info.friendly_name}); 12 | std.debug.print(" - Description: {s}\n", .{info.description}); 13 | std.debug.print(" - Manufacturer: {s}\n", .{info.manufacturer}); 14 | std.debug.print(" - Serial #: {s}\n", .{info.serial_number}); 15 | std.debug.print(" - HW ID: {s}\n", .{info.hw_id}); 16 | std.debug.print(" - VID: 0x{X:0>4} PID: 0x{X:0>4}\n", .{ info.vid, info.pid }); 17 | } 18 | 19 | return 0; 20 | } 21 | -------------------------------------------------------------------------------- /src/serial.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const c = @cImport(@cInclude("termios.h")); 4 | 5 | pub fn list() !PortIterator { 6 | return try PortIterator.init(); 7 | } 8 | 9 | pub fn list_info() !InformationIterator { 10 | return try InformationIterator.init(); 11 | } 12 | 13 | pub const PortIterator = switch (builtin.os.tag) { 14 | .windows => WindowsPortIterator, 15 | .linux => LinuxPortIterator, 16 | .macos => DarwinPortIterator, 17 | else => @compileError("OS is not supported for port iteration"), 18 | }; 19 | 20 | pub const InformationIterator = switch (builtin.os.tag) { 21 | .windows => WindowsInformationIterator, 22 | .linux => LinuxInformationIterator, 23 | // .linux, .macos => @panic("'Port Information' not yet implemented for this OS"), 24 | else => @compileError("OS is not supported for information iteration"), 25 | }; 26 | 27 | pub const SerialPortDescription = struct { 28 | file_name: []const u8, 29 | display_name: []const u8, 30 | driver: ?[]const u8, 31 | }; 32 | 33 | pub const PortInformation = struct { 34 | port_name: []const u8, 35 | system_location: []const u8, 36 | friendly_name: []const u8, 37 | description: []const u8, 38 | manufacturer: []const u8, 39 | serial_number: []const u8, 40 | // TODO: review whether to remove `hw_id`. 41 | // Is this useless/being used in a Windows-only way? 42 | hw_id: []const u8, 43 | vid: u16, 44 | pid: u16, 45 | }; 46 | 47 | const HKEY = std.os.windows.HKEY; 48 | const HWND = std.os.windows.HANDLE; 49 | const HDEVINFO = std.os.windows.HANDLE; 50 | const DEVINST = std.os.windows.DWORD; 51 | const SP_DEVINFO_DATA = extern struct { 52 | cbSize: std.os.windows.DWORD, 53 | classGuid: std.os.windows.GUID, 54 | devInst: std.os.windows.DWORD, 55 | reserved: std.os.windows.ULONG_PTR, 56 | }; 57 | 58 | const WindowsPortIterator = struct { 59 | const Self = @This(); 60 | 61 | key: HKEY, 62 | index: u32, 63 | 64 | name: [256:0]u8 = undefined, 65 | name_size: u32 = 256, 66 | 67 | data: [256]u8 = undefined, 68 | filepath_data: [256]u8 = undefined, 69 | data_size: u32 = 256, 70 | 71 | pub fn init() !Self { 72 | const HKEY_LOCAL_MACHINE = @as(HKEY, @ptrFromInt(0x80000002)); 73 | const KEY_READ = 0x20019; 74 | 75 | var self: Self = undefined; 76 | self.index = 0; 77 | if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, "HARDWARE\\DEVICEMAP\\SERIALCOMM\\", 0, KEY_READ, &self.key) != 0) 78 | return error.WindowsError; 79 | 80 | return self; 81 | } 82 | 83 | pub fn deinit(self: *Self) void { 84 | _ = RegCloseKey(self.key); 85 | self.* = undefined; 86 | } 87 | 88 | pub fn next(self: *Self) !?SerialPortDescription { 89 | defer self.index += 1; 90 | 91 | self.name_size = 256; 92 | self.data_size = 256; 93 | 94 | return switch (RegEnumValueA(self.key, self.index, &self.name, &self.name_size, null, null, &self.data, &self.data_size)) { 95 | 0 => SerialPortDescription{ 96 | .file_name = try std.fmt.bufPrint(&self.filepath_data, "\\\\.\\{s}", .{self.data[0 .. self.data_size - 1]}), 97 | .display_name = self.data[0 .. self.data_size - 1], 98 | .driver = self.name[0..self.name_size], 99 | }, 100 | 259 => null, 101 | else => error.WindowsError, 102 | }; 103 | } 104 | }; 105 | 106 | const WindowsInformationIterator = struct { 107 | const Self = @This(); 108 | 109 | index: std.os.windows.DWORD, 110 | device_info_set: HDEVINFO, 111 | 112 | port_buffer: [256:0]u8, 113 | sys_buffer: [256:0]u8, 114 | name_buffer: [256:0]u8, 115 | desc_buffer: [256:0]u8, 116 | man_buffer: [256:0]u8, 117 | serial_buffer: [256:0]u8, 118 | hw_id: [256:0]u8, 119 | 120 | const Property = enum(std.os.windows.DWORD) { 121 | SPDRP_DEVICEDESC = 0x00000000, 122 | SPDRP_MFG = 0x0000000B, 123 | SPDRP_FRIENDLYNAME = 0x0000000C, 124 | }; 125 | 126 | // GUID taken from 127 | const DIGCF_PRESENT = 0x00000002; 128 | const DIGCF_DEVICEINTERFACE = 0x00000010; 129 | const device_setup_tokens = .{ 130 | .{ std.os.windows.GUID{ .Data1 = 0x4d36e978, .Data2 = 0xe325, .Data3 = 0x11ce, .Data4 = .{ 0xbf, 0xc1, 0x08, 0x00, 0x2b, 0xe1, 0x03, 0x18 } }, DIGCF_PRESENT }, 131 | .{ std.os.windows.GUID{ .Data1 = 0x4d36e96d, .Data2 = 0xe325, .Data3 = 0x11ce, .Data4 = .{ 0xbf, 0xc1, 0x08, 0x00, 0x2b, 0xe1, 0x03, 0x18 } }, DIGCF_PRESENT }, 132 | .{ std.os.windows.GUID{ .Data1 = 0x86e0d1e0, .Data2 = 0x8089, .Data3 = 0x11d0, .Data4 = .{ 0x9c, 0xe4, 0x08, 0x00, 0x3e, 0x30, 0x1f, 0x73 } }, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE }, 133 | .{ std.os.windows.GUID{ .Data1 = 0x2c7089aa, .Data2 = 0x2e0e, .Data3 = 0x11d1, .Data4 = .{ 0xb1, 0x14, 0x00, 0xc0, 0x4f, 0xc2, 0xaa, 0xe4 } }, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE }, 134 | }; 135 | 136 | pub fn init() !Self { 137 | var self: Self = undefined; 138 | self.index = 0; 139 | 140 | inline for (device_setup_tokens) |token| { 141 | const guid = token[0]; 142 | const flags = token[1]; 143 | 144 | self.device_info_set = SetupDiGetClassDevsW( 145 | &guid, 146 | null, 147 | null, 148 | flags, 149 | ); 150 | 151 | if (self.device_info_set != std.os.windows.INVALID_HANDLE_VALUE) break; 152 | } 153 | 154 | if (self.device_info_set == std.os.windows.INVALID_HANDLE_VALUE) return error.WindowsError; 155 | 156 | return self; 157 | } 158 | 159 | pub fn deinit(self: *Self) void { 160 | _ = SetupDiDestroyDeviceInfoList(self.device_info_set); 161 | self.* = undefined; 162 | } 163 | 164 | pub fn next(self: *Self) !?PortInformation { 165 | var device_info_data: SP_DEVINFO_DATA = .{ 166 | .cbSize = @sizeOf(SP_DEVINFO_DATA), 167 | .classGuid = std.mem.zeroes(std.os.windows.GUID), 168 | .devInst = 0, 169 | .reserved = 0, 170 | }; 171 | 172 | if (SetupDiEnumDeviceInfo(self.device_info_set, self.index, &device_info_data) != std.os.windows.TRUE) { 173 | return null; 174 | } 175 | 176 | defer self.index += 1; 177 | 178 | var info: PortInformation = std.mem.zeroes(PortInformation); 179 | @memset(&self.hw_id, 0); 180 | 181 | // NOTE: have not handled if port startswith("LPT") 182 | var length = getPortName(&self.device_info_set, &device_info_data, &self.port_buffer); 183 | info.port_name = self.port_buffer[0..length]; 184 | 185 | info.system_location = try std.fmt.bufPrint(&self.sys_buffer, "\\\\.\\{s}", .{info.port_name}); 186 | 187 | length = deviceRegistryProperty(&self.device_info_set, &device_info_data, Property.SPDRP_FRIENDLYNAME, &self.name_buffer); 188 | info.friendly_name = self.name_buffer[0..length]; 189 | 190 | length = deviceRegistryProperty(&self.device_info_set, &device_info_data, Property.SPDRP_DEVICEDESC, &self.desc_buffer); 191 | info.description = self.desc_buffer[0..length]; 192 | 193 | length = deviceRegistryProperty(&self.device_info_set, &device_info_data, Property.SPDRP_MFG, &self.man_buffer); 194 | info.manufacturer = self.man_buffer[0..length]; 195 | 196 | if (SetupDiGetDeviceInstanceIdA( 197 | self.device_info_set, 198 | &device_info_data, 199 | @ptrCast(&self.hw_id), 200 | 255, 201 | null, 202 | ) == std.os.windows.TRUE) { 203 | length = @as(u32, @truncate(std.mem.indexOfSentinel(u8, 0, &self.hw_id))); 204 | info.hw_id = self.hw_id[0..length]; 205 | 206 | length = parseSerialNumber(&self.hw_id, &self.serial_buffer) catch 0; 207 | if (length == 0) { 208 | length = getParentSerialNumber(device_info_data.devInst, &self.hw_id, &self.serial_buffer) catch 0; 209 | } 210 | info.serial_number = self.serial_buffer[0..length]; 211 | info.vid = parseVendorId(&self.hw_id) catch 0; 212 | info.pid = parseProductId(&self.hw_id) catch 0; 213 | } else { 214 | return error.WindowsError; 215 | } 216 | 217 | return info; 218 | } 219 | 220 | fn getPortName(device_info_set: *const HDEVINFO, device_info_data: *SP_DEVINFO_DATA, port_name: [*]u8) std.os.windows.DWORD { 221 | const hkey: HKEY = SetupDiOpenDevRegKey( 222 | device_info_set.*, 223 | device_info_data, 224 | 0x00000001, // #define DICS_FLAG_GLOBAL 225 | 0, 226 | 0x00000001, // #define DIREG_DEV, 227 | std.os.windows.KEY_READ, 228 | ); 229 | 230 | defer { 231 | _ = std.os.windows.advapi32.RegCloseKey(hkey); 232 | } 233 | 234 | inline for (.{ "PortName", "PortNumber" }) |key_token| { 235 | var port_length: std.os.windows.DWORD = std.os.windows.NAME_MAX; 236 | var data_type: std.os.windows.DWORD = 0; 237 | 238 | const result = RegQueryValueExA( 239 | hkey, 240 | @as(std.os.windows.LPSTR, @ptrCast(@constCast(key_token))), 241 | null, 242 | &data_type, 243 | port_name, 244 | &port_length, 245 | ); 246 | 247 | // if this is valid, return now 248 | if (result == 0 and port_length > 0) { 249 | return port_length; 250 | } 251 | } 252 | 253 | return 0; 254 | } 255 | 256 | fn deviceRegistryProperty(device_info_set: *const HDEVINFO, device_info_data: *SP_DEVINFO_DATA, property: Property, property_str: [*]u8) std.os.windows.DWORD { 257 | var data_type: std.os.windows.DWORD = 0; 258 | var bytes_required: std.os.windows.DWORD = std.os.windows.MAX_PATH; 259 | 260 | const result = SetupDiGetDeviceRegistryPropertyA( 261 | device_info_set.*, 262 | device_info_data, 263 | @intFromEnum(property), 264 | &data_type, 265 | property_str, 266 | std.os.windows.NAME_MAX, 267 | &bytes_required, 268 | ); 269 | 270 | if (result == std.os.windows.FALSE) { 271 | std.debug.print("GetLastError: {}\n", .{std.os.windows.kernel32.GetLastError()}); 272 | bytes_required = 0; 273 | } 274 | 275 | return bytes_required; 276 | } 277 | 278 | fn getParentSerialNumber(devinst: DEVINST, devid: []const u8, serial_number: [*]u8) !std.os.windows.DWORD { 279 | if (std.mem.startsWith(u8, devid, "FTDI")) { 280 | // Should not be called on "FTDI" so just return the serial number. 281 | return try parseSerialNumber(devid, serial_number); 282 | } else if (std.mem.startsWith(u8, devid, "USB")) { 283 | // taken from pyserial 284 | const max_usb_device_tree_traversal_depth = 5; 285 | const start_vidpid = std.mem.indexOf(u8, devid, "VID") orelse return error.WindowsError; 286 | const vidpid_slice = devid[start_vidpid .. start_vidpid + 17]; // "VIDxxxx&PIDxxxx" 287 | 288 | // keep looping over parent device to extract serial number if it contains the target VID and PID. 289 | var depth: u8 = 0; 290 | var child_inst: DEVINST = devinst; 291 | while (depth <= max_usb_device_tree_traversal_depth) : (depth += 1) { 292 | var parent_id: DEVINST = undefined; 293 | var local_buffer: [256:0]u8 = std.mem.zeroes([256:0]u8); 294 | 295 | if (CM_Get_Parent(&parent_id, child_inst, 0) != 0) return error.WindowsError; 296 | if (CM_Get_Device_IDA(parent_id, @ptrCast(&local_buffer), 256, 0) != 0) return error.WindowsError; 297 | defer child_inst = parent_id; 298 | 299 | if (!std.mem.containsAtLeast(u8, local_buffer[0..255], 1, vidpid_slice)) continue; 300 | 301 | const length = try parseSerialNumber(local_buffer[0..255], serial_number); 302 | if (length > 0) return length; 303 | } 304 | } 305 | 306 | return error.WindowsError; 307 | } 308 | 309 | fn parseSerialNumber(devid: []const u8, serial_number: [*]u8) !std.os.windows.DWORD { 310 | var delimiter: ?[]const u8 = undefined; 311 | 312 | if (std.mem.startsWith(u8, devid, "USB")) { 313 | delimiter = "\\&"; 314 | } else if (std.mem.startsWith(u8, devid, "FTDI")) { 315 | delimiter = "\\+"; 316 | } else { 317 | // What to do here? 318 | delimiter = null; 319 | } 320 | 321 | if (delimiter) |del| { 322 | var it = std.mem.tokenizeAny(u8, devid, del); 323 | 324 | // throw away the start 325 | _ = it.next(); 326 | while (it.next()) |segment| { 327 | if (std.mem.startsWith(u8, segment, "VID_")) continue; 328 | if (std.mem.startsWith(u8, segment, "PID_")) continue; 329 | 330 | // If "MI_{d}{d}", this is an interface number. The serial number will have to be 331 | // sourced from the parent node. Probably do not have to check all these conditions. 332 | if (segment.len == 5 and std.mem.eql(u8, "MI_", segment[0..3]) and std.ascii.isDigit(segment[3]) and std.ascii.isDigit(segment[4])) return 0; 333 | 334 | @memcpy(serial_number, segment); 335 | return @as(std.os.windows.DWORD, @truncate(segment.len)); 336 | } 337 | } 338 | 339 | return error.WindowsError; 340 | } 341 | 342 | fn parseVendorId(devid: []const u8) !u16 { 343 | var delimiter: ?[]const u8 = undefined; 344 | 345 | if (std.mem.startsWith(u8, devid, "USB")) { 346 | delimiter = "\\&"; 347 | } else if (std.mem.startsWith(u8, devid, "FTDI")) { 348 | delimiter = "\\+"; 349 | } else { 350 | delimiter = null; 351 | } 352 | 353 | if (delimiter) |del| { 354 | var it = std.mem.tokenizeAny(u8, devid, del); 355 | 356 | while (it.next()) |segment| { 357 | if (std.mem.startsWith(u8, segment, "VID_")) { 358 | return try std.fmt.parseInt(u16, segment[4..], 16); 359 | } 360 | } 361 | } 362 | 363 | return error.WindowsError; 364 | } 365 | 366 | fn parseProductId(devid: []const u8) !u16 { 367 | var delimiter: ?[]const u8 = undefined; 368 | 369 | if (std.mem.startsWith(u8, devid, "USB")) { 370 | delimiter = "\\&"; 371 | } else if (std.mem.startsWith(u8, devid, "FTDI")) { 372 | delimiter = "\\+"; 373 | } else { 374 | delimiter = null; 375 | } 376 | 377 | if (delimiter) |del| { 378 | var it = std.mem.tokenizeAny(u8, devid, del); 379 | 380 | while (it.next()) |segment| { 381 | if (std.mem.startsWith(u8, segment, "PID_")) { 382 | return try std.fmt.parseInt(u16, segment[4..], 16); 383 | } 384 | } 385 | } 386 | 387 | return error.WindowsError; 388 | } 389 | }; 390 | 391 | extern "advapi32" fn RegOpenKeyExA( 392 | key: HKEY, 393 | lpSubKey: std.os.windows.LPCSTR, 394 | ulOptions: std.os.windows.DWORD, 395 | samDesired: std.os.windows.REGSAM, 396 | phkResult: *HKEY, 397 | ) callconv(std.os.windows.WINAPI) std.os.windows.LSTATUS; 398 | extern "advapi32" fn RegCloseKey(key: HKEY) callconv(std.os.windows.WINAPI) std.os.windows.LSTATUS; 399 | extern "advapi32" fn RegEnumValueA( 400 | hKey: HKEY, 401 | dwIndex: std.os.windows.DWORD, 402 | lpValueName: std.os.windows.LPSTR, 403 | lpcchValueName: *std.os.windows.DWORD, 404 | lpReserved: ?*std.os.windows.DWORD, 405 | lpType: ?*std.os.windows.DWORD, 406 | lpData: [*]std.os.windows.BYTE, 407 | lpcbData: *std.os.windows.DWORD, 408 | ) callconv(std.os.windows.WINAPI) std.os.windows.LSTATUS; 409 | extern "advapi32" fn RegQueryValueExA( 410 | hKey: HKEY, 411 | lpValueName: std.os.windows.LPSTR, 412 | lpReserved: ?*std.os.windows.DWORD, 413 | lpType: ?*std.os.windows.DWORD, 414 | lpData: ?[*]std.os.windows.BYTE, 415 | lpcbData: ?*std.os.windows.DWORD, 416 | ) callconv(std.os.windows.WINAPI) std.os.windows.LSTATUS; 417 | extern "setupapi" fn SetupDiGetClassDevsW( 418 | classGuid: ?*const std.os.windows.GUID, 419 | enumerator: ?std.os.windows.PCWSTR, 420 | hwndParanet: ?HWND, 421 | flags: std.os.windows.DWORD, 422 | ) callconv(std.os.windows.WINAPI) HDEVINFO; 423 | extern "setupapi" fn SetupDiEnumDeviceInfo( 424 | devInfoSet: HDEVINFO, 425 | memberIndex: std.os.windows.DWORD, 426 | device_info_data: *SP_DEVINFO_DATA, 427 | ) callconv(std.os.windows.WINAPI) std.os.windows.BOOL; 428 | extern "setupapi" fn SetupDiDestroyDeviceInfoList(device_info_set: HDEVINFO) callconv(std.os.windows.WINAPI) std.os.windows.BOOL; 429 | extern "setupapi" fn SetupDiOpenDevRegKey( 430 | device_info_set: HDEVINFO, 431 | device_info_data: *SP_DEVINFO_DATA, 432 | scope: std.os.windows.DWORD, 433 | hwProfile: std.os.windows.DWORD, 434 | keyType: std.os.windows.DWORD, 435 | samDesired: std.os.windows.REGSAM, 436 | ) callconv(std.os.windows.WINAPI) HKEY; 437 | extern "setupapi" fn SetupDiGetDeviceRegistryPropertyA( 438 | hDevInfo: HDEVINFO, 439 | pSpDevInfoData: *SP_DEVINFO_DATA, 440 | property: std.os.windows.DWORD, 441 | propertyRegDataType: ?*std.os.windows.DWORD, 442 | propertyBuffer: ?[*]std.os.windows.BYTE, 443 | propertyBufferSize: std.os.windows.DWORD, 444 | requiredSize: ?*std.os.windows.DWORD, 445 | ) callconv(std.os.windows.WINAPI) std.os.windows.BOOL; 446 | extern "setupapi" fn SetupDiGetDeviceInstanceIdA( 447 | device_info_set: HDEVINFO, 448 | device_info_data: *SP_DEVINFO_DATA, 449 | deviceInstanceId: *?std.os.windows.CHAR, 450 | deviceInstanceIdSize: std.os.windows.DWORD, 451 | requiredSize: ?*std.os.windows.DWORD, 452 | ) callconv(std.os.windows.WINAPI) std.os.windows.BOOL; 453 | extern "cfgmgr32" fn CM_Get_Parent( 454 | pdnDevInst: *DEVINST, 455 | dnDevInst: DEVINST, 456 | ulFlags: std.os.windows.ULONG, 457 | ) callconv(std.os.windows.WINAPI) std.os.windows.DWORD; 458 | extern "cfgmgr32" fn CM_Get_Device_IDA( 459 | dnDevInst: DEVINST, 460 | buffer: std.os.windows.LPSTR, 461 | bufferLen: std.os.windows.ULONG, 462 | ulFlags: std.os.windows.ULONG, 463 | ) callconv(std.os.windows.WINAPI) std.os.windows.DWORD; 464 | 465 | const LinuxPortIterator = struct { 466 | const Self = @This(); 467 | 468 | const root_dir = "/sys/class/tty"; 469 | 470 | // ls -hal /sys/class/tty/*/device/driver 471 | 472 | dir: std.fs.Dir, 473 | iterator: std.fs.Dir.Iterator, 474 | 475 | full_path_buffer: [std.fs.max_path_bytes]u8 = undefined, 476 | driver_path_buffer: [std.fs.max_path_bytes]u8 = undefined, 477 | 478 | pub fn init() !Self { 479 | var dir = try std.fs.cwd().openDir(root_dir, .{ .iterate = true }); 480 | errdefer dir.close(); 481 | 482 | return Self{ 483 | .dir = dir, 484 | .iterator = dir.iterate(), 485 | }; 486 | } 487 | 488 | pub fn deinit(self: *Self) void { 489 | self.dir.close(); 490 | self.* = undefined; 491 | } 492 | 493 | pub fn next(self: *Self) !?SerialPortDescription { 494 | while (true) { 495 | if (try self.iterator.next()) |entry| { 496 | // not a dir => we don't care 497 | var tty_dir = self.dir.openDir(entry.name, .{}) catch continue; 498 | defer tty_dir.close(); 499 | 500 | // we need the device dir 501 | // no device dir => virtual device 502 | var device_dir = tty_dir.openDir("device", .{}) catch continue; 503 | defer device_dir.close(); 504 | 505 | // We need the symlink for "driver" 506 | const link = device_dir.readLink("driver", &self.driver_path_buffer) catch continue; 507 | 508 | // full_path_buffer 509 | // driver_path_buffer 510 | 511 | var fba = std.heap.FixedBufferAllocator.init(&self.full_path_buffer); 512 | 513 | const path = try std.fs.path.join(fba.allocator(), &.{ 514 | "/dev/", 515 | entry.name, 516 | }); 517 | 518 | return SerialPortDescription{ 519 | .file_name = path, 520 | .display_name = path, 521 | .driver = std.fs.path.basename(link), 522 | }; 523 | } else { 524 | return null; 525 | } 526 | } 527 | return null; 528 | } 529 | }; 530 | 531 | const LinuxInformationIterator = struct { 532 | const Self = @This(); 533 | 534 | const root_dir = "/sys/class/tty"; 535 | 536 | index: u8, 537 | dir: std.fs.Dir, 538 | iterator: std.fs.Dir.Iterator, 539 | 540 | driver_path_buffer: [std.fs.max_path_bytes]u8 = undefined, 541 | sys_buffer: [256:0]u8 = undefined, 542 | desc_buffer: [256:0]u8 = undefined, 543 | man_buffer: [256:0]u8 = undefined, 544 | serial_buffer: [256:0]u8 = undefined, 545 | port: PortInformation = undefined, 546 | 547 | pub fn init() !Self { 548 | var dir = try std.fs.cwd().openDir(root_dir, .{ .iterate = true }); 549 | errdefer dir.close(); 550 | 551 | return Self{ .index = 0, .dir = dir, .iterator = dir.iterate() }; 552 | } 553 | 554 | pub fn deinit(self: *Self) void { 555 | self.dir.close(); 556 | self.* = undefined; 557 | } 558 | 559 | pub fn next(self: *Self) !?PortInformation { 560 | self.index += 1; 561 | while (try self.iterator.next()) |entry| { 562 | @memset(&self.sys_buffer, 0); 563 | @memset(&self.desc_buffer, 0); 564 | @memset(&self.man_buffer, 0); 565 | @memset(&self.serial_buffer, 0); 566 | @memset(&self.driver_path_buffer, 0); 567 | 568 | // not a dir => we don't care 569 | var tty_dir = self.dir.openDir(entry.name, .{}) catch continue; 570 | defer tty_dir.close(); 571 | 572 | // we need the device dir 573 | // no device dir => virtual device 574 | var device_dir = tty_dir.openDir("device", .{}) catch continue; 575 | defer device_dir.close(); 576 | 577 | // start filling port informations 578 | { 579 | var fba = std.heap.FixedBufferAllocator.init(&self.sys_buffer); 580 | self.port.system_location = try std.fs.path.join(fba.allocator(), &.{ 581 | "/dev/", 582 | entry.name, 583 | }); 584 | self.port.friendly_name = entry.name; 585 | self.port.port_name = entry.name; 586 | self.port.hw_id = "N/A"; 587 | } 588 | // We need the symlink for "driver" 589 | const subsystem_path = device_dir.readLink("subsystem", &self.driver_path_buffer) catch continue; 590 | const subsystem = std.fs.path.basename(subsystem_path); 591 | var device_path: []u8 = undefined; 592 | if (std.mem.eql(u8, subsystem, "usb") == true) { 593 | device_path = try device_dir.realpath("../", &self.driver_path_buffer); 594 | } else if (std.mem.eql(u8, subsystem, "usb-serial") == true) { 595 | device_path = try device_dir.realpath("../../", &self.driver_path_buffer); 596 | } else { 597 | //must be remove to manage other device type 598 | self.port.description = "Not Managed"; 599 | self.port.manufacturer = "Not Managed"; 600 | self.port.serial_number = "Not Managed"; 601 | self.port.vid = 0; 602 | self.port.pid = 0; 603 | return self.port; 604 | } 605 | 606 | var data_dir = std.fs.openDirAbsolute(device_path, .{}) catch continue; 607 | defer data_dir.close(); 608 | var tmp: [4]u8 = undefined; 609 | { 610 | self.port.manufacturer = data_dir.readFile("manufacturer", &self.man_buffer) catch "N/A"; 611 | Self.clean_file_read(&self.man_buffer); 612 | self.port.description = data_dir.readFile("product", &self.desc_buffer) catch "N/A"; 613 | Self.clean_file_read(&self.desc_buffer); 614 | self.port.serial_number = data_dir.readFile("serial", &self.serial_buffer) catch "N/A"; 615 | Self.clean_file_read(&self.serial_buffer); 616 | } 617 | { 618 | @memset(&tmp, 0); 619 | _ = data_dir.readFile("idVendor", &tmp) catch 0; 620 | self.port.vid = try std.fmt.parseInt(u16, &tmp, 16); 621 | } 622 | { 623 | @memset(&tmp, 0); 624 | _ = data_dir.readFile("idProduct", &tmp) catch 0; 625 | self.port.pid = try std.fmt.parseInt(u16, &tmp, 16); 626 | } 627 | 628 | return self.port; 629 | } 630 | return null; 631 | } 632 | fn clean_file_read(buf: []u8) void { 633 | for (buf) |*item| { 634 | if (item.* == '\n') { 635 | item.* = 0; 636 | break; 637 | } 638 | } 639 | } 640 | }; 641 | 642 | const DarwinPortIterator = struct { 643 | const Self = @This(); 644 | 645 | const root_dir = "/dev/"; 646 | 647 | dir: std.fs.Dir, 648 | iterator: std.fs.Dir.Iterator, 649 | 650 | full_path_buffer: [std.fs.max_path_bytes]u8 = undefined, 651 | driver_path_buffer: [std.fs.max_path_bytes]u8 = undefined, 652 | 653 | pub fn init() !Self { 654 | var dir = try std.fs.cwd().openDir(root_dir, .{ .iterate = true }); 655 | errdefer dir.close(); 656 | 657 | return Self{ 658 | .dir = dir, 659 | .iterator = dir.iterate(), 660 | }; 661 | } 662 | 663 | pub fn deinit(self: *Self) void { 664 | self.dir.close(); 665 | self.* = undefined; 666 | } 667 | 668 | pub fn next(self: *Self) !?SerialPortDescription { 669 | while (true) { 670 | if (try self.iterator.next()) |entry| { 671 | if (!std.mem.startsWith(u8, entry.name, "cu.")) { 672 | continue; 673 | } else { 674 | var fba = std.heap.FixedBufferAllocator.init(&self.full_path_buffer); 675 | 676 | const path = try std.fs.path.join(fba.allocator(), &.{ 677 | "/dev/", 678 | entry.name, 679 | }); 680 | 681 | return SerialPortDescription{ 682 | .file_name = path, 683 | .display_name = path, 684 | .driver = "darwin", 685 | }; 686 | } 687 | } else { 688 | return null; 689 | } 690 | } 691 | return null; 692 | } 693 | }; 694 | 695 | pub const Parity = enum(u8) { 696 | /// No parity bit is used 697 | none = 'N', 698 | /// Parity bit is `0` when an even number of bits is set in the data. 699 | even = 'E', 700 | /// Parity bit is `0` when an odd number of bits is set in the data. 701 | odd = 'O', 702 | /// Parity bit is always `1` 703 | mark = 'M', 704 | /// Parity bit is always `0` 705 | space = 'S', 706 | }; 707 | 708 | pub const StopBits = enum(u2) { 709 | /// The length of the stop bit is 1 bit 710 | one = 1, 711 | /// The length of the stop bit is 2 bits 712 | two = 2, 713 | }; 714 | 715 | pub const Handshake = enum { 716 | /// No handshake is used 717 | none, 718 | /// XON-XOFF software handshake is used. 719 | software, 720 | /// Hardware handshake with RTS/CTS is used. 721 | hardware, 722 | }; 723 | 724 | pub const WordSize = enum(u4) { 725 | five = 5, 726 | six = 6, 727 | seven = 7, 728 | eight = 8, 729 | }; 730 | 731 | pub const SerialConfig = struct { 732 | const Self = @This(); 733 | 734 | /// Symbol rate in bits/second. Not that these 735 | /// include also parity and stop bits. 736 | baud_rate: u32, 737 | 738 | /// Parity to verify transport integrity. 739 | parity: Parity = .none, 740 | 741 | /// Number of stop bits after the data 742 | stop_bits: StopBits = .one, 743 | 744 | /// Number of data bits per word. 745 | /// Allowed values are 5, 6, 7, 8 746 | word_size: WordSize = .eight, 747 | 748 | /// Defines the handshake protocol used. 749 | handshake: Handshake = .none, 750 | 751 | pub fn format(self: Self, fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { 752 | _ = options; 753 | _ = fmt; 754 | return writer.print("{d}@{d}{c}{d}{s}", .{ self.baud_rate, @intFromEnum(self.word_size), @intFromEnum(self.parity), @intFromEnum(self.stop_bits), switch (self.handshake) { 755 | .none => "", 756 | .hardware => " RTS/CTS", 757 | .software => " XON/XOFF", 758 | } }); 759 | } 760 | }; 761 | 762 | const CBAUD = 0o000000010017; //Baud speed mask (not in POSIX). 763 | const CMSPAR = 0o010000000000; 764 | const CRTSCTS = 0o020000000000; 765 | 766 | const VTIME = 5; 767 | const VMIN = 6; 768 | const VSTART = 8; 769 | const VSTOP = 9; 770 | 771 | /// This function configures a serial port with the given config. 772 | /// `port` is an already opened serial port, on windows these 773 | /// are either called `\\.\COMxx\` or `COMx`, on unixes the serial 774 | /// port is called `/dev/ttyXXX`. 775 | pub fn configureSerialPort(port: std.fs.File, config: SerialConfig) !void { 776 | switch (builtin.os.tag) { 777 | .windows => { 778 | var dcb = std.mem.zeroes(DCB); 779 | dcb.DCBlength = @sizeOf(DCB); 780 | 781 | if (GetCommState(port.handle, &dcb) == 0) 782 | return error.WindowsError; 783 | 784 | // std.log.err("{s} {s}", .{ dcb, flags }); 785 | 786 | dcb.BaudRate = config.baud_rate; 787 | 788 | dcb.flags = @bitCast(DCBFlags{ 789 | .fParity = config.parity != .none, 790 | .fOutxCtsFlow = config.handshake == .hardware, 791 | .fOutX = config.handshake == .software, 792 | .fInX = config.handshake == .software, 793 | .fRtsControl = @as(u2, if (config.handshake == .hardware) 1 else 0), 794 | }); 795 | 796 | dcb.wReserved = 0; 797 | dcb.ByteSize = switch (config.word_size) { 798 | .five => @as(u8, 5), 799 | .six => @as(u8, 6), 800 | .seven => @as(u8, 7), 801 | .eight => @as(u8, 8), 802 | }; 803 | dcb.Parity = switch (config.parity) { 804 | .none => @as(u8, 0), 805 | .even => @as(u8, 2), 806 | .odd => @as(u8, 1), 807 | .mark => @as(u8, 3), 808 | .space => @as(u8, 4), 809 | }; 810 | dcb.StopBits = switch (config.stop_bits) { 811 | .one => @as(u2, 0), 812 | .two => @as(u2, 2), 813 | }; 814 | dcb.XonChar = 0x11; 815 | dcb.XoffChar = 0x13; 816 | dcb.wReserved1 = 0; 817 | 818 | if (SetCommState(port.handle, &dcb) == 0) 819 | return error.WindowsError; 820 | }, 821 | .linux, .macos => |tag| { 822 | var settings = try std.posix.tcgetattr(port.handle); 823 | 824 | var macos_nonstandard_baud = false; 825 | const baudmask: std.c.speed_t = switch (tag) { 826 | .macos => mapBaudToMacOSEnum(config.baud_rate) orelse b: { 827 | macos_nonstandard_baud = true; 828 | break :b @enumFromInt(@as(u64, @bitCast(settings.cflag))); 829 | }, 830 | .linux => try mapBaudToLinuxEnum(config.baud_rate), 831 | else => unreachable, 832 | }; 833 | 834 | // initialize CFLAG with the baudrate bits 835 | settings.cflag = @bitCast(@intFromEnum(baudmask)); 836 | settings.cflag.PARODD = config.parity == .odd or config.parity == .mark; 837 | settings.cflag.PARENB = config.parity != .none; 838 | settings.cflag.CLOCAL = config.handshake == .none; 839 | settings.cflag.CSTOPB = config.stop_bits == .two; 840 | settings.cflag.CREAD = true; 841 | settings.cflag.CSIZE = switch (config.word_size) { 842 | .five => .CS5, 843 | .six => .CS6, 844 | .seven => .CS7, 845 | .eight => .CS8, 846 | }; 847 | 848 | settings.iflag = .{}; 849 | settings.iflag.INPCK = config.parity != .none; 850 | settings.iflag.IXON = config.handshake == .software; 851 | settings.iflag.IXOFF = config.handshake == .software; 852 | // these are common between linux and macos 853 | // settings.iflag.IGNBRK = false; 854 | // settings.iflag.BRKINT = false; 855 | // settings.iflag.IGNPAR = false; 856 | // settings.iflag.PARMRK = false; 857 | // settings.iflag.ISTRIP = false; 858 | // settings.iflag.INLCR = false; 859 | // settings.iflag.IGNCR = false; 860 | // settings.iflag.ICRNL = false; 861 | // settings.iflag.IXANY = false; 862 | // settings.iflag.IMAXBEL = false; 863 | // settings.iflag.IUTF8 = false; 864 | 865 | // these are where they diverge 866 | if (builtin.os.tag == .linux) { 867 | if (@hasField(std.c.tc_cflag_t, "CMSPAR")) { 868 | settings.cflag.CMSPAR = config.parity == .mark; 869 | } 870 | if (@hasField(std.c.tc_cflag_t, "CRTSCTS")) { 871 | settings.cflag.CRTSCTS = config.handshake == .hardware; 872 | } 873 | // settings.cflag.ADDRB = false; 874 | // settings.iflag.IUCLC = false; 875 | 876 | // these are actually the same, but for simplicity 877 | // just setting baud on mac with cfsetspeed 878 | } 879 | if (builtin.os.tag == .macos) { 880 | settings.cflag.CCTS_OFLOW = config.handshake == .hardware; 881 | settings.cflag.CRTS_IFLOW = config.handshake == .hardware; 882 | // settings.cflag.CIGNORE = false; 883 | // settings.cflag.CDTR_IFLOW = false; 884 | // settings.cflag.CDSR_OFLOW = false; 885 | // settings.cflag.CCAR_OFLOW = false; 886 | } 887 | 888 | if (!macos_nonstandard_baud) { 889 | settings.ispeed = baudmask; 890 | settings.ospeed = baudmask; 891 | } 892 | 893 | settings.oflag = .{}; 894 | settings.lflag = .{}; 895 | 896 | settings.cc[VMIN] = 1; 897 | settings.cc[VSTOP] = 0x13; // XOFF 898 | settings.cc[VSTART] = 0x11; // XON 899 | settings.cc[VTIME] = 0; 900 | 901 | try std.posix.tcsetattr(port.handle, .NOW, settings); 902 | 903 | if (builtin.os.tag == .macos and macos_nonstandard_baud) { 904 | const IOSSIOSPEED: c_uint = 0x80085402; 905 | const speed: c_uint = @intCast(config.baud_rate); 906 | if (std.c.ioctl(port.handle, @bitCast(IOSSIOSPEED), &speed) == -1) { 907 | return error.UnsupportedBaudRate; 908 | } 909 | } 910 | }, 911 | else => @compileError("unsupported OS, please implement!"), 912 | } 913 | } 914 | 915 | const Flush = enum { 916 | input, 917 | output, 918 | both, 919 | }; 920 | 921 | /// Flushes the serial port `port`. If `input` is set, all pending data in 922 | /// the receive buffer is flushed, if `output` is set all pending data in 923 | /// the send buffer is flushed. 924 | pub fn flushSerialPort(port: std.fs.File, flush: Flush) !void { 925 | switch (builtin.os.tag) { 926 | .windows => { 927 | const mode: std.os.windows.DWORD = switch (flush) { 928 | .input => PURGE_RXCLEAR, 929 | .output => PURGE_TXCLEAR, 930 | .both => PURGE_TXCLEAR | PURGE_RXCLEAR, 931 | }; 932 | if (0 == PurgeComm(port.handle, mode)) 933 | return error.FlushError; 934 | }, 935 | .linux => { 936 | const TCFLSH = 0x540B; 937 | const mode: usize = switch (flush) { 938 | .input => 0, // TCIFLUSH 939 | .output => 1, // TCOFLUSH 940 | .both => 2, // TCIOFLUSH 941 | }; 942 | if (0 != std.os.linux.syscall3(.ioctl, @as(usize, @bitCast(@as(isize, port.handle))), TCFLSH, mode)) 943 | return error.FlushError; 944 | }, 945 | .macos => { 946 | const mode: c_int = switch (flush) { 947 | .input => c.TCIFLUSH, 948 | .output => c.TCOFLUSH, 949 | .both => c.TCIOFLUSH, 950 | }; 951 | if (0 != c.tcflush(port.handle, mode)) 952 | return error.FlushError; 953 | }, 954 | else => @compileError("unsupported OS, please implement!"), 955 | } 956 | } 957 | 958 | pub const ControlPins = struct { 959 | rts: ?bool = null, 960 | dtr: ?bool = null, 961 | }; 962 | 963 | pub fn changeControlPins(port: std.fs.File, pins: ControlPins) !void { 964 | switch (builtin.os.tag) { 965 | .windows => { 966 | const CLRDTR = 6; 967 | const CLRRTS = 4; 968 | const SETDTR = 5; 969 | const SETRTS = 3; 970 | 971 | if (pins.dtr) |dtr| { 972 | if (EscapeCommFunction(port.handle, if (dtr) SETDTR else CLRDTR) == 0) 973 | return error.WindowsError; 974 | } 975 | if (pins.rts) |rts| { 976 | if (EscapeCommFunction(port.handle, if (rts) SETRTS else CLRRTS) == 0) 977 | return error.WindowsError; 978 | } 979 | }, 980 | .linux => { 981 | const TIOCM_RTS: c_int = 0x004; 982 | const TIOCM_DTR: c_int = 0x002; 983 | 984 | // from /usr/include/asm-generic/ioctls.h 985 | // const TIOCMBIS: u32 = 0x5416; 986 | // const TIOCMBIC: u32 = 0x5417; 987 | const TIOCMGET: u32 = 0x5415; 988 | const TIOCMSET: u32 = 0x5418; 989 | 990 | var flags: c_int = 0; 991 | if (std.os.linux.ioctl(port.handle, TIOCMGET, @intFromPtr(&flags)) != 0) 992 | return error.Unexpected; 993 | 994 | if (pins.dtr) |dtr| { 995 | if (dtr) { 996 | flags |= TIOCM_DTR; 997 | } else { 998 | flags &= ~TIOCM_DTR; 999 | } 1000 | } 1001 | if (pins.rts) |rts| { 1002 | if (rts) { 1003 | flags |= TIOCM_RTS; 1004 | } else { 1005 | flags &= ~TIOCM_RTS; 1006 | } 1007 | } 1008 | 1009 | if (std.os.linux.ioctl(port.handle, TIOCMSET, @intFromPtr(&flags)) != 0) 1010 | return error.Unexpected; 1011 | }, 1012 | 1013 | .macos => {}, 1014 | 1015 | else => @compileError("changeControlPins not implemented for " ++ @tagName(builtin.os.tag)), 1016 | } 1017 | } 1018 | 1019 | const PURGE_RXABORT = 0x0002; 1020 | const PURGE_RXCLEAR = 0x0008; 1021 | const PURGE_TXABORT = 0x0001; 1022 | const PURGE_TXCLEAR = 0x0004; 1023 | 1024 | extern "kernel32" fn PurgeComm(hFile: std.os.windows.HANDLE, dwFlags: std.os.windows.DWORD) callconv(std.os.windows.WINAPI) std.os.windows.BOOL; 1025 | extern "kernel32" fn EscapeCommFunction(hFile: std.os.windows.HANDLE, dwFunc: std.os.windows.DWORD) callconv(std.os.windows.WINAPI) std.os.windows.BOOL; 1026 | 1027 | fn mapBaudToLinuxEnum(baudrate: usize) !std.c.speed_t { 1028 | return switch (baudrate) { 1029 | // from termios.h 1030 | 50 => .B50, 1031 | 75 => .B75, 1032 | 110 => .B110, 1033 | 134 => .B134, 1034 | 150 => .B150, 1035 | 200 => .B200, 1036 | 300 => .B300, 1037 | 600 => .B600, 1038 | 1200 => .B1200, 1039 | 1800 => .B1800, 1040 | 2400 => .B2400, 1041 | 4800 => .B4800, 1042 | 9600 => .B9600, 1043 | 19200 => .B19200, 1044 | 38400 => .B38400, 1045 | // from termios-baud.h 1046 | 57600 => .B57600, 1047 | 115200 => .B115200, 1048 | 230400 => .B230400, 1049 | 460800 => .B460800, 1050 | 500000 => .B500000, 1051 | 576000 => .B576000, 1052 | 921600 => .B921600, 1053 | 1000000 => .B1000000, 1054 | 1152000 => .B1152000, 1055 | 1500000 => .B1500000, 1056 | 2000000 => .B2000000, 1057 | 2500000 => .B2500000, 1058 | 3000000 => .B3000000, 1059 | 3500000 => .B3500000, 1060 | 4000000 => .B4000000, 1061 | else => error.UnsupportedBaudRate, 1062 | }; 1063 | } 1064 | 1065 | fn mapBaudToMacOSEnum(baudrate: usize) ?std.c.speed_t { 1066 | return switch (baudrate) { 1067 | // from termios.h 1068 | 50 => .B50, 1069 | 75 => .B75, 1070 | 110 => .B110, 1071 | 134 => .B134, 1072 | 150 => .B150, 1073 | 200 => .B200, 1074 | 300 => .B300, 1075 | 600 => .B600, 1076 | 1200 => .B1200, 1077 | 1800 => .B1800, 1078 | 2400 => .B2400, 1079 | 4800 => .B4800, 1080 | 9600 => .B9600, 1081 | 19200 => .B19200, 1082 | 38400 => .B38400, 1083 | 7200 => .B7200, 1084 | 14400 => .B14400, 1085 | 28800 => .B28800, 1086 | 57600 => .B57600, 1087 | 76800 => .B76800, 1088 | 115200 => .B115200, 1089 | 230400 => .B230400, 1090 | else => null, 1091 | }; 1092 | } 1093 | 1094 | const DCBFlags = packed struct(u32) { 1095 | fBinary: bool = true, // u1 1096 | fParity: bool = false, // u1 1097 | fOutxCtsFlow: bool = false, // u1 1098 | fOutxDsrFlow: bool = false, // u1 1099 | fDtrControl: u2 = 1, // u2 1100 | fDsrSensitivity: bool = false, // u1 1101 | fTXContinueOnXoff: bool = false, // u1 1102 | fOutX: bool = false, // u1 1103 | fInX: bool = false, // u1 1104 | fErrorChar: bool = false, // u1 1105 | fNull: bool = false, // u1 1106 | fRtsControl: u2 = 0, // u2 1107 | fAbortOnError: bool = false, // u1 1108 | fDummy2: u17 = 0, // u17 1109 | }; 1110 | 1111 | /// Configuration for the serial port 1112 | /// 1113 | /// Details: https://learn.microsoft.com/es-es/windows/win32/api/winbase/ns-winbase-dcb 1114 | const DCB = extern struct { 1115 | DCBlength: std.os.windows.DWORD, 1116 | BaudRate: std.os.windows.DWORD, 1117 | flags: u32, 1118 | wReserved: std.os.windows.WORD, 1119 | XonLim: std.os.windows.WORD, 1120 | XoffLim: std.os.windows.WORD, 1121 | ByteSize: std.os.windows.BYTE, 1122 | Parity: std.os.windows.BYTE, 1123 | StopBits: std.os.windows.BYTE, 1124 | XonChar: u8, 1125 | XoffChar: u8, 1126 | ErrorChar: u8, 1127 | EofChar: u8, 1128 | EvtChar: u8, 1129 | wReserved1: std.os.windows.WORD, 1130 | }; 1131 | 1132 | extern "kernel32" fn GetCommState(hFile: std.os.windows.HANDLE, lpDCB: *DCB) callconv(std.os.windows.WINAPI) std.os.windows.BOOL; 1133 | extern "kernel32" fn SetCommState(hFile: std.os.windows.HANDLE, lpDCB: *DCB) callconv(std.os.windows.WINAPI) std.os.windows.BOOL; 1134 | 1135 | test "iterate ports" { 1136 | var it = try list(); 1137 | while (try it.next()) |port| { 1138 | _ = port; 1139 | // std.debug.print("{s} (file: {s}, driver: {s})\n", .{ port.display_name, port.file_name, port.driver }); 1140 | } 1141 | } 1142 | 1143 | test "basic configuration test" { 1144 | const cfg = SerialConfig{ 1145 | .handshake = .none, 1146 | .baud_rate = 115200, 1147 | .parity = .none, 1148 | .word_size = .eight, 1149 | .stop_bits = .one, 1150 | }; 1151 | 1152 | var tty: []const u8 = undefined; 1153 | 1154 | switch (builtin.os.tag) { 1155 | .windows => tty = "\\\\.\\COM3", 1156 | .linux => tty = "/dev/ttyUSB0", 1157 | .macos => tty = "/dev/cu.usbmodem101", 1158 | else => unreachable, 1159 | } 1160 | 1161 | var port = try std.fs.cwd().openFile(tty, .{ .mode = .read_write }); 1162 | defer port.close(); 1163 | 1164 | try configureSerialPort(port, cfg); 1165 | } 1166 | 1167 | test "basic flush test" { 1168 | var tty: []const u8 = undefined; 1169 | // if any, these will likely exist on a machine 1170 | switch (builtin.os.tag) { 1171 | .windows => tty = "\\\\.\\COM3", 1172 | .linux => tty = "/dev/ttyUSB0", 1173 | .macos => tty = "/dev/cu.usbmodem101", 1174 | else => unreachable, 1175 | } 1176 | var port = try std.fs.cwd().openFile(tty, .{ .mode = .read_write }); 1177 | defer port.close(); 1178 | 1179 | try flushSerialPort(port, .both); 1180 | try flushSerialPort(port, .input); 1181 | try flushSerialPort(port, .output); 1182 | } 1183 | 1184 | test "change control pins" { 1185 | _ = changeControlPins; 1186 | } 1187 | 1188 | test "bufPrint tests" { 1189 | var buf: [32]u8 = undefined; 1190 | 1191 | try std.testing.expect(std.mem.eql(u8, try std.fmt.bufPrint(&buf, "{}", .{SerialConfig{ 1192 | .handshake = .software, 1193 | .baud_rate = 115200, 1194 | .parity = .none, 1195 | .word_size = .eight, 1196 | .stop_bits = .one, 1197 | }}), "115200@8N1 XON/XOFF")); 1198 | 1199 | try std.testing.expect(std.mem.eql(u8, try std.fmt.bufPrint(&buf, "{}", .{SerialConfig{ 1200 | .handshake = .hardware, 1201 | .baud_rate = 115200, 1202 | .parity = .none, 1203 | .word_size = .eight, 1204 | .stop_bits = .one, 1205 | }}), "115200@8N1 RTS/CTS")); 1206 | 1207 | try std.testing.expect(std.mem.eql(u8, try std.fmt.bufPrint(&buf, "{}", .{SerialConfig{ 1208 | .handshake = .none, 1209 | .baud_rate = 115200, 1210 | .parity = .none, 1211 | .word_size = .eight, 1212 | .stop_bits = .one, 1213 | }}), "115200@8N1")); 1214 | 1215 | try std.testing.expect(std.mem.eql(u8, try std.fmt.bufPrint(&buf, "{}", .{SerialConfig{ 1216 | .handshake = .none, 1217 | .baud_rate = 115200, 1218 | .parity = .even, 1219 | .word_size = .eight, 1220 | .stop_bits = .one, 1221 | }}), "115200@8E1")); 1222 | 1223 | try std.testing.expect(std.mem.eql(u8, try std.fmt.bufPrint(&buf, "{}", .{SerialConfig{ 1224 | .handshake = .none, 1225 | .baud_rate = 115200, 1226 | .parity = .odd, 1227 | .word_size = .eight, 1228 | .stop_bits = .one, 1229 | }}), "115200@8O1")); 1230 | 1231 | try std.testing.expect(std.mem.eql(u8, try std.fmt.bufPrint(&buf, "{}", .{SerialConfig{ 1232 | .handshake = .none, 1233 | .baud_rate = 115200, 1234 | .parity = .space, 1235 | .word_size = .eight, 1236 | .stop_bits = .one, 1237 | }}), "115200@8S1")); 1238 | 1239 | try std.testing.expect(std.mem.eql(u8, try std.fmt.bufPrint(&buf, "{}", .{SerialConfig{ 1240 | .handshake = .none, 1241 | .baud_rate = 115200, 1242 | .parity = .mark, 1243 | .word_size = .eight, 1244 | .stop_bits = .one, 1245 | }}), "115200@8M1")); 1246 | 1247 | try std.testing.expect(std.mem.eql(u8, try std.fmt.bufPrint(&buf, "{}", .{SerialConfig{ 1248 | .handshake = .none, 1249 | .baud_rate = 9600, 1250 | .parity = .mark, 1251 | .word_size = .five, 1252 | .stop_bits = .one, 1253 | }}), "9600@5M1")); 1254 | } 1255 | --------------------------------------------------------------------------------