seriamp 0.1.10 → 0.1.12

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: 50636555b1276e60e52ddf26c28f42fd1e7b1d27a89fac22f37fefce236c8f88
4
- data.tar.gz: aac82da48b0ac8887a507671af5d3d4e16fd9857bb70a635c145be12939fda70
3
+ metadata.gz: 3a8c5974a39756c641ab5fb6e600d0d67095e4e9f18c872225ffb580cea4e70c
4
+ data.tar.gz: ba47852332bdb66070cf4105c4929ed691fce8eca8dd7f47aaefbeb4effab932
5
5
  SHA512:
6
- metadata.gz: 0c86e471aa614ce5048b168874f6c7ecacf956cf078fb0f327df230a588138e7c65f3fdc272ceaaa0f95775bdf2e9d104bbf47c7fcfa470565a83bb03d12e607
7
- data.tar.gz: dd5f494621dcd63117ce276ac97d4240dc38ec84b7dcb0775851e8b7602e45d8e2789118cbbe10b7efbc74d2a4a7429e5d2be4ee6d469fcbed9185b41da2d6a9
6
+ metadata.gz: 185e56b171c6db9ed0822f57de19ac2fea395d075bc615cddf0795c82c2f044b5212e9eb210b90b3dbc4aa603ce6b1553a610924c9ce8b9f02ae860c1f0c873c
7
+ data.tar.gz: 277dc1f126ca440e3a8539491e1548511f1abfb66e6a21c615ab4ab8bfce67c76c2499b6c6a2ec9efe20050cd1ac732f70cd3870cff98aec927ae57f6f46e1e4
data/bin/sonamp-web CHANGED
@@ -23,9 +23,10 @@ end.parse!
23
23
 
24
24
  logger = Logger.new(STDERR)
25
25
 
26
- #Sonamp::App.set :device, options[:device]
27
- #Sonamp::App.set :logger, logger
28
- Seriamp::Sonamp::App.set :client, Seriamp::Sonamp::Client.new(device: options[:device], logger: logger)
26
+ #Seriamp::Sonamp::App.set :device, options[:device]
27
+ #Seriamp::Sonamp::App.set :logger, logger
28
+ Seriamp::Sonamp::App.set :client, Seriamp::Sonamp::Client.new(
29
+ device: options[:device], logger: logger, thread_safe: true)
29
30
 
30
31
  options = Rack::Server::Options.new.parse!(ARGV)
31
32
  Rack::Server.start(options.merge(app: Seriamp::Sonamp::App))
data/bin/yamaha-web CHANGED
@@ -23,9 +23,10 @@ end.parse!
23
23
 
24
24
  logger = Logger.new(STDERR)
25
25
 
26
- #Yamaha::App.set :device, options[:device]
27
- #Yamaha::App.set :logger, logger
28
- Seriamp::Yamaha::App.set :client, Seriamp::Yamaha::Client.new(device: options[:device], logger: logger)
26
+ #Seriamp::Yamaha::App.set :device, options[:device]
27
+ #Seriamp::Yamaha::App.set :logger, logger
28
+ Seriamp::Yamaha::App.set :client, Seriamp::Yamaha::Client.new(
29
+ device: options[:device], logger: logger, thread_safe: true)
29
30
 
30
31
  options = Rack::Server::Options.new.parse!(ARGV)
31
32
  Rack::Server.start(options.merge(app: Seriamp::Yamaha::App))
@@ -12,6 +12,7 @@ module Seriamp
12
12
  set :device, nil
13
13
  set :logger, nil
14
14
  set :client, nil
15
+ set :retries, true
15
16
 
16
17
  get '/' do
17
18
  render_json(client.status)
@@ -75,7 +76,8 @@ module Seriamp
75
76
 
76
77
  def client
77
78
  settings.client || begin
78
- @client ||= Sonamp::Client.new(settings.device, logger: settings.logger)
79
+ @client ||= Sonamp::Client.new(settings.device,
80
+ logger: settings.logger, retries: settings.retries, thread_safe: true)
79
81
  end
