Wire real PS/2 scancodes through to input events and characters
Replace the keyboard driver's synthetic stream with the real path: - ps2-bus binds IRQ1 (interrupt bits set only after the bind), drains port 0x60 on each interrupt, and forwards every byte to the attached child driver over async ipc_send, routed by the status register's auxiliary-output bit. Children attach via the new well-known ps2_bus service, handing over their endpoint as a capability. - scancode.zig (new, pure, host-tested): scancode set 2 -> USB HID usage decoding (F0/E0/E1 prefix state machine) plus keyboard state — pressed-key bitmap, typematic-repeat classification, modifier and caps-lock tracking. - keyboard.zig decodes the forwarded stream and publishes real key_down/key_press/key_up events, filling key_press characters via xkeyboard-config (layout from argv[2], default us) and synthesizing ASCII control characters for Enter/Tab/Backspace/Escape. - protocol.zig names the full HID usage set in Keycode; build.zig threads the xkeyboard-config module into user binaries. Verified end to end in QEMU via monitor sendkey: shift, caps lock, and control-character synthesis all decode correctly. The mouse driver still publishes its synthetic stream; attaching it to the bus the same way is the follow-up.
This commit is contained in:
parent
80b72db676
commit
5bba5d3363
34
build.zig
34
build.zig
|
|
@ -60,6 +60,7 @@ fn addUserBinary(
|
|||
runtime_module: *std.Build.Module,
|
||||
posix_module: *std.Build.Module,
|
||||
mmio_module: *std.Build.Module,
|
||||
xkeyboard_config_module: *std.Build.Module,
|
||||
name: []const u8,
|
||||
root: []const u8,
|
||||
) *std.Build.Step.Compile {
|
||||
|
|
@ -81,6 +82,9 @@ fn addUserBinary(
|
|||
.{ .name = "posix", .module = posix_module },
|
||||
// Typed volatile MMIO + memory barriers, for drivers. See library/mmio/.
|
||||
.{ .name = "mmio", .module = mmio_module },
|
||||
// Keyboard layouts (keycode + modifiers -> keysym/character), available
|
||||
// to any program that wants it. See library/xkeyboard-config/.
|
||||
.{ .name = "xkeyboard-config", .module = xkeyboard_config_module },
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
|
@ -224,7 +228,6 @@ pub fn build(b: *std.Build) void {
|
|||
.{ .name = "layouts", .module = xkb_layouts_module },
|
||||
},
|
||||
});
|
||||
_ = xkeyboard_config_module;
|
||||
|
||||
// The POSIX / C compatibility layer, a separate library layered strictly over the
|
||||
// runtime (it calls the runtime's IPC/heap, never system calls directly). This is
|
||||
|
|
@ -309,7 +312,7 @@ pub fn build(b: *std.Build) void {
|
|||
// Built by the shared user-binary recipe (see addUserBinary): freestanding,
|
||||
// linked into the kernel's user region against the `runtime` runtime library, and
|
||||
// started in ring 3 by the kernel's user-ELF loader.
|
||||
const init_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "init", "system/services/init/init.zig");
|
||||
const init_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, "init", "system/services/init/init.zig");
|
||||
const init_install = b.addInstallArtifact(init_exe, .{ .dest_dir = .{ .override = .{ .custom = "system/services" } } });
|
||||
b.getInstallStep().dependOn(&init_install.step);
|
||||
|
||||
|
|
@ -317,21 +320,21 @@ pub fn build(b: *std.Build) void {
|
|||
// Each is built by the same user-binary recipe, then packed into one image by
|
||||
// the host-side make-initial-ramdisk tool. The bootloader ferries the image to the kernel,
|
||||
// which unpacks it and spawns each program (system/initial-ramdisk.zig).
|
||||
const vfs_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "vfs", "system/services/vfs/vfs.zig");
|
||||
const vfstest_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "vfs-test", "system/services/vfs/vfs-test.zig");
|
||||
const hpet_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "hpet", "system/drivers/hpet/hpet.zig");
|
||||
const bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "bus", "system/drivers/bus/bus.zig");
|
||||
const ps2_bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "bus", "system/drivers/ps2-bus/ps2-bus.zig");
|
||||
const ps2_keyboard_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "ps2-keyboard", "system/drivers/ps2-bus/keyboard.zig");
|
||||
const ps2_mouse_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "ps2-mouse", "system/drivers/ps2-bus/mouse.zig");
|
||||
const device_manager_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "device-manager", "system/services/device-manager/device-manager.zig");
|
||||
const vfs_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, "vfs", "system/services/vfs/vfs.zig");
|
||||
const vfstest_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, "vfs-test", "system/services/vfs/vfs-test.zig");
|
||||
const hpet_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, "hpet", "system/drivers/hpet/hpet.zig");
|
||||
const bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, "bus", "system/drivers/bus/bus.zig");
|
||||
const ps2_bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, "bus", "system/drivers/ps2-bus/ps2-bus.zig");
|
||||
const ps2_keyboard_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, "ps2-keyboard", "system/drivers/ps2-bus/keyboard.zig");
|
||||
const ps2_mouse_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, "ps2-mouse", "system/drivers/ps2-bus/mouse.zig");
|
||||
const device_manager_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, "device-manager", "system/services/device-manager/device-manager.zig");
|
||||
// The input service and its exercisers: the fan-out server, a hardware-free synthetic
|
||||
// source, and a subscriber that doubles as the `input` test's oracle. See docs/input.md.
|
||||
const input_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "input", "system/services/input/input.zig");
|
||||
const input_source_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "input-source", "system/services/input-source/input-source.zig");
|
||||
const input_test_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "input-test", "system/services/input-test/input-test.zig");
|
||||
const args_echo_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "args-echo", "system/services/args-echo/args-echo.zig");
|
||||
const process_test_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "process-test", "system/services/process-test/process-test.zig");
|
||||
const input_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, "input", "system/services/input/input.zig");
|
||||
const input_source_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, "input-source", "system/services/input-source/input-source.zig");
|
||||
const input_test_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, "input-test", "system/services/input-test/input-test.zig");
|
||||
const args_echo_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, "args-echo", "system/services/args-echo/args-echo.zig");
|
||||
const process_test_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, "process-test", "system/services/process-test/process-test.zig");
|
||||
|
||||
// Pack the user binaries into the initial_ramdisk image with the host-side Python tool
|
||||
// (the container format is trivial, and Python sidesteps std API churn). Args:
|
||||
|
|
@ -507,6 +510,7 @@ pub fn build(b: *std.Build) void {
|
|||
"system/devices/pci-class.zig", // class/subclass/prog-IF name decoding
|
||||
"system/devices/acpi-ids.zig", // _HID name decoding
|
||||
"library/mmio/mmio.zig", // barriers assemble + registers round-trip
|
||||
"system/drivers/ps2-bus/scancode.zig", // set-2 decode + keyboard state machine
|
||||
}) |root| {
|
||||
const mod_tests = b.addTest(.{
|
||||
.root_module = b.createModule(.{
|
||||
|
|
|
|||
|
|
@ -108,22 +108,29 @@ the service delivers to its endpoint, which only the same thread could receive).
|
|||
|
||||
## Status and follow-ups
|
||||
|
||||
- **Synthetic sources, for now.** The `ps2-bus` driver owns PNP0303, which carries *both*
|
||||
the 0x60/0x64 ports and IRQ1, so reading real scancodes/packets has to live in the bus,
|
||||
not in [keyboard.zig](../system/drivers/ps2-bus/keyboard.zig) /
|
||||
[mouse.zig](../system/drivers/ps2-bus/mouse.zig). Until that lands, the keyboard driver
|
||||
publishes a synthetic key stream, the mouse driver a synthetic motion/click stream, and
|
||||
the hardware-free `input-source` rotates through all three classes (including a synthetic
|
||||
joystick, which has no driver yet) — all via the `input.synthetic*Event` helpers. The
|
||||
fan-out and per-device routing are real; only the bytes are placeholder. **Follow-up:** the
|
||||
bus binds IRQ1/IRQ12, reads port 0x60, and `ps2-library` decodes scan-set-1 →
|
||||
keycodes and mouse packets; the drivers publish decoded events.
|
||||
- **Keycode → character** is a keymap, and danos has one:
|
||||
[`library/xkeyboard-config`](../library/xkeyboard-config/README.md) compiles the X11
|
||||
xkeyboard-config layouts (us, gb, de, fr, …) to native Zig —
|
||||
`xkb.map(layout, keycode, mods)` → keysym + Unicode character. Wiring it in to fill a
|
||||
`key_press` event's `character` (in the keyboard driver, or a small keymap service) is the
|
||||
natural next step.
|
||||
- **The keyboard is real.** The `ps2-bus` driver owns PNP0303, which carries *both* the
|
||||
0x60/0x64 ports and IRQ1, so reading the hardware lives in the bus, not in
|
||||
[keyboard.zig](../system/drivers/ps2-bus/keyboard.zig): the bus binds IRQ1 and, on each
|
||||
interrupt, drains port 0x60, routing every byte by the status register's
|
||||
auxiliary-output bit to whichever child driver **attached** for that device (an
|
||||
`AttachRequest` to the well-known `ps2_bus` service, carrying the child's endpoint as a
|
||||
capability; the bytes then arrive as asynchronous `ForwardedByte` messages, so the IRQ
|
||||
path never blocks on a child). The keyboard driver decodes the stream — scancode **set 2**,
|
||||
what the keyboard sends with the 8042's legacy translation off, decoded by
|
||||
[scancode.zig](../system/drivers/ps2-bus/scancode.zig) into USB HID usage keycodes with
|
||||
make/break, typematic-repeat, and modifier tracking (host-tested under `zig build test`) —
|
||||
and publishes real `key_down`/`key_press`/`key_up` events.
|
||||
- **Keycode → character** is wired in: the keyboard driver fills a `key_press` event's
|
||||
`character` through [`library/xkeyboard-config`](../library/xkeyboard-config/README.md)
|
||||
(`xkb.map(layout, keycode, mods)` → keysym + Unicode character), synthesizing the ASCII
|
||||
control characters for Enter/Tab/Backspace/Escape, whose keysyms map to no Unicode. The
|
||||
layout defaults to `us`; the bus can pass another as the driver's argv[2] — the seam for
|
||||
a future settings source.
|
||||
- **The mouse is still synthetic.** [mouse.zig](../system/drivers/ps2-bus/mouse.zig)
|
||||
publishes a placeholder motion/click stream; the hardware-free `input-source` likewise
|
||||
rotates through all three classes (including a synthetic joystick, which has no driver
|
||||
yet) via the `input.synthetic*Event` helpers. **Follow-up:** the mouse driver attaches to
|
||||
the bus the way the keyboard does and decodes 3/4-byte packets into mouse events.
|
||||
- **Drop-oldest under overflow** is a defined loss; the 16-slot ring absorbs normal bursts.
|
||||
Real backpressure/flow-control is future work.
|
||||
- **`publish` is unauthenticated** — any process may publish, consistent with the current
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ pub const ProcessDescriptor = extern struct {
|
|||
pub const ServiceId = enum(u32) {
|
||||
vfs = 1,
|
||||
input = 2,
|
||||
ps2_bus = 3, // the 8042 owner; child device drivers attach here for raw bytes
|
||||
_,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,74 @@
|
|||
//! PS/2 Keyboard Driver
|
||||
//!
|
||||
//! Spawned by the ps2-bus driver once the controller is initialized and port 1
|
||||
//! Spawned by the ps2-bus driver once the controller is initialized and the port
|
||||
//! has passed its interface test and device reset. The bus driver hands us our
|
||||
//! device HID as argv[1]; we use it to locate our own device descriptor.
|
||||
//! device HID as argv[1] and, optionally, a layout name (`"us"`, `"gb"`, ...) as
|
||||
//! argv[2].
|
||||
//!
|
||||
//! The 8042's ports (0x60/0x64) and IRQ1 live on the PNP0303 node, which the
|
||||
//! ps2-bus driver exclusively owns — so this driver never touches the hardware.
|
||||
//! Instead it **attaches** to the bus (handing over its endpoint as a capability)
|
||||
//! and receives every scancode byte as a forwarded asynchronous message. Each byte
|
||||
//! feeds the set-2 decoder; a decoded key becomes input-protocol events:
|
||||
//!
|
||||
//! scancode byte -> HID usage keycode -> key_down / key_up
|
||||
//! -> xkeyboard-config -> character -> key_press
|
||||
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
const xkb = @import("xkeyboard-config");
|
||||
const ps2 = @import("ps2-library.zig");
|
||||
const scancode = @import("scancode.zig");
|
||||
const device = runtime.device;
|
||||
const ipc = runtime.ipc;
|
||||
const protocol = runtime.input_protocol;
|
||||
|
||||
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
var line: [128]u8 = undefined;
|
||||
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||
}
|
||||
|
||||
/// Look up the ps2-bus service, retrying while the bus (which spawned us before
|
||||
/// registering) is still coming up.
|
||||
fn lookupBus() ?ipc.Handle {
|
||||
var attempts: usize = 0;
|
||||
while (attempts < 100) : (attempts += 1) {
|
||||
if (ipc.lookup(.ps2_bus)) |handle| return handle;
|
||||
runtime.system.sleep(50);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// The character a pressed key produces under `modifiers`, or 0 for none. The
|
||||
/// layout lookup answers for printable keys; the keys whose keysym has no Unicode
|
||||
/// mapping but that every consumer still expects as a character (Enter, Tab,
|
||||
/// Backspace, Escape) are given their ASCII control characters here.
|
||||
fn characterFor(layout: *const xkb.Layout, usage: u8, modifiers: scancode.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(protocol.Keycode, @enumFromInt(usage))) {
|
||||
.enter, .keypad_enter => '\n',
|
||||
.tab => '\t',
|
||||
.backspace => 0x08,
|
||||
.escape => 0x1B,
|
||||
else => 0,
|
||||
};
|
||||
}
|
||||
|
||||
/// The input protocol's modifier word for a snapshot.
|
||||
fn modifierWord(modifiers: scancode.ModifierSnapshot) u32 {
|
||||
var word: u32 = 0;
|
||||
if (modifiers.shift) word |= protocol.modifier_shift;
|
||||
if (modifiers.control) word |= protocol.modifier_control;
|
||||
if (modifiers.alt) word |= protocol.modifier_alt;
|
||||
return word;
|
||||
}
|
||||
|
||||
pub fn main(init: runtime.process.Init) void {
|
||||
const hid = init.arguments.get(1).?;
|
||||
if (hid.len == 0) {
|
||||
|
|
@ -31,25 +86,95 @@ pub fn main(init: runtime.process.Init) void {
|
|||
return;
|
||||
}
|
||||
|
||||
// The 8042 ports (0x60/0x64) and this keyboard's IRQ1 both live on the same
|
||||
// PNP0303 node, which the ps2-bus driver exclusively owns — so the keyboard is
|
||||
// served through the bus and does not claim the controller itself.
|
||||
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: served by ps2-bus (controller owned by bus)\n");
|
||||
// The layout is a spawn argument so a later settings source can choose it;
|
||||
// absent (as today) it defaults to us.
|
||||
const layout_name = init.arguments.get(2) orelse "us";
|
||||
const layout = xkb.byName(layout_name) orelse xkb.us;
|
||||
writeLine("system/drivers/ps2-bus/keyboard: layout {s}\n", .{layout.name});
|
||||
|
||||
// Broadcast keyboard events through the input service so programs can listen for them
|
||||
// (docs/input.md). Until the bus reads real IRQ1 scancodes and hands them here (a
|
||||
// follow-up), we publish the same synthetic stand-in stream the demo source uses — the
|
||||
// fan-out path is real, only the source of the bytes is placeholder.
|
||||
// Attach to the bus: hand it our endpoint, and it forwards every byte the
|
||||
// keyboard sends (it owns the controller; we own the decoding).
|
||||
const bus = lookupBus() orelse {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: ps2-bus service unavailable\n");
|
||||
return;
|
||||
};
|
||||
const endpoint = ipc.createIpcEndpoint() orelse {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: no endpoint\n");
|
||||
return;
|
||||
};
|
||||
var attach = ps2.AttachRequest{ .device_type = @intFromEnum(ps2.DeviceType.keyboard) };
|
||||
var attach_reply: [@sizeOf(ps2.AttachReply)]u8 = undefined;
|
||||
const attached = ipc.callCap(bus, std.mem.asBytes(&attach), &attach_reply, endpoint) catch {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: attach call failed\n");
|
||||
return;
|
||||
};
|
||||
if (attached.len < @sizeOf(ps2.AttachReply) or
|
||||
std.mem.bytesToValue(ps2.AttachReply, attach_reply[0..@sizeOf(ps2.AttachReply)]).status != 0)
|
||||
{
|
||||
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: attach refused\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// Broadcast keyboard events through the input service so programs can listen
|
||||
// for them (docs/input.md).
|
||||
var source = runtime.input.connectSource() orelse {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: input service unavailable\n");
|
||||
return;
|
||||
};
|
||||
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: ok\n");
|
||||
|
||||
var step: usize = 0;
|
||||
while (true) : (step +%= 1) {
|
||||
_ = source.publishKeyboardEvent(runtime.input.syntheticKeyEvent(step));
|
||||
runtime.system.sleep(200);
|
||||
var decoder = scancode.Decoder{};
|
||||
var state = scancode.KeyboardState{};
|
||||
var receive: [@sizeOf(ps2.ForwardedByte)]u8 = undefined;
|
||||
while (true) {
|
||||
const got = ipc.replyWait(endpoint, &.{}, &receive, null);
|
||||
if (!got.isMessage() or got.len < @sizeOf(ps2.ForwardedByte)) continue;
|
||||
const forwarded = std.mem.bytesToValue(ps2.ForwardedByte, receive[0..@sizeOf(ps2.ForwardedByte)]);
|
||||
|
||||
const key = decoder.feed(@intCast(forwarded.byte & 0xFF)) orelse continue;
|
||||
const transition = state.apply(key);
|
||||
const modifiers = modifierWord(transition.modifiers);
|
||||
|
||||
switch (transition.action) {
|
||||
.pressed => {
|
||||
_ = source.publishKeyboardEvent(.{
|
||||
.kind = @intFromEnum(protocol.EventKind.key_down),
|
||||
.keycode = key.usage,
|
||||
.character = 0,
|
||||
.modifiers = modifiers,
|
||||
});
|
||||
const character = characterFor(layout, key.usage, transition.modifiers);
|
||||
if (character != 0) {
|
||||
_ = source.publishKeyboardEvent(.{
|
||||
.kind = @intFromEnum(protocol.EventKind.key_press),
|
||||
.keycode = key.usage,
|
||||
.character = character,
|
||||
.modifiers = modifiers,
|
||||
});
|
||||
}
|
||||
},
|
||||
// Typematic repeat: the key did not physically go down again, so no
|
||||
// key_down — but it keeps producing its character.
|
||||
.repeated => {
|
||||
const character = characterFor(layout, key.usage, transition.modifiers);
|
||||
if (character != 0) {
|
||||
_ = source.publishKeyboardEvent(.{
|
||||
.kind = @intFromEnum(protocol.EventKind.key_press),
|
||||
.keycode = key.usage,
|
||||
.character = character,
|
||||
.modifiers = modifiers,
|
||||
});
|
||||
}
|
||||
},
|
||||
.released => {
|
||||
_ = source.publishKeyboardEvent(.{
|
||||
.kind = @intFromEnum(protocol.EventKind.key_up),
|
||||
.keycode = key.usage,
|
||||
.character = 0,
|
||||
.modifiers = modifiers,
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ const std = @import("std");
|
|||
const runtime = @import("runtime");
|
||||
const ps2 = @import("ps2-library.zig");
|
||||
const device = runtime.device;
|
||||
const ipc = runtime.ipc;
|
||||
|
||||
/// Format one whole log line and emit it in a single `debug_write`, so output
|
||||
/// from the child drivers (which run concurrently) can never interleave with it.
|
||||
|
|
@ -23,22 +24,68 @@ fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
|||
|
||||
/// Ask the device on `port` what it is, then spawn the matching driver from the
|
||||
/// initial-ramdisk, handing it the device's HID as argv[1]. The driver is chosen
|
||||
/// from what the device reports, not from the port number.
|
||||
fn spawnIdentifiedDriver(controller: ps2.Controller, port: ps2.Port) void {
|
||||
/// from what the device reports, not from the port number. Returns the identified
|
||||
/// type so the forwarding loop can route that port's bytes to the driver once it
|
||||
/// attaches, or null if nothing was spawned.
|
||||
fn spawnIdentifiedDriver(controller: ps2.Controller, port: ps2.Port) ?ps2.DeviceType {
|
||||
const device_type = controller.identifyDevice(port) orelse {
|
||||
writeLine("system/drivers/ps2-bus: identify timed out on port {s}\n", .{@tagName(port)});
|
||||
return;
|
||||
return null;
|
||||
};
|
||||
const driver_name = device_type.driverName() orelse {
|
||||
writeLine("system/drivers/ps2-bus: unrecognized device on port {s}\n", .{@tagName(port)});
|
||||
return;
|
||||
return null;
|
||||
};
|
||||
const hid = device_type.hid() orelse "";
|
||||
if (runtime.system.spawnWithArguments(driver_name, &.{hid}) != null) {
|
||||
writeLine("system/drivers/ps2-bus: port {s} is a {s}, spawned {s}\n", .{ @tagName(port), hid, driver_name });
|
||||
} else {
|
||||
writeLine("system/drivers/ps2-bus: failed to spawn {s}\n", .{driver_name});
|
||||
return device_type;
|
||||
}
|
||||
writeLine("system/drivers/ps2-bus: failed to spawn {s}\n", .{driver_name});
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Resource index of the controller's IRQ (IRQ1) on the PNP0303 descriptor, found
|
||||
/// the way the ports are found in `Controller.init`.
|
||||
fn findInterruptResourceIndex(descriptor: device.DeviceDescriptor) ?u64 {
|
||||
for (0..descriptor.resource_count) |index| {
|
||||
if (descriptor.resources[index].kind == @intFromEnum(device.ResourceKind.irq)) return index;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Forwarding endpoints of the attached child drivers, indexed by `ps2.Port`.
|
||||
/// Written when a child's `AttachRequest` arrives, read on every forwarded byte.
|
||||
var port_endpoints = [_]?ipc.Handle{ null, null };
|
||||
|
||||
/// Which device type each port identified as, so an attaching child (which knows
|
||||
/// its type, not its port) can be matched to the right port's byte stream.
|
||||
var port_device_types = [_]?ps2.DeviceType{ null, null };
|
||||
|
||||
/// Handle a child driver's `AttachRequest`: record the endpoint capability it
|
||||
/// passed as the forwarding target for the port whose device matches its type.
|
||||
/// Writes an `AttachReply` into `out` and returns its length.
|
||||
fn handleAttach(message: []const u8, got: ipc.Received, out: []u8) usize {
|
||||
const reply = struct {
|
||||
fn write(buffer: []u8, status: i32) usize {
|
||||
const header = ps2.AttachReply{ .status = status };
|
||||
@memcpy(buffer[0..@sizeOf(ps2.AttachReply)], std.mem.asBytes(&header));
|
||||
return @sizeOf(ps2.AttachReply);
|
||||
}
|
||||
};
|
||||
|
||||
if (message.len < @sizeOf(ps2.AttachRequest)) return reply.write(out, -1);
|
||||
const request = std.mem.bytesToValue(ps2.AttachRequest, message[0..@sizeOf(ps2.AttachRequest)]);
|
||||
const endpoint = got.cap orelse return reply.write(out, -1); // no endpoint passed
|
||||
|
||||
for (&port_device_types, 0..) |maybe_type, port_index| {
|
||||
const device_type = maybe_type orelse continue;
|
||||
if (@intFromEnum(device_type) != request.device_type) continue;
|
||||
port_endpoints[port_index] = endpoint;
|
||||
writeLine("system/drivers/ps2-bus: {s} driver attached\n", .{@tagName(device_type)});
|
||||
return reply.write(out, 0);
|
||||
}
|
||||
return reply.write(out, -1); // no port identified as that device type
|
||||
}
|
||||
|
||||
pub fn main() void {
|
||||
|
|
@ -48,6 +95,8 @@ pub fn main() void {
|
|||
};
|
||||
|
||||
var has_two_channels = false;
|
||||
var maybe_controller: ?ps2.Controller = null;
|
||||
var maybe_interrupt_index: ?u64 = null;
|
||||
// The 8042's IO ports (0x60/0x64) are enumerated under the keyboard ACPI node
|
||||
// (PNP0303), so we init the controller from that descriptor — but which device
|
||||
// is on which port is decided later by identify, not by this HID.
|
||||
|
|
@ -65,6 +114,8 @@ pub fn main() void {
|
|||
_ = runtime.system.write("system/drivers/ps2-bus: controller is missing its IO ports\n");
|
||||
return;
|
||||
};
|
||||
maybe_controller = controller;
|
||||
maybe_interrupt_index = findInterruptResourceIndex(controller_device_descriptor);
|
||||
|
||||
controller.disablePort(.One);
|
||||
controller.disablePort(.Two);
|
||||
|
|
@ -126,18 +177,12 @@ pub fn main() void {
|
|||
return;
|
||||
}
|
||||
|
||||
// enable the working ports and, via a read-modify-write, their interrupts
|
||||
// Enable the working ports. Their interrupts stay off until IRQ1 is bound
|
||||
// below — reset and identify use polled reads, which must never race the
|
||||
// interrupt-driven drain loop for bytes.
|
||||
controller.enablePort(.One);
|
||||
if (port_two_works) controller.enablePort(.Two);
|
||||
|
||||
var configuration = controller.readConfigurationByte() orelse {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus: controller configuration timed out\n");
|
||||
return;
|
||||
};
|
||||
if (port_one_works) configuration |= ps2.Port.One.interruptBit();
|
||||
if (port_two_works) configuration |= ps2.Port.Two.interruptBit();
|
||||
_ = controller.writeConfigurationByte(configuration);
|
||||
|
||||
// reset each working device; a failing device is logged but does not
|
||||
// abort bring-up of the other one
|
||||
if (port_one_works) {
|
||||
|
|
@ -158,15 +203,77 @@ pub fn main() void {
|
|||
// Identify the device on each working port and hand it off to the driver
|
||||
// that matches what it reported — a port is not assumed to be a keyboard
|
||||
// or a mouse by its number.
|
||||
if (port_one_works) spawnIdentifiedDriver(controller, .One);
|
||||
if (port_two_works) spawnIdentifiedDriver(controller, .Two);
|
||||
if (port_one_works) port_device_types[@intFromEnum(ps2.Port.One)] = spawnIdentifiedDriver(controller, .One);
|
||||
if (port_two_works) port_device_types[@intFromEnum(ps2.Port.Two)] = spawnIdentifiedDriver(controller, .Two);
|
||||
} else {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus: no PS/2 controller found\n");
|
||||
return;
|
||||
}
|
||||
|
||||
const controller = maybe_controller.?;
|
||||
const interrupt_index = maybe_interrupt_index orelse {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus: controller is missing its IRQ\n");
|
||||
return;
|
||||
};
|
||||
|
||||
// The endpoint the child drivers attach to and IRQ1 wakes. Registered under a
|
||||
// well-known id so the children can find it, the way input subscribers find
|
||||
// the input service.
|
||||
const endpoint = ipc.createIpcEndpoint() orelse {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus: no endpoint\n");
|
||||
return;
|
||||
};
|
||||
if (!ipc.register(.ps2_bus, endpoint)) {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus: register failed\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// From here on, only the interrupt path reads the data port. Drop anything a
|
||||
// device sent between enable-scanning and now, bind the IRQ, and only then
|
||||
// let the controller raise it — an interrupt with nobody bound is lost.
|
||||
controller.drainOutputBuffer();
|
||||
if (!device.irqBind(controller.device_id, interrupt_index, endpoint)) {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus: irq_bind failed\n");
|
||||
return;
|
||||
}
|
||||
var configuration = controller.readConfigurationByte() orelse {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus: controller configuration timed out\n");
|
||||
return;
|
||||
};
|
||||
if (port_device_types[@intFromEnum(ps2.Port.One)] != null) configuration |= ps2.Port.One.interruptBit();
|
||||
if (port_device_types[@intFromEnum(ps2.Port.Two)] != null) configuration |= ps2.Port.Two.interruptBit();
|
||||
_ = controller.writeConfigurationByte(configuration);
|
||||
|
||||
_ = runtime.system.write("system/drivers/ps2-bus: ok\n");
|
||||
while (true) runtime.system.sleep(1000);
|
||||
|
||||
// The forwarding loop: an IRQ1 notification drains the output buffer, routing
|
||||
// each byte to the attached driver of the port it came from; a client message
|
||||
// is a child driver's AttachRequest.
|
||||
var reply_buffer: [@sizeOf(ps2.AttachReply)]u8 = undefined;
|
||||
var reply_len: usize = 0;
|
||||
var receive: [@sizeOf(ps2.AttachRequest)]u8 = undefined;
|
||||
while (true) {
|
||||
const got = ipc.replyWait(endpoint, reply_buffer[0..reply_len], &receive, null);
|
||||
if (got.isNotification()) {
|
||||
reply_len = 0;
|
||||
if (got.isMessage() or got.isChildExit()) continue; // nothing sends us these
|
||||
while (true) {
|
||||
const current_status = ps2.status(controller.device_id, controller.status_index);
|
||||
if (current_status & ps2.status_output_buffer_full == 0) break;
|
||||
const byte = device.ioRead(controller.device_id, controller.data_index, 0, 1) orelse break;
|
||||
const port: ps2.Port = if (current_status & ps2.status_auxiliary_output != 0) .Two else .One;
|
||||
if (port_endpoints[@intFromEnum(port)]) |child| {
|
||||
const forwarded = ps2.ForwardedByte{ .port = @intFromEnum(port), .byte = byte };
|
||||
_ = ipc.send(child, std.mem.asBytes(&forwarded));
|
||||
}
|
||||
// An unattached port's byte is dropped — e.g. a keystroke before
|
||||
// the keyboard driver has attached.
|
||||
}
|
||||
_ = device.irqAck(controller.device_id, interrupt_index);
|
||||
continue;
|
||||
}
|
||||
reply_len = handleAttach(receive[0..got.len], got, &reply_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
pub const panic = runtime.panic;
|
||||
|
|
|
|||
|
|
@ -49,12 +49,15 @@ pub const cmd_write_second_port_output: u8 = 0xD3; // write next data byte to th
|
|||
pub const cmd_write_second_port_input: u8 = 0xD4; // write next data byte to the second port input buffer (to the mouse)
|
||||
pub const cmd_pulse_system_reset: u8 = 0xFE; // pulse output line 0 low: resets the CPU
|
||||
|
||||
/// PS/2 status register bits (read from the status port, 0x64). Bits 4 and 5
|
||||
/// are chipset-specific and intentionally omitted.
|
||||
/// PS/2 status register bits (read from the status port, 0x64). Bit 4 is
|
||||
/// chipset-specific and intentionally omitted.
|
||||
pub const status_output_buffer_full: u8 = 1 << 0; // 1 = a byte is waiting to be read from the data port
|
||||
pub const status_input_buffer_full: u8 = 1 << 1; // 1 = the controller has not yet consumed the last write
|
||||
pub const status_system_flag: u8 = 1 << 2; // set once the controller passes POST
|
||||
pub const status_command_or_data: u8 = 1 << 3; // 1 = last write was a command, 0 = data
|
||||
/// Chipset-specific in the original spec, universal in practice on dual-channel
|
||||
/// controllers: set = the waiting byte came from the second port (the mouse).
|
||||
pub const status_auxiliary_output: u8 = 1 << 5;
|
||||
pub const status_timeout_error: u8 = 1 << 6; // 1 = time-out error
|
||||
pub const status_parity_error: u8 = 1 << 7; // 1 = parity error
|
||||
|
||||
|
|
@ -237,11 +240,12 @@ pub const Port = enum(u2) {
|
|||
};
|
||||
|
||||
/// The kind of device attached to a port, as reported by the device itself in
|
||||
/// response to the identify command — not assumed from the port number.
|
||||
pub const DeviceType = enum {
|
||||
keyboard,
|
||||
mouse,
|
||||
unknown,
|
||||
/// response to the identify command — not assumed from the port number. Fixed
|
||||
/// `u32` values because the type also travels in an `AttachRequest`.
|
||||
pub const DeviceType = enum(u32) {
|
||||
keyboard = 0,
|
||||
mouse = 1,
|
||||
unknown = 2,
|
||||
|
||||
/// Initial-ramdisk name of the driver that serves this device type, or null
|
||||
/// if we could not classify it.
|
||||
|
|
@ -264,6 +268,37 @@ pub const DeviceType = enum {
|
|||
}
|
||||
};
|
||||
|
||||
// --- the bus <-> child-driver forwarding protocol -----------------------------
|
||||
//
|
||||
// The 8042's ports and IRQ1 live on the PNP0303 node that only the ps2-bus driver
|
||||
// claims, so the child device drivers (ps2-keyboard, ps2-mouse) cannot read port
|
||||
// 0x60 themselves. Instead each child **attaches**: it calls the bus's well-known
|
||||
// `ps2_bus` endpoint with an `AttachRequest`, handing over its own endpoint as the
|
||||
// call's capability. From then on the bus forwards every byte the device sends as
|
||||
// a `ForwardedByte` via the asynchronous `ipc.send` — the IRQ path in the bus can
|
||||
// never block on a slow child, and the child never touches the controller.
|
||||
|
||||
/// A child driver registering for its device's bytes. `device_type` is a
|
||||
/// `DeviceType` value; the child's receive endpoint travels as the call's
|
||||
/// capability (`send_cap`).
|
||||
pub const AttachRequest = extern struct {
|
||||
device_type: u32,
|
||||
};
|
||||
|
||||
/// Reply to an `AttachRequest`. `status` is 0 on success or a negative errno.
|
||||
pub const AttachReply = extern struct {
|
||||
status: i32,
|
||||
_padding: u32 = 0,
|
||||
};
|
||||
|
||||
/// One raw byte read from the data port, forwarded to the attached child whose
|
||||
/// port it came from (routed by the status register's auxiliary-output bit).
|
||||
pub const ForwardedByte = extern struct {
|
||||
/// The `Port` the byte came from, as `@intFromEnum`.
|
||||
port: u32,
|
||||
byte: u32,
|
||||
};
|
||||
|
||||
/// A single PS/2 (8042) controller. Construct one with `Controller.init` and
|
||||
/// drive the controller through its methods; there is only ever one 8042 per
|
||||
/// machine, but holding the resolved resource indices in an instance keeps the
|
||||
|
|
|
|||
|
|
@ -0,0 +1,389 @@
|
|||
//! PS/2 scancode set 2 → USB HID usage decoding, plus the keyboard state a driver
|
||||
//! needs on top of it (pressed keys, modifier tracking, caps-lock toggle).
|
||||
//!
|
||||
//! Set 2 is what a keyboard sends when the 8042's legacy set-1 translation is off —
|
||||
//! which is how ps2-bus.zig deliberately configures the controller. A key's **make**
|
||||
//! code is one byte (two with an `E0` prefix for the "extended" keys added after the
|
||||
//! original AT layout); its **break** code is the same code behind an `F0` prefix.
|
||||
//! Pause alone is an eight-byte `E1` sequence with no break.
|
||||
//!
|
||||
//! The output vocabulary is USB HID keyboard-page usages (a=4, enter=40, ...), the
|
||||
//! same numbering the input protocol's `Keycode` and the xkeyboard-config layout
|
||||
//! tables use — so a decoded usage indexes a layout directly.
|
||||
//!
|
||||
//! Everything here is pure (no imports beyond `std`, no IO), so it is host-testable:
|
||||
//! the tests at the bottom run under `zig build test`.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
// --- USB HID usages the state machine itself needs to recognize --------------
|
||||
|
||||
pub const usage_caps_lock: u8 = 0x39;
|
||||
pub const usage_left_control: u8 = 0xE0;
|
||||
pub const usage_left_shift: u8 = 0xE1;
|
||||
pub const usage_left_alt: u8 = 0xE2;
|
||||
pub const usage_right_control: u8 = 0xE4;
|
||||
pub const usage_right_shift: u8 = 0xE5;
|
||||
pub const usage_right_alt: u8 = 0xE6; // AltGr — selects XKB level 3
|
||||
|
||||
// --- scancode set 2 → HID usage tables ---------------------------------------
|
||||
|
||||
/// Single-byte (non-`E0`) make codes. Zero means "no key" — protocol bytes (ACK,
|
||||
/// BAT results) and reserved codes land there and decode to nothing.
|
||||
pub const set2_base: [256]u8 = blk: {
|
||||
var table = [_]u8{0} ** 256;
|
||||
// function row
|
||||
table[0x01] = 0x42; // F9
|
||||
table[0x03] = 0x3E; // F5
|
||||
table[0x04] = 0x3C; // F3
|
||||
table[0x05] = 0x3A; // F1
|
||||
table[0x06] = 0x3B; // F2
|
||||
table[0x07] = 0x45; // F12
|
||||
table[0x09] = 0x43; // F10
|
||||
table[0x0A] = 0x41; // F8
|
||||
table[0x0B] = 0x3F; // F6
|
||||
table[0x0C] = 0x3D; // F4
|
||||
table[0x78] = 0x44; // F11
|
||||
table[0x83] = 0x40; // F7
|
||||
// letters
|
||||
table[0x1C] = 0x04; // A
|
||||
table[0x32] = 0x05; // B
|
||||
table[0x21] = 0x06; // C
|
||||
table[0x23] = 0x07; // D
|
||||
table[0x24] = 0x08; // E
|
||||
table[0x2B] = 0x09; // F
|
||||
table[0x34] = 0x0A; // G
|
||||
table[0x33] = 0x0B; // H
|
||||
table[0x43] = 0x0C; // I
|
||||
table[0x3B] = 0x0D; // J
|
||||
table[0x42] = 0x0E; // K
|
||||
table[0x4B] = 0x0F; // L
|
||||
table[0x3A] = 0x10; // M
|
||||
table[0x31] = 0x11; // N
|
||||
table[0x44] = 0x12; // O
|
||||
table[0x4D] = 0x13; // P
|
||||
table[0x15] = 0x14; // Q
|
||||
table[0x2D] = 0x15; // R
|
||||
table[0x1B] = 0x16; // S
|
||||
table[0x2C] = 0x17; // T
|
||||
table[0x3C] = 0x18; // U
|
||||
table[0x2A] = 0x19; // V
|
||||
table[0x1D] = 0x1A; // W
|
||||
table[0x22] = 0x1B; // X
|
||||
table[0x35] = 0x1C; // Y
|
||||
table[0x1A] = 0x1D; // Z
|
||||
// digit row
|
||||
table[0x16] = 0x1E; // 1
|
||||
table[0x1E] = 0x1F; // 2
|
||||
table[0x26] = 0x20; // 3
|
||||
table[0x25] = 0x21; // 4
|
||||
table[0x2E] = 0x22; // 5
|
||||
table[0x36] = 0x23; // 6
|
||||
table[0x3D] = 0x24; // 7
|
||||
table[0x3E] = 0x25; // 8
|
||||
table[0x46] = 0x26; // 9
|
||||
table[0x45] = 0x27; // 0
|
||||
// control and whitespace
|
||||
table[0x5A] = 0x28; // Enter
|
||||
table[0x76] = 0x29; // Escape
|
||||
table[0x66] = 0x2A; // Backspace
|
||||
table[0x0D] = 0x2B; // Tab
|
||||
table[0x29] = 0x2C; // Space
|
||||
// punctuation
|
||||
table[0x4E] = 0x2D; // - _
|
||||
table[0x55] = 0x2E; // = +
|
||||
table[0x54] = 0x2F; // [ {
|
||||
table[0x5B] = 0x30; // ] }
|
||||
table[0x5D] = 0x31; // \ | (non-US hash on ISO boards, same position)
|
||||
table[0x4C] = 0x33; // ; :
|
||||
table[0x52] = 0x34; // ' "
|
||||
table[0x0E] = 0x35; // ` ~
|
||||
table[0x41] = 0x36; // , <
|
||||
table[0x49] = 0x37; // . >
|
||||
table[0x4A] = 0x38; // / ?
|
||||
table[0x61] = 0x64; // non-US backslash (the extra ISO key between shift and Z)
|
||||
// locks
|
||||
table[0x58] = usage_caps_lock;
|
||||
table[0x77] = 0x53; // Num Lock
|
||||
table[0x7E] = 0x47; // Scroll Lock
|
||||
// keypad
|
||||
table[0x7C] = 0x55; // keypad *
|
||||
table[0x7B] = 0x56; // keypad -
|
||||
table[0x79] = 0x57; // keypad +
|
||||
table[0x69] = 0x59; // keypad 1
|
||||
table[0x72] = 0x5A; // keypad 2
|
||||
table[0x7A] = 0x5B; // keypad 3
|
||||
table[0x6B] = 0x5C; // keypad 4
|
||||
table[0x73] = 0x5D; // keypad 5
|
||||
table[0x74] = 0x5E; // keypad 6
|
||||
table[0x6C] = 0x5F; // keypad 7
|
||||
table[0x75] = 0x60; // keypad 8
|
||||
table[0x7D] = 0x61; // keypad 9
|
||||
table[0x70] = 0x62; // keypad 0
|
||||
table[0x71] = 0x63; // keypad .
|
||||
// modifiers
|
||||
table[0x14] = usage_left_control;
|
||||
table[0x12] = usage_left_shift;
|
||||
table[0x11] = usage_left_alt;
|
||||
table[0x59] = usage_right_shift;
|
||||
break :blk table;
|
||||
};
|
||||
|
||||
/// `E0`-prefixed make codes. `E0 12` is the "fake shift" the keyboard wraps around
|
||||
/// Print Screen and navigation keys when a real shift is involved; it maps to zero
|
||||
/// here, so it decodes to nothing and only the real key comes through.
|
||||
pub const set2_extended: [256]u8 = blk: {
|
||||
var table = [_]u8{0} ** 256;
|
||||
table[0x11] = usage_right_alt;
|
||||
table[0x14] = usage_right_control;
|
||||
table[0x1F] = 0xE3; // left GUI
|
||||
table[0x27] = 0xE7; // right GUI
|
||||
table[0x2F] = 0x65; // application (menu)
|
||||
table[0x7C] = 0x46; // Print Screen (arrives as E0 12 E0 7C; the E0 12 decodes to nothing)
|
||||
table[0x4A] = 0x54; // keypad /
|
||||
table[0x5A] = 0x58; // keypad Enter
|
||||
table[0x70] = 0x49; // Insert
|
||||
table[0x6C] = 0x4A; // Home
|
||||
table[0x7D] = 0x4B; // Page Up
|
||||
table[0x71] = 0x4C; // Delete
|
||||
table[0x69] = 0x4D; // End
|
||||
table[0x7A] = 0x4E; // Page Down
|
||||
table[0x74] = 0x4F; // right arrow
|
||||
table[0x6B] = 0x50; // left arrow
|
||||
table[0x72] = 0x51; // down arrow
|
||||
table[0x75] = 0x52; // up arrow
|
||||
break :blk table;
|
||||
};
|
||||
|
||||
// --- the byte-stream decoder --------------------------------------------------
|
||||
|
||||
/// One decoded key transition: which key (as a USB HID usage) and whether this is
|
||||
/// a make (press or typematic repeat) or a break (release).
|
||||
pub const DecodedKey = struct {
|
||||
usage: u8,
|
||||
make: bool,
|
||||
};
|
||||
|
||||
/// Turns the raw set-2 byte stream into `DecodedKey`s. Feed it every byte the
|
||||
/// keyboard sends; most bytes complete a key and return one, prefix bytes return
|
||||
/// null and arm the state machine for the next byte.
|
||||
pub const Decoder = struct {
|
||||
const State = enum {
|
||||
idle,
|
||||
extended, // saw E0
|
||||
break_prefix, // saw F0
|
||||
extended_break, // saw E0 F0
|
||||
pause_skip, // inside the 8-byte E1 Pause sequence
|
||||
};
|
||||
|
||||
state: State = .idle,
|
||||
/// Bytes still to swallow in `pause_skip`.
|
||||
skip: u8 = 0,
|
||||
|
||||
/// The whole Pause make sequence is `E1 14 77 E1 F0 14 F0 77` — seven bytes
|
||||
/// after the leading `E1`, and no break sequence ever follows.
|
||||
const pause_bytes_after_e1: u8 = 7;
|
||||
|
||||
pub fn feed(self: *Decoder, byte: u8) ?DecodedKey {
|
||||
switch (self.state) {
|
||||
.idle => switch (byte) {
|
||||
0xE0 => self.state = .extended,
|
||||
0xF0 => self.state = .break_prefix,
|
||||
0xE1 => {
|
||||
self.state = .pause_skip;
|
||||
self.skip = pause_bytes_after_e1;
|
||||
},
|
||||
// Anything else is a make code — or a protocol byte (0xFA ACK,
|
||||
// 0xAA BAT-passed, 0xEE echo, ...), which the tables map to zero.
|
||||
else => return decoded(set2_base[byte], true),
|
||||
},
|
||||
.extended => switch (byte) {
|
||||
0xF0 => self.state = .extended_break,
|
||||
else => {
|
||||
self.state = .idle;
|
||||
return decoded(set2_extended[byte], true);
|
||||
},
|
||||
},
|
||||
.break_prefix => {
|
||||
self.state = .idle;
|
||||
return decoded(set2_base[byte], false);
|
||||
},
|
||||
.extended_break => {
|
||||
self.state = .idle;
|
||||
return decoded(set2_extended[byte], false);
|
||||
},
|
||||
.pause_skip => {
|
||||
self.skip -= 1;
|
||||
if (self.skip == 0) self.state = .idle;
|
||||
},
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn decoded(usage: u8, make: bool) ?DecodedKey {
|
||||
if (usage == 0) return null; // unmapped or a protocol byte
|
||||
return .{ .usage = usage, .make = make };
|
||||
}
|
||||
};
|
||||
|
||||
// --- driver-side keyboard state -----------------------------------------------
|
||||
|
||||
/// What a key transition did, plus the modifier state to stamp on the resulting
|
||||
/// events (snapshotted after the transition was applied).
|
||||
pub const Transition = struct {
|
||||
pub const Action = enum {
|
||||
pressed, // physical make of a key that was up
|
||||
repeated, // typematic make of a key already down — no new key_down
|
||||
released, // physical break
|
||||
};
|
||||
action: Action,
|
||||
modifiers: ModifierSnapshot,
|
||||
};
|
||||
|
||||
/// The modifier state at one instant, in both vocabularies a driver needs: the
|
||||
/// input protocol's coarse bits (shift/control/alt) and the level-selection
|
||||
/// inputs xkeyboard-config takes (shift, caps_lock, AltGr as level3).
|
||||
pub const ModifierSnapshot = struct {
|
||||
shift: bool, // either shift held
|
||||
control: bool, // either control held
|
||||
alt: bool, // either alt held (including AltGr)
|
||||
right_alt: bool, // AltGr specifically — the XKB level-3 selector
|
||||
caps_lock: bool, // the toggle, not the key
|
||||
};
|
||||
|
||||
/// Tracks which keys are physically down and the caps-lock toggle, and classifies
|
||||
/// each decoded transition. Pure state — no IO — so repeat detection and modifier
|
||||
/// snapshots are host-testable.
|
||||
pub const KeyboardState = struct {
|
||||
/// One bit per HID usage: set while the key is physically down.
|
||||
pressed: [32]u8 = [_]u8{0} ** 32,
|
||||
caps_lock: bool = false,
|
||||
|
||||
pub fn apply(self: *KeyboardState, key: DecodedKey) Transition {
|
||||
const already_down = self.isPressed(key.usage);
|
||||
if (key.make) {
|
||||
if (!already_down) {
|
||||
self.setPressed(key.usage, true);
|
||||
if (key.usage == usage_caps_lock) self.caps_lock = !self.caps_lock;
|
||||
}
|
||||
return .{
|
||||
.action = if (already_down) .repeated else .pressed,
|
||||
.modifiers = self.snapshot(),
|
||||
};
|
||||
}
|
||||
self.setPressed(key.usage, false);
|
||||
return .{ .action = .released, .modifiers = self.snapshot() };
|
||||
}
|
||||
|
||||
pub fn isPressed(self: *const KeyboardState, usage: u8) bool {
|
||||
return self.pressed[usage / 8] & (@as(u8, 1) << @intCast(usage % 8)) != 0;
|
||||
}
|
||||
|
||||
fn setPressed(self: *KeyboardState, usage: u8, down: bool) void {
|
||||
const bit = @as(u8, 1) << @intCast(usage % 8);
|
||||
if (down) {
|
||||
self.pressed[usage / 8] |= bit;
|
||||
} else {
|
||||
self.pressed[usage / 8] &= ~bit;
|
||||
}
|
||||
}
|
||||
|
||||
fn snapshot(self: *const KeyboardState) ModifierSnapshot {
|
||||
const right_alt = self.isPressed(usage_right_alt);
|
||||
return .{
|
||||
.shift = self.isPressed(usage_left_shift) or self.isPressed(usage_right_shift),
|
||||
.control = self.isPressed(usage_left_control) or self.isPressed(usage_right_control),
|
||||
.alt = self.isPressed(usage_left_alt) or right_alt,
|
||||
.right_alt = right_alt,
|
||||
.caps_lock = self.caps_lock,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// --- tests (host-run via `zig build test`) ------------------------------------
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
/// Feed `bytes` and return the single DecodedKey they should produce (fails the
|
||||
/// test if they produce none or more than one).
|
||||
fn feedOne(decoder: *Decoder, bytes: []const u8) !DecodedKey {
|
||||
var result: ?DecodedKey = null;
|
||||
for (bytes) |byte| {
|
||||
if (decoder.feed(byte)) |key| {
|
||||
try testing.expect(result == null);
|
||||
result = key;
|
||||
}
|
||||
}
|
||||
return result orelse error.TestExpectedResult;
|
||||
}
|
||||
|
||||
fn feedNone(decoder: *Decoder, bytes: []const u8) !void {
|
||||
for (bytes) |byte| try testing.expectEqual(@as(?DecodedKey, null), decoder.feed(byte));
|
||||
}
|
||||
|
||||
test "base make and break: A" {
|
||||
var decoder = Decoder{};
|
||||
try testing.expectEqual(DecodedKey{ .usage = 0x04, .make = true }, try feedOne(&decoder, &.{0x1C}));
|
||||
try testing.expectEqual(DecodedKey{ .usage = 0x04, .make = false }, try feedOne(&decoder, &.{ 0xF0, 0x1C }));
|
||||
}
|
||||
|
||||
test "extended make and break: right arrow" {
|
||||
var decoder = Decoder{};
|
||||
try testing.expectEqual(DecodedKey{ .usage = 0x4F, .make = true }, try feedOne(&decoder, &.{ 0xE0, 0x74 }));
|
||||
try testing.expectEqual(DecodedKey{ .usage = 0x4F, .make = false }, try feedOne(&decoder, &.{ 0xE0, 0xF0, 0x74 }));
|
||||
}
|
||||
|
||||
test "pause: the E1 sequence is consumed silently" {
|
||||
var decoder = Decoder{};
|
||||
try feedNone(&decoder, &.{ 0xE1, 0x14, 0x77, 0xE1, 0xF0, 0x14, 0xF0, 0x77 });
|
||||
// The decoder is back in idle: an ordinary key still decodes.
|
||||
try testing.expectEqual(DecodedKey{ .usage = 0x04, .make = true }, try feedOne(&decoder, &.{0x1C}));
|
||||
}
|
||||
|
||||
test "protocol bytes decode to nothing" {
|
||||
var decoder = Decoder{};
|
||||
try feedNone(&decoder, &.{ 0xFA, 0xAA, 0xEE }); // ACK, BAT-passed, echo
|
||||
}
|
||||
|
||||
test "print screen: the fake-shift E0 12 decodes to nothing" {
|
||||
var decoder = Decoder{};
|
||||
try feedNone(&decoder, &.{ 0xE0, 0x12 });
|
||||
try testing.expectEqual(DecodedKey{ .usage = 0x46, .make = true }, try feedOne(&decoder, &.{ 0xE0, 0x7C }));
|
||||
}
|
||||
|
||||
test "typematic repeat is classified, not re-pressed" {
|
||||
var state = KeyboardState{};
|
||||
const a = DecodedKey{ .usage = 0x04, .make = true };
|
||||
try testing.expectEqual(Transition.Action.pressed, state.apply(a).action);
|
||||
try testing.expectEqual(Transition.Action.repeated, state.apply(a).action);
|
||||
try testing.expectEqual(Transition.Action.repeated, state.apply(a).action);
|
||||
try testing.expectEqual(Transition.Action.released, state.apply(.{ .usage = 0x04, .make = false }).action);
|
||||
try testing.expectEqual(Transition.Action.pressed, state.apply(a).action);
|
||||
}
|
||||
|
||||
test "shift held shows in the snapshot of other keys" {
|
||||
var state = KeyboardState{};
|
||||
_ = state.apply(.{ .usage = usage_left_shift, .make = true });
|
||||
const transition = state.apply(.{ .usage = 0x04, .make = true });
|
||||
try testing.expect(transition.modifiers.shift);
|
||||
try testing.expect(!transition.modifiers.control);
|
||||
_ = state.apply(.{ .usage = usage_left_shift, .make = false });
|
||||
_ = state.apply(.{ .usage = 0x04, .make = false });
|
||||
try testing.expect(!state.apply(.{ .usage = 0x04, .make = true }).modifiers.shift);
|
||||
}
|
||||
|
||||
test "right alt reports both alt and the level-3 selector" {
|
||||
var state = KeyboardState{};
|
||||
_ = state.apply(.{ .usage = usage_right_alt, .make = true });
|
||||
const transition = state.apply(.{ .usage = 0x04, .make = true });
|
||||
try testing.expect(transition.modifiers.alt);
|
||||
try testing.expect(transition.modifiers.right_alt);
|
||||
}
|
||||
|
||||
test "caps lock toggles on make, not on repeat or break" {
|
||||
var state = KeyboardState{};
|
||||
try testing.expect(state.apply(.{ .usage = usage_caps_lock, .make = true }).modifiers.caps_lock);
|
||||
try testing.expect(state.apply(.{ .usage = usage_caps_lock, .make = true }).modifiers.caps_lock); // repeat
|
||||
try testing.expect(state.apply(.{ .usage = usage_caps_lock, .make = false }).modifiers.caps_lock);
|
||||
try testing.expect(!state.apply(.{ .usage = usage_caps_lock, .make = true }).modifiers.caps_lock); // second press: off
|
||||
}
|
||||
|
|
@ -73,16 +73,125 @@ pub const modifier_shift: u32 = 1 << 0;
|
|||
pub const modifier_control: u32 = 1 << 1;
|
||||
pub const modifier_alt: u32 = 1 << 2;
|
||||
|
||||
/// A minimal danos-native keycode namespace — enough for the synthetic source and to show
|
||||
/// the shape. A real set (USB HID usage-style) fills in with the scancode decoder.
|
||||
/// The danos-native keycode namespace: USB HID keyboard-page usages (page 0x07), the
|
||||
/// numbering the PS/2 scancode decoder emits and the xkeyboard-config layout tables are
|
||||
/// indexed by. Non-exhaustive, so an unnamed usage still travels as a valid value.
|
||||
pub const Keycode = enum(u32) {
|
||||
unknown = 0,
|
||||
a = 4, // deliberately USB-HID-usage-aligned so a real decoder can extend this
|
||||
// letters
|
||||
a = 4,
|
||||
b = 5,
|
||||
c = 6,
|
||||
d = 7,
|
||||
e = 8,
|
||||
f = 9,
|
||||
g = 10,
|
||||
h = 11,
|
||||
i = 12,
|
||||
j = 13,
|
||||
k = 14,
|
||||
l = 15,
|
||||
m = 16,
|
||||
n = 17,
|
||||
o = 18,
|
||||
p = 19,
|
||||
q = 20,
|
||||
r = 21,
|
||||
s = 22,
|
||||
t = 23,
|
||||
u = 24,
|
||||
v = 25,
|
||||
w = 26,
|
||||
x = 27,
|
||||
y = 28,
|
||||
z = 29,
|
||||
// digit row
|
||||
one = 30,
|
||||
two = 31,
|
||||
three = 32,
|
||||
four = 33,
|
||||
five = 34,
|
||||
six = 35,
|
||||
seven = 36,
|
||||
eight = 37,
|
||||
nine = 38,
|
||||
zero = 39,
|
||||
// control and whitespace
|
||||
enter = 40,
|
||||
escape = 41,
|
||||
backspace = 42,
|
||||
tab = 43,
|
||||
spacebar = 44,
|
||||
// punctuation
|
||||
minus = 45,
|
||||
equal = 46,
|
||||
left_bracket = 47,
|
||||
right_bracket = 48,
|
||||
backslash = 49,
|
||||
non_us_hash = 50,
|
||||
semicolon = 51,
|
||||
apostrophe = 52,
|
||||
grave = 53,
|
||||
comma = 54,
|
||||
period = 55,
|
||||
slash = 56,
|
||||
caps_lock = 57,
|
||||
// function row
|
||||
f1 = 58,
|
||||
f2 = 59,
|
||||
f3 = 60,
|
||||
f4 = 61,
|
||||
f5 = 62,
|
||||
f6 = 63,
|
||||
f7 = 64,
|
||||
f8 = 65,
|
||||
f9 = 66,
|
||||
f10 = 67,
|
||||
f11 = 68,
|
||||
f12 = 69,
|
||||
print_screen = 70,
|
||||
scroll_lock = 71,
|
||||
pause = 72,
|
||||
// navigation
|
||||
insert = 73,
|
||||
home = 74,
|
||||
page_up = 75,
|
||||
delete = 76,
|
||||
end = 77,
|
||||
page_down = 78,
|
||||
right_arrow = 79,
|
||||
left_arrow = 80,
|
||||
down_arrow = 81,
|
||||
up_arrow = 82,
|
||||
// keypad
|
||||
num_lock = 83,
|
||||
keypad_slash = 84,
|
||||
keypad_asterisk = 85,
|
||||
keypad_minus = 86,
|
||||
keypad_plus = 87,
|
||||
keypad_enter = 88,
|
||||
keypad_one = 89,
|
||||
keypad_two = 90,
|
||||
keypad_three = 91,
|
||||
keypad_four = 92,
|
||||
keypad_five = 93,
|
||||
keypad_six = 94,
|
||||
keypad_seven = 95,
|
||||
keypad_eight = 96,
|
||||
keypad_nine = 97,
|
||||
keypad_zero = 98,
|
||||
keypad_period = 99,
|
||||
non_us_backslash = 100,
|
||||
application = 101,
|
||||
// modifiers
|
||||
left_control = 224,
|
||||
left_shift = 225,
|
||||
left_alt = 226,
|
||||
left_gui = 227,
|
||||
right_control = 228,
|
||||
right_shift = 229,
|
||||
right_alt = 230,
|
||||
right_gui = 231,
|
||||
_,
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue