//! `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 ] [--baud ] [--no-reset] \\ \\ --port serial port (default /dev/ttyUSB0) \\ --baud 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); }