seriamp 0.1.2
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 +7 -0
- data/.gitignore +3 -0
- data/LICENSE +23 -0
- data/README.md +75 -0
- data/bin/sonamp +10 -0
- data/bin/sonamp-web +31 -0
- data/bin/yamaha +12 -0
- data/bin/yamaha-web +31 -0
- data/lib/seriamp/backend/ffi.rb +109 -0
- data/lib/seriamp/backend/serial_port.rb +34 -0
- data/lib/seriamp/detect.rb +44 -0
- data/lib/seriamp/error.rb +10 -0
- data/lib/seriamp/sonamp/app.rb +62 -0
- data/lib/seriamp/sonamp/client.rb +314 -0
- data/lib/seriamp/sonamp/cmd.rb +102 -0
- data/lib/seriamp/sonamp.rb +4 -0
- data/lib/seriamp/utils.rb +17 -0
- data/lib/seriamp/version.rb +5 -0
- data/lib/seriamp/yamaha/app.rb +58 -0
- data/lib/seriamp/yamaha/client.rb +279 -0
- data/lib/seriamp/yamaha/cmd.rb +139 -0
- data/lib/seriamp/yamaha/protocol/constants.rb +183 -0
- data/lib/seriamp/yamaha/protocol/methods.rb +134 -0
- data/lib/seriamp/yamaha.rb +4 -0
- data/lib/seriamp.rb +5 -0
- data/seriamp.gemspec +17 -0
- metadata +73 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: cd8d27d7818b801b6734b046fcc3682b1322bc03cfaa8d2472f4963d5974f7d5
|
4
|
+
data.tar.gz: 917a111090ce967b0c28684f60fc0ccf8dca627c0c5d6ae1699b97746be69afc
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 166e4cc102b91987bcb244aee644b682757609ad19ea42eafb88216f09295b87344a7906d925ff0479fe58b4db477691404bcfb7211d9bab7b29d29fe41555f2
|
7
|
+
data.tar.gz: 4e4aac13144356caeb92bf05da4e50181f1f83613e5ed1bb857fe91c1562d6d352b81c9e9eb051f6f8f50cc348f4bb61f0e8f04887880480dbc6e8c19be513de
|
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
Copyright (c) 2022 Oleg Pudeyev
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
6
|
+
|
7
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
8
|
+
list of conditions and the following disclaimer.
|
9
|
+
|
10
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
11
|
+
this list of conditions and the following disclaimer in the documentation
|
12
|
+
and/or other materials provided with the distribution.
|
13
|
+
|
14
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
15
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
16
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
17
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
18
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
19
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
20
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
21
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
22
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
23
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
# Yamaha Receiver Serial Control Ruby Library
|
2
|
+
|
3
|
+
## Protocol Notes
|
4
|
+
|
5
|
+
### RX-V1500 Power Values
|
6
|
+
|
7
|
+
You might expect the power state to be a bit field, but it isn't - each
|
8
|
+
combination is assigned an independent value:
|
9
|
+
|
10
|
+
| Main zone | Zone 2 | Zone 3 | Value | Notes |
|
11
|
+
| --------- | ------ | ------ | ----- | ------- |
|
12
|
+
| On | On | On | 1 | All on |
|
13
|
+
| On | On | Off | 4 | |
|
14
|
+
| On | Off | On | 5 | |
|
15
|
+
| On | Off | Off | 2 | |
|
16
|
+
| Off | On | On | 3 | |
|
17
|
+
| Off | On | Off | 6 | |
|
18
|
+
| Off | Off | On | 7 | |
|
19
|
+
| Off | Off | Off | 0 | All off |
|
20
|
+
|
21
|
+
## Implementation Notes
|
22
|
+
|
23
|
+
In order for the receiver to respond, the RTS bit must be set on the wire.
|
24
|
+
Setting this bit requires a 5-wire cable. I have some RS232 to 3.5 mm cables
|
25
|
+
which aren't usable with Yamahas.
|
26
|
+
|
27
|
+
Linux appears to automatically set the RTS bit upon opening the serial port,
|
28
|
+
thus setting it explicitly may not be needed.
|
29
|
+
|
30
|
+
To monitor serial communications under Linux, I used
|
31
|
+
[slsnif](https://github.com/aeruder/slsnif) which I found via
|
32
|
+
[this summary of serial port monitoring tools](https://serverfault.com/questions/112957/sniff-serial-port-on-linux).
|
33
|
+
|
34
|
+
The receiver is very frequently not responding to the "ready" command.
|
35
|
+
The documentation mentions retrying this command but in my experience the
|
36
|
+
first time this command is sent to a RX-V1500 which is in standby it is
|
37
|
+
*always* igored.
|
38
|
+
|
39
|
+
I have RX-V1500 and RX-V2500, however I couldn't locate RS232 protocol manuals
|
40
|
+
for these receivers. I am primarily using RX-V1700/RX-V2700 manual with some
|
41
|
+
references to RX-V1000/RX-V3000 manual. The commands are mostly or completely
|
42
|
+
identical, with RX-V1700/RX-V2700 manual describing most or all of what
|
43
|
+
RX-V1500/RX-V2500 support, but the status responses are very different.
|
44
|
+
For my RX-V1500/RX-V2500 I had to reverse-engineer the status responses, and
|
45
|
+
because of this they only have a limited number of fields decoded.
|
46
|
+
|
47
|
+
Volume level is set and reported as follows: 0 means muting is active,
|
48
|
+
otherwise the minimum level for the zone is 39 and each step in the level is
|
49
|
+
the next integer value up. For the main zone on RX-V1500/RX-V2500, the volume
|
50
|
+
is adjusted in 0.5 dB increments from -80 dB to 14.5 dB, giving the integer
|
51
|
+
values the range of 39-228. For zones 2 and 3 the volume is adjusted in whole
|
52
|
+
dB increments from -33 dB to 0 dB, giving the integer range of 39-72.
|
53
|
+
|
54
|
+
While testing with Python, I ran into [this issue](https://bugs.python.org/issue20074) -
|
55
|
+
to open a TTY in Python, buffering must be disabled.
|
56
|
+
|
57
|
+
See [here](https://www.avsforum.com/threads/enhancing-yamaha-avrs-via-rs-232.1066484/)
|
58
|
+
for more Yamaha-related software.
|
59
|
+
|
60
|
+
## Other Libraries
|
61
|
+
|
62
|
+
Yamaha RS232/serial protocol:
|
63
|
+
|
64
|
+
- [YRXV1500-MQTT](https://github.com/FireFrei/yrxv1500-mqtt)
|
65
|
+
- [YamahaController](https://github.com/mrworf/yamahacontroller)
|
66
|
+
- [Homie ESP8266 Yamaha RX-Vxxxx Control](https://github.com/memphi2/homie-yamaha-rs232)
|
67
|
+
|
68
|
+
Serial port communication in Ruby:
|
69
|
+
|
70
|
+
- [rubyserial](https://github.com/hybridgroup/rubyserial)
|
71
|
+
- [Ruby/SerialPort](https://github.com/hparra/ruby-serialport)
|
72
|
+
|
73
|
+
## Helpful Links
|
74
|
+
|
75
|
+
- [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
ADDED
data/bin/sonamp-web
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'seriamp/sonamp/app'
|
5
|
+
rescue LoadError
|
6
|
+
$: << File.join(File.dirname(__FILE__), '../lib')
|
7
|
+
require 'seriamp/sonamp/app'
|
8
|
+
end
|
9
|
+
require 'optparse'
|
10
|
+
require 'logger'
|
11
|
+
|
12
|
+
options = {}
|
13
|
+
OptionParser.new do |opts|
|
14
|
+
opts.banner = "Usage: sonamp-web [-d device] [-- rackup-options...]"
|
15
|
+
|
16
|
+
opts.on("-d", "--device DEVICE", "TTY to use (default autodetect)") do |v|
|
17
|
+
options[:device] = v
|
18
|
+
end
|
19
|
+
|
20
|
+
opts.separator ''
|
21
|
+
opts.separator 'To see rackup options: sonamp-web -- -h'
|
22
|
+
end.parse!
|
23
|
+
|
24
|
+
logger = Logger.new(STDERR)
|
25
|
+
|
26
|
+
#Sonamp::App.set :device, options[:device]
|
27
|
+
#Sonamp::App.set :logger, logger
|
28
|
+
Seriamp::Sonamp::App.set :client, Sonamp::Client.new(options[:device], logger: logger)
|
29
|
+
|
30
|
+
options = Rack::Server::Options.new.parse!(ARGV)
|
31
|
+
Rack::Server.start(options.merge(app: Sonamp::App))
|
data/bin/yamaha
ADDED
data/bin/yamaha-web
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'seriamp/yamaha/app'
|
5
|
+
rescue LoadError
|
6
|
+
$: << File.join(File.dirname(__FILE__), '../lib')
|
7
|
+
require 'seriamp/yamaha/app'
|
8
|
+
end
|
9
|
+
require 'optparse'
|
10
|
+
require 'logger'
|
11
|
+
|
12
|
+
options = {}
|
13
|
+
OptionParser.new do |opts|
|
14
|
+
opts.banner = "Usage: yamaha-web [-d device] [-- rackup-options...]"
|
15
|
+
|
16
|
+
opts.on("-d", "--device DEVICE", "TTY to use (default autodetect)") do |v|
|
17
|
+
options[:device] = v
|
18
|
+
end
|
19
|
+
|
20
|
+
opts.separator ''
|
21
|
+
opts.separator 'To see rackup options: yamaha-web -- -h'
|
22
|
+
end.parse!
|
23
|
+
|
24
|
+
logger = Logger.new(STDERR)
|
25
|
+
|
26
|
+
#Yamaha::App.set :device, options[:device]
|
27
|
+
#Yamaha::App.set :logger, logger
|
28
|
+
Seriamp::Yamaha::App.set :client, Yamaha::Client.new(options[:device], logger: logger)
|
29
|
+
|
30
|
+
options = Rack::Server::Options.new.parse!(ARGV)
|
31
|
+
Rack::Server.start(options.merge(app: Yamaha::App))
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
require 'ffi'
|
5
|
+
|
6
|
+
module Seriamp
|
7
|
+
module Backend
|
8
|
+
module FFIBackend
|
9
|
+
|
10
|
+
class Device
|
11
|
+
extend Forwardable
|
12
|
+
|
13
|
+
def initialize(device, logger: nil)
|
14
|
+
@logger = logger
|
15
|
+
|
16
|
+
if @f
|
17
|
+
yield
|
18
|
+
else
|
19
|
+
logger&.debug("Opening device #{device}")
|
20
|
+
File.open(device, 'r+') do |f|
|
21
|
+
unless f.isatty
|
22
|
+
raise BadDevice, "#{device} is not a TTY"
|
23
|
+
end
|
24
|
+
@f = f
|
25
|
+
set_rts
|
26
|
+
|
27
|
+
if IO.select([f], nil, nil, 0)
|
28
|
+
logger&.warn("Serial device readable without having been written to - concurrent access?")
|
29
|
+
end
|
30
|
+
|
31
|
+
tries = 0
|
32
|
+
begin
|
33
|
+
do_status
|
34
|
+
rescue Timeout::Error
|
35
|
+
tries += 1
|
36
|
+
if tries < 5
|
37
|
+
logger&.warn("Timeout handshaking with the receiver - will retry")
|
38
|
+
retry
|
39
|
+
else
|
40
|
+
raise
|
41
|
+
end
|
42
|
+
end
|
43
|
+
yield.tap do
|
44
|
+
@f = nil
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
rescue IOError => e
|
49
|
+
if @f
|
50
|
+
logger&.warn("#{e.class}: #{e} while operating, closing the device")
|
51
|
+
@f.close
|
52
|
+
raise
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
attr_reader :logger
|
57
|
+
def_delegators :@f, :close
|
58
|
+
|
59
|
+
def set_rts
|
60
|
+
ptr = IntPtr.new
|
61
|
+
C.ioctl_p(@f.fileno, TIOCMGET, ptr)
|
62
|
+
if logger&.level <= Logger::DEBUG
|
63
|
+
flags = []
|
64
|
+
%w(DTR RTS CTS).each do |bit|
|
65
|
+
if ptr[:value] & self.class.const_get("TIOCM_#{bit}") > 0
|
66
|
+
flags << bit
|
67
|
+
end
|
68
|
+
end
|
69
|
+
if flags.empty?
|
70
|
+
flags = ['(none)']
|
71
|
+
end
|
72
|
+
logger&.debug("Initial flags: #{flags.join(' ')}")
|
73
|
+
end
|
74
|
+
unless ptr[:value] & TIOCM_RTS
|
75
|
+
logger&.debug("Setting RTS on #{device}")
|
76
|
+
ptr[:value] |= TIOCM_RTS
|
77
|
+
C.ioctl_p(@f.fileno, TIOCMSET, ptr)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
class IntPtr < FFI::Struct
|
84
|
+
layout :value, :int
|
85
|
+
end
|
86
|
+
|
87
|
+
module C
|
88
|
+
extend FFI::Library
|
89
|
+
ffi_lib 'c'
|
90
|
+
|
91
|
+
# Ruby's ioctl doesn't support all of C ioctl interface,
|
92
|
+
# in particular returning integer values that we need.
|
93
|
+
# See https://stackoverflow.com/questions/1446806/getting-essid-via-ioctl-in-ruby.
|
94
|
+
attach_function :ioctl, [:int, :int, :pointer], :int
|
95
|
+
class << self
|
96
|
+
alias :ioctl_p :ioctl
|
97
|
+
end
|
98
|
+
remove_method :ioctl
|
99
|
+
end
|
100
|
+
|
101
|
+
TIOCMGET = 0x5415
|
102
|
+
TIOCMSET = 0x5418
|
103
|
+
TIOCM_DTR = 0x002
|
104
|
+
TIOCM_RTS = 0x004
|
105
|
+
TIOCM_CTS = 0x020
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
require 'serialport'
|
5
|
+
|
6
|
+
module Seriamp
|
7
|
+
module Backend
|
8
|
+
module SerialPortBackend
|
9
|
+
|
10
|
+
class Device
|
11
|
+
extend Forwardable
|
12
|
+
|
13
|
+
def initialize(device, logger: nil)
|
14
|
+
@logger = logger
|
15
|
+
@io = SerialPort.open(device)
|
16
|
+
|
17
|
+
if block_given?
|
18
|
+
begin
|
19
|
+
yield self
|
20
|
+
ensure
|
21
|
+
close rescue nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_reader :device
|
27
|
+
|
28
|
+
attr_reader :io
|
29
|
+
|
30
|
+
def_delegators :io, :close, :sysread, :syswrite
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'timeout'
|
4
|
+
require 'seriamp/error'
|
5
|
+
|
6
|
+
module Seriamp
|
7
|
+
|
8
|
+
DEFAULT_DEVICE_GLOB = '/dev/ttyUSB*'
|
9
|
+
|
10
|
+
module_function def detect_device(mod, *patterns, logger: nil)
|
11
|
+
if patterns.empty?
|
12
|
+
patterns = [DEFAULT_DEVICE_GLOB]
|
13
|
+
end
|
14
|
+
devices = patterns.map do |pattern|
|
15
|
+
Dir.glob(pattern)
|
16
|
+
end.flatten.uniq
|
17
|
+
queue = Queue.new
|
18
|
+
timeout = mod.const_get(:RS232_TIMEOUT)
|
19
|
+
client_cls = mod.const_get(:Client)
|
20
|
+
threads = devices.map do |device|
|
21
|
+
Thread.new do
|
22
|
+
Timeout.timeout(timeout * 2, CommunicationTimeout) do
|
23
|
+
logger&.debug("Trying #{device}")
|
24
|
+
client_cls.new(device: device, logger: logger).present?
|
25
|
+
logger&.debug("Found #{mod} device at #{device}")
|
26
|
+
queue << device
|
27
|
+
end
|
28
|
+
rescue CommunicationTimeout, IOError, SystemCallError => exc
|
29
|
+
logger&.debug("Failed on #{mod} #{device}: #{exc.class}: #{exc}")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
wait_thread = Thread.new do
|
33
|
+
threads.each do |thread|
|
34
|
+
# Unhandled exceptions raised in threads are reraised by the join method
|
35
|
+
thread.join rescue nil
|
36
|
+
end
|
37
|
+
queue << nil
|
38
|
+
end
|
39
|
+
queue.shift.tap do
|
40
|
+
threads.map(&:kill)
|
41
|
+
wait_thread.kill
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Seriamp
|
2
|
+
class Error < StandardError; end
|
3
|
+
class NoDevice < Error; end
|
4
|
+
class BadDevice < Error; end
|
5
|
+
class InvalidCommand < Error; end
|
6
|
+
class NotApplicable < Error; end
|
7
|
+
class UnexpectedResponse < Error; end
|
8
|
+
class HandshakeFailure < UnexpectedResponse; end
|
9
|
+
class CommunicationTimeout < Error; end
|
10
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sinatra/base'
|
4
|
+
require 'seriamp/utils'
|
5
|
+
require 'seriamp/sonamp/client'
|
6
|
+
|
7
|
+
module Seriamp
|
8
|
+
module Sonamp
|
9
|
+
class App < Sinatra::Base
|
10
|
+
|
11
|
+
set :device, nil
|
12
|
+
set :logger, nil
|
13
|
+
set :client, nil
|
14
|
+
|
15
|
+
get '/power' do
|
16
|
+
render_json(client.get_zone_power)
|
17
|
+
end
|
18
|
+
|
19
|
+
get '/zone/:zone/power' do |zone|
|
20
|
+
render_json(client.get_zone_power(zone.to_i))
|
21
|
+
end
|
22
|
+
|
23
|
+
put '/zone/:zone/power' do |zone|
|
24
|
+
state = Utils.parse_on_off(request.body.read)
|
25
|
+
client.set_zone_power(zone.to_i, state)
|
26
|
+
end
|
27
|
+
|
28
|
+
get '/zone/:zone/volume' do |zone|
|
29
|
+
render_json(client.get_zone_volume(zone.to_i))
|
30
|
+
end
|
31
|
+
|
32
|
+
put '/zone/:zone/volume' do |zone|
|
33
|
+
volume = request.body.read.to_i
|
34
|
+
client.set_zone_volume(zone.to_i, volume)
|
35
|
+
end
|
36
|
+
|
37
|
+
get '/channel/:channel/volume' do |channel|
|
38
|
+
render_json(client.get_channel_volume(channel.to_i))
|
39
|
+
end
|
40
|
+
|
41
|
+
put '/channel/:channel/volume' do |channel|
|
42
|
+
volume = request.body.read.to_i
|
43
|
+
client.set_channel_volume(channel.to_i, volume)
|
44
|
+
end
|
45
|
+
|
46
|
+
post '/' do
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def client
|
52
|
+
settings.client || begin
|
53
|
+
@client ||= Sonamp::Client.new(settings.device, logger: settings.logger)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def render_json(data)
|
58
|
+
data.to_json
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|