summaryrefslogtreecommitdiff
path: root/src/esp32p4_gpio.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-06 18:11:36 -0300
committerGabriel Schneider <[email protected]>2026-09-07 13:59:12 -0300
commit60367d8fe23f6af98ec28e3cf6c2094dfe332df0 (patch)
tree310fc734173cf771881f4691c71909135fadde97 /src/esp32p4_gpio.zig
parentfa82cac885cb4738fe36d1e49b4749b5a3e31a4a (diff)
downloadpardes-60367d8fe23f6af98ec28e3cf6c2094dfe332df0.tar.gz
pardes-60367d8fe23f6af98ec28e3cf6c2094dfe332df0.zip
Refactor panes and filesystem; replace FUSE with 9P
Consolidate pane, layout, memory and host code. Serve 9P by default over Unix sockets, with runtime mounts and optional TCP/QUIC transports. Remove FUSE and obsolete proof-of-concept examples. Fix highlighting and terminal-history performance, expand differential and stress-test infrastructure, sort navigation results while preserving the next occurrence, add syntax-colored Braille minimaps, remove SPC-k, and document 9P interaction as a repository skill.
Diffstat (limited to 'src/esp32p4_gpio.zig')
-rw-r--r--src/esp32p4_gpio.zig576
1 files changed, 576 insertions, 0 deletions
diff --git a/src/esp32p4_gpio.zig b/src/esp32p4_gpio.zig
new file mode 100644
index 00000000..92468581
--- /dev/null
+++ b/src/esp32p4_gpio.zig
@@ -0,0 +1,576 @@
+//! ESP32-P4 JP1 GPIO data and its freestanding filesystem.
+const std = @import("std");
+
+// JP1: JC-ESP32P4-M3-DEV schematic, sheet 2 "Expand IO":
+// 01-esp32p4-m3/docs/schematics/2_EXPAND_IO&BAT.png
+pub const Header = struct {
+ pub const Pad = union(enum) {
+ none,
+ gpio: u8,
+ // C6 and power nets are not P4 GPIOs; the schematic gives no GPIO numbers for ES_I2C.
+ net: []const u8,
+
+ fn label(comptime p: Pad) []const u8 {
+ return switch (p) {
+ .none => "--",
+ .gpio => |n| std.fmt.comptimePrint("GPIO {d}", .{n}),
+ .net => |s| s,
+ };
+ }
+ };
+
+ pub const Row = struct { left: Pad, right: Pad };
+
+ pub const rows = [13]Row{
+ .{ .left = .{ .net = "3V3" }, .right = .{ .net = "5V" } },
+ .{ .left = .{ .net = "3V3" }, .right = .{ .net = "5V" } },
+ .{ .left = .{ .net = "GND" }, .right = .{ .net = "GND" } },
+ .{ .left = .{ .gpio = 1 }, .right = .none },
+ .{ .left = .{ .gpio = 2 }, .right = .{ .gpio = 47 } },
+ .{ .left = .{ .gpio = 3 }, .right = .{ .gpio = 46 } },
+ .{ .left = .{ .gpio = 4 }, .right = .{ .gpio = 45 } },
+ .{ .left = .{ .gpio = 5 }, .right = .{ .net = "GND" } },
+ .{ .left = .{ .gpio = 20 }, .right = .{ .net = "3V3" } },
+ .{ .left = .{ .gpio = 32 }, .right = .{ .net = "C6_U0RXD" } },
+ .{ .left = .{ .gpio = 33 }, .right = .{ .net = "C6_U0TXD" } },
+ .{ .left = .{ .net = "ES_I2C_SDA" }, .right = .{ .net = "C6_IO9" } },
+ .{ .left = .{ .net = "ES_I2C_SCL" }, .right = .{ .net = "C6_CHIP_PU" } },
+ };
+
+ const row_format = "{s:>10} | {d:>2} | {d:>2} | {s}\n";
+
+ const border = " +---------+\n";
+
+ pub const text = rendered: {
+ var out: []const u8 =
+ \\JP1 header - 26 pins, pin 1 top left.
+ \\Every number here is DECIMAL.
+ \\
+ \\
+ ;
+ out = out ++ border;
+ for (rows, 0..) |row, i| out = out ++ std.fmt.comptimePrint(
+ row_format,
+ .{ row.left.label(), 2 * i + 1, 2 * i + 2, row.right.label() },
+ );
+ break :rendered out ++ border ++
+ \\
+ \\Gpio <pin> flips one: 0->1 or 1->0.
+ \\
+ ;
+ };
+
+ pub const gpio_pins = pins: {
+ var found: [2 * rows.len]u8 = undefined;
+ var n: usize = 0;
+ for (rows) |row| for ([2]Pad{ row.left, row.right }) |p| switch (p) {
+ .gpio => |g| {
+ found[n] = g;
+ n += 1;
+ },
+ else => {},
+ };
+ std.mem.sort(u8, found[0..n], {}, std.sort.asc(u8));
+ break :pins found[0..n].*;
+ };
+
+ test "the rendered header is the drawing the console has always printed" {
+ try std.testing.expectEqualStrings(
+ \\JP1 header - 26 pins, pin 1 top left.
+ \\Every number here is DECIMAL.
+ \\
+ \\ +---------+
+ \\ 3V3 | 1 | 2 | 5V
+ \\ 3V3 | 3 | 4 | 5V
+ \\ GND | 5 | 6 | GND
+ \\ GPIO 1 | 7 | 8 | --
+ \\ GPIO 2 | 9 | 10 | GPIO 47
+ \\ GPIO 3 | 11 | 12 | GPIO 46
+ \\ GPIO 4 | 13 | 14 | GPIO 45
+ \\ GPIO 5 | 15 | 16 | GND
+ \\ GPIO 20 | 17 | 18 | 3V3
+ \\ GPIO 32 | 19 | 20 | C6_U0RXD
+ \\ GPIO 33 | 21 | 22 | C6_U0TXD
+ \\ES_I2C_SDA | 23 | 24 | C6_IO9
+ \\ES_I2C_SCL | 25 | 26 | C6_CHIP_PU
+ \\ +---------+
+ \\
+ \\Gpio <pin> flips one: 0->1 or 1->0.
+ \\
+ , text);
+ }
+
+ test "the header's own GPIOs, and only those" {
+ try std.testing.expectEqualSlices(u8, &.{ 1, 2, 3, 4, 5, 20, 32, 33, 45, 46, 47 }, &gpio_pins);
+ try std.testing.expectEqual(Pad.none, rows[3].right);
+ try std.testing.expectEqualStrings("C6_IO9", rows[11].right.net);
+ }
+};
+
+pub const E = struct {
+ pub const NOENT: u16 = 2;
+ pub const NOTDIR: u16 = 20;
+ pub const INVAL: u16 = 22;
+};
+
+pub const root: u64 = 1;
+const gpio_node: u64 = 16;
+const pinout_node: u64 = 17;
+
+const Entry = struct {
+ node: u64,
+ parent: u64,
+ name: []const u8,
+ kind: enum { directory, pinout, value },
+ pin: u8 = 0,
+};
+
+const entries = table: {
+ var result: [3 + 2 * Header.gpio_pins.len]Entry = undefined;
+ result[0] = .{ .node = root, .parent = root, .name = "/", .kind = .directory };
+ result[1] = .{ .node = gpio_node, .parent = root, .name = "gpio", .kind = .directory };
+ result[2] = .{ .node = pinout_node, .parent = gpio_node, .name = "pinout", .kind = .pinout };
+ for (Header.gpio_pins, 0..) |pin, i| {
+ const directory = 18 + i;
+ result[3 + 2 * i] = .{
+ .node = directory,
+ .parent = gpio_node,
+ .name = std.fmt.comptimePrint("{d}", .{pin}),
+ .kind = .directory,
+ };
+ result[4 + 2 * i] = .{
+ .node = (2 + i) * 16 + 1,
+ .parent = directory,
+ .name = "value",
+ .kind = .value,
+ .pin = pin,
+ };
+ }
+ break :table result;
+};
+
+const dirent_fixed = 10;
+const out_capacity = size: {
+ var result: usize = 2;
+ for (entries) |directory| {
+ if (directory.kind != .directory) continue;
+ var bytes: usize = 0;
+ for (entries) |entry| {
+ if (entry.parent == directory.node and entry.node != directory.node)
+ bytes += dirent_fixed + entry.name.len;
+ }
+ result = @max(result, bytes);
+ }
+ break :size result;
+};
+
+pub fn Fs(comptime Pads: type) type {
+ return struct {
+ const Self = @This();
+ pub const name_capacity = 28;
+
+ pub const Op = enum(u8) { lookup, getattr, setattr, open, read, write, release, readdir };
+
+ pub const Status = enum(u8) { ok, again, err };
+
+ pub const Req = struct {
+ tag: u64,
+ op: Op,
+ node: u64,
+ handle: u32 = 0,
+ off: u64 = 0,
+ size: u32 = 0,
+ data: []const u8 = &.{},
+ truncate: bool = false,
+ };
+
+ pub const Reply = struct {
+ tag: u64,
+ status: Status = .ok,
+ errno: u16 = 0,
+ attr: Attr = .{},
+ handle: u32 = 0,
+ written: u32 = 0,
+
+ pub const Attr = struct {
+ name: []const u8 = "",
+ node: u64 = 0,
+ dir: bool = false,
+ size: u64 = 0,
+ mode: u16 = 0o600,
+ };
+
+ pub fn fail(tag: u64, e: u16) Reply {
+ return .{ .tag = tag, .status = .err, .errno = e };
+ }
+ };
+
+ // Reply bytes are borrowed until the next handle call.
+ pub const Answer = struct { reply: Reply, bytes: []const u8 = "" };
+
+ out: [out_capacity]u8 = undefined,
+
+ fn find(node: u64) ?*const Entry {
+ for (&entries) |*entry| if (entry.node == node) return entry;
+ return null;
+ }
+
+ fn attributes(entry: *const Entry) Reply.Attr {
+ return .{
+ .name = entry.name,
+ .node = entry.node,
+ .dir = entry.kind == .directory,
+ .mode = switch (entry.kind) {
+ .directory => 0o500,
+ .pinout => 0o400,
+ .value => 0o600,
+ },
+ .size = switch (entry.kind) {
+ .directory => 0,
+ .pinout => Header.text.len,
+ .value => 2,
+ },
+ };
+ }
+
+ pub fn handle(fs: *Self, req: Req) Answer {
+ const entry = find(req.node) orelse return .{ .reply = .fail(req.tag, E.NOENT) };
+ switch (req.op) {
+ .lookup => {
+ if (entry.kind != .directory) return .{ .reply = .fail(req.tag, E.NOTDIR) };
+ if (std.mem.eql(u8, req.data, ".."))
+ return .{ .reply = .{ .tag = req.tag, .attr = attributes(find(entry.parent).?) } };
+ for (&entries) |*child| {
+ if (child.parent != req.node or child.node == req.node) continue;
+ if (!std.mem.eql(u8, child.name, req.data)) continue;
+ return .{ .reply = .{ .tag = req.tag, .attr = attributes(child) } };
+ }
+ return .{ .reply = .fail(req.tag, E.NOENT) };
+ },
+ .getattr => return .{ .reply = .{ .tag = req.tag, .attr = attributes(entry) } },
+ .setattr => {
+ // GPIO register views have no storage to truncate.
+ if (entry.kind == .directory) return .{ .reply = .fail(req.tag, E.INVAL) };
+ return .{ .reply = .{ .tag = req.tag, .attr = attributes(entry) } };
+ },
+ .open => return .{ .reply = .{ .tag = req.tag, .handle = 1 } },
+ .release => return .{ .reply = .{ .tag = req.tag } },
+ .read => {
+ const bytes = switch (entry.kind) {
+ .directory => return .{ .reply = .fail(req.tag, E.INVAL) },
+ .pinout => Header.text,
+ .value => value: {
+ fs.out[0] = '0' + @as(u8, Pads.level(entry.pin));
+ fs.out[1] = '\n';
+ break :value fs.out[0..2];
+ },
+ };
+ const off: usize = @intCast(@min(req.off, bytes.len));
+ return .{ .reply = .{ .tag = req.tag }, .bytes = bytes[off..][0..@min(bytes.len - off, req.size)] };
+ },
+ .write => {
+ if (entry.kind != .value or req.off != 0) return .{ .reply = .fail(req.tag, E.INVAL) };
+ const value = std.mem.trimEnd(u8, req.data, "\r\n");
+ if (value.len != 1 or (value[0] != '0' and value[0] != '1'))
+ return .{ .reply = .fail(req.tag, E.INVAL) };
+ Pads.drive(entry.pin, @intCast(value[0] - '0'));
+ return .{ .reply = .{ .tag = req.tag, .written = @intCast(req.data.len) } };
+ },
+ .readdir => {
+ if (entry.kind != .directory) return .{ .reply = .fail(req.tag, E.NOTDIR) };
+ var len: usize = 0;
+ var skip = req.off;
+ for (entries) |child| {
+ if (child.parent != req.node or child.node == req.node) continue;
+ if (skip != 0) {
+ skip -= 1;
+ continue;
+ }
+ std.mem.writeInt(u64, fs.out[len..][0..8], child.node, .little);
+ fs.out[len + 8] = @intFromBool(child.kind == .directory);
+ fs.out[len + 9] = @intCast(child.name.len);
+ @memcpy(fs.out[len + dirent_fixed ..][0..child.name.len], child.name);
+ len += dirent_fixed + child.name.len;
+ }
+ return .{ .reply = .{ .tag = req.tag }, .bytes = fs.out[0..len] };
+ },
+ }
+ }
+ };
+}
+
+const testing = std.testing;
+
+const StubPads = struct {
+ var driven: [64]u1 = @splat(0);
+ var log: [16]Call = undefined;
+ var log_len: usize = 0;
+
+ const Call = struct { pin: u8, level: u1 };
+
+ fn reset() void {
+ driven = @splat(0);
+ log_len = 0;
+ }
+
+ fn level(pin: u8) u1 {
+ return driven[pin];
+ }
+
+ fn drive(pin: u8, want: u1) void {
+ driven[pin] = want;
+ log[log_len] = .{ .pin = pin, .level = want };
+ log_len += 1;
+ }
+};
+
+const Board = Fs(StubPads);
+
+fn walk(t: *Board, path: []const []const u8) !Board.Reply.Attr {
+ var at: u64 = root;
+ var attr: Board.Reply.Attr = .{ .node = root, .dir = true, .mode = 0o500 };
+ for (path) |name| {
+ const a = t.handle(.{ .tag = 1, .op = .lookup, .node = at, .data = name });
+ if (a.reply.status == .err) return switch (a.reply.errno) {
+ E.NOENT => error.NoEntry,
+ E.NOTDIR => error.NotDirectory,
+ else => error.Refused,
+ };
+ attr = a.reply.attr;
+ at = attr.node;
+ }
+ return attr;
+}
+
+fn readAll(t: *Board, node: u64) !Board.Answer {
+ const open = t.handle(.{ .tag = 1, .op = .open, .node = node });
+ try testing.expectEqual(Board.Status.ok, open.reply.status);
+ return t.handle(.{ .tag = 2, .op = .read, .node = node, .handle = open.reply.handle, .size = 65535 });
+}
+
+test "GPIO: the generated tree has exactly the header's pins, and nothing else" {
+ var t: Board = .{};
+
+ try testing.expect((try walk(&t, &.{"gpio"})).dir);
+ try testing.expect(!(try walk(&t, &.{ "gpio", "pinout" })).dir);
+
+ for (Header.gpio_pins, 0..) |pin, i| {
+ var name: [4]u8 = undefined;
+ const dir = try std.fmt.bufPrint(&name, "{d}", .{pin});
+ const directory = try walk(&t, &.{ "gpio", dir });
+ try testing.expect(directory.dir);
+ try testing.expectEqual(@as(u64, 18 + i), directory.node);
+ const value = try walk(&t, &.{ "gpio", dir, "value" });
+ try testing.expect(!value.dir);
+ try testing.expectEqual(@as(u64, (2 + i) * 16 + 1), value.node);
+ try testing.expectEqual(@as(u16, 0o600), value.mode);
+ }
+
+ try testing.expectError(error.NoEntry, walk(&t, &.{ "gpio", "6" }));
+ try testing.expectError(error.NoEntry, walk(&t, &.{ "gpio", "21" }));
+ try testing.expectError(error.NoEntry, walk(&t, &.{ "gpio", "20", "level" }));
+ try testing.expectError(error.NoEntry, walk(&t, &.{"mem"}));
+}
+
+test "GPIO: a read of gpio/pinout is the bytes the Gpio word draws" {
+ var t: Board = .{};
+ const at = try walk(&t, &.{ "gpio", "pinout" });
+ const a = try readAll(&t, at.node);
+ try testing.expectEqualStrings(Header.text, a.bytes);
+ try testing.expectEqual(Header.text.len, at.size);
+ try testing.expectEqual(@as(u16, 0o400), at.mode);
+ const w = t.handle(.{ .tag = 3, .op = .write, .node = at.node, .data = "x" });
+ try testing.expectEqual(E.INVAL, w.reply.errno);
+}
+
+test "GPIO: writing 1 then 0 drives the pad twice, through the seam" {
+ StubPads.reset();
+ var t: Board = .{};
+ const at = try walk(&t, &.{ "gpio", "20", "value" });
+
+ const before = try readAll(&t, at.node);
+ try testing.expectEqualStrings("0\n", before.bytes);
+
+ const one = t.handle(.{ .tag = 4, .op = .write, .node = at.node, .data = "1" });
+ try testing.expectEqual(Board.Status.ok, one.reply.status);
+ try testing.expectEqual(@as(u32, 1), one.reply.written);
+ try testing.expectEqualStrings("1\n", (try readAll(&t, at.node)).bytes);
+
+ const zero = t.handle(.{ .tag = 5, .op = .write, .node = at.node, .data = "0\n" });
+ try testing.expectEqual(@as(u32, 2), zero.reply.written);
+ try testing.expectEqualStrings("0\n", (try readAll(&t, at.node)).bytes);
+
+ try testing.expectEqual(@as(usize, 2), StubPads.log_len);
+ try testing.expectEqual(StubPads.Call{ .pin = 20, .level = 1 }, StubPads.log[0]);
+ try testing.expectEqual(StubPads.Call{ .pin = 20, .level = 0 }, StubPads.log[1]);
+}
+
+test "GPIO: a pad takes 0 and 1 and refuses everything else, without touching the pads" {
+ StubPads.reset();
+ var t: Board = .{};
+ const at = try walk(&t, &.{ "gpio", "45", "value" });
+
+ for ([_][]const u8{ "2", "", "01", "x", "true", "high", "\n", "1 ", " 1", "10" }) |bad| {
+ const a = t.handle(.{ .tag = 6, .op = .write, .node = at.node, .data = bad });
+ try testing.expectEqual(Board.Status.err, a.reply.status);
+ try testing.expectEqual(E.INVAL, a.reply.errno);
+ }
+ try testing.expectEqual(@as(usize, 0), StubPads.log_len);
+
+ const off = t.handle(.{ .tag = 7, .op = .write, .node = at.node, .off = 1, .data = "1" });
+ try testing.expectEqual(E.INVAL, off.reply.errno);
+ try testing.expectEqual(@as(usize, 0), StubPads.log_len);
+}
+
+test "GPIO: node parents are explicit, not derived from node bits" {
+ for (&entries) |*e| {
+ const serial = e.node >> 4;
+ const file = e.node & 0xF;
+ const derived: u64 = if (e.node == root or serial == 0 or file == 0) root else serial << 4;
+ if (derived == e.parent) continue;
+ try testing.expectEqualStrings("value", e.name);
+ var t: Board = .{};
+ const a = t.handle(.{ .tag = 8, .op = .getattr, .node = derived });
+ try testing.expectEqual(E.NOENT, a.reply.errno);
+ }
+}
+
+test "GPIO: parent lookup follows every generated directory" {
+ var t: Board = .{};
+ for (&entries) |*entry| {
+ if (entry.kind != .directory) continue;
+ const answer = t.handle(.{ .tag = 1, .op = .lookup, .node = entry.node, .data = ".." });
+ try testing.expectEqual(Board.Status.ok, answer.reply.status);
+ try testing.expectEqual(entry.parent, answer.reply.attr.node);
+ try testing.expect(answer.reply.attr.dir);
+ }
+}
+
+test "GPIO: a directory read lists what the table generated, in table order" {
+ var t: Board = .{};
+
+ try testing.expectEqualStrings("gpio", (try names(&t, root, 0))[0]);
+ try testing.expectEqual(@as(usize, 1), (try names(&t, root, 0)).len);
+
+ const gpio = (try walk(&t, &.{"gpio"})).node;
+ const listing = try names(&t, gpio, 0);
+ try testing.expectEqual(Header.gpio_pins.len + 1, listing.len);
+ try testing.expectEqualStrings("pinout", listing[0]);
+ for (Header.gpio_pins, 0..) |pin, i| {
+ var buf: [4]u8 = undefined;
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&buf, "{d}", .{pin}), listing[i + 1]);
+ }
+
+ const rest = try names(&t, gpio, 5);
+ try testing.expectEqual(Header.gpio_pins.len + 1 - 5, rest.len);
+ try testing.expectEqualStrings("5", rest[0]);
+}
+
+var name_slots: [32][]const u8 = undefined;
+fn names(t: *Board, node: u64, skip: u64) ![][]const u8 {
+ const a = t.handle(.{ .tag = 9, .op = .readdir, .node = node, .off = skip, .size = 65535 });
+ try testing.expectEqual(Board.Status.ok, a.reply.status);
+ var n: usize = 0;
+ var i: usize = 0;
+ while (i < a.bytes.len) {
+ const len = a.bytes[i + 9];
+ name_slots[n] = a.bytes[i + 10 ..][0..len];
+ n += 1;
+ i += 10 + len;
+ }
+ return name_slots[0..n];
+}
+
+test "GPIO: the whole tree costs one buffer, and the table says how big" {
+ try testing.expectEqual(@as(usize, 143), out_capacity);
+ try testing.expectEqual(@as(usize, 143), @sizeOf(Board));
+ try testing.expectEqual(@as(usize, 28), Board.name_capacity);
+ try testing.expect(Header.text.len > out_capacity);
+}
+
+test "GPIO: stat and truncation preserve the register value and reads respect offsets" {
+ StubPads.reset();
+ var fs: Board = .{};
+ const value = try walk(&fs, &.{ "gpio", "20", "value" });
+ for (0..2) |_| {
+ const written = fs.handle(.{ .tag = 1, .op = .write, .node = value.node, .data = "1\r\n" });
+ try testing.expectEqual(@as(u32, 3), written.reply.written);
+ }
+ try testing.expectEqual(@as(usize, 2), StubPads.log_len);
+ const stat = fs.handle(.{ .tag = 2, .op = .getattr, .node = value.node });
+ try testing.expectEqual(@as(u64, 2), stat.reply.attr.size);
+ const truncated = fs.handle(.{ .tag = 3, .op = .setattr, .node = value.node, .truncate = true });
+ try testing.expectEqual(Board.Status.ok, truncated.reply.status);
+ try testing.expectEqual(@as(u1, 1), StubPads.driven[20]);
+ const part = fs.handle(.{ .tag = 4, .op = .read, .node = value.node, .off = 1, .size = 1 });
+ try testing.expectEqualStrings("\n", part.bytes);
+ const eof = fs.handle(.{ .tag = 5, .op = .read, .node = value.node, .off = 2, .size = 1 });
+ try testing.expectEqual(@as(usize, 0), eof.bytes.len);
+ const pinout = try walk(&fs, &.{ "gpio", "pinout" });
+ const text = fs.handle(.{ .tag = 6, .op = .read, .node = pinout.node, .off = 10, .size = 12 });
+ try testing.expectEqualStrings(Header.text[10..22], text.bytes);
+}
+
+const ninep = @import("9p.zig");
+
+test "GPIO: a real 9P client reads a pin's value off this tree" {
+ StubPads.reset();
+ const Server = ninep.Server(Board, ninep.board_fids);
+ var in: [1024]u8 = undefined;
+ var out: [2048]u8 = undefined;
+ var fsys: Board = .{};
+ var srv = Server.init(.{ .in = &in, .out = &out, .root = root });
+
+ var scratch: [256]u8 = undefined;
+ const send = struct {
+ fn call(s: *Server, f: *Board, buf: []u8, tag: u16, msg: ninep.Msg) !void {
+ const bytes = try ninep.encode(msg, tag, buf);
+ try testing.expectEqual(bytes.len, s.push(bytes));
+ while (s.retry()) |req| {
+ const a = f.handle(req);
+ s.reply(&a.reply, a.bytes);
+ }
+ while (s.next()) |req| {
+ const a = f.handle(req);
+ s.reply(&a.reply, a.bytes);
+ }
+ }
+ }.call;
+ const reap = struct {
+ fn call(s: *Server) !ninep.Decoded {
+ const queued = s.output();
+ const len = ninep.frameLen(queued) orelse return error.NoReply;
+ const got = try ninep.decode(queued[0..len]);
+ s.wrote(len);
+ return got;
+ }
+ }.call;
+
+ try send(&srv, &fsys, &scratch, ninep.notag, .{ .tversion = .{ .msize = 8192, .version = "9P2000" } });
+ const v = try reap(&srv);
+ try testing.expectEqual(@as(u32, 1024), v.msg.rversion.msize);
+
+ try send(&srv, &fsys, &scratch, 1, .{ .tattach = .{ .fid = 0, .afid = ninep.nofid, .uname = "goblin", .aname = "" } });
+ try testing.expectEqual(root, (try reap(&srv)).msg.rattach.qid.path);
+
+ var wname: [ninep.max_welem][]const u8 = @splat("");
+ wname[0] = "gpio";
+ wname[1] = "20";
+ wname[2] = "value";
+ try send(&srv, &fsys, &scratch, 2, .{ .twalk = .{ .fid = 0, .newfid = 1, .nwname = 3, .wname = wname } });
+ try testing.expectEqual(@as(u16, 3), (try reap(&srv)).msg.rwalk.nwqid);
+
+ try send(&srv, &fsys, &scratch, 3, .{ .topen = .{ .fid = 1, .mode = ninep.ordwr } });
+ _ = try reap(&srv);
+
+ try send(&srv, &fsys, &scratch, 4, .{ .twrite = .{ .fid = 1, .offset = 0, .data = "1\n" } });
+ try testing.expectEqual(@as(u32, 2), (try reap(&srv)).msg.rwrite.count);
+ try testing.expectEqual(@as(u1, 1), StubPads.driven[20]);
+
+ try send(&srv, &fsys, &scratch, 5, .{ .tread = .{ .fid = 1, .offset = 0, .count = 512 } });
+ try testing.expectEqualStrings("1\n", (try reap(&srv)).msg.rread.data);
+
+ try send(&srv, &fsys, &scratch, 6, .{ .twrite = .{ .fid = 1, .offset = 0, .data = "on" } });
+ try testing.expectEqualStrings(ninep.errString(E.INVAL), (try reap(&srv)).msg.rerror.ename);
+ try testing.expectEqual(@as(usize, 1), StubPads.log_len);
+}