80
82
  end
81
83
 
@@ -10,17 +10,37 @@ module Seriamp
10
10
  RS232_TIMEOUT = 3
11
11
 
12
12
  class Client
13
- def initialize(device: nil, glob: nil, logger: nil)
13
+ def initialize(device: nil, glob: nil, logger: nil, retries: true, thread_safe: false)
14
14
  @logger = logger
15
15
 
16
16
  @device = device
17
17
  @detect_device = device.nil?
18
18
  @glob = glob
19
+ @retries = case retries
20
+ when nil, false
21
+ 0
22
+ when true
23
+ 1
24
+ when Integer
25
+ retries
26
+ else
27
+ raise ArgumentError, "retries must be an integer, true, false or nil: #{retries}"
28
+ end
29
+ @thread_safe = !!thread_safe
30
+
31
+ if thread_safe?
32
+ @lock = Mutex.new
33
+ end
19
34
  end
20
35
 
21
36
  attr_reader :device
22
37
  attr_reader :glob
23
38
  attr_reader :logger
39
+ attr_reader :retries
40
+
41
+ def thread_safe?
42
+ @thread_safe
43
+ end
24
44
 
25
45
  def detect_device?
26
46
  @detect_device
@@ -134,7 +154,7 @@ module Seriamp
134
154
  end
135
155
 
136
156
  def get_voltage_trigger_input(zone = nil)
137
- get_zone_state('VTI', zone)
157
+ get_zone_state('VTI', zone, include_all: true)
138
158
  end
139
159
 
140
160
  def get_firmware_version
@@ -148,7 +168,7 @@ module Seriamp
148
168
  def status
149
169
  # Reusing the opened device file makes :VTIG? fail even with a delay
150
170
  # in front.
151
- #open_device do
171
+ with_device do
152
172
  {
153
173
  firmware_version: get_firmware_version,
154
174
  temperature: get_temperature,
@@ -165,13 +185,34 @@ module Seriamp
165
185
  voltage_trigger_input: get_voltage_trigger_input,
166
186
  channel_front_panel_level: get_channel_front_panel_level,
167
187
  }
168
- #end
188
+ end
169
189
  end
170
190
 
171
191
  private
172
192
 
193
+ def with_device(&block)
194
+ with_lock do
195
+ if @io
196
+ yield @io
197
+ else
198
+ open_device(&block)
199
+ end
200
+ end
201
+ end
202
+
203
+ def with_lock
204
+ if thread_safe?
205
+ @lock.synchronize do
206
+ yield
207
+ end
208
+ else
209
+ yield
210
+ end
211
+ end
212
+
173
213
  def open_device
174
214
  if detect_device? && device.nil?
215
+ logger&.debug("Detecting device")
175
216
  @device = Seriamp.detect_device(Sonamp, *glob, logger: logger)
176
217
  if @device
177
218
  logger&.info("Using #{device} as TTY device")
@@ -180,31 +221,58 @@ module Seriamp
180
221
  end
181
222
  end
182
223
 
183
- if @f
224
+ logger&.debug("Opening #{device}")
225
+ @io = Backend::SerialPortBackend::Device.new(device, logger: logger)
226
+
227
+ begin
228
+ yield @io
229
+ ensure
230
+ @io.close rescue nil
231
+ @io = nil
232
+ end
233
+ end
234
+
235
+ def with_retry
236
+ try = 1
237
+ begin
184
238
  yield
185
- else
186
- rv = nil
187
- Backend::SerialPortBackend::Device.new(device) do |f|
188
- @f = f
189
- rv = yield
190
- @f = nil
239
+ rescue Seriamp::Error => exc
240
+ if try <= retries
241
+ logger&.warn("Error during operation: #{exc.class}: #{exc} - will retry")
242
+ try += 1
243
+ @device = nil
244
+ retry
245
+ else
246
+ raise
191
247
  end
192
- rv
193
248
  end
