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,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