danos/system/drivers/usb-hid/keyboard.zig

175 lines
7.9 KiB
Zig

//! USB HID boot keyboard driver.
//!
//! Spawned by the device manager when the xHCI bus driver reports a HID / boot /
//! keyboard interface (class 3, subclass 1, protocol 1); its assigned device id
//! arrives as argv[1] and an optional layout name ("us", "gb", ...) as argv[2].
//! It owns no hardware: it opens its device through the USB transfer protocol
//! (`usb`), asks the device for the boot protocol, subscribes to its
//! interrupt-IN endpoint, and turns each 8-byte boot report into input-protocol
//! events, published to the input service — the USB analogue of ps2-bus/keyboard.
//!
//! interrupt report -> hid-report diff -> key_down / key_up
//! -> xkeyboard-config -> character -> key_press
//!
//! Because a USB keyboard's usages ARE the input protocol's keycodes (both are
//! HID keyboard page 0x07), the decode is nearly 1:1 — no scancode translation.
const std = @import("std");
const ipc = @import("ipc");
const process = @import("process");
const service = @import("service");
const input = @import("input-client");
const device_manager = @import("driver");
const logging = @import("logging");
const usb = @import("usb");
const usb_abi = @import("usb-abi");
const xkb = @import("xkeyboard-config");
const hid = @import("hid-report.zig");
const input_protocol = @import("input-protocol");
// The modifier state a character lookup needs — derived from the report's
// modifier byte, plus the driver-tracked caps-lock toggle.
const ModifierSnapshot = struct {
shift: bool,
control: bool,
right_alt: bool,
caps_lock: bool,
};
/// The character a key produces under `modifiers`, or 0 for none — the layout
/// lookup for printable keys, with ASCII control characters for the keys every
/// consumer expects (Enter, Tab, Backspace, Escape), exactly as ps2-bus/keyboard.
fn characterFor(layout: *const xkb.Layout, usage: u8, modifiers: ModifierSnapshot) u32 {
const mapping = xkb.map(layout, usage, .{
.shift = modifiers.shift,
.caps_lock = modifiers.caps_lock,
.level3 = modifiers.right_alt,
.control = modifiers.control,
});
if (mapping.character) |character| return character;
return switch (@as(input_protocol.Keycode, @enumFromInt(usage))) {
.enter, .keypad_enter => '\n',
.tab => '\t',
.backspace => 0x08,
.escape => 0x1B,
else => 0,
};
}
fn modifierWord(modifiers: u8) u32 {
var word: u32 = 0;
if (modifiers & (hid.modifier_left_shift | hid.modifier_right_shift) != 0) word |= input_protocol.modifier_shift;
if (modifiers & (hid.modifier_left_control | hid.modifier_right_control) != 0) word |= input_protocol.modifier_control;
if (modifiers & (hid.modifier_left_alt | hid.modifier_right_alt) != 0) word |= input_protocol.modifier_alt;
return word;
}
pub fn main(init: process.Init) void {
const argument = init.arguments.get(1) orelse {
_ = logging.write("/system/drivers/usb-hid/keyboard: missing device id (argv[1])\n");
return;
};
const device_id = std.fmt.parseInt(u64, argument, 10) catch {
std.log.info("malformed device id '{s}'", .{argument});
return;
};
const layout = xkb.byName(init.arguments.get(2) orelse "us") orelse xkb.us;
// Hello the manager first (meet the spawn deadline), then open the device.
if (device_manager.hello(.device, device_id) == null) return;
var device = usb.open(device_id) orelse {
std.log.info("could not open device {d}", .{device_id});
return;
};
const endpoint = device.findEndpoint(usb.transfer_type_interrupt, true) orelse {
_ = logging.write("/system/drivers/usb-hid/keyboard: no interrupt-IN endpoint\n");
return;
};
// Ask for the boot protocol and an indefinite idle (report only on change).
_ = device.controlOut(@bitCast(usb_abi.setProtocol(@enumFromInt(device.interface_number), .boot)));
_ = device.controlOut(@bitCast(usb_abi.setIdle(@enumFromInt(device.interface_number), 0, 0)));
if (!device.subscribeInterrupt(endpoint.address, endpoint.max_packet_size)) {
_ = logging.write("/system/drivers/usb-hid/keyboard: interrupt subscribe failed\n");
return;
}
var source = input.connectSource() orelse {
_ = logging.write("/system/drivers/usb-hid/keyboard: input service unavailable\n");
return;
};
_ = process.bindSignals(device.endpoint);
std.log.info("ok (device {d}, interface {d}, layout {s})", .{ device_id, device.interface_number, layout.name });
var decoder = hid.KeyboardDecoder{};
var caps_lock = false;
var receive: [64]u8 = undefined;
while (true) {
const got = ipc.replyWait(device.endpoint, &.{}, &receive, null);
if (!got.isNotification()) continue;
if (process.signalsFrom(got.badge)) |signals| {
if (signals.has(.terminate)) return;
continue;
}
if (!got.isMessage() or got.len < @sizeOf(usb.InterruptReport)) continue;
const message = std.mem.bytesToValue(usb.InterruptReport, receive[0..@sizeOf(usb.InterruptReport)]);
if (message.length < @sizeOf(hid.KeyboardReport)) continue;
const report = std.mem.bytesToValue(hid.KeyboardReport, message.data[0..@sizeOf(hid.KeyboardReport)]);
const transitions = decoder.feed(report);
// Caps Lock toggles on its own key-down (a stateful lock, not a modifier).
for (transitions.slice()) |transition| {
if (transition.kind == .pressed and @as(input_protocol.Keycode, @enumFromInt(transition.usage)) == .caps_lock) caps_lock = !caps_lock;
}
const modifiers = ModifierSnapshot{
.shift = report.modifiers & (hid.modifier_left_shift | hid.modifier_right_shift) != 0,
.control = report.modifiers & (hid.modifier_left_control | hid.modifier_right_control) != 0,
.right_alt = report.modifiers & hid.modifier_right_alt != 0,
.caps_lock = caps_lock,
};
const modifier_word = modifierWord(report.modifiers);
for (transitions.slice()) |transition| {
switch (transition.kind) {
.pressed => {
_ = source.publishKeyboardEvent(.{
.kind = @intFromEnum(input_protocol.EventKind.key_down),
.keycode = transition.usage,
.character = 0,
.modifiers = modifier_word,
});
const character = characterFor(layout, transition.usage, modifiers);
if (character != 0) {
_ = source.publishKeyboardEvent(.{
.kind = @intFromEnum(input_protocol.EventKind.key_press),
.keycode = transition.usage,
.character = character,
.modifiers = modifier_word,
});
// Echo the character to the log — a simple end-to-end
// keyboard check on real hardware: type a known phrase,
// then read it back from usb-hid-keyboard.log (or watch
// it appear live on screen in a -Ddiagnose boot, where
// the kernel console is a log sink). Printable ASCII and
// newline only; other keys are left to the input service.
if (character == '\n' or (character >= 0x20 and character < 0x7F)) {
_ = logging.write(&[1]u8{@intCast(character)});
}
}
},
.released => {
_ = source.publishKeyboardEvent(.{
.kind = @intFromEnum(input_protocol.EventKind.key_up),
.keycode = transition.usage,
.character = 0,
.modifiers = modifier_word,
});
},
}
}
}
}