commit 221c258514af1c6f798148a6b8009d0d24ad26e0 Author: Jeffrey C. Ollie Date: Sun Feb 11 19:29:23 2024 -0600 first diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c82b07 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +zig-cache +zig-out diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..91a73d6 --- /dev/null +++ b/build.zig @@ -0,0 +1,30 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + + const optimize = b.standardOptimizeOption(.{}); + + _ = b.addModule("hidapi", .{ + .root_source_file = .{ .path = "src/hidapi.zig" }, + }); + + // hidapi.linkLibC(); + // hidapi.linkSystemLibrary("hidapi-libusb"); + + // b.installArtifact(hidapi); + + const unit_tests = b.addTest(.{ + .root_source_file = .{ .path = "src/hidapi.zig" }, + .target = target, + .optimize = optimize, + }); + + unit_tests.linkLibC(); + unit_tests.linkSystemLibrary("hidapi-libusb"); + + const run_unit_tests = b.addRunArtifact(unit_tests); + + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_unit_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..ad5f0dc --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,6 @@ +.{ + .name = "zig-usbhid", + .version = "0.0.1", + .paths = .{""}, + .dependencies = .{}, +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..e62b93f --- /dev/null +++ b/flake.lock @@ -0,0 +1,144 @@ +{ + "nodes": { + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1673956053, + "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1705309234, + "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "locked": { + "lastModified": 1659877975, + "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "make-shell": { + "locked": { + "lastModified": 1634940815, + "narHash": "sha256-P69OmveboXzS+es1vQGS4bt+ckwbeIExqxfGLjGuJqA=", + "owner": "ursi", + "repo": "nix-make-shell", + "rev": "8add91681170924e4d0591b22f294aee3f5516f9", + "type": "github" + }, + "original": { + "owner": "ursi", + "repo": "nix-make-shell", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1707546158, + "narHash": "sha256-nYYJTpzfPMDxI8mzhQsYjIUX+grorqjKEU9Np6Xwy/0=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "d934204a0f8d9198e1e4515dd6fec76a139c87f0", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-unstable", + "type": "indirect" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1702350026, + "narHash": "sha256-A+GNZFZdfl4JdDphYKBJ5Ef1HOiFsP18vQe9mqjmUis=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "9463103069725474698139ab10f17a9d125da859", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-23.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "make-shell": "make-shell", + "nixpkgs": "nixpkgs", + "zig": "zig" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "zig": { + "inputs": { + "flake-compat": "flake-compat", + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1707611073, + "narHash": "sha256-sMsxVKXP5TLcaVMNlRZ7KlDsYGwDdJAMtY0DKmb+7fQ=", + "owner": "mitchellh", + "repo": "zig-overlay", + "rev": "aa4edff6f53e64443ca77e8d9ffe866f29e5b3d4", + "type": "github" + }, + "original": { + "owner": "mitchellh", + "repo": "zig-overlay", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..48d7c31 --- /dev/null +++ b/flake.nix @@ -0,0 +1,70 @@ +{ + description = "zig-usbnhid"; + + inputs = { + nixpkgs = { + url = "nixpkgs/nixos-unstable"; + }; + flake-utils = { + url = "github:numtide/flake-utils"; + }; + make-shell = { + url = "github:ursi/nix-make-shell"; + }; + zig = { + url = "github:mitchellh/zig-overlay"; + }; + # zls = { + # url = "github:zigtools/zls"; + # inputs.nixpkgs.follows = "nixpkgs"; + # inputs.zig-overlay.follows = "zig"; + # inputs.flake-utils.follows = "flake-utils"; + # }; + }; + + outputs = { + self, + nixpkgs, + flake-utils, + ... + } @ inputs: let + overlays = [ + # ( + # final: prev: { + # zigpkgs = inputs.zig.packages.${prev.system}; + # } + # ) + ]; + systems = builtins.attrNames inputs.zig.packages; + in + flake-utils.lib.eachSystem systems ( + system: let + pkgs = import nixpkgs { + inherit overlays system; + }; + in { + devShells.default = pkgs.mkShell { + nativeBuildInputs = [ + pkgs.hidapi + inputs.zig.packages.${system}.master + # inputs.zls.packages.${system}.zls + ]; + buildInputs = [ + pkgs.hidapi + ]; + LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ + pkgs.hidapi + ]; + shellHook = '' + name="zig-hidapi" + ''; + }; + packages.default = pkgs.zigStdenv.mkDerivation { + pname = "zig-hidapi"; + version = "0.1.0"; + buildInputs = [pkgs.hidapi]; + src = ./.; + }; + } + ); +} diff --git a/src/hidapi.zig b/src/hidapi.zig new file mode 100644 index 0000000..be26001 --- /dev/null +++ b/src/hidapi.zig @@ -0,0 +1,256 @@ +const std = @import("std"); + +const hidapi = @cImport({ + @cInclude("hidapi/hidapi.h"); +}); + +const MAX_REPORT_DESCRIPTOR_SIZE = hidapi.HID_API_MAX_REPORT_DESCRIPTOR_SIZE; + +const HidBusType = enum(hidapi.hid_bus_type) { + UNKNOWN = hidapi.HID_API_BUS_UNKNOWN, + USB = hidapi.HID_API_BUS_USB, + BLUETOOTH = hidapi.HID_API_BUS_BLUETOOTH, + I2C = hidapi.HID_API_BUS_I2C, + SPI = hidapi.HID_API_BUS_SPI, + _, +}; + +pub fn from_wchar(alloc: std.mem.Allocator, wide_string: [*c]const hidapi.wchar_t) !?[]u8 { + if (wide_string == null) return null; + var output = std.ArrayList(u8).init(alloc); + var writer = output.writer(); + var index: usize = 0; + while (wide_string[index] != 0) : (index += 1) { + var buf: [4]u8 = undefined; + const len = try std.unicode.utf8Encode(@intCast(wide_string[index]), &buf); + try writer.writeAll(buf[0..len]); + } + return try output.toOwnedSlice(); +} + +pub fn to_wchar(alloc: std.mem.Allocator, string: []const u8) ![*c]hidapi.wchar_t { + var list = std.ArrayList(hidapi.wchar_t).init(alloc); + errdefer list.deinit(); + var iter = std.unicode.Utf8View.init(string); + while (iter.next()) |codepoint| { + try list.append(@intCast(codepoint)); + } + return list.toOwnedSliceSentinel(0); +} + +pub fn err(alloc: std.mem.Allocator) ![]const u8 { + return try from_wchar(alloc, hidapi.hid_error(null)); +} + +const Device = struct { + device: ?*hidapi.hid_device, + + const Self = @This(); + + pub fn err(self: @This(), alloc: std.mem.Allocator) ![]const u8 { + return try from_wchar(alloc, hidapi.hid_error(self.device)); + } + + /// Sends a HID feature report to an open HID device. + pub fn send_feature_report(self: Self, data: []const u8) !c_int { + return hidapi.hid_send_feature_report(self.device, data.ptr, data.len); + } + + pub fn get_feature_report(self: Self, report_id: u8, buffer: []u8) ![]const u8 { + buffer[0] = report_id; + const length = hidapi.hid_get_feature_report(self.device, buffer.ptr, buffer.len); + if (length < 0) return error.HIDAPiError; + return buffer[0..length]; + } + + pub fn get_input_report(self: @This(), report_id: u8, buffer: []u8) ![]const u8 { + buffer[0] = report_id; + const result = hidapi.hid_get_input_report(self.device, buffer.ptr, buffer.len); + if (result < 1) return error.HIDApiError; + return buffer[0..result]; + } + pub fn set_nonblocking(self: @This(), nonblocking: bool) !void { + const result = hidapi.hid_set_nonblocking(self.device, if (nonblocking) 1 else 0); + if (result < 0) return error.HIDApiError; + } + + pub fn write(self: @This(), data: []const u8) !c_int { + const result = hidapi.hid_write(self.device, data.ptr, data.len); + if (result < 0) return error.HIDApiError; + return result; + } + + pub fn read_timeout(self: @This(), buffer: []u8, milliseconds: c_int) !?[]const u8 { + const result = hidapi.hid_read_timeout(self.device, buffer.ptr, buffer.len, milliseconds); + if (result < 0) return error.HIDApiError; + if (result == 0) return null; + return buffer[0..result]; + } + + pub fn read(self: @This(), buffer: []u8) !?[]const u8 { + const result = hidapi.hid_read(self.device, buffer.ptr, buffer.len); + if (result < 0) return error.HIDApiError; + if (result == 0) return null; + return buffer[0..result]; + } + + pub fn get_indexed_string(self: @This(), string_index: c_int, buffer: []hidapi.wchar_t) !void { + const result = hidapi.hid_get_indexed_string(self.device, string_index, buffer.ptr, buffer.len); + if (result < 0) return error.HIDApiError; + } + + pub fn get_report_descriptor(self: @This(), buffer: []u8) ![]const u8 { + const result = hidapi.hid_get_report_descriptor(self.device, buffer.ptr, buffer.len); + if (result < 0) return error.HIDApiError; + return buffer[0..result]; + } + + pub fn close(self: *Self) void { + hidapi.hid_close(self.device); + self.device = null; + } +}; + +pub fn open(alloc: std.mem.Allocator, vendor_id: c_ushort, product_id: c_ushort, serial_number: ?[]const u8) !Device { + const s = if (serial_number) |s| try to_wchar(alloc, s) else null; + const device = hidapi.hid_open(vendor_id, product_id, s); + if (device) |_| return .{ .device = device }; + return error.HIDApiError; +} + +pub fn open_path(path: [:0]const u8) !Device { + const d = Device{ .device = hidapi.hid_open_path(path.ptr) }; + if (d.device != null) return d; + return error.HIDApiError; +} + +const DeviceInfo = struct { + vendor_id: u16, + product_id: u16, + path: []u8, + serial_number: ?[]u8, + release_number: c_ushort, + manufacturer: ?[]u8, + product: ?[]u8, + usage_page: c_ushort, + usage: c_ushort, + interface_number: c_int, + bus_type: HidBusType, + + const Self = @This(); + + pub fn open(self: Self) !Device { + const d = Device{ + .device = hidapi.hid_open_path(self.path), + }; + if (d.device) return d; + const err = hidapi.hid_error(null); + d.err = try from_wchar(err, &d.err); + return d; + } + + pub fn format(self: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + try writer.print("{x:0>4} {x:0>4} {s} '{?s}'\n", .{ self.vendor_id, self.product_id, self.path, self.serial_number }); + try writer.print("Manufacturer: {?s}\n", .{self.manufacturer}); + try writer.print("Product: {?s}\n", .{self.product}); + try writer.print("Release: 0x{x}\n", .{self.release_number}); + try writer.print("Interface: 0x{x}\n", .{self.interface_number}); + try writer.print("Usage (page): 0x{x} (0x{x})\n", .{ self.usage, self.usage_page }); + try writer.print("Bus type: {} ({})\n", .{ + self.bus_type, + @intFromEnum(self.bus_type), + }); + } +}; + +const DeviceInfos = struct { + arena: std.heap.ArenaAllocator, + devices: []DeviceInfo, + + pub fn deinit(self: @This()) void { + self.arena.deinit(); + } +}; + +pub fn enumerate(allocator: std.mem.Allocator, vendor_id: c_ushort, product_id: c_ushort) !DeviceInfos { + var arena = std.heap.ArenaAllocator.init(allocator); + errdefer arena.deinit(); + + const alloc = arena.allocator(); + + var dl = std.ArrayList(DeviceInfo).init(alloc); + + var current: ?*hidapi.hid_device_info = hidapi.hid_enumerate(vendor_id, product_id); + defer hidapi.hid_free_enumeration(current); + + while (current) |hid_device_info| { + try dl.append(.{ + .vendor_id = hid_device_info.vendor_id, + .product_id = hid_device_info.product_id, + .release_number = hid_device_info.release_number, + .path = try alloc.dupe(u8, std.mem.span(hid_device_info.path)), + .serial_number = try from_wchar(alloc, hid_device_info.serial_number), + .manufacturer = try from_wchar(alloc, hid_device_info.manufacturer_string), + .product = try from_wchar(alloc, hid_device_info.product_string), + .usage_page = hid_device_info.usage_page, + .usage = hid_device_info.usage, + .interface_number = hid_device_info.interface_number, + .bus_type = @enumFromInt(hid_device_info.bus_type), + }); + current = hid_device_info.next; + } + + return .{ + .arena = arena, + .devices = try dl.toOwnedSlice(), + }; +} + +pub fn init() !void { + const ret = hidapi.hid_init(); + if (ret != 0) return error.HidApiInitError; +} + +pub fn exit() !void { + const ret = hidapi.hid_exit(); + if (ret != 0) return error.HidApiExitError; +} + +pub fn version() struct { major: c_int, minor: c_int, patch: c_int } { + if (hidapi.hid_version()) |v| { + return .{ + .major = v.*.major, + .minor = v.*.minor, + .patch = v.*.patch, + }; + } + return .{ .major = 0, .minor = 0, .patch = 0 }; +} + +test "version-1" { + const v = version(); + std.debug.print("\n{} {} {}\n", .{ v.major, v.minor, v.patch }); + try std.testing.expectEqual(@as(i32, 0), v.major); + try std.testing.expectEqual(@as(i32, 14), v.minor); + try std.testing.expectEqual(@as(i32, 0), v.patch); +} + +pub fn version_str() []const u8 { + return std.mem.span(hidapi.hid_version_str()); +} + +test "version-2" { + const v = version_str(); + std.debug.print("\n{s}\n", .{v}); + try std.testing.expectEqualStrings("0.14.0", v); +} + +test "enumerate" { + try init(); + defer exit() catch unreachable; + const device_infos = try enumerate(std.testing.allocator); + defer device_infos.deinit(); + for (device_infos.devices) |device_info| { + std.debug.print("{}\n\n", .{device_info}); + } +}