seriamp 0.1.10 → 0.1.12

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