//! 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); }