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,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# The hub, as the rest of the program sees it: switchable ports and
|
|
5
|
+
# nothing about how they are switched.
|
|
6
|
+
#
|
|
7
|
+
module TribbleControl
|
|
8
|
+
|
|
9
|
+
# What every kind of hub has to answer.
|
|
10
|
+
#
|
|
11
|
+
# The subject of this tool is a USB hub, of more than one kind. This
|
|
12
|
+
# class is the seam that puts each kind behind the same calls. A backend
|
|
13
|
+
# is a subclass answering the methods below, and the commands,
|
|
14
|
+
# each_device and the protections talk to nothing else.
|
|
15
|
+
#
|
|
16
|
+
# Ports are Integers numbered as the hub numbers them, from 1. A list
|
|
17
|
+
# handed to a switching method must be non-empty and name ports the
|
|
18
|
+
# hub has: an empty list never means "every port" anywhere in this
|
|
19
|
+
# program (see DESIGN.md), and the guard here is the last one rather
|
|
20
|
+
# than the first -- every caller checks before it gets this far, and a
|
|
21
|
+
# fourth path into the hub that forgets to is stopped here instead of
|
|
22
|
+
# powering a bench up or down.
|
|
23
|
+
#
|
|
24
|
+
# Reaching for a hub's own methods in a command -- anything a backend
|
|
25
|
+
# can do that this class does not name -- is the thing this class
|
|
26
|
+
# exists to stop. Add the method here, with a default or as a
|
|
27
|
+
# NotImplementedError, and then to the backends.
|
|
28
|
+
class Hub
|
|
29
|
+
# Anything the hub layer has to report: a hub that refuses a
|
|
30
|
+
# command, a host that cannot be looked for one on, a name that
|
|
31
|
+
# matches nothing or matches two. CLI.run prints the message and
|
|
32
|
+
# nothing else, so every one of these must carry one.
|
|
33
|
+
class Error < StandardError
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The kinds of hub there are, by the name a configuration's 'hub =' line
|
|
37
|
+
# uses, and the class answering for each. The configuration says what
|
|
38
|
+
# the hub IS -- an ExSYS managed hub, a standard hub with per-port
|
|
39
|
+
# power switching -- and not which tool drives it on this host, so
|
|
40
|
+
# the same line keeps working when another host learns to drive
|
|
41
|
+
# such a hub. A backend is required when it is asked for, so a
|
|
42
|
+
# host that lacks what one of them needs still runs the others.
|
|
43
|
+
KINDS = { 'exsys' => 'ExSYS', 'usb' => 'USB' }.freeze
|
|
44
|
+
|
|
45
|
+
# The class for a kind, loaded.
|
|
46
|
+
def self.backend(kind)
|
|
47
|
+
unless (klass = KINDS[kind.to_s])
|
|
48
|
+
raise Error, "unknown hub kind '#{kind}' (one of" \
|
|
49
|
+
" #{KINDS.keys.join(', ')})"
|
|
50
|
+
end
|
|
51
|
+
require_relative "hub/#{kind}"
|
|
52
|
+
const_get(klass)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Every port this hub has, in order. Static, and asked before the
|
|
56
|
+
# hub is ever opened: the configuration is checked against it.
|
|
57
|
+
def ports = raise NotImplementedError, "#{self.class}#ports"
|
|
58
|
+
|
|
59
|
+
# The hub's own view of what is powered: { port => true/false },
|
|
60
|
+
# for every port in #ports.
|
|
61
|
+
def state = raise NotImplementedError, "#{self.class}#state"
|
|
62
|
+
|
|
63
|
+
def on(*) = raise NotImplementedError, "#{self.class}#on"
|
|
64
|
+
def off(*) = raise NotImplementedError, "#{self.class}#off"
|
|
65
|
+
def toggle(*) = raise NotImplementedError, "#{self.class}#toggle"
|
|
66
|
+
|
|
67
|
+
# Apply { port => true/false } and, unless +default+ is nil, put
|
|
68
|
+
# every port not named to +default+. A backend that can do the
|
|
69
|
+
# whole thing in one exchange with the hub overrides this; here it
|
|
70
|
+
# is an on and an off.
|
|
71
|
+
def set(changes, default = nil)
|
|
72
|
+
ons = changes.select {|_, v| v }.keys
|
|
73
|
+
offs = changes.reject {|_, v| v }.keys
|
|
74
|
+
unless default.nil?
|
|
75
|
+
(default ? ons : offs).concat(self.ports - changes.keys)
|
|
76
|
+
end
|
|
77
|
+
self.on(*ons) unless ons.empty?
|
|
78
|
+
self.off(*offs) unless offs.empty?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Where a board on +port+ is in this host's USB tree, as a path in
|
|
82
|
+
# the shape ExSYS::ManagedUSB::USB_PATH describes (1-1.2.4.4), or
|
|
83
|
+
# nil when the hub cannot be placed in the tree. --method usb
|
|
84
|
+
# needs it; the other two methods need no topology at all.
|
|
85
|
+
def usb_path(port) = raise NotImplementedError, "#{self.class}#usb_path(#{port})"
|
|
86
|
+
|
|
87
|
+
# The hub as a message names it.
|
|
88
|
+
def to_s = raise NotImplementedError, "#{self.class}#to_s"
|
|
89
|
+
|
|
90
|
+
# Does 'off' remove power from the socket, or only take the port
|
|
91
|
+
# off the bus? Software cannot tell the two apart -- a hub with no
|
|
92
|
+
# power switch wired still reports the port unpowered and drops the
|
|
93
|
+
# link, and the device on it vanishes and returns either way -- so
|
|
94
|
+
# this is a fact a backend is told, or knows about its hardware.
|
|
95
|
+
# false means a board on a cut port keeps running, which is enough
|
|
96
|
+
# to select it by ('power' identifies a board by being the only
|
|
97
|
+
# one visible) and not enough to reboot it. The default is the
|
|
98
|
+
# honest one for a hub that was built to switch VBUS.
|
|
99
|
+
def vbus? = true
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
# A list a switching method was handed, vetted: non-empty, and
|
|
104
|
+
# every port one the hub has.
|
|
105
|
+
def selection(list)
|
|
106
|
+
raise Error, 'no port named: refusing to switch nothing' if list.empty?
|
|
107
|
+
if (bad = list - self.ports).any?
|
|
108
|
+
raise Error, "no such port on this hub: #{bad.join(' ')}" \
|
|
109
|
+
" (it has #{self.ports.first}-#{self.ports.last})"
|
|
110
|
+
end
|
|
111
|
+
list
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
end
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Host-specific ways of finding the boards: their probes, their
|
|
3
|
+
# consoles, and their place in the USB tree. Finding the HUB is each
|
|
4
|
+
# Hub backend's own (Hub::ExSYS.open, through the exsys gem), and so is
|
|
5
|
+
# its geometry, which socket a port is (Hub#usb_path).
|
|
6
|
+
#
|
|
7
|
+
require 'rbconfig'
|
|
8
|
+
require 'shellwords'
|
|
9
|
+
|
|
10
|
+
module TribbleControl
|
|
11
|
+
|
|
12
|
+
module Platform
|
|
13
|
+
|
|
14
|
+
# Vendor ids of the debug probes a bench carries: NXP/mbed for DAPLink
|
|
15
|
+
# (as on an nRF52840-MDK), SEGGER for a J-Link OB (as on a DWM1001-DEV).
|
|
16
|
+
# Both present their console as a CDC interface reporting the PROBE's
|
|
17
|
+
# own serial, which is what makes a serial the key to a console.
|
|
18
|
+
PROBE_VENDORS = %w[0d28 1366].freeze
|
|
19
|
+
|
|
20
|
+
module FreeBSD
|
|
21
|
+
# The sysctl branches that describe the USB bus.
|
|
22
|
+
#
|
|
23
|
+
# Not the whole dev tree: that is 78K of text here against 5K for
|
|
24
|
+
# these, and it is read afresh on every lookup rather than
|
|
25
|
+
# remembered, because --method power switches ports off and on
|
|
26
|
+
# underneath us and a remembered tree would describe a bench that
|
|
27
|
+
# has since changed.
|
|
28
|
+
#
|
|
29
|
+
# uftdi the hub's control adapter
|
|
30
|
+
# uhub every hub, which is what the walk up to the bus
|
|
31
|
+
# passes through and nothing else
|
|
32
|
+
# umodem a probe's CDC console -- the tty, and the serial
|
|
33
|
+
# usbhid a DAPLink's HID interface, and umass its drive:
|
|
34
|
+
# umass the same device under another driver, carrying the
|
|
35
|
+
# same serial, so a probe whose CDC did not attach is
|
|
36
|
+
# still answerable for
|
|
37
|
+
USB_OIDS = %w[dev.uftdi dev.uhub dev.umodem
|
|
38
|
+
dev.usbhid dev.umass].freeze
|
|
39
|
+
|
|
40
|
+
# Every debug probe's console, keyed by the probe's serial.
|
|
41
|
+
#
|
|
42
|
+
# The probe reports its own serial, the configuration already carries
|
|
43
|
+
# that serial to address the board for flashing, and umodem says
|
|
44
|
+
# which tty the probe's CDC interface became. No topology at all,
|
|
45
|
+
# which is what lets `connect --method serial` work anywhere.
|
|
46
|
+
def self.probe_consoles
|
|
47
|
+
self.usb_tree.filter_map {|name, dev|
|
|
48
|
+
next unless name.start_with?('umodem')
|
|
49
|
+
next unless self.probe?(dev)
|
|
50
|
+
# No ttyname, no console. '/dev/tty' + '' is /dev/tty --
|
|
51
|
+
# the controlling terminal -- so an entry whose tty is not
|
|
52
|
+
# named yet would map a probe's serial to the operator's own
|
|
53
|
+
# screen, for connect to read as if it were a board.
|
|
54
|
+
next if dev[:ttyname].to_s.empty?
|
|
55
|
+
[ dev.dig(:'%pnpinfo', :sernum), '/dev/tty' + dev[:ttyname].to_s ]
|
|
56
|
+
}.to_h
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The console of whatever is at that USB path, or nil.
|
|
60
|
+
#
|
|
61
|
+
# At the path or below it: an nRF52840-MDK puts its own hub on the
|
|
62
|
+
# socket and its DAPLink one level down, so the port leads to the
|
|
63
|
+
# hub and the tty belongs to the child.
|
|
64
|
+
def self.usb_to_tty(path, tree: nil)
|
|
65
|
+
raise ArgumentError if path.nil?
|
|
66
|
+
tree ||= self.usb_tree
|
|
67
|
+
dev = self.devices_at(path, tree).find {|_n, d| d[:ttyname] }&.last
|
|
68
|
+
dev && ('/dev/tty' + dev[:ttyname].to_s)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# The probe serial of whatever is at that USB path, or nil.
|
|
72
|
+
#
|
|
73
|
+
# Read from the descriptor the kernel already has, as on Linux, so
|
|
74
|
+
# it needs no SWD session and no powering the rest of the bench
|
|
75
|
+
# down. Several drivers may claim one probe -- an MDK's DAPLink is
|
|
76
|
+
# umodem, usbhid and umass at once -- and all of them report the
|
|
77
|
+
# device's serial, so the first that is a probe with one answers.
|
|
78
|
+
#
|
|
79
|
+
# The limit here that Linux does not have: a device NO driver
|
|
80
|
+
# claimed has no sysctl node at all, there being no dev.ugen, so it
|
|
81
|
+
# cannot be seen. A probe that enumerates and attaches nothing is
|
|
82
|
+
# invisible rather than serial-less.
|
|
83
|
+
def self.usb_to_serial(path, tree: nil)
|
|
84
|
+
raise ArgumentError if path.nil?
|
|
85
|
+
tree ||= self.usb_tree
|
|
86
|
+
self.devices_at(path, tree).each do |_name, dev|
|
|
87
|
+
next unless self.probe?(dev)
|
|
88
|
+
serial = dev.dig(:'%pnpinfo', :sernum).to_s
|
|
89
|
+
return serial unless serial.empty?
|
|
90
|
+
end
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Is this one of the debug probes a bench carries?
|
|
95
|
+
private_class_method def self.probe?(dev)
|
|
96
|
+
vendor = dev.dig(:'%pnpinfo', :vendor).to_s.delete_prefix('0x')
|
|
97
|
+
PROBE_VENDORS.include?(vendor)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Everything sitting at that USB path, or below it, shallowest
|
|
101
|
+
# first and then by name so that the answer does not depend on the
|
|
102
|
+
# order sysctl happened to print.
|
|
103
|
+
private_class_method def self.devices_at(path, tree)
|
|
104
|
+
tree.filter_map {|name, dev|
|
|
105
|
+
p = self.usb_path(dev, tree)
|
|
106
|
+
next unless p == path || p&.start_with?("#{path}.")
|
|
107
|
+
[ p, name, dev ]
|
|
108
|
+
}.sort_by {|p, name, _| [ p.count('.'), p, name ] }
|
|
109
|
+
.map {|_p, name, dev| [ name, dev ] }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Where a device sits in the USB tree, as Linux would write it.
|
|
113
|
+
#
|
|
114
|
+
# FreeBSD states no such path, but every piece of one is in the
|
|
115
|
+
# sysctl tree: %location gives the bus and the port the device
|
|
116
|
+
# occupies on its parent, and %parent names that parent -- always a
|
|
117
|
+
# uhub, up to the root hub, whose own %location is empty.
|
|
118
|
+
#
|
|
119
|
+
# A walk that does not REACH the root answers nil rather than what
|
|
120
|
+
# it collected on the way: stopping one hub short turns 1-1.2.4.4
|
|
121
|
+
# into 1-4, which is not a broken string but a different socket.
|
|
122
|
+
#
|
|
123
|
+
# The exsys gem walks the same tree for its own device, and this is
|
|
124
|
+
# deliberately not that code: its walker is a documented internal
|
|
125
|
+
# of a gem that must stand alone, and a tool reaching into one is a
|
|
126
|
+
# tool that breaks on the next release.
|
|
127
|
+
# Public: Hub::USB walks the same tree, for the hub's own path.
|
|
128
|
+
def self.usb_path(dev, tree)
|
|
129
|
+
bus = nil
|
|
130
|
+
ports = []
|
|
131
|
+
rooted = false
|
|
132
|
+
seen = {}
|
|
133
|
+
while dev
|
|
134
|
+
loc = dev[:'%location']
|
|
135
|
+
unless loc.is_a?(Hash) && loc[:port]
|
|
136
|
+
rooted = true # a root hub occupies no port
|
|
137
|
+
break
|
|
138
|
+
end
|
|
139
|
+
bus ||= loc[:bus]
|
|
140
|
+
ports.unshift(loc[:port])
|
|
141
|
+
parent = dev[:'%parent'].to_s
|
|
142
|
+
# A %parent chain that returns to a device already on the
|
|
143
|
+
# way up is not a tree. No kernel prints one, but this
|
|
144
|
+
# parses whatever it is handed, and without the guard the
|
|
145
|
+
# answer is not a wrong path but an unbounded loop: a
|
|
146
|
+
# command that never returns and never says why.
|
|
147
|
+
break if seen[parent]
|
|
148
|
+
seen[parent] = true
|
|
149
|
+
dev = tree[parent]
|
|
150
|
+
end
|
|
151
|
+
return nil unless rooted && bus && !ports.empty?
|
|
152
|
+
"#{bus}-#{ports.join('.')}"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Parse a sysctl -e dump into the shape below. Split from the
|
|
156
|
+
# reading of it so that the tests can feed a capture in and run on
|
|
157
|
+
# a host with no bench, no probe, and no sysctl at all.
|
|
158
|
+
def self.parse_usb_tree(output)
|
|
159
|
+
output.lines.reduce({}) {|acc, l|
|
|
160
|
+
k, v = l.chomp.split('=', 2)
|
|
161
|
+
next acc if k.nil?
|
|
162
|
+
dev, i, sk = k.split('.')[1..]
|
|
163
|
+
# No unit number in it -- dev.uhub.%parent -- so it
|
|
164
|
+
# describes the driver and not a device.
|
|
165
|
+
next acc if sk.nil? || i !~ /\A\d+\z/
|
|
166
|
+
if [ '%pnpinfo', '%location' ].include?(sk)
|
|
167
|
+
# Not every token in one of these is a pair: some
|
|
168
|
+
# drivers write a bare word, and a parser that died on
|
|
169
|
+
# one of them would take the whole bench with it.
|
|
170
|
+
v = Shellwords.shellsplit(v.to_s).filter_map {|e|
|
|
171
|
+
k2, v2 = e.split('=', 2)
|
|
172
|
+
[ k2.to_sym, v2 ] if v2
|
|
173
|
+
}.to_h
|
|
174
|
+
end
|
|
175
|
+
acc.merge("#{dev}#{i}" => { sk.to_sym => v }) {|_k, o, n|
|
|
176
|
+
o.merge(n)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# The USB branches of the sysctl tree, as
|
|
182
|
+
#
|
|
183
|
+
# { 'umodem0' => { :ttyname => 'U0', :'%parent' => 'uhub6',
|
|
184
|
+
# :'%location' => { :bus => '1', ... } } }
|
|
185
|
+
#
|
|
186
|
+
# Keyed by device name and not by unit number: several branches are
|
|
187
|
+
# read at once, %parent names a parent that way, and unit numbers
|
|
188
|
+
# repeat across drivers.
|
|
189
|
+
# Public: Hub::USB walks the same tree, to find the hubs in it.
|
|
190
|
+
def self.usb_tree
|
|
191
|
+
oids = USB_OIDS.map {|o| Shellwords.escape(o) }.join(' ')
|
|
192
|
+
self.parse_usb_tree(`/sbin/sysctl -e #{oids} 2>/dev/null`)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
module Linux
|
|
197
|
+
# See FreeBSD.probe_consoles. Matched on the probe's vendor, not
|
|
198
|
+
# on one probe firmware: an MDK's DAPLink and a DWM1001-DEV's
|
|
199
|
+
# J-Link OB both report a serial and both become a ttyACM.
|
|
200
|
+
def self.probe_consoles
|
|
201
|
+
Dir['/sys/class/tty/ttyACM*']
|
|
202
|
+
.map {|path| self.udevadm_query(path) }
|
|
203
|
+
.select {|dev| PROBE_VENDORS.include?(dev[:ID_VENDOR_ID]) }
|
|
204
|
+
.to_h {|dev| [ dev[:ID_SERIAL_SHORT], dev[:DEVNAME] ] }
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def self.usb_to_tty(path)
|
|
208
|
+
raise ArgumentError if path.nil?
|
|
209
|
+
if (dev_path = Dir["/sys/bus/usb/devices/#{path}/**/tty/ttyACM*"]&.first)
|
|
210
|
+
File.join('/dev', File.basename(dev_path))
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# The probe's serial, from the USB descriptor the kernel already has.
|
|
215
|
+
#
|
|
216
|
+
# Not asked of openocd: 0.12.0, the current release, prints neither
|
|
217
|
+
# the CMSIS-DAP "Serial# =" line nor a J-Link "S/N", at any debug
|
|
218
|
+
# level. Reading the descriptor needs no SWD session, no openocd,
|
|
219
|
+
# and above all no powering the rest of the bench down to leave one
|
|
220
|
+
# adapter for openocd to find.
|
|
221
|
+
#
|
|
222
|
+
# An MDK puts its own hub in front of its DAPLink, so the probe sits
|
|
223
|
+
# one level below the hub port there while a J-Link sits on it; look
|
|
224
|
+
# at both, and take the first that is a probe with a serial.
|
|
225
|
+
def self.usb_to_serial(path)
|
|
226
|
+
raise ArgumentError if path.nil?
|
|
227
|
+
base = "/sys/bus/usb/devices/#{path}"
|
|
228
|
+
[ base, *Dir["#{base}.*"].sort ].each do |dev|
|
|
229
|
+
next unless File.file?("#{dev}/idVendor")
|
|
230
|
+
next unless PROBE_VENDORS.include?(File.read("#{dev}/idVendor").chomp)
|
|
231
|
+
next unless File.file?("#{dev}/serial")
|
|
232
|
+
serial = File.read("#{dev}/serial").chomp
|
|
233
|
+
return serial unless serial.empty?
|
|
234
|
+
end
|
|
235
|
+
nil
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
private_class_method def self.udevadm_query(path)
|
|
239
|
+
`/usr/bin/udevadm info -q property --export #{Shellwords.escape(path)}`
|
|
240
|
+
.lines.to_h {|l| l.split('=', 2) }
|
|
241
|
+
.transform_keys(&:to_sym)
|
|
242
|
+
.transform_values {|v| Shellwords.split(v).join(' ') }
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
Current = case RbConfig::CONFIG['host_os']
|
|
248
|
+
when /^linux-/ then Platform::Linux
|
|
249
|
+
when /^freebsd/ then Platform::FreeBSD
|
|
250
|
+
else raise 'Unsupported platform'
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def self.probe_consoles(...) = Current.probe_consoles(...)
|
|
255
|
+
|
|
256
|
+
# The console of the board whose probe carries this serial, or nil.
|
|
257
|
+
#
|
|
258
|
+
# Works on both platforms and needs no USB topology, which is what lets
|
|
259
|
+
# `connect --method serial` reach a console on a host with no
|
|
260
|
+
# /sys/bus/usb.
|
|
261
|
+
def self.serial_to_tty(serial)
|
|
262
|
+
return nil if serial.nil?
|
|
263
|
+
self.probe_consoles[serial.to_s]
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def self.usb_to_tty(...) = Current.usb_to_tty(...)
|
|
267
|
+
def self.usb_to_serial(...) = Current.usb_to_serial(...)
|
|
268
|
+
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#
|
|
2
|
+
# What a board's console output means.
|
|
3
|
+
#
|
|
4
|
+
require_relative 'cli'
|
|
5
|
+
|
|
6
|
+
module TribbleControl
|
|
7
|
+
|
|
8
|
+
# A running count of what a board printed, and one line saying so.
|
|
9
|
+
#
|
|
10
|
+
# `connect` knows how to open a board's console, prefix its lines and
|
|
11
|
+
# print them. What a line MEANS -- a completed exchange, a failure, a
|
|
12
|
+
# banner, noise -- is the firmware's business, and firmware is not what
|
|
13
|
+
# this tool is about: the strings to look for change with every build
|
|
14
|
+
# of every project that ever sits on this hub, and none of them belong
|
|
15
|
+
# in a program whose subject is a USB hub.
|
|
16
|
+
#
|
|
17
|
+
# So they are not here. A tally is the seam: `connect` hands it every
|
|
18
|
+
# line it prints and asks it, once, for a summary. The one below
|
|
19
|
+
# counts lines, which is all a tool that knows nothing about the
|
|
20
|
+
# firmware can honestly say. Anything that knows more is a block,
|
|
21
|
+
# registered from a file loaded with -r/--require:
|
|
22
|
+
#
|
|
23
|
+
# TribbleControl::Tally.register(:twr) do |device|
|
|
24
|
+
# MyTally.new(device)
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# and chosen with `tally = twr` in the configuration, for the whole bench or
|
|
28
|
+
# for one board. The block is called once per board per run, so a
|
|
29
|
+
# tally may keep whatever state it likes without sharing it.
|
|
30
|
+
class Tally
|
|
31
|
+
@registry = {}
|
|
32
|
+
|
|
33
|
+
class << self
|
|
34
|
+
# Register a tally builder under +name+. The block is given a
|
|
35
|
+
# device name and must return an object answering #<<, which
|
|
36
|
+
# receives every line, and #summary, which returns the text of
|
|
37
|
+
# the SUMMARY line or nil for no summary at all.
|
|
38
|
+
def register(name, &block)
|
|
39
|
+
raise ArgumentError, 'a tally needs a block' if block.nil?
|
|
40
|
+
@registry[name.to_s] = block
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def registered = @registry.keys.sort
|
|
44
|
+
|
|
45
|
+
# Build the tally called +name+ for the device +device+.
|
|
46
|
+
#
|
|
47
|
+
# An unknown name is an error rather than a silent fallback to
|
|
48
|
+
# counting lines: a configuration asking for 'twr' on a run that
|
|
49
|
+
# forgot -r would otherwise capture a whole bench and report
|
|
50
|
+
# nothing but line counts, which reads as a firmware saying
|
|
51
|
+
# nothing rather than as a missing file.
|
|
52
|
+
def build(name, device)
|
|
53
|
+
builder = @registry[name.to_s]
|
|
54
|
+
if builder.nil?
|
|
55
|
+
raise CLI::Error, "unknown tally '#{name}'" \
|
|
56
|
+
" (known: #{self.registered.join(', ')})." \
|
|
57
|
+
' A tally other than the built-in ones' \
|
|
58
|
+
' comes from a file given with --require'
|
|
59
|
+
end
|
|
60
|
+
builder.call(device)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def initialize(device)
|
|
65
|
+
@device = device
|
|
66
|
+
@lines = 0
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def <<(_line)
|
|
70
|
+
@lines += 1
|
|
71
|
+
self
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def summary = "lines=#{@lines}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The default: a board printed this many lines.
|
|
78
|
+
Tally.register(:lines) {|device| Tally.new(device) }
|
|
79
|
+
|
|
80
|
+
# Counts nothing, says nothing: for a capture that wants the lines and
|
|
81
|
+
# no SUMMARY at all. A null object rather than nil, so that `connect`
|
|
82
|
+
# has one kind of thing to talk to.
|
|
83
|
+
class NullTally
|
|
84
|
+
def initialize(device) ; @device = device ; end
|
|
85
|
+
def <<(_line) = self
|
|
86
|
+
def summary = nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
Tally.register(:none) {|device| NullTally.new(device) }
|
|
90
|
+
|
|
91
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#
|
|
2
|
+
# tribble-control -- power, flash and monitor the boards plugged into a
|
|
3
|
+
# switchable USB hub: an ExSYS 16-port managed hub, or any hub that
|
|
4
|
+
# switches its own ports.
|
|
5
|
+
#
|
|
6
|
+
# tribble-control -C tribble.conf flash zephyr.hex A1 A3
|
|
7
|
+
# tribble-control -C tribble.conf connect --off C2 B2 | tee twr.log
|
|
8
|
+
#
|
|
9
|
+
# A hub port may feed something that must never lose power, so
|
|
10
|
+
# tribble-control refuses to switch off any port its 'protect' block
|
|
11
|
+
# names, and by default any port the configuration does not declare;
|
|
12
|
+
# -F/--force lifts both rules.
|
|
13
|
+
#
|
|
14
|
+
# Full documentation -- port map, device selection, recipes and traps --
|
|
15
|
+
# is in man/man1/tribble-control.1, and is displayed by
|
|
16
|
+
# `tribble-control --man`.
|
|
17
|
+
#
|
|
18
|
+
require_relative 'tribble-control/version'
|
|
19
|
+
require_relative 'tribble-control/platform'
|
|
20
|
+
require_relative 'tribble-control/hub'
|
|
21
|
+
require_relative 'tribble-control/hub/exsys'
|
|
22
|
+
require_relative 'tribble-control/cli'
|
|
23
|
+
require_relative 'tribble-control/tally'
|
|
24
|
+
|
|
25
|
+
# The commands, loaded for their side effect: CLI.commands finds them by
|
|
26
|
+
# asking CLI::Command for its subclasses, so a command file that is
|
|
27
|
+
# never required is a command the tool does not have.
|
|
28
|
+
require_relative 'tribble-control/cli/usb'
|
|
29
|
+
require_relative 'tribble-control/cli/serial'
|
|
30
|
+
require_relative 'tribble-control/cli/flash'
|
|
31
|
+
require_relative 'tribble-control/cli/reset'
|
|
32
|
+
require_relative 'tribble-control/cli/connect'
|