sonamp 0.0.5 → 0.0.8

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: 816d27d5e7d9e1e5c1ab289d18dce3fdddb18a8fa15bb19ca51f776adfe0fdd0
4
- data.tar.gz: faffd6868c0c625f8a7ae839cbc00a53f9d1d8d75124c00691c035f18c1af8e5
3
+ metadata.gz: 6a0be380a5a03184319da1aa8806ebe909a214107990470529bc550aaab6b207
4
+ data.tar.gz: 22835f026d6ee858d59fd5ecce4fe31b73b4e5c21b709ed204c228a6939618fd
5
5
  SHA512:
6
- metadata.gz: 1335f8bf639400ad932b0672919160b4754c41dbd46dbcd7107145d8147a648b4f29705a3eff322ffb39c21d5c13229e6f622c2885c5a792ad9aba8420566bfe
7
- data.tar.gz: 8cbf78ea9632fdefa9d50270890396c3722bbb94654f003fad5746c4e341456f4c66c4cd6d4f5fd7202c1f59dd951afef96aa201079388d8ae818d457c7de63e
6
+ metadata.gz: aedbed354c3bce410be5b6911ffc7f586611901f5322a61fccce527bec01478d374b9921d56600c4ae3a884225babf74866bf019d265e8b1778fbb75292f004f
7
+ data.tar.gz: 212576c917c388143d8db98526f73b6acf6b8340a8ebd56b16026cf415fa4b8a979bbabf3e6ba29e49a4a30f09ea262c3cc6a18a517cb52891484aa17bcde66d
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 amplifier 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
@@ -38,29 +74,25 @@ module Sonamp
38
74
  dispatch_assert(cmd, expected)
39
75
  end
40
76
 
41
- 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
77
+ def power_off
78
+ 1.upto(4).each do |zone|
79
+ set_zone_power(zone, false)
51
80
  end
52
81
  end
53
82
 
83
+ def get_zone_volume(zone = nil)
84
+ get_zone_value('V', zone)
85
+ end
86
+
54
87
  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
88
  if volume < 0 || volume > 100
59
89
  raise ArgumentError, "Volume must be between 0 and 100: #{volume}"
60
90
  end
61
- cmd = ":V#{zone}#{volume}"
62
- expected = cmd[1...cmd.length]
63
- dispatch_assert(cmd, expected)
91
+ set_zone_value('V', zone, volume)
92
+ end
93
+
94
+ def set_zone_mute(zone, state)
95
+ set_zone_value('M', zone, state ? 1 : 0)
64
96
  end
65
97
 
66
98
  def get_channel_volume(channel = nil)
@@ -79,12 +111,16 @@ module Sonamp
79
111
  dispatch_assert(cmd, expected)
80
112
  end
81
113
 
114
+ def set_channel_mute(channel, state)
115
+ set_channel_value('MC', channel, state ? 1 : 0)
116
+ end
117
+
82
118
  def get_zone_mute(zone = nil)
83
119
  get_zone_state('M', zone)
84
120
  end
85
121
 
86
122
  def get_channel_mute(channel = nil)
87
- get_channel_value('MC', channel)
123
+ get_channel_state('MC', channel)
88
124
  end
89
125
 
90
126
  def get_channel_front_panel_level(channel = nil)
@@ -127,23 +163,23 @@ module Sonamp
127
163
  # Reusing the opened device file makes :VTIG? fail even with a delay
128
164
  # in front.
129
165
  #open_device do
166
+ {
167
+ firmware_version: get_firmware_version,
168
+ temperature: get_temperature,
169
+ zone_power: get_zone_power,
170
+ zone_fault: get_zone_fault,
171
+ zone_volume: get_zone_volume,
172
+ channel_volume: get_channel_volume,
173
+ zone_mute: get_zone_mute,
174
+ channel_mute: get_channel_mute,
175
+ bbe: get_bbe,
176
+ bbe_high_boost: get_bbe_high_boost,
177
+ bbe_low_boost: get_bbe_low_boost,
178
+ auto_trigger_input: get_auto_trigger_input,
179
+ voltage_trigger_input: get_voltage_trigger_input,
180
+ channel_front_panel_level: get_channel_front_panel_level,
181
+ }
130
182
  #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
183
  end
148
184
 
149
185
  private
@@ -152,7 +188,7 @@ module Sonamp
152
188
  if @f
153
189
  yield
154
190
  else
155
- File.open(device, 'r+') do |f|
191
+ File.open(device, 'r+b') do |f|
156
192
  @f = f
157
193
  yield.tap do
158
194
  @f = nil
@@ -163,7 +199,9 @@ module Sonamp
163
199
 
164
200
  def dispatch(cmd, resp_lines_count = 1)
165
201
  open_device do
166
- @f << "#{cmd}\x0d"
202
+ with_timeout do
203
+ @f.syswrite("#{cmd}\x0d")
204
+ end
167
205
  resp = 1.upto(resp_lines_count).map do
