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 +4 -4
- data/.gitignore +1 -0
- data/lib/sonamp/client.rb +130 -53
- data/lib/sonamp/cmd.rb +18 -0
- data/lib/sonamp/utils.rb +1 -1
- data/lib/sonamp/version.rb +1 -1
- data/sonamp.gemspec +3 -3
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: caf6fd82a4c7c254e7c132a7b6865094f6c311bd634a92db6a842ab1c3417624
|
|
4
|
+
data.tar.gz: 31e699f752afaef597a2f328f1a9d3eaceae3f0de2e898a75b8ca4af00ac7737
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
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]
|
|
264
|
+
typecast_value(resp[cmd_prefix.length + 1..], boolize)
|
|
218
265
|
else
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
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)
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
data/lib/sonamp/version.rb
CHANGED
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
|
+
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
|
|
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
|
+
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-
|
|
11
|
+
date: 2022-11-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
|
-
description: Library for controlling Sonance Sonamp 875D & 875D MkII amplifiers
|
|
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
|