sonamp 0.0.4 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3b63474ec681132cd0674f5f2772e59494a585188e20b241af83fe865461adcd
4
- data.tar.gz: 0d066e5ef22315e64ae704ddba85735fa996c8c6e7f818ed9f41e38b645a1aaa
3
+ metadata.gz: caf6fd82a4c7c254e7c132a7b6865094f6c311bd634a92db6a842ab1c3417624
4
+ data.tar.gz: 31e699f752afaef597a2f328f1a9d3eaceae3f0de2e898a75b8ca4af00ac7737
5
5
  SHA512:
6
- metadata.gz: 3903c43f129e3a48f74e4a610f9cf3fd5b2e38d176508126c201370e6f171cbe929185f8bd6cc179bdff8d5188334aa157273127fa06df7fb50558ade4fa4a22
7
- data.tar.gz: b8d88a7775bca97209ebdda191939ee4b4e1bc0de2f3a5629665051506e2f083a3c3fa0ec4a9a734da113969166de9e65b5f7ccef44721ee04285c5edd44eb22
6
+ metadata.gz: 0f1a00b0586c04eaece4df0fc7b0af818a827d9b4767862b7b4a12d840fbe3abe1f12f065197fee3f6b734d24fccceb7701aa8b8dc2ed5847d58dd6858ea06b8
7
+ data.tar.gz: 736d03293482038f5b8a592d1f9c6bbc47f526159baeec9ca704290995ed7c1f3b7cc04d29fae178dd82992894f81dbcb269a41f520b2e38b3ac699fe82abda2
data/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ Copyright (c) 2022 Oleg Pudeyev
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ 1. Redistributions of source code must retain the above copyright notice, this
8
+ list of conditions and the following disclaimer.
9
+
10
+ 2. Redistributions in binary form must reproduce the above copyright notice,
11
+ this list of conditions and the following disclaimer in the documentation
12
+ and/or other materials provided with the distribution.
13
+
14
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
15
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
16
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
18
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
20
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
21
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
22
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/bin/sonamp CHANGED
@@ -1,49 +1,10 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  begin
4
- require 'sonamp'
4
+ require 'sonamp/cmd'
5
5
  rescue LoadError
6
6
  $: << File.join(File.dirname(__FILE__), '../lib')
7
- require 'sonamp'
7
+ require 'sonamp/cmd'
8
8
  end
9
- require 'optparse'
10
- require 'logger'
11
- require 'sonamp/utils'
12
9
 
13
- options = {}
14
- OptionParser.new do |opts|
15
- opts.banner = "Usage: sonamp [-d device] command arg..."
16
-
17
- opts.on("-d", "--device DEVICE", "TTY to use (default autodetect)") do |v|
18
- options[:device] = v
19
- end
20
- end.parse!
21
-
22
- logger = Logger.new(STDERR)
23
- client = Sonamp::Client.new(options[:device], logger: logger)
24
-
25
- cmd = ARGV.shift
26
- unless cmd
27
- raise ArgumentError, "No command given"
28
- end
29
-
30
- include Sonamp::Utils
31
-
32
- case cmd
33
- when 'power'
34
- zone = ARGV.shift.to_i
35
- state = parse_on_off(ARGV.shift)
36
- client.set_zone_power(zone, state)
37
- when 'zvol'
38
- zone = ARGV.shift.to_i
39
- volume = ARGV.shift.to_i
40
- client.set_zone_volume(zone, volume)
41
- when 'cvol'
42
- channel = ARGV.shift.to_i
43
- volume = ARGV.shift.to_i
44
- client.set_channel_volume(channel, volume)
45
- when 'status'
46
- client.status
47
- else
48
- raise ArgumentError, "Unknown command: #{cmd}"
49
- end
10
+ Sonamp::Cmd.new(ARGV).run
data/lib/sonamp/client.rb CHANGED
@@ -1,15 +1,51 @@
1
+ require 'timeout'
2
+
1
3
  module Sonamp
2
4
  class Error < StandardError; end
3
5
  class InvalidCommand < Error; end
4
6
  class NotApplicable < Error; end
5
7
  class UnexpectedResponse < Error; end
