seriamp 0.1.6 → 0.1.10

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7afd42caaa8f0dd5815e908b40bfccd8ca042769fea3af431ae334b3d44aa140
4
- data.tar.gz: af4041b6faf2f07ce204a15b99d4cc51b6f4bdfacc5c016006588d096513adef
3
+ metadata.gz: 50636555b1276e60e52ddf26c28f42fd1e7b1d27a89fac22f37fefce236c8f88
4
+ data.tar.gz: aac82da48b0ac8887a507671af5d3d4e16fd9857bb70a635c145be12939fda70
5
5
  SHA512:
6
- metadata.gz: c09d03fe9b24e521eadffef9dc94305f61b90005e9719c349d9dba9ab7cb327a632352a62a3d2831f215374343147e3195fd958e01c4d750cce0a7ee4b7a8fda
7
- data.tar.gz: a5a4f1b992fc6459ed7fa605daecc0715acb5e5938246b9915eb6af32415162b18650a6c45066602c22c27cf15f4879d91f63ab4b75eb05df82c02c0ee12760d
6
+ metadata.gz: 0c86e471aa614ce5048b168874f6c7ecacf956cf078fb0f327df230a588138e7c65f3fdc272ceaaa0f95775bdf2e9d104bbf47c7fcfa470565a83bb03d12e607
7
+ data.tar.gz: dd5f494621dcd63117ce276ac97d4240dc38ec84b7dcb0775851e8b7602e45d8e2789118cbbe10b7efbc74d2a4a7429e5d2be4ee6d469fcbed9185b41da2d6a9
@@ -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.to_i))
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.to_i, state)
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.to_i))
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.to_i, volume)
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.to_i))
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.to_i, volume)
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
@@ -13,5 +13,9 @@ module Seriamp
13
13
  raise ArgumentError, "Invalid on/off value: #{value}"
14
14
  end
15
15
  end
16
+
17
+ module_function def monotime
18
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
19
+ end
16
20
  end
17
21
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Seriamp
4
- VERSION = '0.1.6'
4
+ VERSION = '0.1.10'
5
5
  end
@@ -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,17 @@ module Seriamp
14
15
  set :client, nil
15
16
 
16
17
  get '/' do
17
- render_json(client.status)
18
+ clear_cache
19
+ render_json(client.last_status)
20
+ end
21
+
22
+ post '/' do
23
+ executor = Executor.new
24
+ request.body.read.split("\n").each do |line|
25
+ args = line.strip.split(/\s+/)
26
+ executor.run_command(args)
27
+ end
28
+ standard_response
18
29
  end
19
30
 
20
31
  get '/power' do
@@ -28,8 +39,15 @@ module Seriamp
28
39
 
29
40
  put "/#{zone}/power" do
30
41
  state = Utils.parse_on_off(request.body.read)
31
- client.public_send("set_#{zone}_power", state)
32
- empty_response
42
+ client.with_device do
43
+ client.public_send("set_#{zone}_power", state)
44
+ rs = request.env['HTTP_RETURN_STATUS']
45
+ if rs && Utils.parse_on_off(rs)
46
+ render_json(client.status)
47
+ else
48
+ empty_response
49
+ end
50
+ end
33
51
  end
34
52
 
35
53
  get "/#{zone}/volume" do
@@ -77,14 +95,62 @@ module Seriamp
77
95
  end
78
96
  end
79
97
 
80
- put "/pure-direct" do
98
+ put "/pure_direct" do
81
99
  state = Utils.parse_on_off(request.body.read)