194
249
  end
195
250
 
196
- def dispatch(cmd, resp_lines_count = 1)
197
- open_device do
198
- with_timeout do
199
- @f.syswrite("#{cmd}\x0d")
200
- end
201
- resp = 1.upto(resp_lines_count).map do
202
- read_line(@f, cmd)
203
- end
204
- if resp_lines_count == 1
205
- resp.first
206
- else
207
- resp
251
+ def dispatch(cmd, resp_lines_range_or_count = 1)
252
+ resp_lines_range = if Range === resp_lines_range_or_count || Array === resp_lines_range_or_count
253
+ resp_lines_range_or_count
254
+ else
255
+ 1..resp_lines_range_or_count
256
+ end
257
+
258
+ with_retry do
259
+ with_device do
260
+ with_timeout do
261
+ @io.syswrite("#{cmd}\x0d")
262
+ end
263
+ resp = resp_lines_range.map do
264
+ read_line(@io, cmd)
265
+ end
266
+
267
+ if @io && IO.select([@io.io], nil, nil, 0)
268
+ logger&.warn("Serial device readable after completely reading status response - concurrent access?")
269
+ end
270
+
271
+ if resp_lines_range_or_count == 1
272
+ resp.first
273
+ else
274
+ resp
275
+ end
208
276
  end
209
277
  end
210
278
  end
@@ -266,7 +334,7 @@ module Seriamp
266
334
  dispatch_assert(cmd, expected)
267
335
  end
268
336
 
269
- def get_zone_value(cmd_prefix, zone, boolize: false)
337
+ def get_zone_value(cmd_prefix, zone, boolize: false, include_all: false)
270
338
  if zone
271
339
  if zone < 1 || zone > 4
272
340
  raise ArgumentError, "Zone must be between 1 and 4: #{zone}"
@@ -274,22 +342,23 @@ module Seriamp
274
342
  resp = dispatch(":#{cmd_prefix}#{zone}?")
275
343
  typecast_value(resp[cmd_prefix.length + 1..], boolize)
276
344
  else
277
- hashize_query_result(dispatch(":#{cmd_prefix}G?", 4), cmd_prefix, boolize)
345
+ range = include_all ? [1, 2, 3, 4, 'A'] : (1..4).to_a
346
+ hashize_query_result(dispatch(":#{cmd_prefix}G?", range), cmd_prefix, boolize, range)
278
347
  end
279
348
  end
280
349
 
281
- def hashize_query_result(resp_lines, cmd_prefix, boolize)
350
+ def hashize_query_result(resp_lines, cmd_prefix, boolize, range)
282
351
  index = 1
283
352
  Hash[resp_lines.map do |resp|
284
- value = typecast_value(extract_suffix(resp, "#{cmd_prefix}#{index}"), boolize)
353
+ value = typecast_value(extract_suffix(resp, "#{cmd_prefix}#{range.to_a[index-1]}"), boolize)
285
354
  [index, value].tap do
286
355
  index += 1
287
356
  end
288
357
  end]
289
358
  end
290
359
 
291
- def get_zone_state(cmd_prefix, zone)
292
- get_zone_value(cmd_prefix, zone, boolize: true)
360
+ def get_zone_state(cmd_prefix, zone, include_all: false)
361
+ get_zone_value(cmd_prefix, zone, boolize: true, include_all: include_all)
293
362
  end
294
363
 
295
364
  def set_channel_value(cmd_prefix, channel, value)
@@ -309,7 +378,7 @@ module Seriamp
309
378
  typecast_value(dispatch_extract_suffix(":#{cmd_prefix}#{channel}?", "#{cmd_prefix}#{channel}"), boolize)
310
379
  else
311
380
  index = 1
312
- hashize_query_result(dispatch(":#{cmd_prefix}G?", 8), cmd_prefix, boolize)
381
+ hashize_query_result(dispatch(":#{cmd_prefix}G?", 8), cmd_prefix, boolize, 1..8)
313
382
  end
