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);
}
|