sonamp 0.0.5 → 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: 816d27d5e7d9e1e5c1ab289d18dce3fdddb18a8fa15bb19ca51f776adfe0fdd0
4
- data.tar.gz: faffd6868c0c625f8a7ae839cbc00a53f9d1d8d75124c00691c035f18c1af8e5
3
+ metadata.gz: caf6fd82a4c7c254e7c132a7b6865094f6c311bd634a92db6a842ab1c3417624
4
+ data.tar.gz: 31e699f752afaef597a2f328f1a9d3eaceae3f0de2e898a75b8ca4af00ac7737
5
5
  SHA512:
6
- metadata.gz: 1335f8bf639400ad932b0672919160b4754c41dbd46dbcd7107145d8147a648b4f29705a3eff322ffb39c21d5c13229e6f622c2885c5a792ad9aba8420566bfe
7
- data.tar.gz: 8cbf78ea9632fdefa9d50270890396c3722bbb94654f003fad5746c4e341456f4c66c4cd6d4f5fd7202c1f59dd951afef96aa201079388d8ae818d457c7de63e
6
+ metadata.gz: 0f1a00b0586c04eaece4df0fc7b0af818a827d9b4767862b7b4a12d840fbe3abe1f12f065197fee3f6b734d24fccceb7701aa8b8dc2ed5847d58dd6858ea06b8
7
+ data.tar.gz: 736d03293482038f5b8a592d1f9c6bbc47f526159baeec9ca704290995ed7c1f3b7cc04d29fae178dd82992894f81dbcb269a41f520b2e38b3ac699fe82abda2
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ *.gem
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,28 +75,18 @@ 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
- Integer(dispatch_extract_suffix(":V#{zone}?", 'V'))
47
- else
48
- dispatch(":VG?", 4).map do |resp|
49
- Integer(extract_suffix(resp, 'V'))
50
- end
51
- end
78
+ get_zone_value('V', zone)
52
79
  end
53
80
 
54
81
  def set_zone_volume(zone, volume)
55
- if zone < 1 || zone > 4
56
- raise ArgumentError, "Zone must be between 1 and 4: #{zone}"
57
- end
58
82
  if volume < 0 || volume > 100
59
83
  raise ArgumentError, "Volume must be between 0 and 100: #{volume}"
60
84
  end
61
- cmd = ":V#{zone}#{volume}"
62
- expected = cmd[1...cmd.length]
63
- 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)
64
90
  end
65
91
 
66
92
  def get_channel_volume(channel = nil)
@@ -79,12 +105,16 @@ module Sonamp
79
105
  dispatch_assert(cmd, expected)
80
106
  end
81
107
 
108
+ def set_channel_mute(channel, state)
109
+ set_channel_value('MC', channel, state ? 1 : 0)
110
+ end
111
+
82
112
  def get_zone_mute(zone = nil)
83
113
  get_zone_state('M', zone)
84
114
  end
85
115
 
86
116
  def get_channel_mute(channel = nil)
87
- get_channel_value('MC', channel)
117
+ get_channel_state('MC', channel)
88
118
  end
89
119
 
90
120
  def get_channel_front_panel_level(channel = nil)
@@ -127,23 +157,23 @@ module Sonamp
127
157
  # Reusing the opened device file makes :VTIG? fail even with a delay
128
158
  # in front.
129
159
  #open_device do
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
+ }
130
176
  #end
131
- {
132
- firmware_version: get_firmware_version,
133
- temperature: get_temperature,
134
- zone_power: get_zone_power,
135
- zone_fault: get_zone_fault,
136
- zone_volume: get_zone_volume,
137
- channel_volume: get_channel_volume,
138
- zone_mute: get_zone_mute,
139
- channel_mute: get_channel_mute,
140
- bbe: get_bbe,
141
- bbe_high_boost: get_bbe_high_boost,
142
- bbe_low_boost: get_bbe_low_boost,
143
- auto_trigger_input: get_auto_trigger_input,
144
- voltage_trigger_input: get_voltage_trigger_input,
145
- channel_front_panel_level: get_channel_front_panel_level,
146
- }
147
177
  end
148
178
 
149
179
  private
@@ -152,7 +182,7 @@ module Sonamp
152
182
  if @f
153
183
  yield
154
184
  else
155
- File.open(device, 'r+') do |f|
185
+ File.open(device, 'r+b') do |f|
156
186
  @f = f
157
187
  yield.tap do
158
188
  @f = nil
