diff options
Diffstat (limited to 'tools/console_main.zig')
| -rw-r--r-- | tools/console_main.zig | 108 |
1 files changed, 108 insertions, 0 deletions
diff --git a/tools/console_main.zig b/tools/console_main.zig new file mode 100644 index 0000000..0afe81e --- /dev/null +++ b/tools/console_main.zig @@ -0,0 +1,108 @@ +//! `p4-console`: attach a terminal to whatever is already running on the board. +//! +//! **"Just connect" should not imply a build.** Once the firmware is in flash the board runs it +//! across resets and power cycles, so the common case during *use* - as opposed to during +//! development - is to open the port and nothing else. This program compiles nothing, reads no +//! image, and does not care which application is on the chip. `zig build console` runs this same +//! binary and installs it, so `zig-out/bin/p4-console` is the copy to reach for when no build is +//! wanted at all. +//! +//! The other reason it is a program and not an in-process step is a bug this replaced. An +//! interactive step runs for as long as the human is there, and `std.Progress` redraws the build +//! runner's step tree on stderr every 80 ms for all of it - so the editor's screen arrives shredded +//! by fragments of `[11/13] steps └─ console`. std already solves this for child processes, and the +//! solution is a lock rather than a flag: `Step.Run` with `stdio == .inherit` holds +//! `io.lockStderr()` for the entire lifetime of the child (`std/Build/Step/Run.zig:1588-1592`), +//! which is the same lock `Progress` must take to draw. A hand-rolled step gets no such treatment +//! unless it takes that lock itself, and the one this replaced did not. Measured on this board +//! under a real pty: zero bytes of progress output across an 11-second session. + +const std = @import("std"); +const serial = @import("serial.zig"); +const console = @import("console.zig"); + +const usage = + \\p4-console - attach a terminal to the application already running on the board + \\ + \\ p4-console [--port <path>] [--baud <rate>] [--no-reset] + \\ + \\ --port <path> serial port (default /dev/ttyUSB0) + \\ --baud <rate> line rate (default 115200; what the bootloader leaves UART0 at) + \\ --no-reset attach without pulsing reset, leaving the application mid-session + \\ -h, --help this + \\ + \\Ctrl-] detaches. The board's output goes to stdout and your keystrokes go to the board; + \\your terminal emulator answers the application's capability queries itself, so what runs + \\on the chip sees a real terminal. + \\ +; + +/// `Init.Minimal` rather than the full `std.process.Init`: this needs argv and nothing else, and +/// the POSIX arg iterator walks the real argv without allocating, so there is no allocator here at +/// all. Linux-only is not a new restriction - `tools/serial.zig` speaks `termios2` directly. +pub fn main(init: std.process.Init.Minimal) void { + var it: std.process.Args.Iterator = .init(init.args); + _ = it.skip(); // argv[0] + + var port: []const u8 = "/dev/ttyUSB0"; + var baud: serial.Baud = .b115200; + var reset = true; + + while (it.next()) |a| { + if (eql(a, "-h") or eql(a, "--help")) { + stdout.writeStreamingAll(io, usage) catch {}; + return; + } else if (eql(a, "--no-reset")) { + reset = false; + } else if (eql(a, "--port")) { + port = it.next() orelse fail("--port needs a path"); + } else if (eql(a, "--baud")) { + const text = it.next() orelse fail("--baud needs a rate"); + const rate = std.fmt.parseInt(u32, text, 10) catch fail("--baud must be a number"); + baud = std.enums.fromInt(serial.Baud, rate) orelse + fail("unsupported baud: this tool offers only the rates in serial.Baud"); + } else { + fail("unrecognised argument; try --help"); + } + } + + console.attach(port, baud, .{ .reset = reset }) catch |err| switch (err) { + // The one failure everybody hits, and the message the raw error name does not give. Shared + // with the build steps, so the two cannot drift. + error.AccessDenied => { + var buf: [256]u8 = undefined; + const head = std.fmt.bufPrint(&buf, "cannot open {s}: AccessDenied\n\n", .{port}) catch + "cannot open the port: AccessDenied\n\n"; + // Two writes rather than one buffer, because the help text is a comptime constant and + // copying it into a stack buffer only to print it would be work for nothing. + stderr.writeStreamingAll(io, "p4-console: ") catch {}; + stderr.writeStreamingAll(io, head) catch {}; + stderr.writeStreamingAll(io, serial.access_denied_help ++ "\n") catch {}; + std.process.exit(1); + }, + // Unplugging the CH340 mid-session is ordinary, not a crash: say so and leave. + error.PortDisconnected => fail("the port disappeared - the adapter was unplugged"), + else => { + var buf: [256]u8 = undefined; + fail(std.fmt.bufPrint(&buf, "console failed: {s}", .{@errorName(err)}) catch "console failed"); + }, + }; +} + +fn eql(a: []const u8, b: []const u8) bool { + return std.mem.eql(u8, a, b); +} + +const io = std.Io.Threaded.global_single_threaded.io(); +const stdout: std.Io.File = .{ .handle = 1, .flags = .{ .nonblocking = false } }; +const stderr: std.Io.File = .{ .handle = 2, .flags = .{ .nonblocking = false } }; + +/// Report and leave. `noreturn` rather than an error union because every caller here is a usage +/// mistake with nothing above it to recover: returning an error would add std's "error: Reported" +/// and a stack trace on top of a message written for a human. +fn fail(msg: []const u8) noreturn { + var buf: [512]u8 = undefined; + const line = std.fmt.bufPrint(&buf, "p4-console: {s}\n", .{msg}) catch "p4-console: error\n"; + stderr.writeStreamingAll(io, line) catch {}; + std.process.exit(1); +} |
