seriamp 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,3 @@
1
+ .yardoc
2
+ *.gem
3
+ /doc
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
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'seriamp/sonamp/cmd'
5
+ rescue LoadError
6
+ $: << File.join(File.dirname(__FILE__), '../lib')
7
+ require 'seriamp/sonamp/cmd'
8
+ end
9
+
10
+ Seriamp::Sonamp::Cmd.new(ARGV).run
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
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # frozen_string_literal: true
4
+
5
+ begin
6
+ require 'seriamp/yamaha/cmd'
7
+ rescue LoadError
8
+ $: << File.join(File.dirname(__FILE__), '../lib')
9
+ require 'seriamp/yamaha/cmd'
10
+ end
11
+
12
+ Seriamp::Yamaha::Cmd.new(ARGV).run
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