//! Direct Linux usbfs access for tools that speak jtagice3-over-bulk //! (Microchip PICkit 4/5, SNAP, ...). Avoids a libusb dependency: the //! unprivileged device node provided by the udev rule supports claiming an //! interface and synchronous bulk transfers via ioctls. const std = @import("std"); const Io = std.Io; const Allocator = std.mem.Allocator; const ArrayList = std.ArrayList; const Dir = Io.Dir; const File = Io.File; const posix = std.posix; const mem = std.mem; fn closeFile(io: Io, file: File) void { file.close(io); } fn closeFd(io: Io, fd: posix.fd_t) void { (File{ .handle = fd, .flags = .{ .nonblocking = false } }).close(io); } const DeviceInfo = struct { /// /dev/bus/usb/BBB/DDD node used for IO. node: []u8, vid: u16, pid: u16, name: []u8, serial: []u8, pub fn deinit(self: DeviceInfo, allocator: Allocator) void { allocator.free(self.node); allocator.free(self.name); allocator.free(self.serial); } }; pub const Id = struct { vid: u16, pid: u16 }; /// All connected devices whose VID:PID matches `wanted`. pub fn listDevices( io: Io, gpa: Allocator, wanted: []const Id, ) ![]DeviceInfo { var devices: ArrayList(DeviceInfo) = .empty; errdefer { for (devices.items) |dev| dev.deinit(gpa); devices.deinit(gpa); } var dir: Dir = Dir.openDirAbsolute(io, "/sys/bus/usb/devices", .{ .iterate = true }) catch |err| switch (err) { error.FileNotFound => return devices.toOwnedSlice(gpa), else => return err, }; defer dir.close(io); var it = dir.iterateAssumeFirstIteration(); while (try it.next(io)) |entry| { // Skip root hubs ("usbN") and interface entries ("N-P:M.N"). if (mem.findScalar(u8, entry.name, '-') == null) continue; if (mem.findScalar(u8, entry.name, ':') != null) continue; const vid_raw = readAttr(io, gpa, dir, entry.name, "idVendor") catch continue; defer gpa.free(vid_raw); const pid_raw = readAttr(io, gpa, dir, entry.name, "idProduct") catch continue; defer gpa.free(pid_raw); const vid = std.fmt.parseInt(u16, mem.trim(u8, vid_raw, " \n\r\t"), 16) catch continue; const pid = std.fmt.parseInt(u16, mem.trim(u8, pid_raw, " \n\r\t"), 16) catch continue; var matched = false; for (wanted) |w| { if (w.vid == vid and w.pid == pid) { matched = true; break; } } if (!matched) continue; const busnum_raw = readAttr(io, gpa, dir, entry.name, "busnum") catch continue; defer gpa.free(busnum_raw); const devnum_raw = readAttr(io, gpa, dir, entry.name, "devnum") catch continue; defer gpa.free(devnum_raw); const busnum = std.fmt.parseInt(u16, mem.trim(u8, busnum_raw, " \n\r\t"), 10) catch continue; const devnum = std.fmt.parseInt(u16, mem.trim(u8, devnum_raw, " \n\r\t"), 10) catch continue; const product = readAttr(io, gpa, dir, entry.name, "product") catch try gpa.dupe(u8, ""); defer gpa.free(product); const serial = readAttr(io, gpa, dir, entry.name, "serial") catch try gpa.dupe(u8, ""); defer gpa.free(serial); try devices.append(gpa, .{ .node = try std.fmt.allocPrint(gpa, "/dev/bus/usb/{d:0>3}/{d:0>3}", .{ busnum, devnum }), .vid = vid, .pid = pid, .name = try gpa.dupe(u8, mem.trim(u8, product, " \n\r\t")), .serial = try gpa.dupe(u8, mem.trim(u8, serial, " \n\r\t")), }); } return devices.toOwnedSlice(gpa); } fn childName(gpa: Allocator, parent: []const u8, child: []const u8) ![]u8 { return std.fmt.allocPrint(gpa, "{s}/{s}", .{ parent, child }); } fn readAttr(io: Io, gpa: Allocator, dir: Io.Dir, device: []const u8, attr: []const u8) ![]u8 { const path = try childName(gpa, device, attr); defer gpa.free(path); return dir.readFileAlloc(io, path, gpa, .limited(256)); } pub const OpenError = error{ AccessDenied, FileNotFound, NoBulkEndpoints, Unexpected, } || posix.OpenError || posix.ReadError || Allocator.Error; pub const TransferError = error{ Timeout, Disconnected, WriteFailed, }; pub const Transport = struct { pub const header_size = 16; fd: posix.fd_t, interface: u32, ep_out: u8, ep_in: u8, gpa: Allocator, io: Io, /// Claim the vendor-specific scripting interface of `node`. pub fn open(node_path: []const u8, gpa: Allocator, io: Io) OpenError!Transport { // Peek at the descriptors before opening RDWR so missing nodes and // permission problems produce distinct errors. const desc = blk: { const ro = posix.openat(posix.AT.FDCWD, node_path, .{ .ACCMODE = .RDONLY }, 0) catch |err| { return switch (err) { error.AccessDenied => error.AccessDenied, error.FileNotFound => error.FileNotFound, else => error.Unexpected, }; }; defer closeFd(io, ro); var buf: [1024]u8 = undefined; const n = posix.read(ro, &buf) catch return error.Unexpected; break :blk try gpa.dupe(u8, buf[0..n]); }; defer gpa.free(desc); const eps = findVendorBulkEndpoints(desc) catch return error.NoBulkEndpoints; const fd = posix.openat(posix.AT.FDCWD, node_path, .{ .ACCMODE = .RDWR, .CLOEXEC = true }, 0) catch |err| { return switch (err) { error.AccessDenied => error.AccessDenied, error.FileNotFound => error.FileNotFound, else => error.Unexpected, }; }; errdefer closeFd(io, fd); const rc = std.os.linux.ioctl(fd, USBDEVFS_CLAIMINTERFACE, @intFromPtr(&eps.interface)); if (posix.errno(rc) != .SUCCESS) return error.Unexpected; return .{ .fd = fd, .interface = eps.interface, .ep_out = eps.out, .ep_in = eps.in, .gpa = gpa, .io = io, }; } pub fn close(self: *Transport) void { var iface = self.interface; _ = std.os.linux.ioctl(self.fd, USBDEVFS_RELEASEINTERFACE, @intFromPtr(&iface)); closeFd(self.io, self.fd); } /// One synchronous bulk-IN transfer; short reads are normal (the tool /// terminates messages with a short packet). pub fn read(self: *Transport, buf: []u8, timeout_ms: u32) TransferError!usize { var t = BulkTransfer{ .ep = self.ep_in, .len = @intCast(buf.len), .timeout_ms = timeout_ms, .data = buf.ptr, }; const rc = std.os.linux.ioctl(self.fd, USBDEVFS_BULK, @intFromPtr(&t)); switch (posix.errno(rc)) { .SUCCESS => return @intCast(rc), .TIMEDOUT => return error.Timeout, .NODEV, .PIPE => return error.Disconnected, .INTR => return self.read(buf, timeout_ms), else => return error.WriteFailed, } } pub fn write(self: *Transport, data: []const u8, timeout_ms: u32) TransferError!void { var t = BulkTransfer{ .ep = self.ep_out, .len = @intCast(data.len), .timeout_ms = timeout_ms, .data = @constCast(data.ptr), }; const rc = std.os.linux.ioctl(self.fd, USBDEVFS_BULK, @intFromPtr(&t)); switch (posix.errno(rc)) { .SUCCESS => return, .TIMEDOUT => return error.Timeout, .NODEV, .PIPE => return error.Disconnected, .INTR => return self.write(data, timeout_ms), else => return error.WriteFailed, } } /// usbfs bulk ioctl argument; void* makes it 16 bytes on 64-bit. const BulkTransfer = extern struct { ep: u32, len: u32, timeout_ms: u32, data: [*]u8, }; const USBDEVFS_CLAIMINTERFACE: usize = 0x8004550F; // _IOR('U', 15, uint) const USBDEVFS_RELEASEINTERFACE: usize = 0x80045510; // _IOR('U', 16, uint) const USBDEVFS_BULK: usize = 0xC0185502; // _IOWR('U', 2, struct usbdevfs_bulktransfer) const Endpoints = struct { interface: u32, out: u8, in: u8, }; /// Scan concatenated USB descriptors (what usbfs exposes at offset 0) /// for the bulk IN/OUT pair of the first vendor-specific interface. fn findVendorBulkEndpoints(desc: []const u8) !Endpoints { var i: usize = 18; // skip device descriptor var interface: u32 = 0; var vendor_interface = false; var out: ?u8 = null; var in: ?u8 = null; while (i + 2 <= desc.len) { const len = desc[i]; if (len < 2 or i + len > desc.len) break; const kind = desc[i + 1]; switch (kind) { 0x04 => { // interface if (out != null and in != null) break; out = null; in = null; interface = desc[i + 2]; vendor_interface = desc[i + 5] == 0xFF; }, 0x05 => { // endpoint if (!vendor_interface) break; const address = desc[i + 2]; const attributes = desc[i + 3]; if ((attributes & 0x03) == 0x02) { // bulk if ((address & 0x80) != 0) { if (in == null) in = address; } else { if (out == null) out = address; } } }, else => {}, } i += len; } if (out == null or in == null) return error.NoBulkEndpoints; return .{ .interface = interface, .out = out.?, .in = in.? }; } }; test "findVendorBulkEndpoints parses a PICkit-5 style config" { // device descriptor (18B) + config with one vendor interface (IF0) // holding one bulk OUT (0x01) and one bulk IN (0x81) endpoint. const desc = [_]u8{ // device descriptor 0x12, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x40, 0xD8, 0x04, 0x36, 0x90, 0x00, 0x01, 0x01, 0x02, 0x03, 0x01, // config descriptor (9B) wTotalLength = 9+9+7+7 = 32 0x09, 0x02, 32, 0x00, 0x01, 0x01, 0x00, 0x80, 0xFA, // interface 0, vendor specific 0x09, 0x04, 0x00, 0x00, 0x02, 0xFF, 0x00, 0x00, 0x00, // bulk OUT 0x01, max packet 512 0x07, 0x05, 0x01, 0x02, 0x00, 0x02, 0x00, 0x00, // bulk IN 0x81 0x07, 0x05, 0x81, 0x02, 0x00, 0x02, 0x00, 0x00, }; const eps = try Transport.findVendorBulkEndpoints(&desc); try std.testing.expectEqual(@as(u32, 0), eps.interface); try std.testing.expectEqual(@as(u8, 0x01), eps.out); try std.testing.expectEqual(@as(u8, 0x81), eps.in); } test "findVendorBulkEndpoints ignores non-vendor interfaces" { const desc = [_]u8{ 0x12, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x40, 0xD8, 0x04, 0x36, 0x90, 0x00, 0x01, 0x01, 0x02, 0x03, 0x01, // config with a CDC-data style interface (class 0x0A) 0x09, 0x02, 25, 0x00, 0x01, 0x01, 0x00, 0x80, 0xFA, 0x09, 0x04, 0x00, 0x00, 0x02, 0x0A, 0x00, 0x00, 0x00, 0x07, 0x05, 0x01, 0x02, 0x00, 0x02, 0x00, 0x00, 0x07, 0x05, 0x81, 0x02, 0x00, 0x02, 0x00, 0x00, }; try std.testing.expectError(error.NoBulkEndpoints, Transport.findVendorBulkEndpoints(&desc)); }