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,102 @@
|
|
|
1
|
+
require_relative '../cli'
|
|
2
|
+
|
|
3
|
+
module TribbleControl
|
|
4
|
+
|
|
5
|
+
class CLI
|
|
6
|
+
class Flash < CLI::Command
|
|
7
|
+
DESCRIPTION = 'Flash devices'
|
|
8
|
+
|
|
9
|
+
# Every board goes through openocd: check for it up front.
|
|
10
|
+
OPENOCD = true
|
|
11
|
+
|
|
12
|
+
# 'serial' first, so it is the default (Methods.first): boards stay
|
|
13
|
+
# powered and are flashed in parallel. It needs serial= on every
|
|
14
|
+
# board, and it was measured on 2026-09-16 rather than assumed: with
|
|
15
|
+
# all four J-Links live, a flash addressed by serial landed on the
|
|
16
|
+
# board whose FICR.DEVICEID matched and left its neighbours' flash
|
|
17
|
+
# byte-for-byte unchanged, and power_cycle = after-flash still
|
|
18
|
+
# fired. 'power' -- every port cut, one board powered at a time,
|
|
19
|
+
# the bench left off -- is for a board whose serial is missing or
|
|
20
|
+
# wrong.
|
|
21
|
+
Methods = [ 'serial', 'power' ]
|
|
22
|
+
Defaults = {}
|
|
23
|
+
Parser = OptionParser.new do |opts|
|
|
24
|
+
opts.banner = "Usage: #{PROGNAME} flash [options] FIRMWARE [PORT|DEVNAME]..."
|
|
25
|
+
|
|
26
|
+
opts.separator ''
|
|
27
|
+
opts.separator "#{DESCRIPTION}."
|
|
28
|
+
opts.separator ''
|
|
29
|
+
|
|
30
|
+
opts.separator 'Options:'
|
|
31
|
+
opts.on '--power-cycle', 'Power the port off and on again after a',
|
|
32
|
+
'successful flash, whatever the configuration',
|
|
33
|
+
'says (power_cycle = after-flash)'
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def flash(firmware, **hopts)
|
|
37
|
+
openocd('init', 'targets', 'reset init',
|
|
38
|
+
"flash write_image erase #{firmware}",
|
|
39
|
+
'reset run', **hopts)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def run(argv, **opts)
|
|
43
|
+
firmware = argv.shift
|
|
44
|
+
# Checked before any port is switched, not left to openocd once
|
|
45
|
+
# per board -- under --method power, after the whole bench has
|
|
46
|
+
# been cut. openocd resolves the path against the current
|
|
47
|
+
# directory, as File.file? does.
|
|
48
|
+
raise Error, 'flash: FIRMWARE missing' if firmware.nil?
|
|
49
|
+
unless File.file?(firmware) && File.readable?(firmware)
|
|
50
|
+
raise Error, "flash: no readable firmware file '#{firmware}'"
|
|
51
|
+
end
|
|
52
|
+
tty&.info "Firmware: #{firmware}"
|
|
53
|
+
|
|
54
|
+
each_device(argv).map do |name, hopts={}|
|
|
55
|
+
flash(firmware, **hopts).tap do |ok|
|
|
56
|
+
if ok
|
|
57
|
+
then tty&.success "Device #{name}: Flashed"
|
|
58
|
+
else tty&.error "Device #{name}: Flashed failed"
|
|
59
|
+
end
|
|
60
|
+
# A DWM1001-DEV comes out of the openocd flash sequence
|
|
61
|
+
# (reset init, write, reset run) in a state where its DW1000
|
|
62
|
+
# never reports a transmission again until the module is
|
|
63
|
+
# power-cycled: it receives, resolves nothing, and logs
|
|
64
|
+
# "our TX timestamps are missing". An SWD reset alone does
|
|
65
|
+
# not do it, so only the flash needs the cycle. The configuration
|
|
66
|
+
# says which boards need it (power_cycle = after-flash), so
|
|
67
|
+
# a flash by hand on the hub gets it right; --power-cycle
|
|
68
|
+
# forces it for any board.
|
|
69
|
+
if ok && (opts[:'power-cycle'] ||
|
|
70
|
+
@cli.power_cycle?(name, 'after-flash'))
|
|
71
|
+
_, port = @cli.name_port(name)
|
|
72
|
+
if !hub.vbus?
|
|
73
|
+
# Cutting the link would only re-enumerate the
|
|
74
|
+
# probe; the board would keep the very state
|
|
75
|
+
# the cycle exists to clear. Skip it and say
|
|
76
|
+
# so, in the same words as the protected case.
|
|
77
|
+
tty&.warn "Device #{name}: NOT power-cycling," \
|
|
78
|
+
" #{hub} cuts the link, not the power" \
|
|
79
|
+
" (#{CLI::SWITCH_KEY} = link). The board" \
|
|
80
|
+
' may be in the state power_cycle' \
|
|
81
|
+
' exists to avoid'
|
|
82
|
+
elsif offable?(port)
|
|
83
|
+
tty&.info "Device #{name}: power-cycling after flash"
|
|
84
|
+
hub.off(port); sleep(2); hub.on(port)
|
|
85
|
+
else
|
|
86
|
+
# Saying it plainly matters: a DWM1001 that misses
|
|
87
|
+
# its cycle comes out of the flash with a DW1000
|
|
88
|
+
# that reports no transmission, which looks like a
|
|
89
|
+
# radio fault rather than a skipped step.
|
|
90
|
+
tty&.warn "Device #{name}: NOT power-cycling," \
|
|
91
|
+
" the configuration protects port #{port}." \
|
|
92
|
+
' The board may be in the state' \
|
|
93
|
+
' power_cycle exists to avoid'
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end.all?(&:itself)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require_relative '../cli'
|
|
2
|
+
|
|
3
|
+
module TribbleControl
|
|
4
|
+
|
|
5
|
+
class CLI
|
|
6
|
+
class Reset < CLI::Command
|
|
7
|
+
DESCRIPTION = 'Reset devices over SWD'
|
|
8
|
+
|
|
9
|
+
# Every board goes through openocd: check for it up front.
|
|
10
|
+
OPENOCD = true
|
|
11
|
+
|
|
12
|
+
Methods = [ 'serial' ]
|
|
13
|
+
Defaults = {}
|
|
14
|
+
Parser = OptionParser.new do |opts|
|
|
15
|
+
opts.banner = "Usage: #{PROGNAME} reset [options] [PORT|DEVNAME]..."
|
|
16
|
+
|
|
17
|
+
opts.separator ''
|
|
18
|
+
opts.separator "#{DESCRIPTION}."
|
|
19
|
+
opts.separator ''
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def reset(**hopts)
|
|
23
|
+
openocd('init', 'targets', 'reset run', **hopts)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def run(argv, **_opts)
|
|
27
|
+
each_device(argv).map do |name, hopts={}|
|
|
28
|
+
reset(**hopts).tap do |ok|
|
|
29
|
+
if ok
|
|
30
|
+
then tty&.success "Device #{name}: Reseted"
|
|
31
|
+
else tty&.error "Device #{name}: Reset failed"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end.all?(&:itself)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
require_relative '../cli'
|
|
2
|
+
|
|
3
|
+
module TribbleControl
|
|
4
|
+
|
|
5
|
+
class CLI
|
|
6
|
+
class Serial < CLI::Command
|
|
7
|
+
DESCRIPTION = "Report a debug probe's serial number"
|
|
8
|
+
|
|
9
|
+
# usb first, so it is the default: it reads the USB descriptor of
|
|
10
|
+
# one named board and disturbs nothing else. Asking a board which
|
|
11
|
+
# serial it has should not black out the bench.
|
|
12
|
+
#
|
|
13
|
+
# power needs no USB topology. It identifies a board by its being
|
|
14
|
+
# the only one powered -- the probe on it is then the only probe
|
|
15
|
+
# present, and reports its own serial -- at the cost of a power
|
|
16
|
+
# cycle of the whole bench, left off; see DEVICE SELECTION.
|
|
17
|
+
Methods = [ 'usb', 'power' ]
|
|
18
|
+
Defaults = {}
|
|
19
|
+
Parser = OptionParser.new do |opts|
|
|
20
|
+
opts.banner = "Usage: #{PROGNAME} serial [options] [PORT|DEVNAME]..."
|
|
21
|
+
|
|
22
|
+
opts.separator ''
|
|
23
|
+
opts.separator "#{DESCRIPTION}."
|
|
24
|
+
opts.separator ''
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def get_serial(**hopts)
|
|
28
|
+
return Platform.usb_to_serial(hopts[:usb]) if hopts[:usb]
|
|
29
|
+
|
|
30
|
+
# --method power: this board is the only one powered, so the
|
|
31
|
+
# only probe enumerated is its own. each_device has already
|
|
32
|
+
# refused more than one (CLI#only_probe!); the case below is
|
|
33
|
+
# the same rule kept where the answer is read.
|
|
34
|
+
probes = Platform.probe_consoles.keys
|
|
35
|
+
case probes.size
|
|
36
|
+
when 1 then probes.first
|
|
37
|
+
when 0 then nil
|
|
38
|
+
else
|
|
39
|
+
raise Error, "#{probes.size} probes are powered, so none of" \
|
|
40
|
+
' them can be attributed to this board:' \
|
|
41
|
+
" #{probes.map {|p| p[0, 12] }.join(', ')}." \
|
|
42
|
+
' A probe on a port the configuration protects will' \
|
|
43
|
+
' do this; read it on Linux with --method usb'
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def run(argv, **_opts)
|
|
48
|
+
each_device(argv).map do |name, hopts={}|
|
|
49
|
+
case serial = get_serial(**hopts)
|
|
50
|
+
when String then tty&.success "Serial for #{name}: #{serial}"
|
|
51
|
+
when nil then tty&.warn "No serial for #{name}"
|
|
52
|
+
else tty&.error "Accessing #{name} failed"
|
|
53
|
+
end
|
|
54
|
+
serial.is_a?(String)
|
|
55
|
+
end.all?(&:itself)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
require_relative '../cli'
|
|
2
|
+
|
|
3
|
+
module TribbleControl
|
|
4
|
+
|
|
5
|
+
class CLI
|
|
6
|
+
class USB < CLI::Command
|
|
7
|
+
DESCRIPTION = 'USB hub control'
|
|
8
|
+
|
|
9
|
+
# What 'set' accepts on the right of the colon. The hub does not
|
|
10
|
+
# see these words: they are read here into true and false.
|
|
11
|
+
ON_WORDS = %w[1 on ON true TRUE t T].freeze
|
|
12
|
+
OFF_WORDS = %w[0 off OFF false FALSE f F].freeze
|
|
13
|
+
|
|
14
|
+
Defaults = { :default => nil }
|
|
15
|
+
Parser = OptionParser.new do |opts|
|
|
16
|
+
opts.banner = "Usage: #{PROGNAME} usb [options]" \
|
|
17
|
+
" status|on|off|toggle|set [PORTS...]"
|
|
18
|
+
|
|
19
|
+
opts.separator ''
|
|
20
|
+
opts.separator "#{DESCRIPTION}."
|
|
21
|
+
opts.separator ''
|
|
22
|
+
|
|
23
|
+
opts.separator 'Options:'
|
|
24
|
+
opts.on '-D', '--default=BOOLEAN', TrueClass,
|
|
25
|
+
'Default state if not specified'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def run(argv, **opts)
|
|
29
|
+
force = opts[:force]
|
|
30
|
+
|
|
31
|
+
case action = argv.shift
|
|
32
|
+
when 'status'
|
|
33
|
+
# Read-only: asks the hub for its port mask and prints it
|
|
34
|
+
# next to the configuration, so you can see what is on and what
|
|
35
|
+
# tribble-control is allowed to switch before you switch it.
|
|
36
|
+
state = hub.state
|
|
37
|
+
named = devices.to_h {|n| [ port_list([ n ]).first, n ] }
|
|
38
|
+
safe = begin
|
|
39
|
+
switchable
|
|
40
|
+
rescue Error
|
|
41
|
+
[]
|
|
42
|
+
end
|
|
43
|
+
state.each do |port, on|
|
|
44
|
+
puts format('%2d %-6s %-3s %s', port, named[port] || '-', on ? 'on' : 'off',
|
|
45
|
+
safe.include?(port) ? '' : '(protected)')
|
|
46
|
+
end
|
|
47
|
+
when 'on'
|
|
48
|
+
# Powering up is always safe: no guard.
|
|
49
|
+
ports = port_list(argv)
|
|
50
|
+
# Every port, named outright: an empty list never reaches
|
|
51
|
+
# the hub meaning "all".
|
|
52
|
+
if ports.empty?
|
|
53
|
+
then tty&.info "Turning on all ports"
|
|
54
|
+
hub.on(*hub.ports)
|
|
55
|
+
else tty&.info "Turning on ports: #{ports.join(' ')}"
|
|
56
|
+
hub.on(*ports)
|
|
57
|
+
end
|
|
58
|
+
when 'off'
|
|
59
|
+
ports = offable(port_list(argv), force: force)
|
|
60
|
+
tty&.info "Turning off ports: #{ports.join(' ')}"
|
|
61
|
+
hub.off(*ports)
|
|
62
|
+
warn_link_only(ports)
|
|
63
|
+
when 'toggle'
|
|
64
|
+
# Toggle can power a port down, so it is guarded like 'off'.
|
|
65
|
+
ports = offable(port_list(argv), force: force)
|
|
66
|
+
tty&.info "Toggling ports: #{ports.join(' ')}"
|
|
67
|
+
hub.toggle(*ports)
|
|
68
|
+
warn_link_only(ports)
|
|
69
|
+
when 'set'
|
|
70
|
+
words = (ON_WORDS + OFF_WORDS).map {|w| Regexp.escape(w) }
|
|
71
|
+
a = argv.to_h {|e|
|
|
72
|
+
unless e =~ /^([^:]+):(#{words.join('|')})$/
|
|
73
|
+
raise Error, "invalid argument (#{e})"
|
|
74
|
+
end
|
|
75
|
+
[ port_list([ $1 ]).first, ON_WORDS.include?($2) ]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# Vet the ports being powered down.
|
|
79
|
+
offable(a.reject {|_,on| on }.keys, force: force)
|
|
80
|
+
|
|
81
|
+
# A false default would sweep every unnamed port off, the
|
|
82
|
+
# protected ones included. Expand it over the switchable
|
|
83
|
+
# ports instead, and leave the rest as they are.
|
|
84
|
+
default = opts[:default]
|
|
85
|
+
if (default == false) && !force
|
|
86
|
+
(switchable - a.keys).each {|p| a[p] = false }
|
|
87
|
+
default = nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
tty&.info "Applying port configuration:" \
|
|
91
|
+
" #{a.map {|p, on| "#{p}:#{on ? 'on' : 'off'}" }.join(' ')}" \
|
|
92
|
+
" (default=#{default&.to_s || 'current'})"
|
|
93
|
+
hub.set(a, default)
|
|
94
|
+
offs = a.reject {|_, on| on }.keys
|
|
95
|
+
offs = hub.ports - a.keys + offs if default == false
|
|
96
|
+
warn_link_only(offs.sort) unless offs.empty?
|
|
97
|
+
when nil then raise Error, 'usb: action missing'
|
|
98
|
+
else raise Error, "usb: unknown action (#{action})"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
end
|