exsys 0.5 → 1.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8f8a542193c2ef67c427b47ee5ee95c448ecb9f3aced7d1a763f4e3159340eb2
4
- data.tar.gz: 04da256b271c0c43d07e258c808eff8659c2b95ecc03d7424ec8101d46f5c126
3
+ metadata.gz: b64ad35673afb17f7d07e21cd404043cf832637c9247a6697a08148ca781977e
4
+ data.tar.gz: fd871f3815d45d51731a686cda177a28d1fb8315c72dd9a299da91bfeee1a091
5
5
  SHA512:
6
- metadata.gz: 6e2555919ac712c8b4d4064b4d4d9cd8e69d8aa74c3ce71dbb86be23deb6e20cf0be89907d7d41bf087010f139dec32dcdd6fd6b989ea21aad53a46e83721bf1
7
- data.tar.gz: da9630c153d83bbedc4f840707aa32ac90f2320d038e43ae01d8bfea62db52c090d2da9a39694545518e7b4829cf197a3487e64c4c052a9233cc4a8254154a07
6
+ metadata.gz: 0ba2590f1165d88b24642f02a27b14ce5855f42c793e46ad1d2a697ebe6c795eeddb34ffb8e0d411988e920740de7bc189839545446107bdc850332c906d19a7
7
+ data.tar.gz: 6c9f2cdf259a245308884fba34601a3c7040c656659472b99d0c8fed03435bc8233bb5aad21eaf87970eb2503aabdce611a62d37448a2f74e177be15d65b43be
data/README.md CHANGED
@@ -1,44 +1,236 @@
1
+ # exsys
1
2
 
2
- Controlling a [ExSYS Managed USB hub][1] without being
3
- constrained by the official binary-only support.
3
+ Switch the ports of an [ExSYS managed USB hub][1] on and off, from the
4
+ shell or from Ruby, without being constrained by the vendor's
5
+ binary-only tool.
4
6
 
