summaryrefslogtreecommitdiff
path: root/tools/console_main.zig
blob: 0afe81e67df6e7b8badcf0ea6344af41c8b67875 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
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);
}