144 lines
5.6 KiB
Zig
144 lines
5.6 KiB
Zig
//! PS/2 mouse packet assembly — the byte stream a streaming mouse sends, turned
|
|
//! into decoded movement/button reports.
|
|
//!
|
|
//! A standard PS/2 mouse in stream mode sends three-byte packets:
|
|
//!
|
|
//! byte 0: | Y ovf | X ovf | Y sign | X sign | 1 | middle | right | left |
|
|
//! byte 1: X movement (low eight bits; the sign bit lives in byte 0)
|
|
//! byte 2: Y movement (likewise)
|
|
//!
|
|
//! Movement is nine-bit two's complement, PS/2 convention: positive X right,
|
|
//! positive Y **up**. The decoded packet converts Y to the screen convention
|
|
//! (positive down), matching what every consumer of relative motion expects.
|
|
//! Bit 3 of byte 0 is always set — the resynchronization anchor: a byte at
|
|
//! packet start with bit 3 clear cannot be a packet header and is dropped.
|
|
//!
|
|
//! 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");
|
|
|
|
/// One decoded movement/button report, in screen convention (positive dy down).
|
|
pub const Packet = struct {
|
|
left: bool,
|
|
right: bool,
|
|
middle: bool,
|
|
dx: i16,
|
|
dy: i16,
|
|
};
|
|
|
|
const header_always_set: u8 = 1 << 3;
|
|
const header_left: u8 = 1 << 0;
|
|
const header_right: u8 = 1 << 1;
|
|
const header_middle: u8 = 1 << 2;
|
|
const header_x_sign: u8 = 1 << 4;
|
|
const header_y_sign: u8 = 1 << 5;
|
|
const header_x_overflow: u8 = 1 << 6;
|
|
const header_y_overflow: u8 = 1 << 7;
|
|
|
|
/// Device protocol bytes that can reach the packet stream around bring-up (the
|
|
/// acknowledge to enable-reporting, a reset's self-test result). Both have bit 3
|
|
/// set, so the header check alone cannot reject them; they are recognized only
|
|
/// at packet start, where a real header cannot be one of them in practice.
|
|
const response_acknowledge: u8 = 0xFA;
|
|
const response_self_test_passed: u8 = 0xAA;
|
|
|
|
/// Accumulates the byte stream into `Packet`s. Feed it every byte the mouse
|
|
/// sends; the third byte of each well-formed packet returns one.
|
|
pub const Assembler = struct {
|
|
bytes: [3]u8 = undefined,
|
|
count: u8 = 0,
|
|
|
|
pub fn feed(self: *Assembler, byte: u8) ?Packet {
|
|
if (self.count == 0) {
|
|
// Resynchronize: a packet must start with a plausible header.
|
|
if (byte & header_always_set == 0) return null;
|
|
if (byte == response_acknowledge or byte == response_self_test_passed) return null;
|
|
}
|
|
self.bytes[self.count] = byte;
|
|
self.count += 1;
|
|
if (self.count < 3) return null;
|
|
self.count = 0;
|
|
|
|
const header = self.bytes[0];
|
|
// An overflowed count is garbage by definition; discard the packet.
|
|
if (header & (header_x_overflow | header_y_overflow) != 0) return null;
|
|
|
|
return .{
|
|
.left = header & header_left != 0,
|
|
.right = header & header_right != 0,
|
|
.middle = header & header_middle != 0,
|
|
.dx = movement(self.bytes[1], header & header_x_sign != 0),
|
|
// PS/2 positive Y is up; screen positive Y is down.
|
|
.dy = -movement(self.bytes[2], header & header_y_sign != 0),
|
|
};
|
|
}
|
|
|
|
/// Nine-bit two's complement: the eight movement bits plus the header's sign.
|
|
fn movement(low: u8, negative: bool) i16 {
|
|
const value: i16 = low;
|
|
return if (negative) value - 256 else value;
|
|
}
|
|
};
|
|
|
|
// --- tests (host-run via `zig build test`) ------------------------------------
|
|
|
|
const testing = std.testing;
|
|
|
|
fn feedAll(assembler: *Assembler, bytes: []const u8) ?Packet {
|
|
var result: ?Packet = null;
|
|
for (bytes) |byte| {
|
|
if (assembler.feed(byte)) |packet| result = packet;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
test "plain motion decodes with screen-convention y" {
|
|
var assembler = Assembler{};
|
|
const packet = feedAll(&assembler, &.{ 0x08, 5, 3 }).?;
|
|
try testing.expectEqual(@as(i16, 5), packet.dx);
|
|
try testing.expectEqual(@as(i16, -3), packet.dy); // PS/2 up 3 -> screen -3
|
|
try testing.expect(!packet.left and !packet.right and !packet.middle);
|
|
}
|
|
|
|
test "negative movement sign-extends through the header bits" {
|
|
var assembler = Assembler{};
|
|
// X sign and Y sign set: dx = 0xFB - 256 = -5, dy raw = 0xFE - 256 = -2 -> screen +2.
|
|
const packet = feedAll(&assembler, &.{ 0x08 | 0x10 | 0x20, 0xFB, 0xFE }).?;
|
|
try testing.expectEqual(@as(i16, -5), packet.dx);
|
|
try testing.expectEqual(@as(i16, 2), packet.dy);
|
|
}
|
|
|
|
test "buttons decode from the header" {
|
|
var assembler = Assembler{};
|
|
const packet = feedAll(&assembler, &.{ 0x08 | 0x01 | 0x02, 0, 0 }).?;
|
|
try testing.expect(packet.left);
|
|
try testing.expect(packet.right);
|
|
try testing.expect(!packet.middle);
|
|
}
|
|
|
|
test "a byte with bit 3 clear at packet start is dropped" {
|
|
var assembler = Assembler{};
|
|
// The stray 0x02 cannot be a header; the following packet still decodes.
|
|
try testing.expectEqual(@as(?Packet, null), assembler.feed(0x02));
|
|
const packet = feedAll(&assembler, &.{ 0x09, 1, 0 }).?;
|
|
try testing.expect(packet.left);
|
|
try testing.expectEqual(@as(i16, 1), packet.dx);
|
|
}
|
|
|
|
test "protocol bytes at packet start are dropped" {
|
|
var assembler = Assembler{};
|
|
try testing.expectEqual(@as(?Packet, null), assembler.feed(0xFA)); // enable-reporting ACK
|
|
try testing.expectEqual(@as(?Packet, null), assembler.feed(0xAA)); // self-test passed
|
|
const packet = feedAll(&assembler, &.{ 0x08, 7, 0 }).?;
|
|
try testing.expectEqual(@as(i16, 7), packet.dx);
|
|
}
|
|
|
|
test "an overflowed packet is discarded whole" {
|
|
var assembler = Assembler{};
|
|
try testing.expectEqual(@as(?Packet, null), feedAll(&assembler, &.{ 0x08 | 0x40, 0xFF, 0xFF }));
|
|
// The assembler is back at packet start.
|
|
const packet = feedAll(&assembler, &.{ 0x08, 1, 1 }).?;
|
|
try testing.expectEqual(@as(i16, 1), packet.dx);
|
|
}
|