7
+ [![ExSYS EX-1526HMVS: managed 16-port USB 3.2 Gen 1 metal hub][hub]][1]
8
+
9
+
10
+ ## How it works
11
+
12
+ The hub is driven over a serial line, not over USB: switching a port is
13
+ a serial command, and the data path is not involved. Every command is
14
+ answered, and that reply is how the port state is read back and how a
15
+ refused command is reported.
16
+
17
+ ~~~text
18
+ ┌───────────────┐ ┌─────────────────────┐
19
+ │ │ │ │
20
+ │ host │ │ EX-1526HMVS │
21
+ │ │ USB 3.2 (data) │ │
22
+ │ │◂──────────────────────────▸│ UP-A / UP-B │
23
+ │ │ │ │
24
+ │ exsys-usb │ serial, 9600 8N1 │ │
25
+ │ (this gem) │◂──────────────────────────▸│ management (FTDI) │
26
+ │ │ │ │
27
+ │ │ │ 16 ports, 1 .. 16 │
28
+ └───────────────┘ └─────────────────────┘
29
+ ~~~
30
+
31
+ The serial line is `/dev/ttyU0` on FreeBSD, `/dev/ttyUSB0` on Linux.
32
+ You need read and write access to it: the device is usually owned by a
33
+ group such as `dialout` or `plugdev`, so check `ls -l` on it and add
34
+ yourself to that group rather than reaching for `sudo`.
35
+
36
+ The hub answers only to its password, `pass` unless it has been
37
+ changed. Port numbering starts at 1 and runs to however many ports
38
+ the hub reports having: the gem asks it, rather than assuming sixteen.
39
+
40
+
41
+ ## Install
42
+
43
+ ~~~sh
44
+ gem install exsys
45
+ ~~~
46
+
47
+ The only runtime dependency is [uart][2], which pulls in `ruby-termios`
48
+ -- a C extension, so a compiler and the Ruby headers must be available
49
+ when it builds.
50
+
51
+ Tested on Ruby 3.1, 3.3 and 3.4, on FreeBSD and on Linux.
52
+
53
+
54
+ ## Command line
55
+
56
+ ~~~sh
57
+ dev=/dev/ttyU0
58
+ exsys-usb -d ${dev} on # All on
59
+ exsys-usb -d ${dev} off # All off
60
+ exsys-usb -d ${dev} on 1 2 # Only turn on port 1 and 2
61
+ exsys-usb -d ${dev} toggle 3, 5 # Toggle port 3 and 5
62
+ exsys-usb -d ${dev} set 3:on 5:off # Turn on port 3, turn off port 5
63
+ exsys-usb -d ${dev} -D false set 3:on # Turn on port 3, all others off
64
+ exsys-usb -d ${dev} -c on 1 # Turn on port 1, and save to flash
65
+ ~~~
66
+
67
+ ### Actions
68
+
69
+ | Action | Effect |
70
+ | :------------------ | :---------------------------------------------- |
71
+ | `on [PORT...]` | Power the listed ports, or every port |
72
+ | `off [PORT...]` | Unpower the listed ports, or every port |
73
+ | `toggle [PORT...]` | Invert the listed ports, or every port |
74
+ | `set PORT:STATE...` | Set the listed ports; `-D` decides the rest |
75
+ | `status [PORT...]` | Report the ports, one `N on` / `N off` per line |
76
+ | `query` | What the hub says it is: id, ports, firmware |
77
+ | `commit` | Save the current port state to flash |
78
+ | `factory-reset` | Factory reset; refuses without `--yes` |
79
+ | `reset` | Reboot the hub; refuses without `--yes` |
80
+
81
+ > [!WARNING]
82
+ > `reset` and `factory-reset` both drop power to every port, so both
83
+ > refuse without `--yes`. `reset` reboots the whole hub; it is not the
84
+ > way to power-cycle one device, which is `off` then `on`.
85
+ >
86
+ > `factory-reset` is not the inverse of `commit`. It issues the hub's
87
+ > `RD` command: every port drops and the password goes back to `pass`.
88
+ > Nothing in the protocol reloads a saved state -- the hub applies it
89
+ > at power-on by itself.
90
+
91
+
92
+ A port state in `set` is written `PORT:STATE`, where `STATE` is one of
93
+ `1`, `on`, `ON`, `true`, `TRUE`, `t`, `T` or their false counterparts
94
+ `0`, `off`, `OFF`, `false`, `FALSE`, `f`, `F`.
95
+
96
+ ### Options
97
+
98
+ | Option | Meaning |
99
+ | :-------------------- | :-------------------------------------------- |
100
+ | `-d`, `--device=DEV` | Serial line to the hub (required) |
101
+ | `-p`, `--password=STR`| Hub password; defaults to `pass` |
102
+ | `-c`, `--commit` | Also write the new state to flash |
103
+ | `-y`, `--yes` | Mean a destructive action |
104
+ | `-v`, `--verbose` | Report the port states after a change |
105
+ | `-D`, `--default=BOOL`| State for the ports `set` does not name |
106
+ | `--debug[=FILE]` | Trace the serial exchange to stderr, or FILE |
107
+ | `-V`, `--version` | Print the library version |
108
+ | `-h`, `--help` | Print the usage |
109
+
110
+ The debug trace shows every frame sent and received, with the password
111
+ blanked out; when it is written to a file, that file is created
112
+ readable only by you.
113
+
114
+ `status` prints one port per line, which greps and awks without
115
+ parsing:
116
+
117
+ ~~~sh
118
+ exsys-usb -d ${dev} status # every port
119
+ exsys-usb -d ${dev} status 3 7 # just those two
120
+ exsys-usb -d ${dev} -v on 3 # switch, then report
121
+ ~~~
122
+
123
+ ### Exit status
124
+
125
+ `0` when the command was carried out, `1` otherwise -- a malformed
126
+ argument, a port outside 1..16, an unreachable serial line, or a
127
+ command the hub refused. The error goes to stderr, so a script can
128
+ rely on the status:
5
129
 
6
- Executable
7
130
  ~~~sh
8
- dev=/dev/ttyU0
9
- exsys-hub -d ${dev} on # All on
10
- exsys-hub -d ${dev} off # All off
11
- exsys-hub -d ${dev} on 1 2 # Only turn on port 1 and 2
12
- exsys-hub -d ${dev} toggle 3, 5 # Toggle port 3 and 5
13
- exsys-hub -d ${dev} set 3:on 5:off # Turn on port 3, turn off port 5
14
- exsys-hub -d ${dev} -D false set 3:on # Turn on port 3, turn off all other ports
131
+ exsys-usb -d /dev/ttyU0 off 3 || echo "could not switch port 3 off"
15
132
  ~~~
16
133
 
17
- Library:
134
+
135
+ ## Library
18
136
 
19
137
  ~~~ruby