82
- client.public_send("set_pure_direct", state)
100
+ client.set_pure_direct(state)
101
+ empty_response
102
+ end
103
+
104
+ put "/center_speaker_layout" do
105
+ client.set_center_speaker_layout(request.body.read)
106
+ empty_response
107
+ end
108
+
109
+ put "/front_speaker_layout" do
110
+ client.set_front_speaker_layout(request.body.read)
111
+ empty_response
112
+ end
113
+
114
+ put "/surround_speaker_layout" do
115
+ client.set_surround_speaker_layout(request.body.read)
116
+ empty_response
117
+ end
118
+
119
+ put "/surround_back_speaker_layout" do
120
+ client.set_surround_back_speaker_layout(request.body.read)
121
+ empty_response
122
+ end
123
+
124
+ put "/presence_speaker_layout" do
125
+ client.set_presence_speaker_layout(request.body.read)
126
+ empty_response
127
+ end
128
+
129
+ put "/bass_out" do
130
+ client.set_bass_out(request.body.read)
131
+ empty_response
132
+ end
133
+
134
+ put "/subwoofer_phase" do
135
+ client.set_subwoofer_phase(request.body.read)
136
+ empty_response
137
+ end
138
+
139
+ put "/subwoofer_crossover" do
140
+ client.set_subwoofer_crossover(Integer(request.body.read))
83
141
  empty_response
84
142
  end
85
143
 
86
144
  private
87
145
 
146
+ def clear_cache
147
+ if settings.client
148
+ settings.client.clear_cache
149
+ else
150
+ @client&.clear_cache
151
+ end
152
+ end
153
+
88
154
  def client
89
155
  settings.client || begin
90
156
  @client ||= Yamaha::Client.new(settings.device, logger: settings.logger)
@@ -97,13 +163,22 @@ module Seriamp
97
163
  end
98
164
 
99
165
  def empty_response
100
- render_json({})
166
+ [204, '']
101
167
  end
102
168
 
103
169
  def plain_response(data)
104
170
  headers['content-type'] = 'text/plain'
105
171
  data.to_s
106
172
  end
173
+
174
+ def standart_response
175
+ rs = request.env['HTTP_X_RETURN_STATUS']
176
+ if rs && Utils.parse_on_off(rs)
177
+ render_json(client.status)
178
+ else
179
+ empty_response
180
+ end
181
+ end
107
182
  end
108
183
  end
109
184
  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
@@ -45,6 +57,9 @@ module Seriamp
45
57
  def last_status
46
58
  unless @status
47
59
  with_device do
60
+ unless @status
61
+ do_status
62
+ end
48
63
  end
49
64
  end
50
65
  @status.dup
@@ -63,6 +78,10 @@ module Seriamp
63
78
  last_status
64
79
  end
65
80
 
81
+ def clear_cache
82
+ @status = nil
83
+ end
84
+
66
85
  %i(
67
86
  model_code firmware_version system_status power main_power zone2_power
68
87
  zone3_power input input_name audio_select audio_select_name
@@ -94,12 +113,47 @@ module Seriamp
94
113
  end
95
114
  end
96
115
 
116
+ def with_device(&block)
117
+ if @io
118
+ yield @io
119
+ else
120
+ open_device(&block)
121
+ end
122
+ end
123
+
124
+ # Shows a message via the on-screen display. The message must be 16
125
+ # characters or fewer. The message is NOT displayed on the front panel,
126
+ # it is shown only on the connected TV's OSD.
127
+ def osd_message(msg)
128
+ if msg.length < 16
129
+ msg = msg.dup
130
+ while msg.length < 16
131
+ msg += ' '
132
+ end
133
+ elsif msg.length > 16
134
+ raise ArgumentError, "Message must be no more than 16 characters, #{msg.length} given"
135
+ end
136
+
137
+ with_retry do
138
+ with_device do
139
+ @io.syswrite("#{STX}21000#{ETX}".encode('ascii'))
140
+ @io.syswrite("#{STX}3#{msg[0..3]}#{ETX}".encode('ascii'))
141
+ @io.syswrite("#{STX}3#{msg[4..7]}#{ETX}".encode('ascii'))
142
+ @io.syswrite("#{STX}3#{msg[8..11]}#{ETX}".encode('ascii'))
143
+ @io.syswrite("#{STX}3#{msg[12..15]}#{ETX}".encode('ascii'))
144
+ end
145
+ end
146
+
147
+ nil
148
+ end
149
+
97
150
  private
98
151
 
99
152
  include Protocol::Constants
100
153
 
101
154
  def open_device
102
155
  if detect_device? && device.nil?
156
+ logger&.debug("Detecting device")
103
157
  @device = Seriamp.detect_device(Yamaha, *glob, logger: logger)
104
158
  if @device
105
159
  logger&.info("Using #{device} as TTY device")
@@ -108,6 +162,7 @@ module Seriamp
108
162
  end
109
163
  end
110
164
 
165
+ logger&.debug("Opening #{device}")
111
166
  @io = Backend::SerialPortBackend::Device.new(device, logger: logger)
112
167
 
113
168
  begin
@@ -131,14 +186,6 @@ module Seriamp
131
186
  end
132
187
  end
133
188
 
134
- def with_device(&block)
135
- if @io
136
- yield @io
137
- else
138
- open_device(&block)
139
- end
140
- end
141
-
142
189
  # ASCII table: https://www.asciitable.com/
143
190
  DC1 = ?\x11
144
191
  DC2 = ?\x12
@@ -151,9 +198,13 @@ module Seriamp
151
198
  ZERO_ORD = '0'.ord
152
199
 
153
200
  def dispatch(cmd)
201
+ start = Utils.monotime
154
202
  with_device do
155
203
  @io.syswrite(cmd.encode('ascii'))
156
204
  read_response
205
+ end.tap do
206
+ elapsed = Utils.monotime - start
207
+ logger&.debug("Yamaha: dispatched #{cmd} in #{'%.2f' % elapsed} s")
157
208
  end
158
209
  end
159
210
 
@@ -202,95 +253,101 @@ module Seriamp
202
253
  }.freeze
