danos/library/protocol/input/input-protocol.zig

395 lines
15 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`); a subscriber declares which
//! classes it wants with a `device_mask`, and the service routes accordingly.
//!
//! Three shapes ride over the channel, and the envelope names all three
//! (docs/os-development/protocol-namespace.md):
//!
//! - **subscribe** is the *reserved* verb, not one of this protocol's own: its shape — a
//! synchronous call whose attached capability is the subscriber's endpoint — is exactly
//! what `envelope.operation_subscribe` means everywhere. The interest mask travels as the
//! packet's tail (`Subscribe`), because a reserved verb carries no typed request.
//! - **publish** is this protocol's one verb: a source sends one `InputEvent` and the
//! service answers at once, so publishing never blocks on a slow subscriber.
//! - **delivery** is an event push: the service `ipc_send`s each event to every interested
//! subscriber — no reply owed, so a dead subscriber can never stall the broadcast. The
//! packet is the folded header plus the typed event, and **the device class is the
//! header's operation**: one event per class, so a subscriber reads the kind from the
//! packet rather than from a tag inside the payload.
//!
//! `Header.target` is unused (0) in both directions: the service is the only object either
//! side addresses.
const std = @import("std");
const envelope = @import("envelope");
/// 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;
/// 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,
// 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,
_,
};
// --- 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 tagged union of the three ------------------------------------------
/// 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)));
/// One event of any class: a `DeviceKind` plus the raw bytes of the matching per-device
/// event. This is what a source `publish`es (one verb for all three classes) and what a
/// subscriber's helper hands back after decoding a delivery — on the *delivery* wire the
/// class is the packet header's operation instead, so this tag never travels there. 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;
}
};
// --- the contract -----------------------------------------------------------
/// The body of a `subscribe` — the envelope's reserved verb 2, whose shape (a call whose
/// capability is the subscriber's own endpoint) this protocol adopts wholesale. A reserved
/// verb has no typed request, so the mask travels as the packet's tail and `encodeSubscribe`
/// is how a client lays it down. Zero means every class.
pub const Subscribe = extern struct { device_mask: u32 = 0 };
pub const Protocol = envelope.Define(.{
.name = "input",
.version = 1,
.operations = &.{
// A source submits one event; the service broadcasts it to whoever wants that class.
.{ .name = "publish", .request = InputEvent },
},
.events = &.{
// One per device class: the class is the packet's operation, the typed event its
// payload. The push floor is 64 bytes and the header spends 16 of them, so the
// widest of these — the 28-byte mouse event — leaves the budget with room to spare.
.{ .name = "keyboard", .payload = KeyEvent },
.{ .name = "mouse", .payload = MouseEvent },
.{ .name = "joystick", .payload = JoystickEvent },
},
});
pub const Operation = Protocol.Operation;
pub const Event = Protocol.Event;
pub const message_maximum: usize = Protocol.message_maximum;
pub const event_size: usize = @sizeOf(InputEvent);
/// The event class a `DeviceKind` value (as it appears in `InputEvent.device`) is delivered
/// as. Null for a value no class claims, which is delivered to nobody.
pub fn eventOfDevice(device: u32) ?Event {
return switch (device) {
@intFromEnum(DeviceKind.keyboard) => .keyboard,
@intFromEnum(DeviceKind.mouse) => .mouse,
@intFromEnum(DeviceKind.joystick) => .joystick,
else => null,
};
}
/// Frame a `subscribe` request: the reserved verb's header, then the interest mask. Null if
/// the buffer is too small. Spelled here rather than at each caller so the one place that
/// knows a reserved verb carries its body in the tail is the protocol module.
pub fn encodeSubscribe(device_mask: u32, buffer: []u8) ?[]u8 {
const total = envelope.prefix_size + @sizeOf(Subscribe);
if (buffer.len < total) return null;
const header = envelope.Header{ .operation = envelope.operation_subscribe };
const body = Subscribe{ .device_mask = device_mask };
@memcpy(buffer[0..envelope.prefix_size], std.mem.asBytes(&header));
@memcpy(buffer[envelope.prefix_size..][0..@sizeOf(Subscribe)], std.mem.asBytes(&body));
return buffer[0..total];
}
/// The interest mask out of a `subscribe` packet's tail, on the provider's side. A caller
/// that sent no mask at all means every class, which is what a zero mask means anyway.
pub fn decodeSubscribe(tail: []const u8) Subscribe {
if (tail.len < @sizeOf(Subscribe)) return .{};
return std.mem.bytesToValue(Subscribe, tail[0..@sizeOf(Subscribe)]);
}
test "an event of every class fits the push floor, header included" {
// What the hand-rolled comptime assert used to say about `InputEvent`, now
// said by `Define` about each typed event — and counting the header, which
// the old check did not.
try std.testing.expectEqual(envelope.prefix_size + @sizeOf(MouseEvent), Protocol.event_maximum);
try std.testing.expect(Protocol.event_maximum <= envelope.post_maximum);
}
test "the verb numbering, and the class an event carries" {
try std.testing.expectEqual(@as(u32, 16), @intFromEnum(Operation.publish));
// Events number in their own space, so the three classes start at 16 too.
try std.testing.expectEqual(@as(u32, 16), @intFromEnum(Event.keyboard));
try std.testing.expectEqual(@as(u32, 17), @intFromEnum(Event.mouse));
try std.testing.expectEqual(@as(u32, 18), @intFromEnum(Event.joystick));
// subscribe is the RESERVED verb, below the protocol range entirely.
try std.testing.expectEqual(@as(u32, 2), envelope.operation_subscribe);
var buffer: [envelope.post_maximum]u8 = undefined;
const packet = Protocol.encodeEvent(.mouse, 0, .{
.kind = @intFromEnum(MouseEventKind.motion),
.button = 0,
.dx = 3,
.dy = -4,
.scroll_x = 0,
.scroll_y = 0,
.buttons = 0,
}, &buffer).?;
try std.testing.expectEqual(Event.mouse, Protocol.eventOf(packet).?);
try std.testing.expectEqual(@as(i32, -4), Protocol.decodeEvent(.mouse, packet).?.dy);
}
test "a subscribe carries its mask in the tail of the reserved verb" {
var buffer: [envelope.packet_maximum]u8 = undefined;
const packet = encodeSubscribe(device_mouse, &buffer).?;
try std.testing.expectEqual(envelope.operation_subscribe, envelope.headerOf(packet).?.operation);
try std.testing.expectEqual(device_mouse, decodeSubscribe(packet[envelope.prefix_size..]).device_mask);
// A caller that sent nothing at all reads as the every-class mask.
try std.testing.expectEqual(@as(u32, 0), decodeSubscribe(&.{}).device_mask);
}