20
- # Instanciate hub (Linux: ttyUSB?, FreeBSD: ttyU?)
138
+ # Instantiate the hub (Linux: ttyUSB?, FreeBSD: ttyU?)
21
139
  # and enable debug output to stderr
22
140
  hub = ExSYS::ManagedUSB.new('/dev/ttyU0', debug: STDERR)
23
141
 
24
- # Chaining turning on all port, and swithing off ports 4,5,6
25
- hub.on.off(4,5,6)
142
+ # Chain turning on every port, then switch off ports 4, 5 and 6
143
+ hub.on(:all).off(4,5,6)
26
144
 
27
- # Perform sequential toggle of all individual ports
28
- ExSYS::ManagedUSB::PORTS.each do |p|
145
+ # Toggle each port in turn
146
+ hub.ports.each do |p|
29
147
  hub.toggle(p)
30
148
  end
31
149
 
32
- # Set ports states for 1 and 2
150
+ # Set the state of ports 1 and 2
33
151
  hub.set({ 1 => true, 2 => false })
34
152
 
35
- # Set ports states for 1 and 2, forcing other ports to off
153
+ # Set ports 1 and 2, forcing every other port off
36
154
  hub.set({ 1 => true, 2 => true }, false)
37
155
  ~~~
38
156
 
157
+ Reading the state back has no command-line equivalent; it is a library
158
+ call, and answers in whichever shape suits the caller:
159
+
160
+ ~~~ruby
161
+ hub.get # => { 1 => true, 2 => false, ... }
162
+ hub.get(:on_off) # => { :on => [1, 3], :off => [2, 4, ...] }
163
+ hub.get(:on) # => [ 1, 3 ]
164
+ hub.get(:off) # => [ 2, 4, ... ]
165
+ ~~~
166
+
167
+ `on`, `off` and `toggle` want an explicit port list, and `:all` is how
168
+ you say every port. An empty list is refused rather than read as
169
+ "all": `hub.off(*ports)` with an empty `ports` is the very same call as
170
+ `hub.off`, so a computed list that came back empty would otherwise
171
+ switch all sixteen. The command line is unaffected -- naming no port
172
+ there still means every port.
173
+
174
+ Switching is a read-modify-write, and the library holds the serial
175
+ line -- locked -- across the whole exchange, so two processes driving
176
+ the same hub cannot lose each other's changes.
177
+
178
+ A read-decide-write spans two calls, so it needs the line held across
179
+ both. Wrap them in a session:
180
+
181
+ ~~~ruby
182
+ hub.session do
183
+ hub.on(1) unless hub.get[1]
184
+ end
185
+ ~~~
186
+
187
+ The hub will also describe itself, over the same line and without a
188
+ password:
189
+
190
+ ~~~ruby
191
+ hub.query # => { id: "CENTOS", ports: 16, firmware: "v02",
192
+ # raw: "CENTOS000516v02" }
193
+ hub.port_count # => 16, asked once and remembered
194
+ ~~~
195
+
196
+ `:all` covers exactly those ports, and a port the hub does not have is
197
+ refused. The count is read from the same field the vendor's own tool
198
+ reads, checked against it for hubs reporting 4, 8, 16 and 32 ports.
199
+
200
+ It is asked once and kept for the life of the object, which outlasts
201
+ any one connection -- the serial line is opened per operation, not
202
+ held. So a hub object is bound to the hub it first asked. If the
203
+ device is unplugged and another appears under the same name, build a
204
+ new one; nothing in the library can notice the swap.
205
+
206
+ `hub.factory_reset(confirm: true)` issues `RD` and carries the warning
207
+ above. The keyword is required: it is the one operation here that
208
+ nothing undoes, and the one most easily reached by misunderstanding.
209
+ It was called `restore` up to 0.6; the old name now raises rather than
210
+ run.
211
+
212
+ Sessions nest, so the methods above stay correct when called inside
213
+ one, and a session belongs to the thread that opened it: another thread
214
+ opens, and locks, its own line. The wire protocol is documented in the
215
+ `ExSYS::ManagedUSB` class comment.
216
+
217
+
218
+ ## Tests
219
+
220
+ ~~~sh
221
+ rake test # or: ruby -Itest test/test_managed_usb.rb
222
+ ~~~
223
+
224
+ The suite drives the library and the executable against a model of the
225
+ hub's serial protocol (`test/support/`), so it needs neither a hub nor
226
+ the `uart`/`termios` gems.
39
227
 
40
228
 
229
+ ## License
41
230
 
