summaryrefslogtreecommitdiff
path: root/tools/console_main.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-25 16:16:15 -0300
committerGabriel Schneider <[email protected]>2026-08-25 16:17:41 -0300
commit0b11540923d5ec2de57fc199c4c9309f838133f5 (patch)
tree81c93e1c34b41fba5d20b7706a8ccb05ee1d0c4e /tools/console_main.zig
parent6b176c598e55a48572e5e508a213491eec122bf4 (diff)
downloadesp32p4-0b11540923d5ec2de57fc199c4c9309f838133f5.tar.gz
esp32p4-0b11540923d5ec2de57fc199c4c9309f838133f5.zip
Move the console into its own binary; the step was fighting the progress display
`zig build interact` shredded the editor's screen with fragments of `[11/13] steps +- console`. The cause is structural, not cosmetic: an interactive step lives for as long as the human does, and `std.Progress` redraws the build runner's step tree on stderr every 80 ms for all of it. std already solves this, 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) - the same lock `Progress` must take to draw. A hand-rolled step gets none of that unless it takes the lock itself, and `ConsoleStep` did not. So the console is now a real host program, `tools/console_main.zig`, and `console`/`interact` are `Run` steps on it. `--color off` is not needed and is no longer suggested anywhere. Measured under a real pty (a pipe hides the bug, because progress only draws to a terminal - which is why every earlier end-to-end test here looked clean): zero bytes of progress output across an 11-second session, 3 frames, the editor's `^` modified-marker landing at row 2 after two keystrokes. The second reason for a program is that "just connect" should not imply a build. Once the firmware is in flash the board runs it across resets, so the common case during use is to open the port and nothing else: zig-out/bin/p4-console # the board is already programmed zig-out/bin/p4-console --no-reset # ...and leave a live session running `--no-reset` is the interesting one and it is proven on the die: attaching to the running editor produced 309 bytes with no ESP-ROM banner and zero `boot:` lines, then a `^` frame in response to typing. The session survived a detach and reattach with no repaint, which is exactly what a 11.9 KB/s link wants. `zig build console` is 4 steps and builds no image, no app and no object. It installs `p4-console` itself rather than going through `installArtifact`, so `zig build` alone still lands exactly one file in `zig-out` - verified from an empty tree. Also here: * `tools/console.zig`'s `ModeWatch` tests were reachable by nothing. `zig build test` runs them now; the matcher has to resynchronise when the byte that broke a match is the next match's ESC, which is worth a regression test. * The port-permission advice existed twice, in `failPort` and in the new program. It now lives once, in `tools/serial.zig` beside the code that opens ports, and says nothing about the path so each caller can name its own on the first line.
Diffstat (limited to 'tools/console_main.zig')
-rw-r--r--tools/console_main.zig108
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);
+}