314
383
  end
315
384
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Seriamp
4
- VERSION = '0.1.10'
4
+ VERSION = '0.1.12'
5
5
  end
@@ -13,6 +13,7 @@ module Seriamp
13
13
  set :device, nil
14
14
  set :logger, nil
15
15
  set :client, nil
16
+ set :retries, true
16
17
 
17
18
  get '/' do
18
19
  clear_cache
@@ -153,7 +154,8 @@ module Seriamp
153
154
 
154
155
  def client
155
156
  settings.client || begin
156
- @client ||= Yamaha::Client.new(settings.device, logger: settings.logger)
157
+ @client ||= Yamaha::Client.new(settings.device,
158
+ logger: settings.logger, retries: settings.retries, thread_safe: true)
157
159
  end
158
160
  end
159
161
 
@@ -14,7 +14,7 @@ module Seriamp
14
14
  class Client
15
15
  include Protocol::Methods
16
16
 
17
- def initialize(device: nil, glob: nil, logger: nil, retries: true)
17
+ def initialize(device: nil, glob: nil, logger: nil, retries: true, thread_safe: false)
18
18
  @logger = logger
19
19
 
20
20
  @device = device
@@ -30,6 +30,11 @@ module Seriamp
30
30
  else
31
31
  raise ArgumentError, "retries must be an integer, true, false or nil: #{retries}"
32
32
  end
33
+ @thread_safe = !!thread_safe
34
+
35
+ if thread_safe?
36
+ @lock = Mutex.new
37
+ end
33
38
 
34
39
  if block_given?
35
40
  begin
@@ -45,6 +50,10 @@ module Seriamp
45
50
  attr_reader :logger
46
51
  attr_reader :retries
47
52
 
53
+ def thread_safe?
54
+ @thread_safe
55
+ end
56
+
48
57
  def detect_device?
49
58
  @detect_device
50
59
  end
@@ -56,18 +65,29 @@ module Seriamp
56
65
 
57
66
  def last_status
58
67
  unless @status
59
- with_device do
60
- unless @status
61
- do_status
68
+ with_lock do
69
+ with_retry do
70
+ with_device do
71
+ unless @status
72
+ do_status
73
+ end
74
+ end
62
75
  end
63
76
  end
64
77
  end
78
+ if @status.nil?
79
+ raise "This should not happen"
80
+ end
65
81
  @status.dup
66
82
  end
67
83
 
68
84
  def last_status_string
69
85
  unless @status_string
70
- with_device do
86
+ with_lock do
87
+ with_retry do
88
+ with_device do
89
+ end
90
+ end
71
91
  end
72
92
  end
73
93
  @status_string.dup
@@ -121,6 +141,16 @@ module Seriamp
121
141
  end
122
142
  end
123
143
 
144
+ def with_lock
145
+ if thread_safe?
146
+ @lock.synchronize do
147
+ yield
148
+ end
149
+ else
150
+ yield
151
+ end
152
+ end
153
+
124
154
  # Shows a message via the on-screen display. The message must be 16
125
155
  # characters or fewer. The message is NOT displayed on the front panel,
126
156
  # it is shown only on the connected TV's OSD.
@@ -134,13 +164,15 @@ module Seriamp
134
164
  raise ArgumentError, "Message must be no more than 16 characters, #{msg.length} given"
135
165
  end
136
166
 
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'))
167
+ with_lock do
168
+ with_retry do
169
+ with_device do
170
+ @io.syswrite("#{STX}21000#{ETX}".encode('ascii'))
171
+ @io.syswrite("#{STX}3#{msg[0..3]}#{ETX}".encode('ascii'))
172
+ @io.syswrite("#{STX}3#{msg[4..7]}#{ETX}".encode('ascii'))
173
+ @io.syswrite("#{STX}3#{msg[8..11]}#{ETX}".encode('ascii'))
174
+ @io.syswrite("#{STX}3#{msg[12..15]}#{ETX}".encode('ascii'))
175
+ end
144
176
  end