231
+ MIT, see [LICENSE](LICENSE).
42
232
 
43
233
 
44
- [1]: https://www.exsys-shop.de/shopware/en/categories/hubsdocks/usb-hubs-managed/1263/managed-16-port-usb-3.2-gen-1-metal-hub-with-15kv-esd-surge-protection-din-rail?c=35
234
+ [1]: https://www.exsys.de/en/managed-16-port-usb-3.2-gen-1-metal-hub-with-15kv-esd-surge-protection-din-rail/EX-1526HMVS
235
+ [2]: https://rubygems.org/gems/uart
236
+ [hub]: https://www.exsys.de/thumbnail/df/a9/63/1716816684/EX-1526HMVS_-_Managed_16-Port_USB_3.2_Gen_1_Metall_HUB_15KV_ESD-1_800x800.jpg
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ require 'bundler'
2
+ require 'rake/testtask'
3
+ require 'yard'
4
+
5
+ Bundler::GemHelper.install_tasks
6
+
7
+ Rake::TestTask.new do |t|
8
+ t.test_files = FileList['test/test_*.rb']
9
+ t.verbose = true
10
+ t.warning = false
11
+ end
12
+
13
+ task :default => :test
14
+
15
+ YARD::Rake::YardocTask.new do |t|
16
+ t.files = [ 'lib/**/*.rb', 'ext/ucl.c' ]
17
+ t.options = [ '-m', 'markdown' ]
18
+ t.stats_options = [ '--list-undoc' ]
19
+ end
data/bin/exsys-usb CHANGED
@@ -4,7 +4,7 @@ require 'optparse'
4
4
  require 'exsys'
5
5
  require 'exsys/managed-usb'
6
6
 
7
- $opts = { :commit => false, :default => nil }
7
+ $opts = { :commit => false, :default => nil, :yes => false }
8
8
  parser = OptionParser.new do |op|
9
9
  op.banner = "Usage: #{op.program_name} ACTION"
10
10
 
@@ -12,8 +12,9 @@ parser = OptionParser.new do |op|
12
12
  op.on '-D', '--default=BOOLEAN', TrueClass, 'Default state if not specified'
13
13
  op.on '-p', '--password=STRING', 'Hub password'
14
14
  op.on '-c', '--commit', 'Commit change to flash memory'
15
+ op.on '-y', '--yes', 'Confirm a destructive action'
15
16
  op.on '--debug[=FILE]', 'Debug output file'
16
- op.on '-v', '--[no-]verbose', 'Run verbosely'
17
+ op.on '-v', '--[no-]verbose', 'Report port states after a change'
17
18
  op.on '-V', '--version', 'Version' do
18
19
  puts "ExSYS library : #{ExSYS::VERSION}"
19
20
  exit
@@ -23,33 +24,60 @@ parser = OptionParser.new do |op|
23
24
  exit
24
25
  end
25
26
  end
26
- parser.parse!(into: $opts)
27
27
 
28
- debug = if $opts.include?(:debug)
29
- if $opts[:debug].nil?
30
- then STDERR
31
- else File.open($opts[:debug], File::RDWR|File::CREAT)
32
- end
33
- end
34
- $hub = ExSYS::ManagedUSB.new($opts[:device], $opts[:password],
35
- debug: debug)
28
+ # Option parsing, debug file and hub creation all live inside the
29
+ # rescue, so that their failures are reported the same way as a hub
30
+ # failure instead of escaping as a backtrace.
31
+ begin
32
+ parser.parse!(into: $opts)
36
33
 
34
+ # Appended and kept private: the trace carries the hub password,
35
+ # and opening RDWR without truncating used to leave the tail of a
36
+ # longer previous session behind, reading as if it were hub traffic.
37
+ debug = if $opts.include?(:debug)
38
+ if $opts[:debug].nil?
39
+ then STDERR
40
+ else File.open($opts[:debug],
41
+ File::WRONLY|File::CREAT|File::APPEND, 0600)
42
+ end
43
+ end
44
+ $hub = ExSYS::ManagedUSB.new($opts[:device], $opts[:password],
45
+ debug: debug)
46
+
47
+ # Naming no port on the command line still means every port. The
48
+ # library wants that said outright rather than inferred from an
49
+ # empty list, so it is spelled out here.
50
+ ports = -> { ARGV.empty? ? [ ExSYS::ManagedUSB::ALL ]
51
+ : ARGV.map(&:to_i) }
52
+
53
+ # One line per port, "3 on", which greps and awks cleanly. Used by
54
+ # the status action and by --verbose after a change.
55
+ report = ->(only = []) {
56
+ state = $hub.get
57
+ (only.empty? ? $hub.ports : only).each do |p|
58
+ raise "invalid port: #{p}" unless state.key?(p)
59
+ puts "#{p} #{state[p] ? 'on' : 'off'}"
60
+ end
61
+ }
62
+ # Only meaningful after something changed.
63
+ verbose = -> { report.call if $opts[:verbose] }
37
64
 