168
206
  read_line(@f, cmd)
169
207
  end
@@ -184,7 +222,7 @@ module Sonamp
184
222
 
185
223
  def extract_suffix(resp, expected_prefix)
186
224
  unless resp[0..expected_prefix.length-1] == expected_prefix
187
- raise "Unexpected response: expected #{expected_prefix}..., actual #{resp}"
225
+ raise UnexpectedResponse, "Unexpected response: expected #{expected_prefix}..., actual #{resp}"
188
226
  end
189
227
  resp[expected_prefix.length..]
190
228
  end
@@ -198,44 +236,89 @@ module Sonamp
198
236
  dispatch_extract_suffix(":#{cmd}?", cmd)
199
237
  end
200
238
 
239
+ def with_timeout(&block)
240
+ Timeout.timeout(RS232_TIMEOUT, CommunicationTimeout, &block)
241
+ end
242
+
201
243
  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?"
244
+ with_timeout do
245
+ f.readline.strip.tap do |resp|
246
+ if resp == 'ERR'
247
+ raise InvalidCommand, "Invalid command: #{cmd}"
248
+ elsif resp == 'N/A'
249
+ raise NotApplicable, "Command was recognized but could not be executed - is serial control enabled on the amplifier?"
250
+ end
207
251
  end
208
252
  end
209
253
  end
210
254
 
211
- def get_zone_state(cmd_prefix, zone)
255
+ def set_zone_value(cmd_prefix, zone, value)
256
+ if zone < 1 || zone > 4
257
+ raise ArgumentError, "Zone must be between 1 and 4: #{zone}"
258
+ end
259
+ cmd = ":#{cmd_prefix}#{zone}#{value}"
260
+ expected = cmd[1...cmd.length]
261
+ dispatch_assert(cmd, expected)
262
+ end
263
+
264
+ def get_zone_value(cmd_prefix, zone, boolize: false)
212
265
  if zone
213
266
  if zone < 1 || zone > 4
214
267
  raise ArgumentError, "Zone must be between 1 and 4: #{zone}"
215
268
  end
216
269
  resp = dispatch(":#{cmd_prefix}#{zone}?")
217
- resp[cmd_prefix.length + 1] == '1' ? true : false
270
+ typecast_value(resp[cmd_prefix.length + 1..], boolize)
218
271
  else
219
- dispatch(":#{cmd_prefix}G?", 4).map do |resp|
220
- resp[cmd_prefix.length + 1] == '1' ? true : false
272
+ index = 1
273
+ hashize_query_result(dispatch(":#{cmd_prefix}G?", 4), cmd_prefix, boolize)
274
+ end
275
+ end
276
+
277
+ def hashize_query_result(resp_lines, cmd_prefix, boolize)
278
+ index = 1
279
+ Hash[resp_lines.map do |resp|
280
+ value = typecast_value(extract_suffix(resp, "#{cmd_prefix}#{index}"), boolize)
281
+ [index, value].tap do
282
+ index += 1
221
283
  end
284
+ end]
285
+ end
286
+
287
+ def get_zone_state(cmd_prefix, zone)
288
+ get_zone_value(cmd_prefix, zone, boolize: true)
289
+ end
290
+
291
+ def set_channel_value(cmd_prefix, channel, value)
292
+ if channel < 1 || channel > 8
293
+ raise ArgumentError, "Channel must be between 1 and 8: #{channel}"
222
294
  end
295
+ cmd = ":#{cmd_prefix}#{channel}#{value}"
296
+ expected = cmd[1...cmd.length]
297
+ dispatch_assert(cmd, expected)
223
298
  end
224
299
 
225
- def get_channel_value(cmd_prefix, channel)
300
+ def get_channel_value(cmd_prefix, channel, boolize: false)
226
301
  if channel
227
302
  if channel < 1 || channel > 8
228
303
  raise ArgumentError, "Channel must be between 1 and 8: #{channel}"
229
304
  end
230
- Integer(dispatch_extract_suffix(":#{cmd_prefix}#{channel}?", "#{cmd_prefix}#{channel}"))
305
+ typecast_value(dispatch_extract_suffix(":#{cmd_prefix}#{channel}?", "#{cmd_prefix}#{channel}"), boolize)
231
306
  else
232
307
  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
308
+ hashize_query_result(dispatch(":#{cmd_prefix}G?", 8), cmd_prefix, boolize)
309
+ end
310
+ end
311
+
312
+ def get_channel_state(cmd_prefix, channel)
313
+ get_channel_value(cmd_prefix, channel, boolize: true)
314
+ end
315
+
316
+ def typecast_value(value, boolize)
317
+ value = Integer(value)
318
+ if boolize
319
+ value = value == 1
238
320
  end
321
+ value
239
322
  end
240
323
  end
241
324
  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.8'.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.8'
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.8
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-16 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