first
This commit is contained in:
commit
221c258514
6 changed files with 508 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
zig-cache
|
||||
zig-out
|
30
build.zig
Normal file
30
build.zig
Normal file
|
@ -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);
|
||||
}
|
6
build.zig.zon
Normal file
6
build.zig.zon
Normal file
|
@ -0,0 +1,6 @@
|
|||
.{
|
||||
.name = "zig-usbhid",
|
||||
.version = "0.0.1",
|
||||
.paths = .{""},
|
||||
.dependencies = .{},
|
||||
}
|
144
flake.lock
Normal file
144
flake.lock
Normal file
|
@ -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
|
||||
}
|
70
flake.nix
Normal file
70
flake.nix
Normal file
|
@ -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 = ./.;
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
256
src/hidapi.zig
Normal file
256
src/hidapi.zig
Normal file
|
@ -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});
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue