Extend the input service beyond the keyboard so mouse and joystick/gamepad drivers can broadcast too, with per-device publish and subscribe methods. - protocol: KeyEvent joins MouseEvent (motion/buttons/scroll) and JoystickEvent (axes/buttons), all carried in a common InputEvent envelope tagged with a DeviceKind. A subscribe request carries a device_mask, so a subscriber names the classes it wants and the service routes each event only to interested subscribers (a mouse-only listener never wakes for keystrokes). - runtime: per-device publish methods (publishKeyboardEvent/publishMouseEvent/ publishJoystickEvent) and subscribe helpers (subscribeKeyboard/Mouse/Joystick, each typed, plus subscribe(mask)/subscribeAll returning the tagged envelope). - service: subscriber table gains a device_mask; broadcast routes by the event's device class. - mouse driver now publishes (synthetic) mouse events like the keyboard driver; input-source cycles all three classes; input-test subscribes to all and only emits its "ok" marker once it has received one of each class — so the passing test proves per-device routing, not just delivery. Real HID decoding stays a follow-up. No kernel changes: ipc_send is generic and the 36-byte InputEvent fits its 64-byte payload. Full QEMU suite 48/48; serial log confirms keyboard, mouse, and joystick all reach one subscription.
211 lines
8.8 KiB
Zig
211 lines
8.8 KiB
Zig
//! The input wire protocol — the message format spoken between the user-space input
|
|
//! service ([input.zig](input.zig)) and the two kinds of process that reach it: a
|
|
//! **source** (a keyboard, mouse, or joystick/gamepad driver) that publishes events, and a
|
|
//! **subscriber** (any program) that subscribes and is then pushed each event.
|
|
//!
|
|
//! The service handles several device classes over one endpoint. Each class has its own
|
|
//! typed event (`KeyEvent`, `MouseEvent`, `JoystickEvent`); they all travel in a common
|
|
//! `InputEvent` envelope tagged with a `DeviceKind`, so the fan-out path is one code path
|
|
//! and a subscriber can take a mix of devices on a single stream. A subscriber declares
|
|
//! which classes it wants with a `device_mask`, and the service routes accordingly.
|
|
//!
|
|
//! Two message shapes ride over the endpoint, tagged by `Operation`, like the
|
|
//! [VFS protocol](../vfs/protocol.zig):
|
|
//!
|
|
//! - **subscribe / publish**: a synchronous `ipc_call` carrying a `Request`. `subscribe`
|
|
//! hands the service the subscriber's own endpoint as a capability (`send_cap`) and a
|
|
//! `device_mask`; `publish` carries an `InputEvent`. The reply is a `Reply`.
|
|
//! - **delivery**: the service pushes each `InputEvent` to every interested subscriber with
|
|
//! the asynchronous `ipc_send` — no reply owed, and a dead subscriber can never stall the
|
|
//! broadcast. Received in the subscriber's buffer with `Received.isMessage()` set.
|
|
//!
|
|
//! This is a danos-native contract, shared by the input service, the `runtime.input`
|
|
//! client helpers, and every source/subscriber. Everything fits one IPC message.
|
|
|
|
const std = @import("std");
|
|
|
|
/// The classes of input device the service fans out. Each names a typed event and a bit in
|
|
/// the subscription mask.
|
|
pub const DeviceKind = enum(u32) {
|
|
keyboard = 0,
|
|
mouse = 1,
|
|
joystick = 2, // joysticks and gamepads/controllers
|
|
};
|
|
|
|
/// Subscription-interest bits (`Request.device_mask`) — which device classes a subscriber
|
|
/// wants. OR them together, or use `device_all`.
|
|
pub const device_keyboard: u32 = 1 << 0;
|
|
pub const device_mouse: u32 = 1 << 1;
|
|
pub const device_joystick: u32 = 1 << 2;
|
|
pub const device_all: u32 = device_keyboard | device_mouse | device_joystick;
|
|
|
|
/// The subscription bit for a `DeviceKind` value (as it appears in `InputEvent.device`).
|
|
/// An unknown device maps to 0, so it matches no subscriber.
|
|
pub fn deviceBit(device: u32) u32 {
|
|
return switch (device) {
|
|
@intFromEnum(DeviceKind.keyboard) => device_keyboard,
|
|
@intFromEnum(DeviceKind.mouse) => device_mouse,
|
|
@intFromEnum(DeviceKind.joystick) => device_joystick,
|
|
else => 0,
|
|
};
|
|
}
|
|
|
|
// --- keyboard ---------------------------------------------------------------
|
|
|
|
/// What happened to a key. `key_down`/`key_up` are the physical make/break; `key_press`
|
|
/// is the higher-level "a character was produced", carrying it in `KeyEvent.character`.
|
|
pub const EventKind = enum(u32) {
|
|
key_down = 0,
|
|
key_up = 1,
|
|
key_press = 2,
|
|
};
|
|
|
|
/// One keyboard event. `keycode` names the physical key (layout-independent); `character`
|
|
/// is the Unicode scalar for `key_press` (else 0); `modifiers` is an OR of `modifier_*`.
|
|
pub const KeyEvent = extern struct {
|
|
kind: u32, // an EventKind
|
|
keycode: u32, // a Keycode
|
|
character: u32, // Unicode scalar for key_press, else 0
|
|
modifiers: u32, // OR of modifier_*
|
|
};
|
|
|
|
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.
|
|
pub const Keycode = enum(u32) {
|
|
unknown = 0,
|
|
a = 4, // deliberately USB-HID-usage-aligned so a real decoder can extend this
|
|
b = 5,
|
|
c = 6,
|
|
d = 7,
|
|
e = 8,
|
|
enter = 40,
|
|
_,
|
|
};
|
|
|
|
// --- mouse ------------------------------------------------------------------
|
|
|
|
/// What a mouse event reports. `motion` carries relative `dx`/`dy`; `button_down`/`up`
|
|
/// name a button in `button`; `scroll` carries `scroll_x`/`scroll_y`.
|
|
pub const MouseEventKind = enum(u32) {
|
|
motion = 0,
|
|
button_down = 1,
|
|
button_up = 2,
|
|
scroll = 3,
|
|
};
|
|
|
|
pub const mouse_button_left: u32 = 1 << 0;
|
|
pub const mouse_button_right: u32 = 1 << 1;
|
|
pub const mouse_button_middle: u32 = 1 << 2;
|
|
|
|
/// One mouse event. Relative motion (`dx`/`dy`) and wheel (`scroll_*`) are signed;
|
|
/// `buttons` is the current pressed-button bitmask (`mouse_button_*`).
|
|
pub const MouseEvent = extern struct {
|
|
kind: u32, // a MouseEventKind
|
|
button: u32, // the mouse_button_* bit for button_down/up, else 0
|
|
dx: i32, // relative X motion (.motion)
|
|
dy: i32, // relative Y motion (.motion)
|
|
scroll_x: i32, // horizontal wheel (.scroll)
|
|
scroll_y: i32, // vertical wheel (.scroll)
|
|
buttons: u32, // current pressed-button bitmask
|
|
};
|
|
|
|
// --- joystick / gamepad -----------------------------------------------------
|
|
|
|
/// What a joystick/gamepad event reports. `axis` carries a signed `value` on axis
|
|
/// `control`; `button_down`/`up` name a button index in `control`.
|
|
pub const JoystickEventKind = enum(u32) {
|
|
axis = 0,
|
|
button_down = 1,
|
|
button_up = 2,
|
|
};
|
|
|
|
/// One joystick/gamepad event. `control` is the axis index (`.axis`) or button index
|
|
/// (button events); `value` is the axis position (signed, e.g. -32768..32767) for `.axis`;
|
|
/// `buttons` is the current pressed-button bitmask.
|
|
pub const JoystickEvent = extern struct {
|
|
kind: u32, // a JoystickEventKind
|
|
control: u32, // axis index (.axis) or button index (button events)
|
|
value: i32, // axis value for .axis, else 0
|
|
buttons: u32, // current pressed-button bitmask
|
|
};
|
|
|
|
// --- the common envelope ----------------------------------------------------
|
|
|
|
/// The largest per-device event, so `InputEvent` can hold any of them inline.
|
|
pub const max_event_size: usize = @max(@sizeOf(KeyEvent), @max(@sizeOf(MouseEvent), @sizeOf(JoystickEvent)));
|
|
|
|
/// The tagged envelope broadcast to subscribers: a `DeviceKind` plus the raw bytes of the
|
|
/// matching per-device event. Decode it with `asKeyboard`/`asMouse`/`asJoystick` (each
|
|
/// returns null unless `device` matches), or build one with the `from*` constructors.
|
|
pub const InputEvent = extern struct {
|
|
device: u32, // a DeviceKind
|
|
_padding: u32 = 0,
|
|
data: [max_event_size]u8 = [_]u8{0} ** max_event_size,
|
|
|
|
pub fn asKeyboard(self: InputEvent) ?KeyEvent {
|
|
if (self.device != @intFromEnum(DeviceKind.keyboard)) return null;
|
|
return std.mem.bytesToValue(KeyEvent, self.data[0..@sizeOf(KeyEvent)]);
|
|
}
|
|
pub fn asMouse(self: InputEvent) ?MouseEvent {
|
|
if (self.device != @intFromEnum(DeviceKind.mouse)) return null;
|
|
return std.mem.bytesToValue(MouseEvent, self.data[0..@sizeOf(MouseEvent)]);
|
|
}
|
|
pub fn asJoystick(self: InputEvent) ?JoystickEvent {
|
|
if (self.device != @intFromEnum(DeviceKind.joystick)) return null;
|
|
return std.mem.bytesToValue(JoystickEvent, self.data[0..@sizeOf(JoystickEvent)]);
|
|
}
|
|
|
|
pub fn fromKeyboard(event: KeyEvent) InputEvent {
|
|
return pack(.keyboard, std.mem.asBytes(&event));
|
|
}
|
|
pub fn fromMouse(event: MouseEvent) InputEvent {
|
|
return pack(.mouse, std.mem.asBytes(&event));
|
|
}
|
|
pub fn fromJoystick(event: JoystickEvent) InputEvent {
|
|
return pack(.joystick, std.mem.asBytes(&event));
|
|
}
|
|
|
|
fn pack(device: DeviceKind, bytes: []const u8) InputEvent {
|
|
var self = InputEvent{ .device = @intFromEnum(device) };
|
|
@memcpy(self.data[0..bytes.len], bytes);
|
|
return self;
|
|
}
|
|
};
|
|
|
|
// --- request / reply --------------------------------------------------------
|
|
|
|
/// Which side of a request this is.
|
|
pub const Operation = enum(u32) {
|
|
subscribe = 0, // register the caller's endpoint (send_cap) for the classes in device_mask
|
|
publish = 1, // a source submits `event` to broadcast to interested subscribers
|
|
};
|
|
|
|
/// Request header. For `subscribe`, `device_mask` is the OR of `device_*` bits the caller
|
|
/// wants (0 means all) and the caller's receive endpoint travels as the call's capability;
|
|
/// `event` is ignored. For `publish`, `event` is the event to broadcast.
|
|
pub const Request = extern struct {
|
|
operation: u32, // an Operation
|
|
device_mask: u32 = 0, // subscribe: interested device classes (0 => all)
|
|
event: InputEvent = .{ .device = 0 },
|
|
};
|
|
|
|
/// Reply header. `status` is 0 on success or a negative errno.
|
|
pub const Reply = extern struct {
|
|
status: i32,
|
|
_padding: u32 = 0,
|
|
};
|
|
|
|
pub const request_size: usize = @sizeOf(Request);
|
|
pub const reply_size: usize = @sizeOf(Reply);
|
|
pub const event_size: usize = @sizeOf(InputEvent);
|
|
|
|
comptime {
|
|
// The delivery path posts a bare InputEvent through ipc_send, so it must fit an
|
|
// endpoint's async payload slot (POST_MAXIMUM is 64).
|
|
if (event_size > 64) @compileError("InputEvent must fit the ipc_send payload (POST_MAXIMUM)");
|
|
}
|