seriamp 0.1.6 → 0.1.9
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/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
|