8
+ class CommunicationTimeout < Error; end
9
+
10
+ RS232_TIMEOUT = 3
11
+ DEFAULT_DEVICE_GLOB = '/dev/ttyUSB*'
12
+
13
+ module_function def detect_device(*patterns, logger: nil)
14
+ if patterns.empty?
15
+ patterns = [DEFAULT_DEVICE_GLOB]
16
+ end
17
+ devices = patterns.map do |pattern|
18
+ Dir.glob(pattern)
19
+ end.flatten.uniq
20
+ queue = Queue.new
21
+ threads = devices.map do |device|
22
+ Thread.new do
23
+ Timeout.timeout(RS232_TIMEOUT * 2) do
24
+ logger&.debug("Trying #{device}")
25
+ Client.new(device, logger: logger).get_zone_power
26
+ logger&.debug("Found receiver at #{device}")
27
+ queue << device
28
+ end
29
+ rescue CommunicationTimeout, IOError, SystemCallError => exc
30
+ logger&.debug("Failed on #{device}: #{exc.class}: #{exc}")
31
+ end
32
+ end
33
+ wait_thread = Thread.new do
34
+ threads.map(&:join)
35
+ queue << nil
36
+ end
37
+ queue.shift.tap do
38
+ threads.map(&:kill)
39
+ wait_thread.kill
40
+ end
41
+ end
6
42
 
7
43
  class Client
8
44
  def initialize(device = nil, logger: nil)
9
45
  @logger = logger
10
46
 
11
47
  if device.nil?
12
- device = Dir['/dev/ttyUSB*'].sort.first
48
+ device = Sonamp.detect_device(logger: logger)
13
49
  if device
14
50
  logger&.info("Using #{device} as TTY device")
15
51
  end
@@ -39,29 +75,22 @@ module Sonamp
39
75
  end
40
76
 
41
77
  def get_zone_volume(zone = nil)
42
- if zone
43
- if zone < 1 || zone > 4
44
- raise ArgumentError, "Zone must be between 1 and 4: #{zone}"
45
- end
46
- resp = dispatch(":V#{zone}?")
47
- resp[2...].to_i
48
- else
49
- dispatch(":VG?", 4).map do |resp|
50
- resp[2...].to_i
51
- end
52
- end
78
+ get_zone_value('V', zone)
53
79
  end
54
80
 
55
81
  def set_zone_volume(zone, volume)
56
- if zone < 1 || zone > 4
57
- raise ArgumentError, "Zone must be between 1 and 4: #{zone}"
58
- end
59
82
  if volume < 0 || volume > 100
60
83
  raise ArgumentError, "Volume must be between 0 and 100: #{volume}"
61
84
  end
62
- cmd = ":V#{zone}#{volume}"
63
- expected = cmd[1...cmd.length]
64
- dispatch_assert(cmd, expected)
85
+ set_zone_value('V', zone, volume)
86
+ end
87
+
88
+ def set_zone_mute(zone, state)
89
+ set_zone_value('M', zone, state ? 1 : 0)
90
+ end
91
+
92
+ def get_channel_volume(channel = nil)
93
+ get_channel_value('VC', channel)
65
94
  end
66
95
 
67
96
  def set_channel_volume(channel, volume)
@@ -76,10 +105,26 @@ module Sonamp
76
105
  dispatch_assert(cmd, expected)
77
106
  end
78
107
 
108
+ def set_channel_mute(channel, state)
109
+ set_channel_value('MC', channel, state ? 1 : 0)
110
+ end
111
+
79
112
  def get_zone_mute(zone = nil)
80
113
  get_zone_state('M', zone)
81
114
  end
82
115
 
116
+ def get_channel_mute(channel = nil)
117
+ get_channel_state('MC', channel)
118
+ end
119
+
120
+ def get_channel_front_panel_level(channel = nil)
121
+ get_channel_value('TVL', channel)
122
+ end
123
+
124
+ def get_zone_fault(zone = nil)
125
+ get_zone_state('FP', zone)
126
+ end
127
+
83
128
  def get_bbe(zone = nil)
84
129
  get_zone_state('BP', zone)
85
130
  end
@@ -100,25 +145,34 @@ module Sonamp
100
145
  get_zone_state('VTI', zone)
101
146
  end
102
147
 
