seriamp 0.1.11 → 0.1.13

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: f9a46cb5fe16a4b24f54a6552e98db18e6e97281f7d12ce76c78f1ca5d18d8b8
4
- data.tar.gz: '082c51ad737e37e237248acca21d9ee6d0b74bca0be135cf99f52fbf26679c31'
3
+ metadata.gz: 608c49104ee16a8740439f09911464a51539d1a52907752bc7f0bf31fa0da85c
4
+ data.tar.gz: dc11ec89924b2c7b06a7a1a44be13e64c832de805c0c039f4a7bf3d66964a876
5
5
  SHA512:
6
- metadata.gz: 2b450a12d445a5b9199212eee5dba2eac298a9aa19e0e826df2fd322cd1d86d78a05815d39b3f75e417afa357720edb5ead5d1c9bfff1efefc171cb0b7e73f6e
7
- data.tar.gz: 9461488acf8478e6f7d52ea4f824042eede1a13a99f357092f37f55afddee6608e70a0306f23844a12a594e4658c5f87a40b1b2e090539a910f6ef963acd52ae
6
+ metadata.gz: 98879e190d39e69030155b492c72d3543e1db219f5315721705909b3cc177c1c150b6b5c3d5b0d3cb58ffccc59ea8d0610a064d95d5159ed11a91c7289a2ba27
7
+ data.tar.gz: 8f766224a06c19e14a71d5aa290d4c035031fd89a6abe1be1bd962a6fa36306d720e2032ac1ab804425cb40dcdbb6b84c050a15ebc77d8bec3b51673b8428cb5
data/README.md CHANGED
@@ -32,6 +32,39 @@ necessary.
32
32
  - Null-modem cable required
33
33
  - Receiver socket is male
34
34
 
35
+ The following table shows which Yamaha receivers have RS-232 connector
36
+ and which do not:
37
+
38
+ | Family | RS-232C Present | RS-232C Absent |
39
+ | -------- | ---------------------------- | -------------- |
40
+ | RX-Vx000 | RX-V3000, RX-V1000 | |
41
+ | | | HTR-5280 |
42
+ | RX-Vx200 | RX-V2200 | RX-V1200 |
43
+ | | | HTR-5490 |
44
+ | RX-Vx300 | RX-V2300 | RX-V1300 |
45
+ | | | HTR-5590 |
46
+ | | | HTR-5660 |
47
+ | RX-Vx400 | RX-V2400 | RX-V1400 |
48
+ | | | HTR-5790 |
49
+ | RX-Vx500 | RX-V2500, RX-V1500 | |
50
+ | | HTR-5890 | HTR-5860 |
51
+ | RX-Vx600 | RX-V2600, RX-V1600 | |
52
+ | | HTR-5990 | HTR-5960 |
53
+ | RX-Vx700 | RX-V2700, RX-V1700 | |
54
+ | | | HTR-6090 |
55
+ | RX-Vx800 | RX-V3800, RX-V1800 | |
56
+ | | HTR-6190 | HTR-6180 |
57
+ | RX-Vx900 | RX-V3900, RX-V1900 | |
58
+ | | | HTR-6290 |
59
+ | RX-Vx67 | RX-V3067, RX-V2067, RX-V1067 | RX-V867 |
60
+
61
+ RX-V2700, RX-V3800 and RX-V3900 have an Ethernet port in addition to
62
+ RS-232C and should be controllable via the Yamaha YNCA protocol via the
63
+ Ethernet port. Over time Yamaha has been adding networking functionality
64
+ to lower tier models, for example it is present in RX-V867, RX-V671 and RX-V475.
65
+
66
+ Models lower than 1000 level receivers have never had RS-232C to my knowledge.
67
+
35
68
  ### Denon AVR-2308CI
36
69
 
37
70
  - Straight cable required
