seriamp 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|