148
+ def get_firmware_version
149
+ global_query('VER')
150
+ end
151
+
152
+ def get_temperature
153
+ Integer(global_query('TP'))
154
+ end
155
+
103
156
  def status
104
157
  # Reusing the opened device file makes :VTIG? fail even with a delay
105
158
  # in front.
106
159
  #open_device do
107
- p dispatch(':VER?')
108
- p dispatch(':TP?')
109
- p get_power
110
- p dispatch(':FPG?', 4)
111
- p get_zone_volume
112
- p dispatch(':VCG?', 8)
113
- p get_auto_trigger_input
114
- sleep 0.1
115
- p get_voltage_trigger_input
116
- p dispatch(':TVLG?', 8)
117
- p get_zone_mute
118
- p dispatch(':MCG?', 8)
119
- p get_bbe
120
- p get_bbe_high_boost
121
- p get_bbe_low_boost
160
+ {
161
+ firmware_version: get_firmware_version,
162
+ temperature: get_temperature,
163
+ zone_power: get_zone_power,
164
+ zone_fault: get_zone_fault,
165
+ zone_volume: get_zone_volume,
166
+ channel_volume: get_channel_volume,
167
+ zone_mute: get_zone_mute,
168
+ channel_mute: get_channel_mute,
169
+ bbe: get_bbe,
170
+ bbe_high_boost: get_bbe_high_boost,
171
+ bbe_low_boost: get_bbe_low_boost,
172
+ auto_trigger_input: get_auto_trigger_input,
173
+ voltage_trigger_input: get_voltage_trigger_input,
174
+ channel_front_panel_level: get_channel_front_panel_level,
175
+ }
122
176
  #end
123
177
  end
124
178
 
@@ -128,7 +182,7 @@ module Sonamp
128
182
  if @f
129
183
  yield
130
184
  else
131
- File.open(device, 'r+') do |f|
185
+ File.open(device, 'r+b') do |f|
132
186
  @f = f
133
187
  yield.tap do
134
188
  @f = nil
@@ -139,7 +193,9 @@ module Sonamp
139
193
 
140
194
  def dispatch(cmd, resp_lines_count = 1)
141
195
  open_device do
142
- @f << "#{cmd}\x0d"
196
+ with_timeout do
197
+ @f.syswrite("#{cmd}\x0d")
198
+ end
143
199
  resp = 1.upto(resp_lines_count).map do
144
200
  read_line(@f, cmd)
145
201
  end
@@ -158,28 +214,105 @@ module Sonamp
158
214
  end
159
215
  end
160
216
 
217
+ def extract_suffix(resp, expected_prefix)
218
+ unless resp[0..expected_prefix.length-1] == expected_prefix
219
+ raise UnexpectedResponse, "Unexpected response: expected #{expected_prefix}..., actual #{resp}"
220
+ end
221
+ resp[expected_prefix.length..]
222
+ end
223
+
224
+ def dispatch_extract_suffix(cmd, expected_prefix)
225
+ resp = dispatch(cmd)
226
+ extract_suffix(resp, expected_prefix)
227
+ end
228
+
229
+ def global_query(cmd)
230
+ dispatch_extract_suffix(":#{cmd}?", cmd)
231
+ end
232
+
233
+ def with_timeout(&block)
234
+ Timeout.timeout(RS232_TIMEOUT, CommunicationTimeout, &block)
235
+ end
236
+
161
237
  def read_line(f, cmd)
162
- f.readline.strip.tap do |resp|
163
- if resp == 'ERR'
164
- raise InvalidCommand, "Invalid command: #{cmd}"
165
- elsif resp == 'N/A'
166
- raise NotApplicable, "Command was recognized but could not be executed - is serial control enabled on the amplifier?"
238
+ with_timeout do
239
+ f.readline.strip.tap do |resp|
240
+ if resp == 'ERR'
241
+ raise InvalidCommand, "Invalid command: #{cmd}"
242
+ elsif resp == 'N/A'
243
+ raise NotApplicable, "Command was recognized but could not be executed - is serial control enabled on the amplifier?"
244
+ end
167
245
  end
168
246
  end
169
247
  end
170
248
 