145
177
  end
146
178
 
@@ -265,9 +297,12 @@ module Seriamp
265
297
  end
266
298
  break unless again
267
299
  end
300
+ if resp.length < 10
301
+ raise HandshakeFailure, "Broken status response: expected at least 10 bytes, got #{resp.length} bytes; concurrent operation on device?"
302
+ end
268
303
  payload = resp[1...-1]
269
- @model_code = payload[0..4]
270
- @version = payload[5]
304
+ model_code = payload[0..4]
305
+ version = payload[5]
271
306
  length = payload[6..7].to_i(16)
272
307
  data = payload[8...-2]
273
308
  if data.length != length
@@ -276,11 +311,11 @@ module Seriamp
276
311
  unless data.start_with?('@E01900')
277
312
  raise HandshakeFailure, "Broken status response: expected to start with @E01900, actual #{data[0..6]}"
278
313
  end
279
- @status_string = data
280
- @status = {
281
- model_code: @model_code,
282
- model_name: MODEL_NAMES[@model_code],
283
- firmware_version: @version,
314
+ status_string = data
315
+ status = {
316
+ model_code: model_code,
317
+ model_name: MODEL_NAMES[model_code],
318
+ firmware_version: version,
284
319
  system_status: data[7].ord - ZERO_ORD,
285
320
  power: power = data[8].ord - ZERO_ORD,
286
321
  main_power: [1, 4, 5, 2].include?(power),
@@ -288,7 +323,7 @@ module Seriamp
288
323
  zone3_power: [1, 5, 3, 7].include?(power),
289
324
  }
290
325
  if data.length > 9
291
- @status.update(
326
+ status.update(
292
327
  input: input = data[9],
293
328
  input_name: MAIN_INPUTS_GET.fetch(input),
294
329
  multi_ch_input: data[10] == '1',
@@ -319,34 +354,41 @@ module Seriamp
319
354
  sleep: SLEEP_GET.fetch(data[24]),
320
355
  night: night = data[27],
321
356
  night_name: NIGHT_GET.fetch(night),
322
- pure_direct: data[PURE_DIRECT_FIELD.fetch(@model_code)] == '1',
357
+ pure_direct: data[PURE_DIRECT_FIELD.fetch(model_code)] == '1',
323
358
  speaker_a: data[29] == '1',
324
359
  speaker_b: data[30] == '1',
325
360
  # 2 positions on RX-Vx700
326
361
  #format: data[31..32],
327
362
  #sampling: data[33..34],
328
363
  )
329
- if @model_code == 'R0178'
330
- @status.update(
364
+ if model_code == 'R0178'
365
+ status.update(
331
366
  input_mode: INPUT_MODE_R0178.fetch(data[11]),
332
367
  sampling: data[32],
333
368
  sample_rate: SAMPLE_RATE_R0178.fetch(data[32]),
334
369
  )
335
370
  end
336
371
  end
337
- @status
372
+
373
+ @model_code, @version, @status_string =
374
+ model_code, version, status_string
375
+ @status = status
338
376
  end
339
377
  end
340
378
 
341
379
  def remote_command(cmd)
342
- with_retry do
343
- dispatch("#{STX}0#{cmd}#{ETX}")
380
+ with_lock do
381
+ with_retry do
382
+ dispatch("#{STX}0#{cmd}#{ETX}")
383
+ end
344
384
  end
345
385
  end
346
386
 
347
387
  def system_command(cmd)
348
- with_retry do
349
- dispatch("#{STX}2#{cmd}#{ETX}")
388
+ with_lock do
389
+ with_retry do
390
+ dispatch("#{STX}2#{cmd}#{ETX}")
391
+ end
350
392
  end
351
393
  end
352
394
 
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.10'
5
+ spec.version = '0.1.12'
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.10
4
+ version: 0.1.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oleg Pudeyev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-01-15 00:00:00.000000000 Z
11
+ date: 2023-01-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: serialport