38
- begin
39
65
  case action = ARGV.shift
40
66
  when nil
41
67
  puts parser
42
68
  exit
43
69
 
44
70
  when 'on'
45
- puts ARGV.map(&:to_i).join(',')
46
- $hub.on(*ARGV.map(&:to_i), commit: $opts[:commit])
71
+ $hub.on(*ports.call, commit: $opts[:commit])
72
+ verbose.call
47
73
 
48
74
  when 'off'
49
- $hub.off(*ARGV.map(&:to_i), commit: $opts[:commit])
75
+ $hub.off(*ports.call, commit: $opts[:commit])
76
+ verbose.call
50
77
 
51
78
  when 'toggle'
52
- $hub.toggle(*ARGV.map(&:to_i), commit: $opts[:commit])
79
+ $hub.toggle(*ports.call, commit: $opts[:commit])
80
+ verbose.call
53
81
 
54
82
  when 'set'
55
83
  t = ExSYS::ManagedUSB::TRUE_LIST .to_h {|e| [ e.to_s, e ]}
@@ -61,16 +89,45 @@ begin
61
89
  [$1.to_i, tf[$2]]
62
90
  }
63
91
  $hub.set(a, $opts[:default], commit: $opts[:commit])
92
+ verbose.call
64
93
 
94
+ when 'status'
95
+ report.call(ARGV.map(&:to_i))
96
+
97
+ when 'query'
98
+ q = $hub.query
99
+ puts "id: #{q[:id]}"
100
+ puts "ports: #{q[:ports]}"
101
+ puts "firmware: #{q[:firmware]}"
102
+
65
103
  when 'commit'
66
104
  $hub.commit
67
105
 
68
106
  when 'reset'
69
- $hub.reset
107
+ unless $opts[:yes]
108
+ raise 'reset reboots the hub, and every port loses power ' \
109
+ 'while it does; pass --yes to mean it'
110
+ end
111
+ $hub.reset(confirm: true)
70
112
 
113
+ when 'factory-reset'
114
+ unless $opts[:yes]
115
+ raise 'factory-reset drops every port and resets the ' \
116
+ 'password, and nothing undoes it; pass --yes to mean it'
117
+ end
118
+ $hub.factory_reset(confirm: true)
119
+
120
+ # A tombstone rather than an alias: anyone typing the old name is
121
+ # expecting the inverse of commit, which this never was.
71
122
  when 'restore'
72
- $hub.restore
123
+ raise 'restore was renamed factory-reset: it restores the hub ' \
124
+ 'to factory defaults, dropping every port and resetting ' \
125
+ 'the password, and is not the inverse of commit'
126
+
127
+ else
128
+ raise "unknown action: #{action}"
73
129
  end
74
- #rescue => e
75
- # $stderr.puts "#{parser.program_name}: #{e}"
130
+ rescue => e
131
+ $stderr.puts "#{parser.program_name}: #{e}"
132
+ exit 1
76
133
  end
data/exsys.gemspec CHANGED
@@ -18,13 +18,15 @@ Gem::Specification.new do |s|
18
18
  s.authors = [ "Stéphane D'Alu" ]
19
19
  s.email = [ 'stephane.dalu@insa-lyon.fr' ]
20
20
 
21
- s.files = %w[ README.md exsys.gemspec ] +
22
- Dir['lib/**/*.rb']
21
+ s.files = %w[ README.md exsys.gemspec Rakefile ] +
22
+ Dir['lib/**/*.rb'] +
23
+ Dir['test/**/*.rb']
23
24
 
24
25
  s.bindir = 'bin'
25
26
  s.executables << 'exsys-usb'
26
27
 
27
28
  s.add_dependency 'uart'
29
+ s.add_development_dependency 'minitest', '~>5'
28
30
  s.add_development_dependency 'yard', '~>0'
29
31
  s.add_development_dependency 'rake', '~>13'
30
32
  end