seriamp 0.1.6 → 0.1.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/seriamp/sonamp/app.rb +26 -6
- data/lib/seriamp/sonamp/client.rb +27 -1
- data/lib/seriamp/utils.rb +4 -0
- data/lib/seriamp/version.rb +1 -1
- data/lib/seriamp/yamaha/app.rb +72 -6
- data/lib/seriamp/yamaha/client.rb +153 -87
- data/lib/seriamp/yamaha/cmd.rb +6 -87
- data/lib/seriamp/yamaha/executor.rb +131 -0
- data/lib/seriamp/yamaha/protocol/constants.rb +49 -0
- data/lib/seriamp/yamaha/protocol/methods.rb +64 -0
- data/seriamp.gemspec +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 25ded6fbd5f32c13385dc7704f60d9d88c46bf528f79e7c67d41f295800b596d
|
4
|
+
data.tar.gz: 7c2cab83327cf081c73c28743d46406213bdfaccd18f177d632f3c0922b55466
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5484fe172a3d65fb4d9778fc3734dd0a2aa5b24f459f864b5d8bc68a4766b06e5d6f2fd3ccc234b21b27fdc534abeb76655cdf29c68e1dca7afacf476e847186
|
7
|
+
data.tar.gz: 64cb7cfed4265a174020f59a04671e9865739d4f573b361f560c88051fad4a63355e2401cd0d84cc2aa394babed6cc5efa6ae775573362fb3da6e09dfa716840
|
data/lib/seriamp/sonamp/app.rb
CHANGED
@@ -21,31 +21,51 @@ module Seriamp
|
|
21
21
|
render_json(client.get_zone_power)
|
22
22
|
end
|
23
23
|
|
24
|
+
get '/volume' do
|
25
|
+
payload = {
|
26
|
+
zone_volume: client.get_zone_volume,
|
27
|
+
zone_mute: client.get_zone_mute,
|
28
|
+
channel_volume: client.get_channel_volume,
|
29
|
+
channel_mute: client.get_channel_mute,
|
30
|
+
}
|
31
|
+
render_json(payload)
|
32
|
+
end
|
33
|
+
|
24
34
|
get '/zone/:zone/power' do |zone|
|
25
|
-
render_json(client.get_zone_power(zone
|
35
|
+
render_json(client.get_zone_power(Integer(zone)))
|
26
36
|
end
|
27
37
|
|
28
38
|
put '/zone/:zone/power' do |zone|
|
29
39
|
state = Utils.parse_on_off(request.body.read)
|
30
|
-
client.set_zone_power(zone
|
40
|
+
client.set_zone_power(Integer(zone), state)
|
31
41
|
end
|
32
42
|
|
33
43
|
get '/zone/:zone/volume' do |zone|
|
34
|
-
render_json(client.get_zone_volume(zone
|
44
|
+
render_json(client.get_zone_volume(Integer(zone)))
|
35
45
|
end
|
36
46
|
|
37
47
|
put '/zone/:zone/volume' do |zone|
|
38
48
|
volume = request.body.read.to_i
|
39
|
-
client.set_zone_volume(zone
|
49
|
+
client.set_zone_volume(Integer(zone), volume)
|
50
|
+
end
|
51
|
+
|
52
|
+
put '/zone/:zone/mute' do |zone|
|
53
|
+
state = Utils.parse_on_off(request.body.read)
|
54
|
+
client.set_zone_mute(Integer(zone), state)
|
40
55
|
end
|
41
56
|
|
42
57
|
get '/channel/:channel/volume' do |channel|
|
43
|
-
render_json(client.get_channel_volume(channel
|
58
|
+
render_json(client.get_channel_volume(Integer(channel)))
|
44
59
|
end
|
45
60
|
|
46
61
|
put '/channel/:channel/volume' do |channel|
|
47
62
|
volume = request.body.read.to_i
|
48
|
-
client.set_channel_volume(channel
|
63
|
+
client.set_channel_volume(Integer(channel), volume)
|
64
|
+
end
|
65
|
+
|
66
|
+
put '/channel/:channel/mute' do |channel|
|
67
|
+
state = Utils.parse_on_off(request.body.read)
|
68
|
+
client.set_channel_mute(Integer(channel), state)
|
49
69
|
end
|
50
70
|
|
51
71
|
post '/' do
|
@@ -101,14 +101,30 @@ module Seriamp
|
|
101
101
|
get_zone_state('FP', zone)
|
102
102
|
end
|
103
103
|
|
104
|
+
def set_bbe(zone, state)
|
105
|
+
set_zone_value('BP', zone, state ? 1 : 0)
|
106
|
+
end
|
107
|
+
|
104
108
|
def get_bbe(zone = nil)
|
105
109
|
get_zone_state('BP', zone)
|
106
110
|
end
|
107
111
|
|
112
|
+
def set_bbe_boost(zone, state)
|
113
|
+
set_zone_value('BB', zone, convert_boolean_out(state))
|
114
|
+
end
|
115
|
+
|
116
|
+
def set_bbe_high_boost(zone, state)
|
117
|
+
set_zone_value('BH', zone, convert_boolean_out(state))
|
118
|
+
end
|
119
|
+
|
108
120
|
def get_bbe_high_boost(zone = nil)
|
109
121
|
get_zone_state('BH', zone)
|
110
122
|
end
|
111
123
|
|
124
|
+
def set_bbe_low_boost(zone, state)
|
125
|
+
set_zone_value('BL', zone, convert_boolean_out(state))
|
126
|
+
end
|
127
|
+
|
112
128
|
def get_bbe_low_boost(zone = nil)
|
113
129
|
get_zone_state('BL', zone)
|
114
130
|
end
|
@@ -258,7 +274,6 @@ module Seriamp
|
|
258
274
|
resp = dispatch(":#{cmd_prefix}#{zone}?")
|
259
275
|
typecast_value(resp[cmd_prefix.length + 1..], boolize)
|
260
276
|
else
|
261
|
-
index = 1
|
262
277
|
hashize_query_result(dispatch(":#{cmd_prefix}G?", 4), cmd_prefix, boolize)
|
263
278
|
end
|
264
279
|
end
|
@@ -309,6 +324,17 @@ module Seriamp
|
|
309
324
|
end
|
310
325
|
value
|
311
326
|
end
|
327
|
+
|
328
|
+
def convert_boolean_out(value)
|
329
|
+
case value
|
330
|
+
when true, 1
|
331
|
+
1
|
332
|
+
when false, 0
|
333
|
+
0
|
334
|
+
else
|
335
|
+
raise ArgumentError, "Invalid boolean value: #{value}"
|
336
|
+
end
|
337
|
+
end
|
312
338
|
end
|
313
339
|
end
|
314
340
|
end
|
data/lib/seriamp/utils.rb
CHANGED
data/lib/seriamp/version.rb
CHANGED
data/lib/seriamp/yamaha/app.rb
CHANGED
@@ -4,6 +4,7 @@ require 'sinatra/base'
|
|
4
4
|
require 'seriamp/utils'
|
5
5
|
require 'seriamp/detect'
|
6
6
|
require 'seriamp/yamaha/client'
|
7
|
+
require 'seriamp/yamaha/executor'
|
7
8
|
|
8
9
|
module Seriamp
|
9
10
|
module Yamaha
|
@@ -14,7 +15,16 @@ module Seriamp
|
|
14
15
|
set :client, nil
|
15
16
|
|
16
17
|
get '/' do
|
17
|
-
render_json(client.
|
18
|
+
render_json(client.last_status)
|
19
|
+
end
|
20
|
+
|
21
|
+
post '/' do
|
22
|
+
executor = Executor.new
|
23
|
+
request.body.read.split("\n").each do |line|
|
24
|
+
args = line.strip.split(/\s+/)
|
25
|
+
executor.run_command(args)
|
26
|
+
end
|
27
|
+
standard_response
|
18
28
|
end
|
19
29
|
|
20
30
|
get '/power' do
|
@@ -28,8 +38,15 @@ module Seriamp
|
|
28
38
|
|
29
39
|
put "/#{zone}/power" do
|
30
40
|
state = Utils.parse_on_off(request.body.read)
|
31
|
-
client.
|
32
|
-
|
41
|
+
client.with_device do
|
42
|
+
client.public_send("set_#{zone}_power", state)
|
43
|
+
rs = request.env['HTTP_RETURN_STATUS']
|
44
|
+
if rs && Utils.parse_on_off(rs)
|
45
|
+
render_json(client.status)
|
46
|
+
else
|
47
|
+
empty_response
|
48
|
+
end
|
49
|
+
end
|
33
50
|
end
|
34
51
|
|
35
52
|
get "/#{zone}/volume" do
|
@@ -77,9 +94,49 @@ module Seriamp
|
|
77
94
|
end
|
78
95
|
end
|
79
96
|
|
80
|
-
put "/
|
97
|
+
put "/pure_direct" do
|
81
98
|
state = Utils.parse_on_off(request.body.read)
|
82
|
-
client.
|
99
|
+
client.set_pure_direct(state)
|
100
|
+
empty_response
|
101
|
+
end
|
102
|
+
|
103
|
+
put "/center_speaker_layout" do
|
104
|
+
client.set_center_speaker_layout(request.body.read)
|
105
|
+
empty_response
|
106
|
+
end
|
107
|
+
|
108
|
+
put "/front_speaker_layout" do
|
109
|
+
client.set_front_speaker_layout(request.body.read)
|
110
|
+
empty_response
|
111
|
+
end
|
112
|
+
|
113
|
+
put "/surround_speaker_layout" do
|
114
|
+
client.set_surround_speaker_layout(request.body.read)
|
115
|
+
empty_response
|
116
|
+
end
|
117
|
+
|
118
|
+
put "/surround_back_speaker_layout" do
|
119
|
+
client.set_surround_back_speaker_layout(request.body.read)
|
120
|
+
empty_response
|
121
|
+
end
|
122
|
+
|
123
|
+
put "/presence_speaker_layout" do
|
124
|
+
client.set_presence_speaker_layout(request.body.read)
|
125
|
+
empty_response
|
126
|
+
end
|
127
|
+
|
128
|
+
put "/bass_out" do
|
129
|
+
client.set_bass_out(request.body.read)
|
130
|
+
empty_response
|
131
|
+
end
|
132
|
+
|
133
|
+
put "/subwoofer_phase" do
|
134
|
+
client.set_subwoofer_phase(request.body.read)
|
135
|
+
empty_response
|
136
|
+
end
|
137
|
+
|
138
|
+
put "/subwoofer_crossover" do
|
139
|
+
client.set_subwoofer_crossover(Integer(request.body.read))
|
83
140
|
empty_response
|
84
141
|
end
|
85
142
|
|
@@ -97,13 +154,22 @@ module Seriamp
|
|
97
154
|
end
|
98
155
|
|
99
156
|
def empty_response
|
100
|
-
|
157
|
+
[204, '']
|
101
158
|
end
|
102
159
|
|
103
160
|
def plain_response(data)
|
104
161
|
headers['content-type'] = 'text/plain'
|
105
162
|
data.to_s
|
106
163
|
end
|
164
|
+
|
165
|
+
def standart_response
|
166
|
+
rs = request.env['HTTP_X_RETURN_STATUS']
|
167
|
+
if rs && Utils.parse_on_off(rs)
|
168
|
+
render_json(client.status)
|
169
|
+
else
|
170
|
+
empty_response
|
171
|
+
end
|
172
|
+
end
|
107
173
|
end
|
108
174
|
end
|
109
175
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'timeout'
|
4
|
+
require 'seriamp/utils'
|
4
5
|
require 'seriamp/backend/serial_port'
|
5
6
|
require 'seriamp/yamaha/protocol/methods'
|
6
7
|
|
@@ -13,12 +14,22 @@ module Seriamp
|
|
13
14
|
class Client
|
14
15
|
include Protocol::Methods
|
15
16
|
|
16
|
-
def initialize(device: nil, glob: nil, logger: nil)
|
17
|
+
def initialize(device: nil, glob: nil, logger: nil, retries: true)
|
17
18
|
@logger = logger
|
18
19
|
|
19
20
|
@device = device
|
20
21
|
@detect_device = device.nil?
|
21
22
|
@glob = glob
|
23
|
+
@retries = case retries
|
24
|
+
when nil, false
|
25
|
+
0
|
26
|
+
when true
|
27
|
+
1
|
28
|
+
when Integer
|
29
|
+
retries
|
30
|
+
else
|
31
|
+
raise ArgumentError, "retries must be an integer, true, false or nil: #{retries}"
|
32
|
+
end
|
22
33
|
|
23
34
|
if block_given?
|
24
35
|
begin
|
@@ -32,6 +43,7 @@ module Seriamp
|
|
32
43
|
attr_reader :device
|
33
44
|
attr_reader :glob
|
34
45
|
attr_reader :logger
|
46
|
+
attr_reader :retries
|
35
47
|
|
36
48
|
def detect_device?
|
37
49
|
@detect_device
|
@@ -94,12 +106,47 @@ module Seriamp
|
|
94
106
|
end
|
95
107
|
end
|
96
108
|
|
109
|
+
def with_device(&block)
|
110
|
+
if @io
|
111
|
+
yield @io
|
112
|
+
else
|
113
|
+
open_device(&block)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Shows a message via the on-screen display. The message must be 16
|
118
|
+
# characters or fewer. The message is NOT displayed on the front panel,
|
119
|
+
# it is shown only on the connected TV's OSD.
|
120
|
+
def osd_message(msg)
|
121
|
+
if msg.length < 16
|
122
|
+
msg = msg.dup
|
123
|
+
while msg.length < 16
|
124
|
+
msg += ' '
|
125
|
+
end
|
126
|
+
elsif msg.length > 16
|
127
|
+
raise ArgumentError, "Message must be no more than 16 characters, #{msg.length} given"
|
128
|
+
end
|
129
|
+
|
130
|
+
with_retry do
|
131
|
+
with_device do
|
132
|
+
@io.syswrite("#{STX}21000#{ETX}".encode('ascii'))
|
133
|
+
@io.syswrite("#{STX}3#{msg[0..3]}#{ETX}".encode('ascii'))
|
134
|
+
@io.syswrite("#{STX}3#{msg[4..7]}#{ETX}".encode('ascii'))
|
135
|
+
@io.syswrite("#{STX}3#{msg[8..11]}#{ETX}".encode('ascii'))
|
136
|
+
@io.syswrite("#{STX}3#{msg[12..15]}#{ETX}".encode('ascii'))
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
nil
|
141
|
+
end
|
142
|
+
|
97
143
|
private
|
98
144
|
|
99
145
|
include Protocol::Constants
|
100
146
|
|
101
147
|
def open_device
|
102
148
|
if detect_device? && device.nil?
|
149
|
+
logger&.debug("Detecting device")
|
103
150
|
@device = Seriamp.detect_device(Yamaha, *glob, logger: logger)
|
104
151
|
if @device
|
105
152
|
logger&.info("Using #{device} as TTY device")
|
@@ -108,6 +155,7 @@ module Seriamp
|
|
108
155
|
end
|
109
156
|
end
|
110
157
|
|
158
|
+
logger&.debug("Opening #{device}")
|
111
159
|
@io = Backend::SerialPortBackend::Device.new(device, logger: logger)
|
112
160
|
|
113
161
|
begin
|
@@ -131,14 +179,6 @@ module Seriamp
|
|
131
179
|
end
|
132
180
|
end
|
133
181
|
|
134
|
-
def with_device(&block)
|
135
|
-
if @io
|
136
|
-
yield @io
|
137
|
-
else
|
138
|
-
open_device(&block)
|
139
|
-
end
|
140
|
-
end
|
141
|
-
|
142
182
|
# ASCII table: https://www.asciitable.com/
|
143
183
|
DC1 = ?\x11
|
144
184
|
DC2 = ?\x12
|
@@ -151,9 +191,13 @@ module Seriamp
|
|
151
191
|
ZERO_ORD = '0'.ord
|
152
192
|
|
153
193
|
def dispatch(cmd)
|
194
|
+
start = Utils.monotime
|
154
195
|
with_device do
|
155
196
|
@io.syswrite(cmd.encode('ascii'))
|
156
197
|
read_response
|
198
|
+
end.tap do
|
199
|
+
elapsed = Utils.monotime - start
|
200
|
+
logger&.debug("Yamaha: dispatched #{cmd} in #{'%.2f' % elapsed} s")
|
157
201
|
end
|
158
202
|
end
|
159
203
|
|
@@ -202,95 +246,101 @@ module Seriamp
|
|
202
246
|
}.freeze
|
203
247
|
|
204
248
|
def do_status
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
249
|
+
with_retry do
|
250
|
+
resp = nil
|
251
|
+
loop do
|
252
|
+
resp = dispatch(STATUS_REQ)
|
253
|
+
again = false
|
254
|
+
while @io && IO.select([@io.io], nil, nil, 0)
|
255
|
+
logger&.warn("Serial device readable after completely reading status response - concurrent access?")
|
256
|
+
read_response
|
257
|
+
again = true
|
258
|
+
end
|
259
|
+
break unless again
|
213
260
|
end
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
}
|
238
|
-
if data.length > 9
|
239
|
-
@status.update(
|
240
|
-
input: input = data[9],
|
241
|
-
input_name: MAIN_INPUTS_GET.fetch(input),
|
242
|
-
multi_ch_input: data[10] == '1',
|
243
|
-
audio_select: audio_select = data[11],
|
244
|
-
audio_select_name: AUDIO_SELECT_GET.fetch(audio_select),
|
245
|
-
mute: data[12] == '1',
|
246
|
-
# Volume values (0.5 dB increment):
|
247
|
-
# mute: 0
|
248
|
-
# -80.0 dB (min): 39
|
249
|
-
# 0 dB: 199
|
250
|
-
# +14.5 dB (max): 228
|
251
|
-
# Zone2 volume values (1 dB increment):
|
252
|
-
# mute: 0
|
253
|
-
# -33 dB (min): 39
|
254
|
-
# 0 dB (max): 72
|
255
|
-
main_volume: volume = data[15..16].to_i(16),
|
256
|
-
main_volume_db: int_to_half_db(volume),
|
257
|
-
zone2_volume: zone2_volume = data[17..18].to_i(16),
|
258
|
-
zone2_volume_db: int_to_full_db(zone2_volume),
|
259
|
-
zone3_volume: zone3_volume = data[129..130].to_i(16),
|
260
|
-
zone3_volume_db: int_to_full_db(zone3_volume),
|
261
|
-
program: program = data[19..20],
|
262
|
-
program_name: PROGRAM_GET.fetch(program),
|
263
|
-
# true: straight; false: effect
|
264
|
-
effect: data[21] == '1',
|
265
|
-
#extended_surround: data[22],
|
266
|
-
#short_message: data[23],
|
267
|
-
sleep: SLEEP_GET.fetch(data[24]),
|
268
|
-
night: night = data[27],
|
269
|
-
night_name: NIGHT_GET.fetch(night),
|
270
|
-
pure_direct: data[PURE_DIRECT_FIELD.fetch(@model_code)] == '1',
|
271
|
-
speaker_a: data[29] == '1',
|
272
|
-
speaker_b: data[30] == '1',
|
273
|
-
# 2 positions on RX-Vx700
|
274
|
-
#format: data[31..32],
|
275
|
-
#sampling: data[33..34],
|
276
|
-
)
|
277
|
-
if @model_code == 'R0178'
|
261
|
+
payload = resp[1...-1]
|
262
|
+
@model_code = payload[0..4]
|
263
|
+
@version = payload[5]
|
264
|
+
length = payload[6..7].to_i(16)
|
265
|
+
data = payload[8...-2]
|
266
|
+
if data.length != length
|
267
|
+
raise HandshakeFailure, "Broken status response: expected #{length} bytes, got #{data.length} bytes; concurrent operation on device?"
|
268
|
+
end
|
269
|
+
unless data.start_with?('@E01900')
|
270
|
+
raise HandshakeFailure, "Broken status response: expected to start with @E01900, actual #{data[0..6]}"
|
271
|
+
end
|
272
|
+
@status_string = data
|
273
|
+
@status = {
|
274
|
+
model_code: @model_code,
|
275
|
+
model_name: MODEL_NAMES[@model_code],
|
276
|
+
firmware_version: @version,
|
277
|
+
system_status: data[7].ord - ZERO_ORD,
|
278
|
+
power: power = data[8].ord - ZERO_ORD,
|
279
|
+
main_power: [1, 4, 5, 2].include?(power),
|
280
|
+
zone2_power: [1, 4, 3, 6].include?(power),
|
281
|
+
zone3_power: [1, 5, 3, 7].include?(power),
|
282
|
+
}
|
283
|
+
if data.length > 9
|
278
284
|
@status.update(
|
279
|
-
|
280
|
-
|
281
|
-
|
285
|
+
input: input = data[9],
|
286
|
+
input_name: MAIN_INPUTS_GET.fetch(input),
|
287
|
+
multi_ch_input: data[10] == '1',
|
288
|
+
audio_select: audio_select = data[11],
|
289
|
+
audio_select_name: AUDIO_SELECT_GET.fetch(audio_select),
|
290
|
+
mute: data[12] == '1',
|
291
|
+
# Volume values (0.5 dB increment):
|
292
|
+
# mute: 0
|
293
|
+
# -80.0 dB (min): 39
|
294
|
+
# 0 dB: 199
|
295
|
+
# +14.5 dB (max): 228
|
296
|
+
# Zone2 volume values (1 dB increment):
|
297
|
+
# mute: 0
|
298
|
+
# -33 dB (min): 39
|
299
|
+
# 0 dB (max): 72
|
300
|
+
main_volume: volume = data[15..16].to_i(16),
|
301
|
+
main_volume_db: int_to_half_db(volume),
|
302
|
+
zone2_volume: zone2_volume = data[17..18].to_i(16),
|
303
|
+
zone2_volume_db: int_to_full_db(zone2_volume),
|
304
|
+
zone3_volume: zone3_volume = data[129..130].to_i(16),
|
305
|
+
zone3_volume_db: int_to_full_db(zone3_volume),
|
306
|
+
program: program = data[19..20],
|
307
|
+
program_name: PROGRAM_GET.fetch(program),
|
308
|
+
# true: straight; false: effect
|
309
|
+
effect: data[21] == '1',
|
310
|
+
#extended_surround: data[22],
|
311
|
+
#short_message: data[23],
|
312
|
+
sleep: SLEEP_GET.fetch(data[24]),
|
313
|
+
night: night = data[27],
|
314
|
+
night_name: NIGHT_GET.fetch(night),
|
315
|
+
pure_direct: data[PURE_DIRECT_FIELD.fetch(@model_code)] == '1',
|
316
|
+
speaker_a: data[29] == '1',
|
317
|
+
speaker_b: data[30] == '1',
|
318
|
+
# 2 positions on RX-Vx700
|
319
|
+
#format: data[31..32],
|
320
|
+
#sampling: data[33..34],
|
282
321
|
)
|
322
|
+
if @model_code == 'R0178'
|
323
|
+
@status.update(
|
324
|
+
input_mode: INPUT_MODE_R0178.fetch(data[11]),
|
325
|
+
sampling: data[32],
|
326
|
+
sample_rate: SAMPLE_RATE_R0178.fetch(data[32]),
|
327
|
+
)
|
328
|
+
end
|
283
329
|
end
|
330
|
+
@status
|
284
331
|
end
|
285
|
-
@status
|
286
332
|
end
|
287
333
|
|
288
334
|
def remote_command(cmd)
|
289
|
-
|
335
|
+
with_retry do
|
336
|
+
dispatch("#{STX}0#{cmd}#{ETX}")
|
337
|
+
end
|
290
338
|
end
|
291
339
|
|
292
340
|
def system_command(cmd)
|
293
|
-
|
341
|
+
with_retry do
|
342
|
+
dispatch("#{STX}2#{cmd}#{ETX}")
|
343
|
+
end
|
294
344
|
end
|
295
345
|
|
296
346
|
def extract_text(resp)
|
@@ -313,6 +363,22 @@ module Seriamp
|
|
313
363
|
(value - 39) - 33
|
314
364
|
end
|
315
365
|
end
|
366
|
+
|
367
|
+
def with_retry
|
368
|
+
try = 1
|
369
|
+
begin
|
370
|
+
yield
|
371
|
+
rescue Seriamp::Error => exc
|
372
|
+
if try <= retries
|
373
|
+
logger&.warn("Error during operation: #{exc.class}: #{exc} - will retry")
|
374
|
+
try += 1
|
375
|
+
@device = nil
|
376
|
+
retry
|
377
|
+
else
|
378
|
+
raise
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|
316
382
|
end
|
317
383
|
end
|
318
384
|
end
|
data/lib/seriamp/yamaha/cmd.rb
CHANGED
@@ -6,6 +6,7 @@ require 'pp'
|
|
6
6
|
require 'seriamp/utils'
|
7
7
|
require 'seriamp/detect'
|
8
8
|
require 'seriamp/yamaha/client'
|
9
|
+
require 'seriamp/yamaha/executor'
|
9
10
|
|
10
11
|
module Seriamp
|
11
12
|
module Yamaha
|
@@ -61,100 +62,18 @@ module Seriamp
|
|
61
62
|
STDERR.puts("Yamaha receiver not found")
|
62
63
|
exit 3
|
63
64
|
end
|
64
|
-
when 'power'
|
65
|
-
which = args.shift&.downcase
|
66
|
-
if %w(main zone2 zone3).include?(which)
|
67
|
-
method = "set_#{which}_power"
|
68
|
-
state = Utils.parse_on_off(args.shift)
|
69
|
-
else
|
70
|
-
method = 'set_power'
|
71
|
-
state = Utils.parse_on_off(which)
|
72
|
-
end
|
73
|
-
client.public_send(method, state)
|
74
|
-
when 'volume'
|
75
|
-
which = args.shift
|
76
|
-
if %w(main zone2 zone3).include?(which)
|
77
|
-
value = args.shift
|
78
|
-
else
|
79
|
-
value = which
|
80
|
-
which = 'main'
|
81
|
-
end
|
82
|
-
prefix = "set_#{which}"
|
83
|
-
if value.nil?
|
84
|
-
puts client.send("last_#{which}_volume_db")
|
85
|
-
return
|
86
|
-
end
|
87
|
-
value = value.downcase
|
88
|
-
if value == 'up'
|
89
|
-
# Just like with remote, the first volume up or down command
|
90
|
-
# doesn't do anything.
|
91
|
-
client.public_send("#{which}_volume_up")
|
92
|
-
client.public_send("#{which}_volume_up")
|
93
|
-
elsif value == 'down'
|
94
|
-
client.public_send("#{which}_volume_down")
|
95
|
-
client.public_send("#{which}_volume_down")
|
96
|
-
else
|
97
|
-
if %w(. - mute).include?(value)
|
98
|
-
method = "#{prefix}_mute"
|
99
|
-
value = true
|
100
|
-
elsif value == 'unmute'
|
101
|
-
method = "#{prefix}_mute"
|
102
|
-
value = false
|
103
|
-
else
|
104
|
-
method = "#{prefix}_volume_db"
|
105
|
-
if value[0] == ','
|
106
|
-
value = value[1..]
|
107
|
-
end
|
108
|
-
value = Float(value)
|
109
|
-
end
|
110
|
-
client.public_send(method, value)
|
111
|
-
end
|
112
|
-
when 'input'
|
113
|
-
which = args.shift&.downcase
|
114
|
-
if %w(main zone2 zone3).include?(which)
|
115
|
-
input = args.shift
|
116
|
-
else
|
117
|
-
input = which
|
118
|
-
which = 'main'
|
119
|
-
end
|
120
|
-
if input.nil?
|
121
|
-
puts client.public_send("last_#{which}_input_name")
|
122
|
-
return
|
123
|
-
end
|
124
|
-
client.public_send("set_#{which}_input", input)
|
125
|
-
when 'program'
|
126
|
-
value = args.shift.downcase
|
127
|
-
client.set_program(value)
|
128
|
-
when 'pure-direct'
|
129
|
-
state = Utils.parse_on_off(args.shift)
|
130
|
-
client.set_pure_direct(state)
|
131
|
-
when 'status'
|
132
|
-
pp client.last_status
|
133
|
-
when 'dev-status'
|
134
|
-
status = client.last_status_string
|
135
|
-
0.upto(status.length-1).each do |i|
|
136
|
-
puts "%3d %s" % [i, status[i]]
|
137
|
-
end
|
138
|
-
when 'test'
|
139
|
-
client.set_power(false)
|
140
|
-
[true, false].each do |main_state|
|
141
|
-
[true, false].each do |zone2_state|
|
142
|
-
[true, false].each do |zone3_state|
|
143
|
-
client.set_main_power(main_state)
|
144
|
-
client.set_zone2_power(zone2_state)
|
145
|
-
client.set_zone3_power(zone3_state)
|
146
|
-
puts "#{main_state ?1:0} #{zone2_state ?1:0} #{zone3_state ?1:0} #{client.status[:power]}"
|
147
|
-
end
|
148
|
-
end
|
149
|
-
end
|
150
65
|
else
|
151
|
-
|
66
|
+
executor.run_command(cmd, *args)
|
152
67
|
end
|
153
68
|
end
|
154
69
|
|
155
70
|
private
|
156
71
|
|
157
72
|
attr_reader :client
|
73
|
+
|
74
|
+
def executor
|
75
|
+
@executor ||= Executor.new(client)
|
76
|
+
end
|
158
77
|
end
|
159
78
|
end
|
160
79
|
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Seriamp
|
4
|
+
module Yamaha
|
5
|
+
class Executor
|
6
|
+
def initialize(client)
|
7
|
+
@client = client
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :client
|
11
|
+
|
12
|
+
def run_command(cmd, *args)
|
13
|
+
case cmd
|
14
|
+
when 'detect'
|
15
|
+
device = Seriamp.detect_device(Yamaha, *args, logger: logger)
|
16
|
+
if device
|
17
|
+
puts device
|
18
|
+
exit 0
|
19
|
+
else
|
20
|
+
STDERR.puts("Yamaha receiver not found")
|
21
|
+
exit 3
|
22
|
+
end
|
23
|
+
when 'power'
|
24
|
+
which = args.shift&.downcase
|
25
|
+
if %w(main zone2 zone3).include?(which)
|
26
|
+
method = "set_#{which}_power"
|
27
|
+
state = Utils.parse_on_off(args.shift)
|
28
|
+
else
|
29
|
+
method = 'set_power'
|
30
|
+
state = Utils.parse_on_off(which)
|
31
|
+
end
|
32
|
+
client.public_send(method, state)
|
33
|
+
when 'volume'
|
34
|
+
which = args.shift
|
35
|
+
if %w(main zone2 zone3).include?(which)
|
36
|
+
value = args.shift
|
37
|
+
else
|
38
|
+
value = which
|
39
|
+
which = 'main'
|
40
|
+
end
|
41
|
+
prefix = "set_#{which}"
|
42
|
+
if value.nil?
|
43
|
+
puts client.send("last_#{which}_volume_db")
|
44
|
+
return
|
45
|
+
end
|
46
|
+
value = value.downcase
|
47
|
+
if value == 'up'
|
48
|
+
# Just like with remote, the first volume up or down command
|
49
|
+
# doesn't do anything.
|
50
|
+
client.public_send("#{which}_volume_up")
|
51
|
+
client.public_send("#{which}_volume_up")
|
52
|
+
elsif value == 'down'
|
53
|
+
client.public_send("#{which}_volume_down")
|
54
|
+
client.public_send("#{which}_volume_down")
|
55
|
+
else
|
56
|
+
if %w(. - mute).include?(value)
|
57
|
+
method = "#{prefix}_mute"
|
58
|
+
value = true
|
59
|
+
elsif value == 'unmute'
|
60
|
+
method = "#{prefix}_mute"
|
61
|
+
value = false
|
62
|
+
else
|
63
|
+
method = "#{prefix}_volume_db"
|
64
|
+
if value[0] == ','
|
65
|
+
value = value[1..]
|
66
|
+
end
|
67
|
+
value = Float(value)
|
68
|
+
end
|
69
|
+
client.public_send(method, value)
|
70
|
+
end
|
71
|
+
when 'input'
|
72
|
+
which = args.shift&.downcase
|
73
|
+
if %w(main zone2 zone3).include?(which)
|
74
|
+
input = args.shift
|
75
|
+
else
|
76
|
+
input = which
|
77
|
+
which = 'main'
|
78
|
+
end
|
79
|
+
if input.nil?
|
80
|
+
puts client.public_send("last_#{which}_input_name")
|
81
|
+
return
|
82
|
+
end
|
83
|
+
client.public_send("set_#{which}_input", input)
|
84
|
+
when 'program'
|
85
|
+
value = args.shift.downcase
|
86
|
+
client.set_program(value)
|
87
|
+
when 'pure-direct'
|
88
|
+
state = Utils.parse_on_off(args.shift)
|
89
|
+
client.set_pure_direct(state)
|
90
|
+
when 'center-speaker-layout'
|
91
|
+
client.set_center_speaker_layout(args.shift)
|
92
|
+
when 'surround-speaker-layout'
|
93
|
+
client.set_surround_speaker_layout(args.shift)
|
94
|
+
when 'surround-back-speaker-layout'
|
95
|
+
client.set_surround_back_speaker_layout(args.shift)
|
96
|
+
when 'front-speaker-layout'
|
97
|
+
client.set_front_speaker_layout(args.shift)
|
98
|
+
when 'presence-speaker-layout'
|
99
|
+
client.set_presence_speaker_layout(args.shift)
|
100
|
+
when 'bass-out'
|
101
|
+
client.set_bass_out(args.shift)
|
102
|
+
when 'subwoofer-phase'
|
103
|
+
client.set_subwoofer_phase(args.shift)
|
104
|
+
when 'subwoofer-crossover'
|
105
|
+
client.set_subwoofer_crossover(Integer(args.shift))
|
106
|
+
when 'status'
|
107
|
+
pp client.last_status
|
108
|
+
when 'dev-status'
|
109
|
+
status = client.last_status_string
|
110
|
+
0.upto(status.length-1).each do |i|
|
111
|
+
puts "%3d %s" % [i, status[i]]
|
112
|
+
end
|
113
|
+
when 'test'
|
114
|
+
client.set_power(false)
|
115
|
+
[true, false].each do |main_state|
|
116
|
+
[true, false].each do |zone2_state|
|
117
|
+
[true, false].each do |zone3_state|
|
118
|
+
client.set_main_power(main_state)
|
119
|
+
client.set_zone2_power(zone2_state)
|
120
|
+
client.set_zone3_power(zone3_state)
|
121
|
+
puts "#{main_state ?1:0} #{zone2_state ?1:0} #{zone3_state ?1:0} #{client.status[:power]}"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
else
|
126
|
+
raise ArgumentError, "Unknown command: #{cmd}"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -177,6 +177,55 @@ module Seriamp
|
|
177
177
|
'43' => 'Enhancer 7ch High',
|
178
178
|
'80' => 'Straight',
|
179
179
|
}.freeze
|
180
|
+
|
181
|
+
CENTER_SPEAKER_LAYOUTS = {
|
182
|
+
'large' => '00',
|
183
|
+
'small' => '01',
|
184
|
+
'none' => '02',
|
185
|
+
}.freeze
|
186
|
+
|
187
|
+
FRONT_SPEAKER_LAYOUTS = {
|
188
|
+
'large' => '00',
|
189
|
+
'small' => '01',
|
190
|
+
}.freeze
|
191
|
+
|
192
|
+
SURROUND_SPEAKER_LAYOUTS = CENTER_SPEAKER_LAYOUTS
|
193
|
+
|
194
|
+
SURROUND_BACK_SPEAKER_LAYOUTS = {
|
195
|
+
'large_x2' => '00',
|
196
|
+
'large_x1' => '01',
|
197
|
+
'small_x2' => '02',
|
198
|
+
'small_x1' => '03',
|
199
|
+
'none' => '04',
|
200
|
+
}.freeze
|
201
|
+
|
202
|
+
PRESENCE_SPEAKER_LAYOUTS = {
|
203
|
+
'yes' => '00',
|
204
|
+
'none' => '01',
|
205
|
+
}.freeze
|
206
|
+
|
207
|
+
BASS_OUTS = {
|
208
|
+
'subwoofer' => '00',
|
209
|
+
'front' => '01',
|
210
|
+
'both' => '02',
|
211
|
+
}.freeze
|
212
|
+
|
213
|
+
SUBWOOFER_PHASES = {
|
214
|
+
'normal' => '00',
|
215
|
+
'reverse' => '10',
|
216
|
+
}.freeze
|
217
|
+
|
218
|
+
SUBWOOFER_CROSSOVERS = {
|
219
|
+
40 => '00',
|
220
|
+
60 => '01',
|
221
|
+
80 => '02',
|
222
|
+
90 => '03',
|
223
|
+
100 => '04',
|
224
|
+
110 => '05',
|
225
|
+
120 => '06',
|
226
|
+
160 => '07',
|
227
|
+
200 => '08',
|
228
|
+
}.freeze
|
180
229
|
end
|
181
230
|
end
|
182
231
|
end
|
@@ -139,6 +139,70 @@ module Seriamp
|
|
139
139
|
source_code = ZONE3_INPUTS_SET.fetch(source.downcase.gsub(/[^a-z]/, '_'))
|
140
140
|
remote_command("7A#{source_code}")
|
141
141
|
end
|
142
|
+
|
143
|
+
def set_center_speaker_layout(layout)
|
144
|
+
value = CENTER_SPEAKER_LAYOUTS[layout.to_s]
|
145
|
+
unless value
|
146
|
+
raise ArgumentError, "Invalid center speaker layout: #{layout}; valid layouts: #{CENTER_SPEAKER_LAYOUTS.keys.join(', ')}"
|
147
|
+
end
|
148
|
+
system_command("70#{value}")
|
149
|
+
end
|
150
|
+
|
151
|
+
def set_front_speaker_layout(layout)
|
152
|
+
value = FRONT_SPEAKER_LAYOUTS[layout.to_s]
|
153
|
+
unless value
|
154
|
+
raise ArgumentError, "Invalid front speaker layout: #{layout}; valid layouts: #{FRONT_SPEAKER_LAYOUTS.keys.join(', ')}"
|
155
|
+
end
|
156
|
+
system_command("71#{value}")
|
157
|
+
end
|
158
|
+
|
159
|
+
def set_surround_speaker_layout(layout)
|
160
|
+
value = SURROUND_SPEAKER_LAYOUTS[layout.to_s]
|
161
|
+
unless value
|
162
|
+
raise ArgumentError, "Invalid surround speaker layout: #{layout}; valid layouts: #{SURROUND_SPEAKER_LAYOUTS.keys.join(', ')}"
|
163
|
+
end
|
164
|
+
system_command("72#{value}")
|
165
|
+
end
|
166
|
+
|
167
|
+
def set_surround_back_speaker_layout(layout)
|
168
|
+
value = SURROUND_BACK_SPEAKER_LAYOUTS[layout.to_s]
|
169
|
+
unless value
|
170
|
+
raise ArgumentError, "Invalid surround back speaker layout: #{layout}; valid layouts: #{SURROUND_BACK_SPEAKER_LAYOUTS.keys.join(', ')}"
|
171
|
+
end
|
172
|
+
system_command("73#{value}")
|
173
|
+
end
|
174
|
+
|
175
|
+
def set_presence_speaker_layout(layout)
|
176
|
+
value = PRESENCE_SPEAKER_LAYOUTS[layout.to_s]
|
177
|
+
unless value
|
178
|
+
raise ArgumentError, "Invalid presence speaker layout: #{layout}; valid layouts: #{PRESENCE_SPEAKER_LAYOUTS.keys.join(', ')}"
|
179
|
+
end
|
180
|
+
system_command("74#{value}")
|
181
|
+
end
|
182
|
+
|
183
|
+
def set_bass_out(v)
|
184
|
+
value = BASS_OUTS[v.to_s]
|
185
|
+
unless value
|
186
|
+
raise ArgumentError, "Invalid bass out value: #{v}; valid values: #{BASS_OUTS.keys.join(', ')}"
|
187
|
+
end
|
188
|
+
system_command("75#{value}")
|
189
|
+
end
|
190
|
+
|
191
|
+
def set_subwoofer_phase(v)
|
192
|
+
value = SUBWOOFER_PHASES[v.to_s]
|
193
|
+
unless value
|
194
|
+
raise ArgumentError, "Invalid subwoofer phase value: #{v}; valid values: #{SUBWOOFER_PHASES.keys.join(', ')}"
|
195
|
+
end
|
196
|
+
system_command("76#{value}")
|
197
|
+
end
|
198
|
+
|
199
|
+
def set_subwoofer_crossover(v)
|
200
|
+
value = SUBWOOFER_CROSSOVERS[v]
|
201
|
+
unless value
|
202
|
+
raise ArgumentError, "Invalid subwoofer crossover frequency: #{v}; valid freuencies: #{SUBWOOFER_CROSSOVERS.keys.join(', ')}"
|
203
|
+
end
|
204
|
+
system_command("7E#{value}")
|
205
|
+
end
|
142
206
|
end
|
143
207
|
end
|
144
208
|
end
|
data/seriamp.gemspec
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: seriamp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Oleg Pudeyev
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-01-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: serialport
|
@@ -58,6 +58,7 @@ files:
|
|
58
58
|
- lib/seriamp/yamaha/app.rb
|
59
59
|
- lib/seriamp/yamaha/client.rb
|
60
60
|
- lib/seriamp/yamaha/cmd.rb
|
61
|
+
- lib/seriamp/yamaha/executor.rb
|
61
62
|
- lib/seriamp/yamaha/protocol/constants.rb
|
62
63
|
- lib/seriamp/yamaha/protocol/methods.rb
|
63
64
|
- seriamp.gemspec
|