P1 of docs/establishment-planes-plan.md. The bus no longer binds
/protocol/usb-transfer — the bind race made whichever instance came second
unreachable, which on a real three-controller Ryzen meant a mouse no class
driver could reach ("could not open device 50"). Each instance hands its
serving endpoint up in the hello that already delegates its controller, and
class drivers receive their OWN controller's channel from helloForChannel —
routed by the manager's lineage, retried while a provider is mid-restart,
on one manager handle so retries never spend handle-table slots.
usb.open(bus, id) now takes the channel it used to look up; the name rows
leave protocol.csv with the code (bind 67, opens 119/120/122); the
conformance fixture's prose stops claiming the bus binds; and the stale
input-client import leaves the bus with the channel one.
Gate: 19 QEMU cases green (usb family, hubs, both IOMMU variants, fat chain,
boot-from-USB, orderly shutdown, conformance).
177 lines
8.1 KiB
Zig
177 lines
8.1 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;
|
|
|
|
// The hello both meets the spawn deadline and brings back the channel to
|
|
// THIS device's controller — routed by lineage, never found by name.
|
|
const bus = device_manager.helloForChannel(.device, device_id) orelse return;
|
|
var device = usb.open(bus, 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()) continue;
|
|
// An `interrupt_report` event packet: the verb in its folded header, the
|
|
// report after it. Anything else on this endpoint is not ours.
|
|
const message = usb.reportOf(receive[0..got.len]) orelse continue;
|
|
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,
|
|
});
|
|
},
|
|
}
|
|
}
|
|
}
|
|
}
|