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.
- checksums.yaml +7 -0
- data/DESIGN.md +871 -0
- data/LICENSE +21 -0
- data/README.md +541 -0
- data/examples/tribble.conf +208 -0
- data/examples/vbus-check +140 -0
- data/exe/tribble-control +14 -0
- data/lib/tribble-control/cli/connect.rb +282 -0
- data/lib/tribble-control/cli/flash.rb +102 -0
- data/lib/tribble-control/cli/reset.rb +39 -0
- data/lib/tribble-control/cli/serial.rb +60 -0
- data/lib/tribble-control/cli/usb.rb +104 -0
- data/lib/tribble-control/cli.rb +1230 -0
- data/lib/tribble-control/hub/exsys.rb +248 -0
- data/lib/tribble-control/hub/usb.rb +418 -0
- data/lib/tribble-control/hub.rb +115 -0
- data/lib/tribble-control/platform.rb +271 -0
- data/lib/tribble-control/tally.rb +91 -0
- data/lib/tribble-control/version.rb +8 -0
- data/lib/tribble-control.rb +32 -0
- data/man/man1/tribble-control.1 +1483 -0
- data/tribble-control.gemspec +99 -0
- metadata +192 -0
|
@@ -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
|
+
}
|
data/examples/vbus-check
ADDED
|
@@ -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
|
data/exe/tribble-control
ADDED
|
@@ -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
|