tribble-control 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,208 @@
1
+ # -*- conf -*-
2
+ #
3
+ # Which device sits on which hub port, and how to reach it. Copy this
4
+ # beside your own setup's configuration, edit it, and point
5
+ # tribble-control at it with -C/--config.
6
+ #
7
+ # See `tribble-control --man`: CONFIGURATION for every key, PROTECTED PORTS
8
+ # for what 'protect' does.
9
+ #
10
+ # Two keys on one line need a comma between them; one key per line, as
11
+ # below, needs nothing.
12
+
13
+ # Which hub these ports are on. Three shapes, told apart by what they
14
+ # look like:
15
+ #
16
+ # AL03GD7X the serial number of its FT232 control adapter
17
+ # 1-1.2.4.4 a USB path: the adapter in that socket (Linux)
18
+ # /dev/ttyUSB1 the serial line itself, if it has a '/' in it
19
+ #
20
+ # Do not write the third. The number in /dev/ttyUSB1 is the usbserial
21
+ # layer's index -- not the hub's, not the USB device number -- and it
22
+ # is the lowest one free when the adapter is probed. It is reused,
23
+ # too: unplug whatever holds ttyUSB0 and the next thing to attach takes
24
+ # it. Two hubs can swap names across a reboot or while the machine is
25
+ # up, and nothing notices.
26
+ #
27
+ # The serial follows the HUB, the USB path follows the SOCKET. The
28
+ # serial is the usual want; the path is for a hub whose EEPROM carries
29
+ # no serial, which has no other stable name, and for a bench where the
30
+ # socket is what is fixed. A path is this host's own numbering and
31
+ # does not mean the same thing on another machine.
32
+ #
33
+ # A host with one hub does not need this line at all: the tool finds
34
+ # the one FTDI 0403:6001 on it. A host with two refuses to guess, and
35
+ # lists the serials to put here. With the line, -C alone says which
36
+ # bench, which is already the thing a command has to say.
37
+ #
38
+ # Quote a serial that is all digits. UCL reads an unquoted one as a
39
+ # number, and a number has no leading zeros: 00760040233 arrives here
40
+ # as 760040233 and matches nothing. Quoting costs nothing on a serial
41
+ # that does not need it, so quote them all.
42
+ device = 'AL03GD7X'
43
+
44
+ # Which KIND of hub the lines above describe. 'exsys' (the default) is
45
+ # the ExSYS managed hub over its FT232 line; 'usb' is any standard USB
46
+ # hub with per-port power switching, driven on FreeBSD through
47
+ # usbconfig(8) hub-class requests and needing membership of group
48
+ # operator, not root. It has to be said, because it cannot be read off
49
+ # the 'device' line: a USB path names the FT232's socket for the one and
50
+ # the hub itself for the other. --hub on the command line overrides it.
51
+ #
52
+ # A 'usb' hub is named the same three ways, in its own shapes:
53
+ #
54
+ # AC0528515619 the hub's own serial number
55
+ # 1-1.1 a USB path: the hub in that socket
56
+ # ugen1.4 that device, used as given
57
+ #
58
+ # Write the serial. The path is for a hub that carries none, or for a
59
+ # bench where the socket is the fixed thing. The third is for the
60
+ # one-off only: the number is enumeration order, so a replug renumbers
61
+ # it, the same way /dev/ttyUSB1 above is renumbered. Leave the line out
62
+ # on a host with one switchable hub below its root hubs and it is found;
63
+ # with two, the refusal lists them with serial, ugen name, path and port
64
+ # count. The ports are then the hub's own, 1 to the bNbrPorts its hub
65
+ # descriptor reports, and not the ExSYS hub's fixed 16.
66
+ hub = exsys
67
+
68
+ # For hub = usb only: what that hub's 'off' does. 'link' (the default)
69
+ # takes the port off the bus and leaves the board powered; 'vbus' cuts
70
+ # the socket's power. Software cannot tell the two apart -- a hub with
71
+ # no power switch wired still reports the port unpowered and drops the
72
+ # link, so the device vanishes and returns either way -- so watch a
73
+ # board's LED during `tribble-control usb off <port>`: an LED that goes
74
+ # out is vbus, one that stays lit while the board disappears from the
75
+ # host is link. Test the socket you will use, since the USB 2 and USB 3
76
+ # sides of one socket are different ports on different hubs. Under
77
+ # 'link' every power-down still works and warns, and the after-flash
78
+ # power cycle is skipped rather than pretended: the board does not
79
+ # restart. The ExSYS hub refuses this key rather than ignoring it; it
80
+ # always cuts power.
81
+ #switch = link
82
+
83
+ # What must never be powered down.
84
+ #
85
+ # 'undeclared' decides the fate of a port this file does not mention:
86
+ # yes, the default, means only declared ports may be powered down; no
87
+ # means any port the hub has may be. It matters more on a dock, where
88
+ # one hub port feeds the next hub in the chain and another the Ethernet
89
+ # adapter.
90
+ #
91
+ # 'ports' and 'nodes' name what stays powered whichever way that falls:
92
+ # a port number for what has no entry in this file, the name of an
93
+ # entry for what has one. Anything the hub feeds but nobody talks to
94
+ # belongs in one of them -- a single-board computer, a powered
95
+ # peripheral. Nothing notices if their VBUS disappears, they simply
96
+ # reboot, uncleanly, mid-write.
97
+ #
98
+ # Prefer the name where there is an entry to name: the port is then
99
+ # written once instead of twice, and the protection moves with the
100
+ # board. A name nothing declares is refused, not a line that quietly
101
+ # protects nothing.
102
+ protect {
103
+ undeclared = yes
104
+ ports = [ 13, 14, 15, 16 ]
105
+ nodes = [ rpi ]
106
+ }
107
+
108
+ # Which tally reads the boards' consoles: it counts what the firmware
109
+ # prints and writes the SUMMARY line at the end of a 'connect' capture.
110
+ # 'lines' (the default) counts lines and nothing else; 'none' writes no
111
+ # summary; anything else is registered by a Ruby file given with
112
+ # -r/--require, and is where knowledge of one firmware's output lives.
113
+ # See `tribble-control --man`: TALLIES. A device entry may override it.
114
+ tally = lines
115
+
116
+ # Settings a device can inherit, so that what a KIND of board is gets
117
+ # said once instead of on every board of that kind. A device names one
118
+ # with `type =`, and its own keys win over the type's.
119
+ #
120
+ # A type may not set 'port' or 'serial' -- both name one particular
121
+ # board -- and types do not nest. A type nothing defines here is an
122
+ # error, not a quiet fall back to the tool's defaults: a board that
123
+ # asked for jlink and silently got cmsis-dap is a flash through the
124
+ # wrong probe, reported as success.
125
+ types {
126
+ nrf52840-mdk {
127
+ interface = cmsis-dap
128
+ target = nrf52
129
+ transport = swd
130
+ baud = 230400
131
+ }
132
+ dwm1001-dev {
133
+ interface = jlink
134
+ target = nrf52
135
+ transport = swd
136
+ baud = 115200
137
+ # For a module whose radio does not come back from a plain SWD
138
+ # reset: cut and restore the port after a successful flash.
139
+ power_cycle = after-flash
140
+ }
141
+ }
142
+
143
+ # Every other key is a device. 'port' is required: the hub's own port
144
+ # number, 1 to 16.
145
+ #
146
+ # 'serial' is the debug probe's serial number, not the board's. It is
147
+ # what addresses a board for 'reset' and for flashing in parallel
148
+ # (--method serial), so a board without one can only be reached by
149
+ # cutting every other port (--method power). Read it off a powered
150
+ # board with `tribble-control serial <name>` and paste it whole: 48 hex
151
+ # characters for CMSIS-DAP, 12 for J-Link.
152
+ alpha {
153
+ type = nrf52840-mdk
154
+ serial = '1026360216055e5b00000000000000000000000097969902'
155
+ port = 1
156
+ }
157
+
158
+ # interface defaults to cmsis-dap, target to nrf52 and baud to 230400.
159
+ # Spelling them out on every board, as above, is worth the noise once a
160
+ # setup carries more than one kind: a default that fits most of them and
161
+ # not the rest is the sort of thing nobody checks until a flash goes to
162
+ # the wrong probe or a console comes back as noise.
163
+ #
164
+ # interface and target are independent: the first is the openocd
165
+ # interface script (which probe), the second its target script (which
166
+ # chip). Either is any name openocd can find, without the .cfg.
167
+ #
168
+ # transport defaults to swd; set it to jtag, or to whatever the pair
169
+ # speaks, and to none to let the interface script decide. work_area,
170
+ # not shown, is the target RAM openocd may use for flash algorithms:
171
+ # 0x4000 by default, or none to leave it to the target script.
172
+ beta {
173
+ type = dwm1001-dev
174
+ serial = '000760040233'
175
+ port = 7
176
+ }
177
+
178
+ # A type says what a KIND has in common; an entry may still differ.
179
+ # This one is an MDK whose console has been moved.
180
+ delta {
181
+ type = nrf52840-mdk
182
+ serial = '1026360213072dde00000000000000000000000097969902'
183
+ baud = 9600
184
+ port = 3
185
+ }
186
+
187
+ # A board with no serial is still switchable and still has a console;
188
+ # it just cannot be reset, or flashed in parallel.
189
+ gamma {
190
+ port = 2
191
+ }
192
+
193
+ # Something the bench only feeds. It is declared for two reasons:
194
+ # 'protect { nodes }' can then name it, which keeps the port number in
195
+ # one place, and 'usb status' prints 'rpi' on that row instead of a
196
+ # dash -- so the protected port says what it is protecting.
197
+ rpi {
198
+ port = 12
199
+ }
200
+
201
+ # 'port = none' keeps a record without a board: the entry is never
202
+ # selected, switched or flashed, and naming it says so. Write it when
203
+ # a board stops enumerating and its port is given away, so that its
204
+ # serial is not lost.
205
+ retired {
206
+ serial = '1026360202c4dc0f00000000000000000000000097969902'
207
+ port = none
208
+ }
@@ -0,0 +1,140 @@
1
+ #!/bin/sh
2
+ # vbus-check — find out what a hub's 'off' does to a socket: cut one
3
+ # port for a few seconds while you watch the LED of the board on it,
4
+ # restore it, and print the 'switch =' line the answer implies.
5
+ #
6
+ # usage: vbus-check [-n] [-t seconds] [tribble-control options...] PORT|NAME
7
+ #
8
+ # Everything between the script's own flags and the last argument is
9
+ # handed to tribble-control unchanged, so name the hub the way you
10
+ # would for any command: -C tribble.conf, or --hub usb -d SERIAL -F.
11
+ # Without a configuration tribble-control refuses to power anything down,
12
+ # which is what -F is for here.
13
+ #
14
+ # -n dry run: print what would be switched, switch nothing
15
+ # -t seconds how long the port stays off (default 5)
16
+ #
17
+ # Environment: TRIBBLE_CONTROL names the binary (default: tribble-control
18
+ # on PATH). Exit 0 with an answer, 1 when tribble-control refused or
19
+ # the port could not be switched, 2 on a usage error.
20
+ #
21
+ # Why a script: software cannot tell whether a hub's power switch is
22
+ # wired to VBUS. A hub with none still reports the port unpowered and
23
+ # drops the link, so the board vanishes and returns either way. Only
24
+ # an LED knows -- see 'switch' in tribble-control(1).
25
+ set -eu
26
+
27
+ : "${TRIBBLE_CONTROL:=tribble-control}"
28
+ readonly countdown=3
29
+
30
+ progname=${0##*/}
31
+
32
+ die() { printf '%s: %s\n' "$progname" "$*" >&2; exit 1; }
33
+ warn() { printf '%s: %s\n' "$progname" "$*" >&2; }
34
+
35
+ usage() {
36
+ printf 'usage: %s [-n] [-t seconds] [tribble-control options...] PORT|NAME\n' \
37
+ "$progname" >&2
38
+ exit 2
39
+ }
40
+
41
+ # The script's own flags come first and are few; everything after them
42
+ # belongs to tribble-control, so getopts cannot be let loose on the
43
+ # whole line.
44
+ dry_run=0 hold=5
45
+ while [ $# -gt 0 ]; do
46
+ case $1 in
47
+ -n) dry_run=1; shift ;;
48
+ -t) [ $# -ge 2 ] || usage
49
+ hold=$2; shift 2 ;;
50
+ -t*) hold=${1#-t}; shift ;;
51
+ --) shift; break ;;
52
+ *) break ;;
53
+ esac
54
+ done
55
+ case $hold in
56
+ ''|*[!0-9]*) warn "-t takes a number of seconds, not '$hold'"; usage ;;
57
+ esac
58
+ [ $# -ge 1 ] || usage
59
+
60
+ # The last argument is the port; the rest are tribble-control's. POSIX
61
+ # sh has no arrays, so the list is rebuilt: the first n-1 arguments are
62
+ # appended after the originals, then the originals are shifted off.
63
+ port=
64
+ for arg in "$@"; do port=$arg; done
65
+ n=$(( $# - 1 ))
66
+ i=0
67
+ for arg in "$@"; do
68
+ i=$(( i + 1 ))
69
+ [ "$i" -le "$n" ] && set -- "$@" "$arg"
70
+ done
71
+ shift $(( n + 1 ))
72
+
73
+ command -v "$TRIBBLE_CONTROL" >/dev/null 2>&1 \
74
+ || die "no $TRIBBLE_CONTROL on PATH; set TRIBBLE_CONTROL to the binary"
75
+
76
+ tc() { "$TRIBBLE_CONTROL" "$@"; }
77
+
78
+ # Status listings are commentary: the product of this script is the one
79
+ # 'switch =' line at the end, and that is all that goes to stdout.
80
+ status() {
81
+ printf -- '--- %s\n' "$1" >&2
82
+ shift
83
+ tc "$@" usb status >&2 || die 'usb status failed; is the hub named?'
84
+ }
85
+
86
+ # Restore the port on every exit once it has been cut, Ctrl-C during
87
+ # the hold included: a check that leaves the board off has done the
88
+ # one thing it exists to warn about.
89
+ cut=0
90
+ restore() {
91
+ if [ "$cut" -eq 1 ]; then
92
+ cut=0
93
+ warn "making sure port $port is on"
94
+ tc "$@" usb on "$port" >&2 || warn "could not restore port $port"
95
+ fi
96
+ }
97
+ # shellcheck disable=SC2064 # "$@" must be captured now: the trap runs after the list is gone
98
+ trap "restore $(printf "'%s' " "$@")" EXIT
99
+ trap 'exit 130' INT TERM
100
+
101
+ printf 'Watch the LED of the board on port %s.\n' "$port" >&2
102
+ printf 'In %s seconds the port is cut for %s seconds, then restored.\n' \
103
+ "$countdown" "$hold" >&2
104
+
105
+ status 'before' "$@"
106
+
107
+ if [ "$dry_run" -eq 1 ]; then
108
+ printf 'dry run: would run %s %s usb off %s, wait %ss, then usb on %s\n' \
109
+ "$TRIBBLE_CONTROL" "$*" "$port" "$hold" "$port" >&2
110
+ exit 0
111
+ fi
112
+
113
+ sleep "$countdown"
114
+ cut=1
115
+ tc "$@" usb off "$port" >&2 || die "tribble-control refused to cut port $port"
116
+ status 'while cut' "$@"
117
+ sleep "$hold"
118
+ restore "$@"
119
+ status 'after' "$@"
120
+
121
+ # The question, asked on the terminal even when stdin is a pipe. With
122
+ # no terminal at all there is nobody to ask, so both readings are
123
+ # printed and the caller decides.
124
+ if [ ! -r /dev/tty ]; then
125
+ warn 'no terminal to ask on; the LED decides:'
126
+ printf '%s\n' 'switch = vbus # if the LED went out' \
127
+ 'switch = link # if it stayed lit'
128
+ exit 0
129
+ fi
130
+ while :; do
131
+ printf 'Did the LED go out while the port was off? [y/n] ' >&2
132
+ IFS= read -r answer < /dev/tty || die 'no answer'
133
+ case $answer in
134
+ [Yy]*) printf 'Write this at the top of the configuration:\n' >&2
135
+ printf '%s\n' 'switch = vbus'; break ;;
136
+ [Nn]*) printf 'Write this at the top of the configuration, or leave it out (it is the default):\n' >&2
137
+ printf '%s\n' 'switch = link'; break ;;
138
+ *) warn 'y or n' ;;
139
+ esac
140
+ done
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # tribble-control -- power, flash and monitor the boards plugged into a
4
+ # switchable USB hub. See `tribble-control --man`.
5
+ #
6
+ require 'tribble-control'
7
+
8
+ begin
9
+ TribbleControl::CLI.run
10
+ rescue => e
11
+ warn "#{TribbleControl::CLI::PROGNAME}: #{e.message || 'unknown'}"
12
+ warn e.backtrace if $DEBUG
13
+ exit 1
14
+ end
@@ -0,0 +1,282 @@
1
+ require_relative '../cli'
2
+ require_relative '../tally'
3
+ require 'uart'
4
+
5
+ module TribbleControl
6
+
7
+ class CLI
8
+ class Connect < CLI::Command
9
+ DESCRIPTION = 'Read board consoles'
10
+
11
+ # usb first, so it stays the default where it works. serial is
12
+ # what reaches a console on a host with no /sys/bus/usb: it needs
13
+ # no topology, only the probe serial the configuration already carries
14
+ # to address the board for flashing.
15
+ Methods = [ 'usb', 'serial' ]
16
+ Defaults = {}
17
+ Repeatable = [ :tally ]
18
+ Parser = OptionParser.new do |opts|
19
+ opts.banner = "Usage: #{PROGNAME} connect [options] PORT"
20
+
21
+ opts.separator ''
22
+ opts.separator "#{DESCRIPTION}."
23
+ opts.separator ''
24
+
25
+ opts.separator 'Options:'
26
+ opts.on '--off', 'Start with all devices off'
27
+ opts.on '--reset', 'Reset each board once its console is open,' \
28
+ ' so boot output is captured'
29
+ opts.on '--duration=SECONDS', Integer,
30
+ 'How long to capture for (default 600)'
31
+ opts.on '--command=CMD', 'Shell command to send to every selected' \
32
+ " board's own console once it is up"
33
+ opts.on '--interactive', 'Type at the board: forward this' \
34
+ ' standard input to it, and stay until' \
35
+ ' end of input rather than for a duration'
36
+ # NAME, not [DEV=]NAME: OptionParser reads brackets after the
37
+ # '=' as an optional argument, which '--tally twr' never fills.
38
+ opts.on '--tally=NAME', Array,
39
+ 'Read the consoles with tally NAME for this run,' \
40
+ ' whatever the configuration says; DEV=NAME for one' \
41
+ ' board only (repeatable, comma-separated)'
42
+ end
43
+
44
+ # Each selected board's tally, built: { name => tally }.
45
+ #
46
+ # +given+ is what --tally said, in order. A bare NAME is the run's
47
+ # tally, DEV=NAME is one board's; the configuration answers for
48
+ # whatever neither names. So, for one board, the first of: DEV=NAME,
49
+ # NAME, the board's own tally key (or its type's), the file's, lines.
50
+ #
51
+ # Built here, all of them, before anything is switched, so an unknown
52
+ # name stops the run before --off cuts the bench or a reader starts.
53
+ # A board later skipped for having no console costs a block call.
54
+ #
55
+ # The run gets the last word because the configuration is about
56
+ # boards, not about what is flashed on them, and the same board
57
+ # carries different firmware from one run to the next.
58
+ def tallies(ids, given)
59
+ names = (ids.empty? ? devices : ids).map {|id| name_of(id) }
60
+ run = nil
61
+ boards = {}
62
+
63
+ # '--tally=' stores an empty list and 'a,,b' a nil between a and
64
+ # b. Taken as no --tally at all, the first would hand every
65
+ # board back to the configuration -- the silent fallback this
66
+ # option exists to prevent.
67
+ if given && (given.empty? || given.any? {|s| s.to_s.empty? })
68
+ raise Error, '--tally: an empty NAME (--tally= or a doubled' \
69
+ ' comma); give NAME or DEV=NAME'
70
+ end
71
+
72
+ Array(given).each do |spec|
73
+ dev, eq, which = spec.rpartition('=')
74
+ if eq.empty?
75
+ if run && run != which
76
+ raise Error, "--tally gives two tallies for the run" \
77
+ " (#{run}, #{which}): use DEV=NAME for" \
78
+ ' one board'
79
+ end
80
+ run = which
81
+ next
82
+ end
83
+ if dev.empty? || which.empty?
84
+ raise Error, "--tally #{spec}: expected NAME or DEV=NAME"
85
+ end
86
+ name = name_of(dev)
87
+ unless names.include?(name)
88
+ raise Error, "--tally #{spec}: #{name} is not captured by" \
89
+ " this run (#{names.join(' ')})"
90
+ end
91
+ if boards[name] && boards[name] != which
92
+ raise Error, "--tally gives #{name} two tallies" \
93
+ " (#{boards[name]}, #{which})"
94
+ end
95
+ boards[name] = which
96
+ end
97
+
98
+ names.to_h {|n| [ n, Tally.build(boards[n] || run || tally(n), n) ] }
99
+ end
100
+
101
+ # A device name, from a name or a port number as the command line
102
+ # takes them. An id the configuration does not know is the user's
103
+ # error, said as one, not a KeyError.
104
+ def name_of(id)
105
+ @cli.name_port(id).first
106
+ rescue KeyError
107
+ raise Error, "no device '#{id}' in the configuration"
108
+ end
109
+
110
+ # Where this board's console is.
111
+ #
112
+ # The USB path first, because it names the device itself and is
113
+ # what --method usb went to the trouble of working out. The probe
114
+ # serial second: it identifies the console just as exactly, needs
115
+ # no USB tree to be walked, and is therefore the only one of the
116
+ # two that a FreeBSD host can answer. nil means neither found it,
117
+ # which is a board that is not there.
118
+ def console(hopts)
119
+ if hopts[:usb] && (path = Platform.usb_to_tty(hopts[:usb]))
120
+ path
121
+ else
122
+ Platform.serial_to_tty(hopts[:serial])
123
+ end
124
+ end
125
+
126
+ def run(argv, **opts)
127
+ # No configuration, no boards to name: each_device says so below,
128
+ # in its own words, before a counter is ever asked for.
129
+ counters = opts.include?(:config) ? tallies(argv, opts[:tally]) : {}
130
+
131
+ # Before anything is powered, reset or started.
132
+ if opts[:interactive] && opts.include?(:config) && counters.size != 1
133
+ raise Error, 'connect: --interactive takes a single device' \
134
+ " (#{counters.size} selected)"
135
+ end
136
+ @failed = []
137
+
138
+ if opts[:off]
139
+ off_ports = offable(force: opts[:force])
140
+ tty&.info "Starting from off state: #{off_ports.join(' ')}"
141
+ hub.off(*off_ports)
142
+ warn_link_only(off_ports)
143
+ end
144
+
145
+ connected = []
146
+ each_device(argv).each do |name, hopts={}|
147
+ # No tty, no reader. usb_to_tty returns nil when the glob finds
148
+ # no ttyACM under the port: the board is dead, unplugged, or
149
+ # simply slower to enumerate than --warm-up allowed. Said, and
150
+ # counted as a failure: a board that cannot be read must not
151
+ # look like one that is up and saying nothing, which is the one
152
+ # question 'connect' exists to answer.
153
+ dev_tty = console(hopts)
154
+ if dev_tty.nil?
155
+ where = hopts[:usb] || "probe #{hopts[:serial] || '(no serial)'}"
156
+ tty&.error "Device #{name}: no console enumerated at" \
157
+ " #{where}; not capturing it"
158
+ @failed << name
159
+ next
160
+ end
161
+ tty&.info "Connecting to #{name} on #{dev_tty}"
162
+ connected << [ name, hopts ]
163
+
164
+ # What the lines MEAN is not this tool's business: the
165
+ # strings worth counting belong to whatever firmware
166
+ # happens to be on the bench this month, and they change
167
+ # without a hub changing. The tally --tally or the
168
+ # configuration names is handed every line and asked, at the
169
+ # end, for one summary. See #tallies, TribbleControl::Tally,
170
+ # and --require.
171
+ counter = counters.fetch(name)
172
+ Thread.new { read_console(name, dev_tty, counter) }
173
+ end
174
+
175
+ # A board that is already running has usually said everything it
176
+ # had to say before its console was opened: the banner and the
177
+ # driver's init lines are long gone. Resetting once the readers
178
+ # are attached is the only way to see them.
179
+ if opts[:reset]
180
+ sleep(1) # let the reader threads settle
181
+ connected.each do |name, hopts|
182
+ tty&.info "Resetting #{name}"
183
+ unless openocd('init', 'reset run', **hopts)
184
+ tty&.error "Device #{name}: Reset failed"
185
+ end
186
+ end
187
+ end
188
+
189
+ # The interesting output is often below the firmware's compiled-in
190
+ # log level, and spank can be turned up at run time, by typing at
191
+ # the shell. Written on a second, write-only handle: the reader
192
+ # thread already holds the port, and sharing one IO across threads
193
+ # for opposite directions is a race waiting for a long bench run.
194
+ if (cmd = opts[:command])
195
+ sleep(opts[:reset] ? 2 : 0.5) # let the shell come up
196
+ connected.each do |name, hopts|
197
+ dev_tty = console(hopts)
198
+ tty&.info "Sending to #{name}: #{cmd}"
199
+ begin
200
+ File.open(dev_tty, File::WRONLY | File::NOCTTY) do |w|
201
+ w.sync = true
202
+ w.write("\r#{cmd}\r")
203
+ end
204
+ rescue SystemCallError => e
205
+ tty&.error "Device #{name}: could not send command" \
206
+ " (#{e.message})"
207
+ end
208
+ end
209
+ end
210
+
211
+ # Either we are being watched or we are being typed at. A
212
+ # duration is what an unattended capture needs; a console is
213
+ # what a question needs, and it ends when the person asking
214
+ # says so, not on a timer they would have to guess in advance.
215
+ if opts[:interactive]
216
+ interact(connected)
217
+ else
218
+ sleep(opts[:duration] || 600)
219
+ end
220
+
221
+ # A board that was never read is not a quiet board: exit 1.
222
+ @failed.empty?
223
+ end
224
+
225
+ # Read one board's console until the run ends, and say how it went.
226
+ #
227
+ # A console that will not open (permission, busy, gone), a read that
228
+ # fails on unplug, or a tally that raises ends the reader with an
229
+ # ERROR line on stdout, where a capture keeps it, and no SUMMARY: a
230
+ # summary of a board never read would read as one that said
231
+ # nothing. The run then exits 1.
232
+ def read_console(name, dev_tty, counter)
233
+ failed = false
234
+ UART.open dev_tty, @cli.baud(name) do |serial|
235
+ loop do
236
+ line = serial.readline
237
+ counter << line
238
+ puts "<#{name}> #{line}"
239
+ rescue EOFError
240
+ retry
241
+ end
242
+ end
243
+ rescue StandardError => e
244
+ failed = true
245
+ (@failed ||= []) << name
246
+ puts "<#{name}> ERROR: #{e.message} (#{e.class}); not read past this point"
247
+ ensure
248
+ if !failed && (summary = counter.summary)
249
+ puts "<#{name}> SUMMARY: #{summary}"
250
+ end
251
+ end
252
+
253
+ # Stdin to the board, a line at a time.
254
+ #
255
+ # On a second, write-only handle, as --command is. What comes back
256
+ # is printed by the reader thread, prefixed like everything else, so
257
+ # the answer to what was typed appears where the rest of the board's
258
+ # output does.
259
+ #
260
+ # Lines, not characters: the shell on the far end wants a complete
261
+ # line terminated by \r, and there is nowhere here to run a line
262
+ # editor. Over ssh -t that is no loss -- the pty at the other end
263
+ # of the connection does the editing and the echo, so what arrives
264
+ # is already the line that was meant.
265
+ def interact(connected)
266
+ # One device was selected (run checked); none here means its
267
+ # console did not enumerate, which run has already reported.
268
+ raise Error, 'connect: no console to type at' if connected.empty?
269
+ name, hopts = connected.first
270
+ dev_tty = console(hopts)
271
+ tty&.info "Typing at #{name} (^D to leave)"
272
+ File.open(dev_tty, File::WRONLY | File::NOCTTY) do |w|
273
+ w.sync = true
274
+ while (line = $stdin.gets)
275
+ w.write("#{line.chomp}\r")
276
+ end
277
+ end
278
+ end
279
+ end
280
+ end
281
+
282
+ end