exsys 0.5

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8f8a542193c2ef67c427b47ee5ee95c448ecb9f3aced7d1a763f4e3159340eb2
4
+ data.tar.gz: 04da256b271c0c43d07e258c808eff8659c2b95ecc03d7424ec8101d46f5c126
5
+ SHA512:
6
+ metadata.gz: 6e2555919ac712c8b4d4064b4d4d9cd8e69d8aa74c3ce71dbb86be23deb6e20cf0be89907d7d41bf087010f139dec32dcdd6fd6b989ea21aad53a46e83721bf1
7
+ data.tar.gz: da9630c153d83bbedc4f840707aa32ac90f2320d038e43ae01d8bfea62db52c090d2da9a39694545518e7b4829cf197a3487e64c4c052a9233cc4a8254154a07
data/README.md ADDED
@@ -0,0 +1,44 @@
1
+
2
+ Controlling a [ExSYS Managed USB hub][1] without being
3
+ constrained by the official binary-only support.
4
+
5
+
6
+ Executable
7
+ ~~~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
15
+ ~~~
16
+
17
+ Library:
18
+
19
+ ~~~ruby
20
+ # Instanciate hub (Linux: ttyUSB?, FreeBSD: ttyU?)
21
+ # and enable debug output to stderr
22
+ hub = ExSYS::ManagedUSB.new('/dev/ttyU0', debug: STDERR)
23
+
24
+ # Chaining turning on all port, and swithing off ports 4,5,6
25
+ hub.on.off(4,5,6)
26
+
27
+ # Perform sequential toggle of all individual ports
28
+ ExSYS::ManagedUSB::PORTS.each do |p|
29
+ hub.toggle(p)
30
+ end
31
+
32
+ # Set ports states for 1 and 2
33
+ hub.set({ 1 => true, 2 => false })
34
+
35
+ # Set ports states for 1 and 2, forcing other ports to off
36
+ hub.set({ 1 => true, 2 => true }, false)
37
+ ~~~
38
+
39
+
40
+
41
+
42
+
43
+
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
data/bin/exsys-usb ADDED
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'exsys'
5
+ require 'exsys/managed-usb'
6
+
7
+ $opts = { :commit => false, :default => nil }
8
+ parser = OptionParser.new do |op|
9
+ op.banner = "Usage: #{op.program_name} ACTION"
10
+
11
+ op.on '-d', '--device=DEV', 'Serial line to hub'
12
+ op.on '-D', '--default=BOOLEAN', TrueClass, 'Default state if not specified'
13
+ op.on '-p', '--password=STRING', 'Hub password'
14
+ op.on '-c', '--commit', 'Commit change to flash memory'
15
+ op.on '--debug[=FILE]', 'Debug output file'
16
+ op.on '-v', '--[no-]verbose', 'Run verbosely'
17
+ op.on '-V', '--version', 'Version' do
18
+ puts "ExSYS library : #{ExSYS::VERSION}"
19
+ exit
20
+ end
21
+ op.on '-h', '--help', 'Help' do
22
+ puts op
23
+ exit
24
+ end
25
+ end
26
+ parser.parse!(into: $opts)
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)
36
+
37
+
38
+ begin
39
+ case action = ARGV.shift
40
+ when nil
41
+ puts parser
42
+ exit
43
+
44
+ when 'on'
45
+ puts ARGV.map(&:to_i).join(',')
46
+ $hub.on(*ARGV.map(&:to_i), commit: $opts[:commit])
47
+
48
+ when 'off'
49
+ $hub.off(*ARGV.map(&:to_i), commit: $opts[:commit])
50
+
51
+ when 'toggle'
52
+ $hub.toggle(*ARGV.map(&:to_i), commit: $opts[:commit])
53
+
54
+ when 'set'
55
+ t = ExSYS::ManagedUSB::TRUE_LIST .to_h {|e| [ e.to_s, e ]}
56
+ f = ExSYS::ManagedUSB::FALSE_LIST.to_h {|e| [ e.to_s, e ]}
57
+ tf = t.merge(f) { raise "true/false conflict (internal error)" }
58
+ r = tf.keys.map {|e| Regexp.escape(e)}
59
+ a = ARGV.to_h {|e|
60
+ raise "invalid argument" unless e =~ /^(\d+):(#{r.join('|')})$/
61
+ [$1.to_i, tf[$2]]
62
+ }
63
+ $hub.set(a, $opts[:default], commit: $opts[:commit])
64
+
65
+ when 'commit'
66
+ $hub.commit
67
+
68
+ when 'reset'
69
+ $hub.reset
70
+
71
+ when 'restore'
72
+ $hub.restore
73
+ end
74
+ #rescue => e
75
+ # $stderr.puts "#{parser.program_name}: #{e}"
76
+ end
data/exsys.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require_relative 'lib/exsys/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'exsys'
7
+ s.version = ExSYS::VERSION
8
+ s.summary = "ExSYS managed USB hub support"
9
+ s.description = <<~EOF
10
+
11
+ Provide command support to control ExSYS Managed USB hub.
12
+
13
+ EOF
14
+
15
+ s.homepage = 'https://github.com/sdalu/ruby-exsys'
16
+ s.license = 'MIT'
17
+
18
+ s.authors = [ "Stéphane D'Alu" ]
19
+ s.email = [ 'stephane.dalu@insa-lyon.fr' ]
20
+
21
+ s.files = %w[ README.md exsys.gemspec ] +
22
+ Dir['lib/**/*.rb']
23
+
24
+ s.bindir = 'bin'
25
+ s.executables << 'exsys-usb'
26
+
27
+ s.add_dependency 'uart'
28
+ s.add_development_dependency 'yard', '~>0'
29
+ s.add_development_dependency 'rake', '~>13'
30
+ end
@@ -0,0 +1,228 @@
1
+ require "uart"
2
+
3
+ module ExSYS
4
+
5
+ # Control a ExSYS Managed USB hub
6
+ class ManagedUSB
7
+ SPEED = 9600 # @!visibility private
8
+ PASSWORD = 'pass'.freeze # @!visibility private
9
+ PORTS = 1.upto(16).to_a.freeze # @!visibility private
10
+ TRUE_LIST = [ 1, :on, :ON, :true, :TRUE, :t, :T, true ].freeze # @!visibility private
11
+ FALSE_LIST = [ 0, :off, :OFF, :false, :FALSE, :f, :F, false ].freeze # @!visibility private
12
+
13
+ # Error handling class
14
+ class Error < StandardError
15
+ end
16
+
17
+ # Initialize object.
18
+ #
19
+ # @param line [String] Serial line
20
+ # (usually /dev/ttyU? or /dev/ttyUSB?)
21
+ # @param password [String] Hub password
22
+ # @param debug [IO] Write debug output
23
+ def initialize(line, password = nil, debug: nil)
24
+ password ||= PASSWORD
25
+ if password.size > 8
26
+ raise ArgumentError, "password too long"
27
+ end
28
+ @line = line
29
+ @password = password.ljust(8)
30
+ @debug = debug
31
+ end
32
+
33
+ # Toggle all or specified ports
34
+ #
35
+ # @param commit [Boolean] Commit to flash memory
36
+ def toggle(*ports, commit: false)
37
+ _set(_get ^ mask(ports, :all), commit: commit)
38
+ end
39
+
40
+ # Turn on all or specified ports
41
+ #
42
+ # @param commit [Boolean] Commit to flash memory
43
+ def on(*ports, commit: false)
44
+ _set(_get | mask(ports, :all), commit: commit)
45
+ end
46
+
47
+ # Turn off all or specified ports
48
+ #
49
+ # @param commit [Boolean] Commit to flash memory
50
+ def off(*ports, commit: false)
51
+ _set(_get & ~mask(ports, :all), commit: commit)
52
+ end
53
+
54
+ # Set state for the specified ports
55
+ #
56
+ # Port specification can have one of the folling format
57
+ #
58
+ # 1. hash of port values: { 1 => :on, 2 => :off, ...}
59
+ # 2. hash of port states: { :on => [1, 3], :off => 4 }
60
+ #
61
+ # In the case 1. the state values can be specified by
62
+ #
63
+ # * True: 1, :on, :ON, :true, :TRUE, true
64
+ # * False: 0, :off, :OFF, :false, :FALSE, false
65
+ #
66
+ # The port states that are not specified will acquire the
67
+ # value specified by the default parameter (nil being the
68
+ # hub port current value)
69
+ #
70
+ # @param dataset [Hash] Port state specification
71
+ # @param default [Boolean,nil] Default value to use if unspecified
72
+ # @param commit [Boolean] Commit to flash memory
73
+ def set(dataset, default = nil, commit: false)
74
+ val = _get
75
+
76
+ # Normalize
77
+ keys = dataset.keys
78
+ if (keys - PORTS).empty?
79
+ dataset = dataset.transform_values do |v|
80
+ case v
81
+ when * TRUE_LIST then true
82
+ when *FALSE_LIST then false
83
+ when nil
84
+ else raise ArgumentError
85
+ end
86
+ end
87
+ elsif (keys - [:on, :off]).empty?
88
+ on = Array(dataset[:on ])
89
+ off = Array(dataset[:off])
90
+
91
+ unless (on & off).empty?
92
+ raise ArgumentError, "on/off overlap"
93
+ end
94
+
95
+ dataset = {}
96
+ dataset.merge!(on .to_h {|k| [k, true ] })
97
+ dataset.merge!(off.to_h {|k| [k, false ] })
98
+ else
99
+ raise ArgumentError
100
+ end
101
+
102
+ # Fill unspecified
103
+ unless default.nil?
104
+ (PORTS - dataset.keys).each do |k|
105
+ dataset.merge!(k => default)
106
+ end
107
+ end
108
+
109
+ # Compute value
110
+ dataset.compact.each do |k,v|
111
+ flg = 1 << (k-1)
112
+ if v
113
+ then val |= flg
114
+ else val &= ~flg
115
+ end
116
+ end
117
+
118
+ _set(val, commit: commit)
119
+ end
120
+
121
+ # Get hub current state for all ports
122
+ #
123
+ # Return value depend of the asked type (default: ports)
124
+ #
125
+ # * ports : { 1 => true, 2 => false, ...}
126
+ # * on_off: { :on => [1,2,3,...], :off => [6,7,...] }
127
+ # * on : [ 1, 2, 3, ... ]
128
+ # * off : [ 1, 2, 3, ... ]
129
+ #
130
+ # @param type [:ports, :on_off, :on, :off] Type of returned value
131
+ def get(type = :ports)
132
+ val = _get
133
+ h = PORTS.reduce({}) {|acc, obj|
134
+ acc.merge(obj => (val & (1 << (obj-1))).positive?)
135
+ }
136
+
137
+ case type
138
+ when :ports
139
+ h
140
+ when :on_off
141
+ h.reduce({}) {|acc, (k,v)|
142
+ acc.merge(v ? :on : :off => [ k ]) {|k,o,n| o + n }
143
+ }
144
+ when :on
145
+ h.select {|k,v| v }.keys
146
+ when :off
147
+ h.reject {|k,v| v }.keys
148
+ else
149
+ raise ArgumentError
150
+ end
151
+ end
152
+
153
+ # Restore port states from the flash memory
154
+ def restore
155
+ action('RD', @password).then { self }
156
+ end
157
+
158
+ # Save the port states to the flash memory
159
+ def commit
160
+ action('WP', @password).then { self }
161
+ end
162
+
163
+ # Perform a hub reset action
164
+ #
165
+ # @note power is not maintained accros a reset
166
+ def reset
167
+ action('RH', @password, reply: false).then { self }
168
+ end
169
+
170
+ # Change the hub protection password
171
+ def password(new)
172
+ new = PASSWORD if new.nil?
173
+ raise ArgumentError, 'password too long' if new.size > 8
174
+ new_password = new.ljust(8)
175
+ action('CP', @password, new_password)
176
+ @password = new_password
177
+ self
178
+ end
179
+
180
+ private
181
+
182
+ def mask(ports, empty = :none)
183
+ case empty
184
+ when :none
185
+ when :all
186
+ ports = PORTS if ports.empty?
187
+ else raise ArgumentError
188
+ end
189
+
190
+ ports.reduce(0) {|acc, obj| acc |= 1 << (obj-1) }
191
+ end
192
+
193
+ def _get
194
+ data = action('GP', check: false)
195
+
196
+ if (data.size == 3) && (data[0] == 'E')
197
+ raise Error, data[1..-1]
198
+ elsif data.size != 8
199
+ raise Error
200
+ end
201
+
202
+ [ data ].pack('H4').unpack1('v')
203
+ end
204
+
205
+ def _set(v, commit: false)
206
+ dataset = ([v].pack('v').unpack1('H*') + 'ffff').upcase
207
+ action(commit ? 'FP' : 'SP', @password, dataset).then { self }
208
+ end
209
+
210
+
211
+ def action(*cmds, reply: true, check: true)
212
+ cmd = cmds.join
213
+ UART.open @line, SPEED do |serial|
214
+ @debug&.puts "<-- #{cmd}"
215
+ serial.write "#{cmd}\r"
216
+ if reply
217
+ serial.read.chomp.tap do |data|
218
+ @debug&.puts "--> #{data}"
219
+ if check && data[0] != 'G'
220
+ raise Error, data[1..-1]
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end
226
+ end
227
+ end
228
+
@@ -0,0 +1,3 @@
1
+ module ExSYS
2
+ VERSION = 0.5 # Version
3
+ end
data/lib/exsys.rb ADDED
@@ -0,0 +1,5 @@
1
+ module ExSYS
2
+ end
3
+
4
+ require_relative 'exsys/version'
5
+ require_relative 'exsys/managed-usb'
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: exsys
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.5'
5
+ platform: ruby
6
+ authors:
7
+ - Stéphane D'Alu
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-11-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: uart
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: yard
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '13'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '13'
55
+ description: |2+
56
+
57
+ Provide command support to control ExSYS Managed USB hub.
58
+
59
+ email:
60
+ - stephane.dalu@insa-lyon.fr
61
+ executables:
62
+ - exsys-usb
63
+ extensions: []
64
+ extra_rdoc_files: []
65
+ files:
66
+ - README.md
67
+ - bin/exsys-usb
68
+ - exsys.gemspec
69
+ - lib/exsys.rb
70
+ - lib/exsys/managed-usb.rb
71
+ - lib/exsys/version.rb
72
+ homepage: https://github.com/sdalu/ruby-exsys
73
+ licenses:
74
+ - MIT
75
+ metadata: {}
76
+ post_install_message:
77
+ rdoc_options: []
78
+ require_paths:
79
+ - lib
80
+ required_ruby_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ requirements: []
91
+ rubygems_version: 3.5.19
92
+ signing_key:
93
+ specification_version: 4
94
+ summary: ExSYS managed USB hub support
95
+ test_files: []
96
+ ...