//! Pure decoders for USB HID **boot-protocol** reports — the simplified, //! fixed-format reports a boot keyboard and boot mouse send, the USB analog of //! the PS/2 scancode and mouse-packet decoders. No I/O: these turn report bytes //! into make/break transitions and motion, which the usb-hid drivers publish to //! the input service. Host-testable in isolation (like mouse-packet.zig). //! //! "Boot protocol" is a USB HID term (USB HID 1.11 §B) — the device reports in //! this fixed layout after SET_PROTOCOL(boot); it has nothing to do with system //! boot. const std = @import("std"); // --- keyboard --------------------------------------------------------------- /// The 8-byte boot keyboard report: a modifier bitmap, a reserved byte, and up /// to six concurrently-pressed key usages. pub const KeyboardReport = extern struct { modifiers: u8 = 0, reserved: u8 = 0, keys: [6]u8 = .{ 0, 0, 0, 0, 0, 0 }, }; // The modifier byte's bits (HID keyboard boot report). pub const modifier_left_control: u8 = 1 << 0; pub const modifier_left_shift: u8 = 1 << 1; pub const modifier_left_alt: u8 = 1 << 2; pub const modifier_left_gui: u8 = 1 << 3; pub const modifier_right_control: u8 = 1 << 4; pub const modifier_right_shift: u8 = 1 << 5; pub const modifier_right_alt: u8 = 1 << 6; pub const modifier_right_gui: u8 = 1 << 7; pub const TransitionKind = enum { pressed, released }; /// One key going down or up. `usage` is a HID keyboard-page usage — modifier keys /// map to usages 224..231 — which is exactly the input protocol's `Keycode`. pub const Transition = struct { kind: TransitionKind, usage: u8 }; // A report can change at most all 8 modifiers and all 6 keys at once. pub const max_transitions = 8 + 6; pub const Transitions = struct { items: [max_transitions]Transition = undefined, count: usize = 0, fn add(self: *Transitions, transition: Transition) void { if (self.count < self.items.len) { self.items[self.count] = transition; self.count += 1; } } pub fn slice(self: *const Transitions) []const Transition { return self.items[0..self.count]; } }; /// Turns a stream of boot keyboard reports into make/break transitions by diffing /// each report against the last. pub const KeyboardDecoder = struct { previous: KeyboardReport = .{}, pub fn feed(self: *KeyboardDecoder, current: KeyboardReport) Transitions { var out = Transitions{}; // Rollover: 0x01 (ErrorRollOver) means more keys are held than the report // can carry, so the key array is invalid. Emit nothing and keep the prior // state (so the eventual releases still resolve against real keys). for (current.keys) |key| { if (key == 0x01) return out; } // Modifiers: one make/break per changed bit; modifier usages are 224..231. const changed = current.modifiers ^ self.previous.modifiers; var bit: u3 = 0; while (true) : (bit += 1) { const mask = @as(u8, 1) << bit; if (changed & mask != 0) { out.add(.{ .kind = if (current.modifiers & mask != 0) .pressed else .released, .usage = 224 + @as(u8, bit), }); } if (bit == 7) break; } // Keys made: present now, absent before. for (current.keys) |key| { if (key != 0 and !contains(&self.previous.keys, key)) out.add(.{ .kind = .pressed, .usage = key }); } // Keys broken: present before, absent now. for (self.previous.keys) |key| { if (key != 0 and !contains(¤t.keys, key)) out.add(.{ .kind = .released, .usage = key }); } self.previous = current; return out; } }; fn contains(keys: *const [6]u8, value: u8) bool { for (keys) |key| { if (key == value) return true; } return false; } // --- mouse ------------------------------------------------------------------ /// A decoded boot mouse report: the button bitmap and relative motion. The wheel /// byte is present only on 4-byte reports (QEMU's usb-mouse sends one). pub const MouseReport = struct { buttons: u8 = 0, dx: i8 = 0, dy: i8 = 0, wheel: i8 = 0, has_wheel: bool = false, }; pub const mouse_button_left: u8 = 1 << 0; pub const mouse_button_right: u8 = 1 << 1; pub const mouse_button_middle: u8 = 1 << 2; /// Parse a 3- or 4-byte boot mouse report. Note HID reports Y in screen /// convention (positive = down), so — unlike PS/2 — `dy` is NOT negated. pub fn parseMouse(bytes: []const u8) ?MouseReport { if (bytes.len < 3) return null; return .{ .buttons = bytes[0], .dx = @bitCast(bytes[1]), .dy = @bitCast(bytes[2]), .wheel = if (bytes.len >= 4) @bitCast(bytes[3]) else 0, .has_wheel = bytes.len >= 4, }; } // --- tests ------------------------------------------------------------------ test "keyboard diff produces make and break transitions" { var decoder = KeyboardDecoder{}; // Press 'a' (usage 4). var t = decoder.feed(.{ .keys = .{ 4, 0, 0, 0, 0, 0 } }); try std.testing.expectEqual(@as(usize, 1), t.count); try std.testing.expectEqual(TransitionKind.pressed, t.items[0].kind); try std.testing.expectEqual(@as(u8, 4), t.items[0].usage); // Hold 'a', press 'b' (usage 5): only 'b' is new. t = decoder.feed(.{ .keys = .{ 4, 5, 0, 0, 0, 0 } }); try std.testing.expectEqual(@as(usize, 1), t.count); try std.testing.expectEqual(@as(u8, 5), t.items[0].usage); // Release everything: 'a' and 'b' both break. t = decoder.feed(.{ .keys = .{ 0, 0, 0, 0, 0, 0 } }); try std.testing.expectEqual(@as(usize, 2), t.count); try std.testing.expectEqual(TransitionKind.released, t.items[0].kind); // Press Left Shift (modifier bit 1 -> usage 225). t = decoder.feed(.{ .modifiers = modifier_left_shift }); try std.testing.expectEqual(@as(usize, 1), t.count); try std.testing.expectEqual(@as(u8, 225), t.items[0].usage); try std.testing.expectEqual(TransitionKind.pressed, t.items[0].kind); } test "rollover report is ignored but state is preserved" { var decoder = KeyboardDecoder{}; _ = decoder.feed(.{ .keys = .{ 4, 0, 0, 0, 0, 0 } }); // press 'a' const rollover = decoder.feed(.{ .keys = .{ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01 } }); try std.testing.expectEqual(@as(usize, 0), rollover.count); // 'a' is still considered down, so releasing all keys now breaks it. const release = decoder.feed(.{ .keys = .{ 0, 0, 0, 0, 0, 0 } }); try std.testing.expectEqual(@as(usize, 1), release.count); try std.testing.expectEqual(@as(u8, 4), release.items[0].usage); try std.testing.expectEqual(TransitionKind.released, release.items[0].kind); } test "mouse report parses motion without inverting Y" { const three = parseMouse(&.{ mouse_button_left, 5, 0xFB }).?; // dy = -5 try std.testing.expectEqual(mouse_button_left, three.buttons); try std.testing.expectEqual(@as(i8, 5), three.dx); try std.testing.expectEqual(@as(i8, -5), three.dy); try std.testing.expect(!three.has_wheel); const four = parseMouse(&.{ 0, 0, 0, 0xFF }).?; // wheel = -1 try std.testing.expect(four.has_wheel); try std.testing.expectEqual(@as(i8, -1), four.wheel); try std.testing.expect(parseMouse(&.{ 0, 0 }) == null); // too short }