sonamp 0.0.5 → 0.0.7

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: 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