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 +4 -4
- data/.gitignore +1 -0
- data/lib/sonamp/client.rb +136 -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: 6a0be380a5a03184319da1aa8806ebe909a214107990470529bc550aaab6b207
|
4
|
+
data.tar.gz: 22835f026d6ee858d59fd5ecce4fe31b73b4e5c21b709ed204c228a6939618fd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 =
|
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
|
42
|
-
|
43
|
-
|
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
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
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
|
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]
|
270
|
+
typecast_value(resp[cmd_prefix.length + 1..], boolize)
|
218
271
|
else
|
219
|
-
|
220
|
-
|
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
|
-
|
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)
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
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
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.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
|
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.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-
|
11
|
+
date: 2022-11-16 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
|