192 lines
7.3 KiB
Zig
192 lines
7.3 KiB
Zig
//! 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
|
|
}
|