171
- def get_zone_state(cmd_prefix, zone)
249
+ def set_zone_value(cmd_prefix, zone, value)
250
+ if zone < 1 || zone > 4
251
+ raise ArgumentError, "Zone must be between 1 and 4: #{zone}"
252
+ end
253
+ cmd = ":#{cmd_prefix}#{zone}#{value}"
254
+ expected = cmd[1...cmd.length]
255
+ dispatch_assert(cmd, expected)
256
+ end
257
+
258
+ def get_zone_value(cmd_prefix, zone, boolize: false)
172
259
  if zone
173
260
  if zone < 1 || zone > 4
174
261
  raise ArgumentError, "Zone must be between 1 and 4: #{zone}"
175
262
  end
176
263
  resp = dispatch(":#{cmd_prefix}#{zone}?")
177
- resp[cmd_prefix.length + 1] == '1' ? true : false
264
+ typecast_value(resp[cmd_prefix.length + 1..], boolize)
178
265
  else
179
- dispatch(":#{cmd_prefix}G?", 4).map do |resp|
180
- resp[cmd_prefix.length + 1] == '1' ? true : false
266
+ index = 1
267
+ hashize_query_result(dispatch(":#{cmd_prefix}G?", 4), cmd_prefix, boolize)
268
+ end
269
+ end
270
+
271
+ def hashize_query_result(resp_lines, cmd_prefix, boolize)
272
+ index = 1
273
+ Hash[resp_lines.map do |resp|
274
+ value = typecast_value(extract_suffix(resp, "#{cmd_prefix}#{index}"), boolize)
275
+ [index, value].tap do
276
+ index += 1
181
277
  end
278
+ end]
279
+ end
280
+
281
+ def get_zone_state(cmd_prefix, zone)
282
+ get_zone_value(cmd_prefix, zone, boolize: true)
283
+ end
284
+
285
+ def set_channel_value(cmd_prefix, channel, value)
286
+ if channel < 1 || channel > 8
287
+ raise ArgumentError, "Channel must be between 1 and 8: #{channel}"
288
+ end
289
+ cmd = ":#{cmd_prefix}#{channel}#{value}"
290
+ expected = cmd[1...cmd.length]
291
+ dispatch_assert(cmd, expected)
292
+ end
293
+
294
+ def get_channel_value(cmd_prefix, channel, boolize: false)
295
+ if channel
296
+ if channel < 1 || channel > 8
297
+ raise ArgumentError, "Channel must be between 1 and 8: #{channel}"
298
+ end
299
+ typecast_value(dispatch_extract_suffix(":#{cmd_prefix}#{channel}?", "#{cmd_prefix}#{channel}"), boolize)
300
+ else
301
+ index = 1
302
+ hashize_query_result(dispatch(":#{cmd_prefix}G?", 8), cmd_prefix, boolize)
303
+ end
304
+ end
305
+
306
+ def get_channel_state(cmd_prefix, channel)
307
+ get_channel_value(cmd_prefix, channel, boolize: true)
308
+ end
309
+
310
+ def typecast_value(value, boolize)
311
+ value = Integer(value)
312
+ if boolize
313
+ value = value == 1
182
314
  end
315
+ value
183
316
  end
184
317
  end
185
318
  end
