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 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