@@ -163,7 +193,9 @@ module Sonamp
163
193
 
164
194
  def dispatch(cmd, resp_lines_count = 1)
165
195
  open_device do
166
- @f << "#{cmd}\x0d"
196
+ with_timeout do
197
+ @f.syswrite("#{cmd}\x0d")
198
+ end
167
199
  resp = 1.upto(resp_lines_count).map do
168
200
  read_line(@f, cmd)
169
201
  end
@@ -184,7 +216,7 @@ module Sonamp
184
216
 
185
217
  def extract_suffix(resp, expected_prefix)
186
218
  unless resp[0..expected_prefix.length-1] == expected_prefix
187
- raise "Unexpected response: expected #{expected_prefix}..., actual #{resp}"
219
+ raise UnexpectedResponse, "Unexpected response: expected #{expected_prefix}..., actual #{resp}"
188
220
  end
189
221
  resp[expected_prefix.length..]
190
222
  end
@@ -198,44 +230,89 @@ module Sonamp
198
230
  dispatch_extract_suffix(":#{cmd}?", cmd)
199
231
  end
200
232
 
233
+ def with_timeout(&block)
234
+ Timeout.timeout(RS232_TIMEOUT, CommunicationTimeout, &block)
235
+ end
236
+
201
237
  def read_line(f, cmd)
202
- f.readline.strip.tap do |resp|
203
- if resp == 'ERR'
204
- raise InvalidCommand, "Invalid command: #{cmd}"
205
- elsif resp == 'N/A'
206
- 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
207
245
  end
208
246
  end
209
247
  end
210
248
 
211
- 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)
212
259
  if zone
213
260
  if zone < 1 || zone > 4
214
261
  raise ArgumentError, "Zone must be between 1 and 4: #{zone}"
215
262
  end
216
263
  resp = dispatch(":#{cmd_prefix}#{zone}?")
217
- resp[cmd_prefix.length + 1] == '1' ? true : false
264
+ typecast_value(resp[cmd_prefix.length + 1..], boolize)
218
265
  else
219
- dispatch(":#{cmd_prefix}G?", 4).map do |resp|
220
- 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
221
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}"
222
288
  end
289
+ cmd = ":#{cmd_prefix}#{channel}#{value}"
290
+ expected = cmd[1...cmd.length]
291
+ dispatch_assert(cmd, expected)
223
292
  end
224
293
 
225
- def get_channel_value(cmd_prefix, channel)
294
+ def get_channel_value(cmd_prefix, channel, boolize: false)
226
295
  if channel
227
296
  if channel < 1 || channel > 8
228
297
  raise ArgumentError, "Channel must be between 1 and 8: #{channel}"
229
298
  end
230
- Integer(dispatch_extract_suffix(":#{cmd_prefix}#{channel}?", "#{cmd_prefix}#{channel}"))
299
+ typecast_value(dispatch_extract_suffix(":#{cmd_prefix}#{channel}?", "#{cmd_prefix}#{channel}"), boolize)
231
300
  else
232
301
  index = 1
233
- dispatch(":#{cmd_prefix}G?", 8).map do |resp|
234
- Integer(extract_suffix(resp, "#{cmd_prefix}#{index}")).tap do
235
- index += 1
236
- end
237
- end
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
238
314
  end
315
+ value
239
316
  end
240
317
  end
241
318
  end
data/lib/sonamp/cmd.rb CHANGED
@@ -24,6 +24,7 @@ module Sonamp
24
24
  end
25
25
 
26
26
  attr_reader :args
27
+ attr_reader :logger
27
28
 
28
29
  def run
29
30
  cmd = args.shift
@@ -32,6 +33,15 @@ module Sonamp
32
33
  end
33
34
 
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
35
45
  when 'off'
36
46
  client.set_zone_power(1, false)
37
47
  client.set_zone_power(2, false)
@@ -49,6 +59,14 @@ module Sonamp
49
59
  channel = args.shift.to_i
50
60
  volume = args.shift.to_i
51
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)
52
70
  when 'status'
53
71
  pp client.status
54
72
  else
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.5'.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.5'
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 [A-Z]* bin lib *.gemspec`.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.5
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-27 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:
@@ -20,6 +20,7 @@ executables:
20
20
  extensions: []
21
21
  extra_rdoc_files: []
22
22
  files:
23
+ - ".gitignore"
23
24
  - LICENSE
24
25
  - bin/sonamp
25
26
  - bin/sonamp-web