203
254
 
204
255
  def do_status
205
- resp = nil
206
- loop do
207
- resp = dispatch(STATUS_REQ)
208
- again = false
209
- while @io && IO.select([@io.io], nil, nil, 0)
210
- logger&.warn("Serial device readable after completely reading status response - concurrent access?")
211
- read_response
212
- again = true
256
+ with_retry do
257
+ resp = nil
258
+ loop do
259
+ resp = dispatch(STATUS_REQ)
260
+ again = false
261
+ while @io && IO.select([@io.io], nil, nil, 0)
262
+ logger&.warn("Serial device readable after completely reading status response - concurrent access?")
263
+ read_response
264
+ again = true
265
+ end
266
+ break unless again
213
267
  end
214
- break unless again
215
- end
216
- payload = resp[1...-1]
217
- @model_code = payload[0..4]
218
- @version = payload[5]
219
- length = payload[6..7].to_i(16)
220
- data = payload[8...-2]
221
- if data.length != length
222
- raise HandshakeFailure, "Broken status response: expected #{length} bytes, got #{data.length} bytes; concurrent operation on device?"
223
- end
224
- unless data.start_with?('@E01900')
225
- raise HandshakeFailure, "Broken status response: expected to start with @E01900, actual #{data[0..6]}"
226
- end
227
- @status_string = data
228
- @status = {
229
- model_code: @model_code,
230
- model_name: MODEL_NAMES[@model_code],
231
- firmware_version: @version,
232
- system_status: data[7].ord - ZERO_ORD,
233
- power: power = data[8].ord - ZERO_ORD,
234
- main_power: [1, 4, 5, 2].include?(power),
235
- zone2_power: [1, 4, 3, 6].include?(power),
236
- zone3_power: [1, 5, 3, 7].include?(power),
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'
268
+ payload = resp[1...-1]
269
+ @model_code = payload[0..4]
270
+ @version = payload[5]
271
+ length = payload[6..7].to_i(16)
272
+ data = payload[8...-2]
273
+ if data.length != length
274
+ raise HandshakeFailure, "Broken status response: expected #{length} bytes, got #{data.length} bytes; concurrent operation on device?"
275
+ end
276
+ unless data.start_with?('@E01900')
277
+ raise HandshakeFailure, "Broken status response: expected to start with @E01900, actual #{data[0..6]}"
278
+ end
279
+ @status_string = data
280
+ @status = {
281
+ model_code: @model_code,
282
+ model_name: MODEL_NAMES[@model_code],
283
+ firmware_version: @version,
284
+ system_status: data[7].ord - ZERO_ORD,
285
+ power: power = data[8].ord - ZERO_ORD,
286
+ main_power: [1, 4, 5, 2].include?(power),
287
+ zone2_power: [1, 4, 3, 6].include?(power),
288
+ zone3_power: [1, 5, 3, 7].include?(power),
289
+ }
290
+ if data.length > 9
278
291
  @status.update(
279
- input_mode: INPUT_MODE_R0178.fetch(data[11]),
280
- sampling: data[32],
281
- sample_rate: SAMPLE_RATE_R0178.fetch(data[32]),
292
+ input: input = data[9],
293
+ input_name: MAIN_INPUTS_GET.fetch(input),
294
+ multi_ch_input: data[10] == '1',
295
+ audio_select: audio_select = data[11],
296
+ audio_select_name: AUDIO_SELECT_GET.fetch(audio_select),
297
+ mute: data[12] == '1',
298
+ # Volume values (0.5 dB increment):
299
+ # mute: 0
300
+ # -80.0 dB (min): 39
301
+ # 0 dB: 199
302
+ # +14.5 dB (max): 228
303
+ # Zone2 volume values (1 dB increment):
304
+ # mute: 0
305
+ # -33 dB (min): 39
306
+ # 0 dB (max): 72
307
+ main_volume: volume = data[15..16].to_i(16),
308
+ main_volume_db: int_to_half_db(volume),
309
+ zone2_volume: zone2_volume = data[17..18].to_i(16),
310
+ zone2_volume_db: int_to_full_db(zone2_volume),
311
+ zone3_volume: zone3_volume = data[129..130].to_i(16),
312
+ zone3_volume_db: int_to_full_db(zone3_volume),
313
+ program: program = data[19..20],
314
+ program_name: PROGRAM_GET.fetch(program),
315
+ # true: straight; false: effect
316
+ effect: data[21] == '1',
317
+ #extended_surround: data[22],
318
+ #short_message: data[23],
319
+ sleep: SLEEP_GET.fetch(data[24]),
320
+ night: night = data[27],
321
+ night_name: NIGHT_GET.fetch(night),
322
+ pure_direct: data[PURE_DIRECT_FIELD.fetch(@model_code)] == '1',
323
+ speaker_a: data[29] == '1',
324
+ speaker_b: data[30] == '1',
325
+ # 2 positions on RX-Vx700
326
+ #format: data[31..32],
327
+ #sampling: data[33..34],
282
328
  )
329
+ if @model_code == 'R0178'
330
+ @status.update(
331
+ input_mode: INPUT_MODE_R0178.fetch(data[11]),
332
+ sampling: data[32],
333
+ sample_rate: SAMPLE_RATE_R0178.fetch(data[32]),
334
+ )
335
+ end
283
336
  end
337
+ @status
284
338
  end
285
- @status
286
339
  end
287
340
 
288
341
  def remote_command(cmd)
289
- dispatch("#{STX}0#{cmd}#{ETX}")
342
+ with_retry do
343
+ dispatch("#{STX}0#{cmd}#{ETX}")
344
+ end
290
345
  end
291
346
 
292
347
  def system_command(cmd)
293
- dispatch("#{STX}2#{cmd}#{ETX}")
348
+ with_retry do
349
+ dispatch("#{STX}2#{cmd}#{ETX}")
350
+ end
294
351
  end
295
352
 
296
353
  def extract_text(resp)
@@ -313,6 +370,22 @@ module Seriamp
313
370
  (value - 39) - 33
314
371
  end
315
372
  end
373
+
374
+ def with_retry
375
+ try = 1
376
+ begin
377
+ yield
378
+ rescue Seriamp::Error => exc
379
+ if try <= retries
380
+ logger&.warn("Error during operation: #{exc.class}: #{exc} - will retry")
381
+ try += 1
382
+ @device = nil
383
+ retry
384
+ else
385
+ raise
386
+ end
387
+ end
388
+ end
316
389
  end
317
390
  end
318
391
  end
@@ -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
- raise ArgumentError, "Unknown command: #{cmd}"
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
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = "seriamp"
5
- spec.version = '0.1.6'
5
+ spec.version = '0.1.10'
6
6
  spec.authors = ['Oleg Pudeyev']
7
7
  spec.email = ['code@olegp.name']
8
8
  spec.summary = %q{Serial control for amplifiers & A/V receivers}
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.6
4
+ version: 0.1.10
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-12-11 00:00:00.000000000 Z
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