@@ -111,6 +144,10 @@ Serial port communication in Ruby:
111
144
  - [rubyserial](https://github.com/hybridgroup/rubyserial)
112
145
  - [Ruby/SerialPort](https://github.com/hparra/ruby-serialport)
113
146
 
147
+ Yamaha YNCA protocol:
148
+
149
+ - [yamaha_ynca](https://github.com/mvdwetering/yamaha_ynca)
150
+
114
151
  ## Helpful Links
115
152
 
116
153
  - [Serial port programming in Ruby](https://www.thegeekdiary.com/serial-port-programming-reading-writing-status-of-control-lines-dtr-rts-cts-dsr/)
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))
@@ -21,7 +21,7 @@ module Seriamp
21
21
  Thread.new do
22
22
  Timeout.timeout(timeout * 2, CommunicationTimeout) do
23
23
  logger&.debug("Trying #{device}")
24
- client_cls.new(device: device, logger: logger).present?
24
+ client_cls.new(device: device, logger: logger, retries: false).present?
25
25
  logger&.debug("Found #{mod} device at #{device}")
26
26
  queue << device
27
27
  end
@@ -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,7 +10,7 @@ module Seriamp
10
10
  RS232_TIMEOUT = 3
11
11
 
12
12
  class Client
13
- def initialize(device: nil, glob: nil, logger: nil, retries: true)
13
+ def initialize(device: nil, glob: nil, logger: nil, retries: true, thread_safe: false)
14
14
  @logger = logger
15
15
 
16
16
  @device = device
@@ -26,6 +26,11 @@ module Seriamp
26
26
  else
27
27
  raise ArgumentError, "retries must be an integer, true, false or nil: #{retries}"
28
28
  end
29
+ @thread_safe = !!thread_safe
30
+
31
+ if thread_safe?
32
+ @lock = Mutex.new
33
+ end
29
34
  end
30
35
 
31
36
  attr_reader :device
@@ -33,6 +38,10 @@ module Seriamp
33
38
  attr_reader :logger
34
39
  attr_reader :retries
35
40
 
41
+ def thread_safe?
42
+ @thread_safe
43
+ end
44
+
36
45
  def detect_device?
37
46
  @detect_device
38
47
  end
@@ -182,10 +191,22 @@ module Seriamp
182
191
  private
183
192
 
184
193
  def with_device(&block)
185
- if @io
186
- yield @io
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
187
208
  else
188
- open_device(&block)
209
+ yield
189
210
  end
190
211
  end
191
212
 
@@ -203,6 +224,15 @@ module Seriamp
203
224
  logger&.debug("Opening #{device}")
204
225
  @io = Backend::SerialPortBackend::Device.new(device, logger: logger)
205
226
 
227
+ warned = false
228
+ while IO.select([@io.io], nil, nil, 0)
229
+ unless warned
230
+ logger&.warn("Serial device readable after opening - unread previous response?")
231
+ warned = true
232
+ end
233
+ IO.read(1)
234
+ end
235
+
206
236
  begin
207
237
  yield @io
208
238
  ensure
@@ -219,7 +249,9 @@ module Seriamp
219
249
  if try <= retries
220
250
  logger&.warn("Error during operation: #{exc.class}: #{exc} - will retry")
221
251
  try += 1
222
- @device = nil
252
+ if detect_device?
253
+ @device = nil
254
+ end
223
255
  retry
224
256
  else
225
257
  raise
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Seriamp
4
- VERSION = '0.1.11'
4
+ VERSION = '0.1.13'
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
 
@@ -165,6 +197,15 @@ module Seriamp
165
197
  logger&.debug("Opening #{device}")
166
198
  @io = Backend::SerialPortBackend::Device.new(device, logger: logger)
167
199
 
200
+ warned = false
201
+ while IO.select([@io.io], nil, nil, 0)
202
+ unless warned
203
+ logger&.warn("Serial device readable after opening - unread previous response?")
204
+ warned = true
205
+ end
206
+ IO.read(1)
207
+ end
208
+
168
209
  begin
169
210
  tries = 0
170
211
  begin
@@ -265,9 +306,12 @@ module Seriamp
265
306
  end
266
307
  break unless again
267
308
  end
309
+ if resp.length < 10
310
+ raise HandshakeFailure, "Broken status response: expected at least 10 bytes, got #{resp.length} bytes; concurrent operation on device?"
311
+ end
268
312
  payload = resp[1...-1]
269
- @model_code = payload[0..4]
270
- @version = payload[5]
313
+ model_code = payload[0..4]
314
+ version = payload[5]
271
315
  length = payload[6..7].to_i(16)
272
316
  data = payload[8...-2]
273
317
  if data.length != length
@@ -276,11 +320,11 @@ module Seriamp
276
320
  unless data.start_with?('@E01900')
277
321
  raise HandshakeFailure, "Broken status response: expected to start with @E01900, actual #{data[0..6]}"
278
322
  end
279
- @status_string = data
280
- @status = {
281
- model_code: @model_code,
282
- model_name: MODEL_NAMES[@model_code],
283
- firmware_version: @version,
323
+ status_string = data
324
+ status = {
325
+ model_code: model_code,
326
+ model_name: MODEL_NAMES[model_code],
327
+ firmware_version: version,
284
328
  system_status: data[7].ord - ZERO_ORD,
285
329
  power: power = data[8].ord - ZERO_ORD,
286
330
  main_power: [1, 4, 5, 2].include?(power),
@@ -288,7 +332,7 @@ module Seriamp
288
332
  zone3_power: [1, 5, 3, 7].include?(power),
289
333
  }
290
334
  if data.length > 9
291
- @status.update(
335
+ status.update(
292
336
  input: input = data[9],
293
337
  input_name: MAIN_INPUTS_GET.fetch(input),
294
338
  multi_ch_input: data[10] == '1',
@@ -319,34 +363,41 @@ module Seriamp
319
363
  sleep: SLEEP_GET.fetch(data[24]),
320
364
  night: night = data[27],
321
365
  night_name: NIGHT_GET.fetch(night),
322
- pure_direct: data[PURE_DIRECT_FIELD.fetch(@model_code)] == '1',
366
+ pure_direct: data[PURE_DIRECT_FIELD.fetch(model_code)] == '1',
323
367
  speaker_a: data[29] == '1',
324
368
  speaker_b: data[30] == '1',
325
369
  # 2 positions on RX-Vx700
326
370
  #format: data[31..32],
327
371
  #sampling: data[33..34],
328
372
  )
329
- if @model_code == 'R0178'
330
- @status.update(
373
+ if model_code == 'R0178'
374
+ status.update(
331
375
  input_mode: INPUT_MODE_R0178.fetch(data[11]),
332
376
  sampling: data[32],
333
377
  sample_rate: SAMPLE_RATE_R0178.fetch(data[32]),
334
378
  )
335
379
  end
336
380
  end
337
- @status
381
+
382
+ @model_code, @version, @status_string =
383
+ model_code, version, status_string
384
+ @status = status
338
385
  end
339
386
  end
340
387
 
341
388
  def remote_command(cmd)
342
- with_retry do
343
- dispatch("#{STX}0#{cmd}#{ETX}")
389
+ with_lock do
390
+ with_retry do
391
+ dispatch("#{STX}0#{cmd}#{ETX}")
392
+ end
344
393
  end
345
394
  end
346
395
 
347
396
  def system_command(cmd)
348
- with_retry do
349
- dispatch("#{STX}2#{cmd}#{ETX}")
397
+ with_lock do
398
+ with_retry do
399
+ dispatch("#{STX}2#{cmd}#{ETX}")
400
+ end
350
401
  end
351
402
  end
352
403
 
@@ -379,7 +430,9 @@ module Seriamp
379
430
  if try <= retries
380
431
  logger&.warn("Error during operation: #{exc.class}: #{exc} - will retry")
381
432
  try += 1
382
- @device = nil
433
+ if detect_device?
434
+ @device = nil
435
+ end
383
436
  retry
384
437
  else
385
438
  raise
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.11'
5
+ spec.version = '0.1.13'
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.11
4
+ version: 0.1.13
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-23 00:00:00.000000000 Z
11
+ date: 2023-01-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: serialport