data/lib/sonamp/cmd.rb ADDED
@@ -0,0 +1,81 @@
1
+ require 'optparse'
2
+ require 'logger'
3
+ require 'sonamp/utils'
4
+ require 'sonamp/client'
5
+
6
+ module Sonamp
7
+ class Cmd
8
+ def initialize(args)
9
+ options = {}
10
+ OptionParser.new do |opts|
11
+ opts.banner = "Usage: sonamp [-d device] command arg..."
12
+
13
+ opts.on("-d", "--device DEVICE", "TTY to use (default autodetect)") do |v|
14
+ options[:device] = v
15
+ end
16
+ end.parse!(args)
17
+
18
+ @options = options
19
+
20
+ @logger = Logger.new(STDERR)
21
+ @client = Sonamp::Client.new(options[:device], logger: @logger)
22
+
23
+ @args = args
24
+ end
25
+
26
+ attr_reader :args
27
+ attr_reader :logger
28
+
29
+ def run
30
+ cmd = args.shift
31
+ unless cmd
32
+ raise ArgumentError, "No command given"
33
+ end
34
+
35
+ case cmd
36
+ when 'detect'
37
+ device = Sonamp.detect_device(*args, logger: logger)
38
+ if device
39
+ puts device
40
+ exit 0
41
+ else
42
+ STDERR.puts("Sonamp amplifier not found")
43
+ exit 3
44
+ end
45
+ when 'off'
46
+ client.set_zone_power(1, false)
47
+ client.set_zone_power(2, false)
48
+ client.set_zone_power(3, false)
49
+ client.set_zone_power(4, false)
50
+ when 'power'
51
+ zone = args.shift.to_i
52
+ state = Utils.parse_on_off(ARGV.shift)
53
+ client.set_zone_power(zone, state)
54
+ when 'zvol'
55
+ zone = args.shift.to_i
56
+ volume = ARGV.shift.to_i
57
+ client.set_zone_volume(zone, volume)
58
+ when 'cvol'
59
+ channel = args.shift.to_i
60
+ volume = args.shift.to_i
61
+ client.set_channel_volume(channel, volume)
62
+ when 'zmute'
63
+ zone = args.shift.to_i
64
+ mute = ARGV.shift.to_i
65
+ client.set_zone_mute(zone, mute)
66
+ when 'cmute'
67
+ channel = args.shift.to_i
68
+ mute = args.shift.to_i
69
+ client.set_channel_mute(channel, mute)
70
+ when 'status'
71
+ pp client.status
72
+ else
73
+ raise ArgumentError, "Unknown command: #{cmd}"
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ attr_reader :client
80
+ end
81
+ end
data/lib/sonamp/utils.rb CHANGED
@@ -5,7 +5,7 @@ module Sonamp
5
5
  case value&.downcase
6
6
  when '1', 'on', 'yes', 'true'
7
7
  true
8
- when '0', 'off', 'no', 'value'
8
+ when '0', 'off', 'no', 'false'
9
9
  false
10
10
  else
11
11
  raise ArgumentError, "Invalid on/off value: #{value}"
@@ -1,3 +1,3 @@
1
1
  module Sonamp
2
- VERSION = '0.0.4'.freeze
2
+ VERSION = '0.0.7'.freeze
3
3
  end
data/sonamp.gemspec CHANGED
@@ -2,15 +2,15 @@
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = "sonamp"
5
- spec.version = '0.0.4'
5
+ spec.version = '0.0.7'
6
6
  spec.authors = ['Oleg Pudeyev']
7
7
  spec.email = ['code@olegp.name']
8
8
  spec.summary = %q{Sonance Sonamp Amplifier Serial Control Interface}
9
- spec.description = %q{Library for controlling Sonance Sonamp 875D & 875D MkII amplifiers via the serial port}
9
+ spec.description = %q{Library for controlling Sonance Sonamp 875D SE & 875D MkII amplifiers via the serial port}
10
10
  spec.homepage = "https://github.com/p/sonamp-ruby"
11
11
  spec.license = "MIT"
12
12
 
13
- spec.files = `git ls-files -z`.split("\x0")
13
+ spec.files = `git ls-files -z`.split("\x0").reject { |path| path.start_with?('docs/') }
14
14
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
15
15
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
16
16
  spec.require_paths = ["lib"]
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sonamp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oleg Pudeyev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-10-26 00:00:00.000000000 Z
11
+ date: 2022-11-07 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description: Library for controlling Sonance Sonamp 875D & 875D MkII amplifiers via
14
- the serial port
13
+ description: Library for controlling Sonance Sonamp 875D SE & 875D MkII amplifiers
14
+ via the serial port
15
15
  email:
16
16
  - code@olegp.name
17
17
  executables:
@@ -21,11 +21,13 @@ extensions: []
21
21
  extra_rdoc_files: []
22
22
  files:
23
23
  - ".gitignore"
24
+ - LICENSE
24
25
  - bin/sonamp
25
26
  - bin/sonamp-web
26
27
  - lib/sonamp.rb
27
28
  - lib/sonamp/app.rb
28
29
  - lib/sonamp/client.rb
30
+ - lib/sonamp/cmd.rb
29
31
  - lib/sonamp/utils.rb
30
32
  - lib/sonamp/version.rb
31
33
  - sonamp.gemspec