lib/sys/src/evdev.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const builtin = @import("builtin");
  3 
  4 pub const supported = builtin.os.tag == .linux;
  5 pub const max_query_bytes: usize = std.math.maxInt(u14);
  6 
  7 pub const Error = error{
  8     UnsupportedPlatform,
  9     AccessDenied,
 10     OpenFailed,
 11     QueryFailed,
 12     ReadFailed,
 13 };
 14 
 15 pub const event_kind = struct {
 16     pub const syn: u16 = 0x00;
 17     pub const key: u16 = 0x01;
 18     pub const abs: u16 = 0x03;
 19 };
 20 
 21 pub const button = struct {
 22     pub const south: u16 = 0x130;
 23     pub const east: u16 = 0x131;
 24     pub const north: u16 = 0x133;
 25     pub const west: u16 = 0x134;
 26     pub const left_bumper: u16 = 0x136;
 27     pub const right_bumper: u16 = 0x137;
 28     pub const select: u16 = 0x13A;
 29     pub const start: u16 = 0x13B;
 30     pub const mode: u16 = 0x13C;
 31     pub const left_thumb: u16 = 0x13D;
 32     pub const right_thumb: u16 = 0x13E;
 33     pub const dpad_up: u16 = 0x220;
 34     pub const dpad_down: u16 = 0x221;
 35     pub const dpad_left: u16 = 0x222;
 36     pub const dpad_right: u16 = 0x223;
 37     pub const gamepad_marker: u16 = south;
 38 };
 39 
 40 pub const axis = struct {
 41     pub const x: u16 = 0x00;
 42     pub const y: u16 = 0x01;
 43     pub const z: u16 = 0x02;
 44     pub const rx: u16 = 0x03;
 45     pub const ry: u16 = 0x04;
 46     pub const rz: u16 = 0x05;
 47     pub const hat0x: u16 = 0x10;
 48     pub const hat0y: u16 = 0x11;
 49 };
 50 
 51 pub const key_bit_count = 0x300;
 52 pub const abs_bit_count = 0x40;
 53 pub const KeyBits = [key_bit_count / 8]u8;
 54 pub const AbsBits = [abs_bit_count / 8]u8;
 55 
 56 pub const InputEvent = extern struct {
 57     seconds: u64,
 58     microseconds: u64,
 59     kind: u16,
 60     code: u16,
 61     value: i32,
 62 };
 63 
 64 pub const AbsInfo = extern struct {
 65     value: i32,
 66     minimum: i32,
 67     maximum: i32,
 68     fuzz: i32,
 69     flat: i32,
 70     resolution: i32,
 71 };
 72 
 73 pub fn hasBit(bits: []const u8, index: u16) bool {
 74     const byte = index / 8;
 75     if (byte >= bits.len) return false;
 76     return (bits[byte] >> @intCast(index % 8)) & 1 != 0;
 77 }
 78 
 79 pub fn isGamepad(key_bits: *const KeyBits) bool {
 80     return hasBit(key_bits, button.gamepad_marker);
 81 }
 82 
 83 pub fn normalizeAxis(info: AbsInfo, value: i32) f32 {
 84     if (info.maximum <= info.minimum) return 0;
 85     const span: f32 = @floatFromInt(info.maximum - info.minimum);
 86     const offset: f32 = @floatFromInt(value - info.minimum);
 87     const unit = (offset / span) * 2.0 - 1.0;
 88     return std.math.clamp(unit, -1.0, 1.0);
 89 }
 90 
 91 pub fn normalizeTrigger(info: AbsInfo, value: i32) f32 {
 92     if (info.maximum <= info.minimum) return 0;
 93     const span: f32 = @floatFromInt(info.maximum - info.minimum);
 94     const offset: f32 = @floatFromInt(value - info.minimum);
 95     return std.math.clamp(offset / span, 0.0, 1.0);
 96 }
 97 
 98 pub fn eventPath(buffer: []u8, index: u32) ![]const u8 {
 99     return std.fmt.bufPrint(buffer, "/dev/input/event{d}", .{index});
100 }
101 
102 const linux = std.os.linux;
103 
104 fn ioctlRead(fd: i32, nr: u8, out: []u8) Error!void {
105     if (comptime !supported) return error.UnsupportedPlatform;
106     if (out.len > max_query_bytes) return error.QueryFailed;
107     const request: u32 = (2 << 30) | (@as(u32, @intCast(out.len)) << 16) | (@as(u32, 'E') << 8) | nr;
108     const rc = linux.ioctl(fd, request, @intFromPtr(out.ptr));
109     if (linux.errno(rc) != .SUCCESS) return error.QueryFailed;
110 }
111 
112 pub const Device = struct {
113     fd: i32,
114 
115     pub fn open(path: []const u8) Error!Device {
116         if (comptime !supported) return error.UnsupportedPlatform;
117         var path_buffer: [128]u8 = undefined;
118         if (path.len >= path_buffer.len) return error.OpenFailed;
119         @memcpy(path_buffer[0..path.len], path);
120         path_buffer[path.len] = 0;
121         const rc = linux.open(
122             path_buffer[0..path.len :0],
123             .{ .ACCMODE = .RDONLY, .NONBLOCK = true, .CLOEXEC = true },
124             0,
125         );
126         return switch (linux.errno(rc)) {
127             .SUCCESS => .{ .fd = @intCast(rc) },
128             .ACCES, .PERM => error.AccessDenied,
129             else => error.OpenFailed,
130         };
131     }
132 
133     pub fn close(self: Device) void {
134         if (comptime !supported) return;
135         _ = linux.close(self.fd);
136     }
137 
138     pub fn name(self: Device, buffer: []u8) Error![]const u8 {
139         try ioctlRead(self.fd, 0x06, buffer);
140         const len = std.mem.indexOfScalar(u8, buffer, 0) orelse buffer.len;
141         return buffer[0..len];
142     }
143 
144     pub fn keyBits(self: Device, bits: *KeyBits) Error!void {
145         @memset(bits, 0);
146         try ioctlRead(self.fd, 0x20 + @as(u8, @intCast(event_kind.key)), bits);
147     }
148 
149     pub fn absBits(self: Device, bits: *AbsBits) Error!void {
150         @memset(bits, 0);
151         try ioctlRead(self.fd, 0x20 + @as(u8, @intCast(event_kind.abs)), bits);
152     }
153 
154     pub fn absInfo(self: Device, code: u16) Error!AbsInfo {
155         var info: AbsInfo = undefined;
156         try ioctlRead(self.fd, 0x40 + @as(u8, @intCast(code)), std.mem.asBytes(&info));
157         return info;
158     }
159 
160     pub fn readEvents(self: Device, events: []InputEvent) Error!usize {
161         if (comptime !supported) return error.UnsupportedPlatform;
162         if (events.len == 0) return 0;
163         const rc = linux.read(self.fd, @ptrCast(events.ptr), events.len * @sizeOf(InputEvent));
164         return switch (linux.errno(rc)) {
165             .SUCCESS => @as(usize, @intCast(rc)) / @sizeOf(InputEvent),
166             .AGAIN => 0,
167             else => error.ReadFailed,
168         };
169     }
170 };
171 
172 test "bit probing reads packed capability masks" {
173     var bits: KeyBits = @splat(0);
174     bits[button.gamepad_marker / 8] = 1 << (button.gamepad_marker % 8);
175     try std.testing.expect(hasBit(&bits, button.gamepad_marker));
176     try std.testing.expect(isGamepad(&bits));
177     try std.testing.expect(!hasBit(&bits, button.start));
178 
179     const empty: KeyBits = @splat(0);
180     try std.testing.expect(!isGamepad(&empty));
181     try std.testing.expect(!hasBit(empty[0..1], 800));
182 }
183 
184 test "axis normalization spans the device range" {
185     const info = AbsInfo{ .value = 0, .minimum = -32768, .maximum = 32767, .fuzz = 0, .flat = 0, .resolution = 0 };
186     try std.testing.expectApproxEqAbs(@as(f32, -1.0), normalizeAxis(info, -32768), 0.001);
187     try std.testing.expectApproxEqAbs(@as(f32, 1.0), normalizeAxis(info, 32767), 0.001);
188     try std.testing.expectApproxEqAbs(@as(f32, 0.0), normalizeAxis(info, 0), 0.01);
189 
190     const trigger = AbsInfo{ .value = 0, .minimum = 0, .maximum = 255, .fuzz = 0, .flat = 0, .resolution = 0 };
191     try std.testing.expectApproxEqAbs(@as(f32, 0.0), normalizeTrigger(trigger, 0), 0.001);
192     try std.testing.expectApproxEqAbs(@as(f32, 1.0), normalizeTrigger(trigger, 255), 0.001);
193     try std.testing.expectApproxEqAbs(@as(f32, 0.5), normalizeTrigger(trigger, 128), 0.01);
194 
195     const degenerate = AbsInfo{ .value = 0, .minimum = 5, .maximum = 5, .fuzz = 0, .flat = 0, .resolution = 0 };
196     try std.testing.expectEqual(@as(f32, 0), normalizeAxis(degenerate, 5));
197 }
198 
199 test "input event layout matches the kernel wire size" {
200     try std.testing.expectEqual(@as(usize, 24), @sizeOf(InputEvent));
201     var buffer: [32]u8 = undefined;
202     try std.testing.expectEqualStrings("/dev/input/event7", try eventPath(&buffer, 7));
203 }
204 
205 test "query buffers reject lengths outside the ioctl encoding" {
206     if (comptime !supported) return error.SkipZigTest;
207     var oversized: [max_query_bytes + 1]u8 = undefined;
208     try std.testing.expectError(error.QueryFailed, ioctlRead(-1, 0, &oversized));
209 }