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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7afd42caaa8f0dd5815e908b40bfccd8ca042769fea3af431ae334b3d44aa140
4
- data.tar.gz: af4041b6faf2f07ce204a15b99d4cc51b6f4bdfacc5c016006588d096513adef
3
+ metadata.gz: 25ded6fbd5f32c13385dc7704f60d9d88c46bf528f79e7c67d41f295800b596d
4
+ data.tar.gz: 7c2cab83327cf081c73c28743d46406213bdfaccd18f177d632f3c0922b55466
5
5
  SHA512:
6
- metadata.gz: c09d03fe9b24e521eadffef9dc94305f61b90005e9719c349d9dba9ab7cb327a632352a62a3d2831f215374343147e3195fd958e01c4d750cce0a7ee4b7a8fda
7
- data.tar.gz: a5a4f1b992fc6459ed7fa605daecc0715acb5e5938246b9915eb6af32415162b18650a6c45066602c22c27cf15f4879d91f63ab4b75eb05df82c02c0ee12760d
6
+ metadata.gz: 5484fe172a3d65fb4d9778fc3734dd0a2aa5b24f459f864b5d8bc68a4766b06e5d6f2fd3ccc234b21b27fdc534abeb76655cdf29c68e1dca7afacf476e847186
7
+ data.tar.gz: 64cb7cfed4265a174020f59a04671e9865739d4f573b361f560c88051fad4a63355e2401cd0d84cc2aa394babed6cc5efa6ae775573362fb3da6e09dfa716840
@@ -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.9'
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,16 @@ module Seriamp
14
15
  set :client, nil
15
16
 
16
17
  get '/' do
17
- render_json(client.status)
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.public_send("set_#{zone}_power", state)
32
- empty_response
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 "/pure-direct" do
97
+ put "/pure_direct" do
81
98
  state = Utils.parse_on_off(request.body.read)
82
- client.public_send("set_pure_direct", state)
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
- render_json({})
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
- 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
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
- 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'
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
- input_mode: INPUT_MODE_R0178.fetch(data[11]),
280
- sampling: data[32],
281
- sample_rate: SAMPLE_RATE_R0178.fetch(data[32]),
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
- dispatch("#{STX}0#{cmd}#{ETX}")
335
+ with_retry do
336
+ dispatch("#{STX}0#{cmd}#{ETX}")
337
+ end
290
338
  end
291
339
 
292
340
  def system_command(cmd)
293
- dispatch("#{STX}2#{cmd}#{ETX}")
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
@@ -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.9'
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.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: 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