onkyo_eiscp_ruby 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +60 -26
- data/VERSION +1 -1
- data/bin/onkyo-server.rb +2 -2
- data/bin/onkyo.rb +9 -7
- data/lib/eiscp/command.rb +31 -12
- data/lib/eiscp/message.rb +99 -0
- data/lib/eiscp/mock_receiver.rb +22 -0
- data/lib/eiscp/receiver.rb +180 -0
- data/lib/eiscp.rb +5 -6
- data/onkyo_eiscp_ruby.gemspec +2 -1
- data/test/tc_command.rb +17 -4
- data/test/tc_message.rb +27 -0
- data/test/{tc_eiscp.rb → tc_receiver.rb} +1 -1
- metadata +21 -27
- data/lib/eiscp/eiscp.rb +0 -125
- data/lib/eiscp/eiscp_packet.rb +0 -51
- data/lib/eiscp/eiscp_server.rb +0 -21
- data/lib/eiscp/iscp_message.rb +0 -24
- data/test/tc_eiscp_packet.rb +0 -17
- data/test/tc_iscp_message.rb +0 -14
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: be075bcd7a1830e7614b8384d610ea59d0b7160f
|
4
|
+
data.tar.gz: 40cea299c77c937a786f387cfbf4c11e8696632c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5066d43a66350cb39e84ddbae87038d4ba04057f2dd884eee1852c3dd108ed4a44842878157dfb854e7802d3e771cbe5789a70af936af0e338a36264e92332d6
|
7
|
+
data.tar.gz: 6c2c8bb4ba7d3b7e9fee332d448a054cf97480194192252965b363b7efaa8d8b095ae76d2f68ab6f064fbf68ec2bac75c63b2ba53a26e891b055c0751f776cc4
|
data/README.md
CHANGED
@@ -1,41 +1,75 @@
|
|
1
1
|
onkyo_eiscp_ruby
|
2
2
|
================
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/onkyo_eiscp_ruby.png)](http://badge.fury.io/rb/onkyo_eiscp_ruby)
|
3
4
|
|
4
5
|
A Ruby implementation of eISCP for controlling Onkyo receivers.
|
5
6
|
|
6
7
|
**This code is still under heavy development and using it might make you sick.**
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
8
|
+
Create ISCP messages and eISCP packets
|
9
|
+
Automatically discover receiver's in the broadcast domain
|
10
|
+
Send/Recieve eISCP messages
|
11
|
+
Open a TCP socket to send commands and receive solicited and non-solicited status updates.
|
12
|
+
Mock reciever (currently only responds to discovery)
|
12
13
|
|
13
14
|
**Inspired by https://github.com/miracle2k/onkyo-eiscp
|
14
15
|
|
15
|
-
|
16
|
-
________________
|
16
|
+
**Protocol information from http://michael.elsdoerfer.name/onkyo/ISCP-V1.21_2011.xls
|
17
17
|
|
18
|
-
# require the library
|
19
18
|
|
20
|
-
require 'eiscp'
|
21
19
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
# Open a TCP connection to monitor solicited updates
|
28
|
-
eiscp = EISCP.new('10.0.0.1')
|
29
|
-
eiscp.connect
|
20
|
+
Using the Library
|
21
|
+
-----------------
|
22
|
+
* require the library
|
30
23
|
|
31
|
-
|
32
|
-
eiscp.connect do |data|
|
33
|
-
puts EISCPPacket.parse(data).iscp_message
|
34
|
-
end
|
24
|
+
require 'eiscp'
|
35
25
|
|
36
|
-
|
37
|
-
iscp_message = ISCPMessage.new("PWR", "01")
|
38
|
-
eiscp_packet = EISCPPacket.new(iscp_message.message)
|
39
|
-
eiscp.send(eiscp_packet.to_s)
|
40
|
-
|
26
|
+
* Discover local receivers
|
41
27
|
|
28
|
+
EISCP::Receiver.discover
|
29
|
+
|
30
|
+
* Create Receiver object from first discovered
|
31
|
+
|
32
|
+
Receiver.new
|
33
|
+
|
34
|
+
* Open a TCP connection to monitor solicited updates
|
35
|
+
|
36
|
+
receiver = Receiver.new('10.0.0.1')
|
37
|
+
receiver.connect
|
38
|
+
|
39
|
+
* You can also pass a block and operate on received packet strings:
|
40
|
+
|
41
|
+
receiver.connect do |data|
|
42
|
+
puts EISCP::Receiver.parse(data).iscp_message
|
43
|
+
end
|
44
|
+
|
45
|
+
* Turn on the receiver
|
46
|
+
|
47
|
+
message = EISCP::Message.parse("PWR", "01")
|
48
|
+
message.send(message.to_eiscp)
|
49
|
+
|
50
|
+
* New 'parse' method makes creating EISCP objects more flexible.
|
51
|
+
This parses messages from command line or raw eiscp data from the socket
|
52
|
+
|
53
|
+
iscp_message = EISCP::Message.parse "PWR01"
|
54
|
+
iscp_message = EISCP::Message.parse "PWR 01"
|
55
|
+
iscp_message = EISCP::Message.parse "!1PWR01"
|
56
|
+
iscp_message = EISCP::Message.parse "!1PWR 01"
|
57
|
+
|
58
|
+
* Parsing raw socket data
|
59
|
+
|
60
|
+
iscp_message_from_raw_eiscp = EISCP::Message.parse iscp_message.to_eiscp
|
61
|
+
|
62
|
+
Using the Binaries
|
63
|
+
------------------
|
64
|
+
|
65
|
+
* Discover local receivers
|
66
|
+
|
67
|
+
$ onkyo.rb -d
|
68
|
+
|
69
|
+
* Connect to the first discovered receiver to see status updates
|
70
|
+
|
71
|
+
$ onkyo.rb -c
|
72
|
+
|
73
|
+
* Start the mock server (only responds to 'ECNQSTN')
|
74
|
+
|
75
|
+
$ onkyo-server.rb
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.3
|
data/bin/onkyo-server.rb
CHANGED
data/bin/onkyo.rb
CHANGED
@@ -3,7 +3,6 @@
|
|
3
3
|
require 'eiscp'
|
4
4
|
require 'optparse'
|
5
5
|
require 'ostruct'
|
6
|
-
|
7
6
|
class Options
|
8
7
|
DEFAULT_OPTIONS = { verbose: true, all: false }
|
9
8
|
USAGE = ' Usage: onkyo_rb [options]'
|
@@ -48,8 +47,8 @@ class Options
|
|
48
47
|
end
|
49
48
|
|
50
49
|
if @options.discover
|
51
|
-
EISCP.discover.each do |receiver|
|
52
|
-
puts
|
50
|
+
EISCP::Receiver.discover.each do |receiver|
|
51
|
+
puts EISCP::Message.parse(receiver[0]).to_iscp
|
53
52
|
end
|
54
53
|
exit 0
|
55
54
|
end
|
@@ -60,8 +59,10 @@ class Options
|
|
60
59
|
end
|
61
60
|
|
62
61
|
if @options.connect
|
63
|
-
eiscp = EISCP.new(EISCP.discover[0][1])
|
64
|
-
eiscp.connect
|
62
|
+
eiscp = EISCP::Receiver.new(EISCP::Receiver.discover[0][1])
|
63
|
+
eiscp.connect do |data|
|
64
|
+
puts msg = EISCP::Receiver.parse(data).to_iscp
|
65
|
+
end
|
65
66
|
end
|
66
67
|
|
67
68
|
if ARGV == []
|
@@ -76,8 +77,9 @@ end
|
|
76
77
|
@options = Options.parse(ARGV)
|
77
78
|
|
78
79
|
|
79
|
-
|
80
|
-
|
80
|
+
receiver = EISCP::Receiver.new(EISCP::Receiver.discover[0][1])
|
81
|
+
message = (EISCP::Message.parse(ARGV.join(" ")).to_eiscp)
|
82
|
+
puts receiver.send_recv message
|
81
83
|
|
82
84
|
|
83
85
|
|
data/lib/eiscp/command.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'yaml'
|
2
|
-
require 'eiscp/
|
2
|
+
require 'eiscp/receiver'
|
3
3
|
require 'ostruct'
|
4
4
|
|
5
5
|
module Command
|
@@ -11,19 +11,12 @@ module Command
|
|
11
11
|
@@zones = @@yaml_object.map{|k, v| k}
|
12
12
|
@@zones.each {|zone| class_variable_set("@@#{zone}", nil) }
|
13
13
|
@@main = @@yaml_object['main']
|
14
|
-
|
15
|
-
|
16
|
-
@@zones.each do |zone|
|
17
|
-
Command.class_variable_set("@@#{zone}", "[]")
|
18
|
-
end
|
19
|
-
|
20
|
-
|
21
14
|
|
22
15
|
def self.command_to_name(command)
|
23
16
|
return @@main[command]['name']
|
24
17
|
end
|
25
18
|
|
26
|
-
def self.
|
19
|
+
def self.command_name_to_command(name)
|
27
20
|
@@main.each_pair do |command, attrs|
|
28
21
|
if attrs['name'] == name
|
29
22
|
return command
|
@@ -44,10 +37,10 @@ module Command
|
|
44
37
|
end
|
45
38
|
|
46
39
|
|
47
|
-
def self.
|
40
|
+
def self.description_from_command_name(name)
|
48
41
|
@@main.each_pair do |command, attrs|
|
49
42
|
if attrs['name'] == name
|
50
|
-
return command['description']
|
43
|
+
return @@main[command]['description']
|
51
44
|
end
|
52
45
|
end
|
53
46
|
end
|
@@ -73,8 +66,34 @@ module Command
|
|
73
66
|
end
|
74
67
|
end
|
75
68
|
|
76
|
-
def self.list_compatible_commands
|
69
|
+
def self.list_compatible_commands(modelstring)
|
70
|
+
sets = []
|
71
|
+
@@modelsets.each_pair do |set, array|
|
72
|
+
if array.include? modelstring
|
73
|
+
sets << set
|
74
|
+
end
|
75
|
+
end
|
76
|
+
return sets
|
77
|
+
end
|
77
78
|
|
79
|
+
def self.parse(string)
|
80
|
+
array = string.split(" ")
|
81
|
+
zone = 'main'
|
82
|
+
command_name = ''
|
83
|
+
parameter_name = ''
|
84
|
+
if array.count == 3
|
85
|
+
zone = array.shift
|
86
|
+
command_name = array.shift
|
87
|
+
parameter_name = array.shift
|
88
|
+
elsif array.count == 2
|
89
|
+
command_name = array.shift
|
90
|
+
parameter_name = array.shift
|
91
|
+
end
|
92
|
+
command = Command.command_name_to_command(command_name)
|
93
|
+
parameter = Command.command_value_name_to_value(command, parameter_name)
|
94
|
+
return EISCP::Message.new(command, parameter)
|
78
95
|
end
|
96
|
+
|
79
97
|
end
|
80
98
|
|
99
|
+
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module EISCP
|
2
|
+
class Message
|
3
|
+
|
4
|
+
# EISCP header
|
5
|
+
attr_accessor :header
|
6
|
+
MAGIC = "ISCP"
|
7
|
+
HEADER_SIZE = 16
|
8
|
+
ISCP_VERSION = "\x01"
|
9
|
+
RESERVED = "\x00\x00\x00"
|
10
|
+
|
11
|
+
|
12
|
+
# ISCP attrs
|
13
|
+
attr_accessor :start
|
14
|
+
attr_accessor :unit_type
|
15
|
+
attr_accessor :command
|
16
|
+
attr_accessor :parameter
|
17
|
+
attr_reader :iscp_message
|
18
|
+
|
19
|
+
|
20
|
+
# REGEX
|
21
|
+
REGEX = /(?<start>!)?(?<unit_type>(\d|x))?(?<command>[A-Z]{3})\s?(?<parameter>.*)(?<end>\x1A)?/
|
22
|
+
|
23
|
+
def initialize(command, parameter, unit_type = "1", start = "!")
|
24
|
+
if unit_type == nil
|
25
|
+
@unit_type = "1"
|
26
|
+
else
|
27
|
+
@unit_type = unit_type
|
28
|
+
end
|
29
|
+
if start == nil
|
30
|
+
@start = "!"
|
31
|
+
else
|
32
|
+
@start = start
|
33
|
+
end
|
34
|
+
@command = command
|
35
|
+
@parameter = parameter
|
36
|
+
@iscp_message = [ @start, @unit_type, @command, @parameter ].inject(:+)
|
37
|
+
@header = { :magic => MAGIC,
|
38
|
+
:header_size => HEADER_SIZE,
|
39
|
+
:data_size => @iscp_message.length,
|
40
|
+
:version => ISCP_VERSION,
|
41
|
+
:reserved => RESERVED
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
# Check if two messages send the same command
|
47
|
+
def ==(message_object)
|
48
|
+
self.iscp_message == message_object.iscp_message ? true : false
|
49
|
+
end
|
50
|
+
# Identifies message format, calls appropriate parse function
|
51
|
+
# returns Message object.
|
52
|
+
|
53
|
+
def self.parse(string)
|
54
|
+
case string
|
55
|
+
when /^ISCP/
|
56
|
+
parse_eiscp_string(string)
|
57
|
+
when REGEX
|
58
|
+
parse_iscp_message(string)
|
59
|
+
else
|
60
|
+
puts "Not a valid ISCP or EISCP message."
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
|
66
|
+
# ISCP Message string parser
|
67
|
+
|
68
|
+
def self.parse_iscp_message(msg_string)
|
69
|
+
match = msg_string.match(REGEX)
|
70
|
+
Message.new(match[:command], match[:parameter], match[:unit_type], match[:start])
|
71
|
+
end
|
72
|
+
|
73
|
+
#parse eiscp_message string
|
74
|
+
def self.parse_eiscp_string(eiscp_message_string)
|
75
|
+
array = eiscp_message_string.unpack("A4NNAa3A*")
|
76
|
+
iscp_message = Message.parse_iscp_message(array[5])
|
77
|
+
packet = Message.new(iscp_message.command, iscp_message.parameter, iscp_message.unit_type, iscp_message.start)
|
78
|
+
packet.header = {
|
79
|
+
:magic => array[0],
|
80
|
+
:header_size => array[1],
|
81
|
+
:data_size => array[2],
|
82
|
+
:version => array[3],
|
83
|
+
:reserved => array[4]
|
84
|
+
}
|
85
|
+
return packet
|
86
|
+
end
|
87
|
+
|
88
|
+
# Return ISCP Message string
|
89
|
+
def to_iscp
|
90
|
+
return "#{@start + @unit_type + @command + @parameter}"
|
91
|
+
end
|
92
|
+
|
93
|
+
# Return EISCP Message string
|
94
|
+
def to_eiscp
|
95
|
+
return [ @header[:magic], @header[:header_size], @header[:data_size], @header[:version], @header[:reserved], @iscp_message.to_s ].pack("A4NNAa3A*")
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'eiscp/receiver'
|
3
|
+
require 'eiscp/message'
|
4
|
+
|
5
|
+
# Mock server that only responds to ECNQSTN.
|
6
|
+
|
7
|
+
module EISCP
|
8
|
+
class MockReceiver
|
9
|
+
|
10
|
+
ONKYO_DISCOVERY_RESPONSE = Message.new("ECN", "TX-NR609/60128/DX/001122334455")
|
11
|
+
|
12
|
+
# Create/start the server object.
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
Socket.udp_server_loop("255.255.255.255", EISCP::ONKYO_PORT) do |msg, msg_src|
|
16
|
+
msg_src.reply ONKYO_DISCOVERY_RESPONSE.to_eiscp
|
17
|
+
puts msg
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'eiscp/message'
|
3
|
+
require 'resolv'
|
4
|
+
|
5
|
+
module EISCP
|
6
|
+
class Receiver
|
7
|
+
|
8
|
+
attr_accessor :host
|
9
|
+
attr_accessor :model
|
10
|
+
attr_accessor :port
|
11
|
+
attr_accessor :area
|
12
|
+
attr_accessor :mac_address
|
13
|
+
|
14
|
+
ONKYO_MAGIC = Message.new("ECN", "QSTN", "x").to_eiscp
|
15
|
+
ONKYO_PORT = 60128
|
16
|
+
|
17
|
+
# Create a new EISCP object to communicate with a receiver.
|
18
|
+
# If no host is given, use auto discovery and create a
|
19
|
+
# receiver object using the first host to respond.
|
20
|
+
|
21
|
+
def initialize(host = nil, port = ONKYO_PORT)
|
22
|
+
if host == nil
|
23
|
+
if first_rec = self.class.discover[0]
|
24
|
+
host = first_rec[1]
|
25
|
+
set_info first_rec[0]
|
26
|
+
else
|
27
|
+
raise Exception
|
28
|
+
end
|
29
|
+
end
|
30
|
+
@host = Resolv.getaddress host
|
31
|
+
@port = port
|
32
|
+
unless @model
|
33
|
+
set_info get_ecn
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def set_info(ecn_string)
|
38
|
+
array = self.class.parse_ecn(ecn_string)
|
39
|
+
@model = array.shift
|
40
|
+
@port = array.shift.to_i
|
41
|
+
@area = array.shift
|
42
|
+
@mac_address = array.shift.split("\x19")[0]
|
43
|
+
return self
|
44
|
+
end
|
45
|
+
|
46
|
+
def get_ecn
|
47
|
+
self.class.discover.each do |entry|
|
48
|
+
if @host == entry[1]
|
49
|
+
return entry[0]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Gets the ECNQSTN response of self using @host
|
55
|
+
# then parses it with parse_ecn, returning an array
|
56
|
+
# with receiver info
|
57
|
+
|
58
|
+
def get_ecn_array
|
59
|
+
self.class.discover.each do |entry|
|
60
|
+
if @host == entry[1]
|
61
|
+
array = self.class.parse_ecn(entry[0])
|
62
|
+
end
|
63
|
+
return array
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns array containing @model, @port, @area, and @mac_address
|
68
|
+
# from ECNQSTN response
|
69
|
+
|
70
|
+
def self.parse_ecn(ecn_string)
|
71
|
+
message = EISCP::Message.parse(ecn_string)
|
72
|
+
message.parameter.split("/")
|
73
|
+
end
|
74
|
+
|
75
|
+
# Internal method for receiving data with a timeout
|
76
|
+
|
77
|
+
def self.recv(sock, timeout = 0.5)
|
78
|
+
data = []
|
79
|
+
while true
|
80
|
+
ready = IO.select([sock], nil, nil, timeout)
|
81
|
+
if ready != nil
|
82
|
+
then readable = ready[0]
|
83
|
+
else
|
84
|
+
return data
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
readable.each do |socket|
|
89
|
+
begin
|
90
|
+
if socket == sock
|
91
|
+
data << sock.recv_nonblock(1024).chomp
|
92
|
+
end
|
93
|
+
rescue IO::WaitReadable
|
94
|
+
retry
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Returns an array of arrays consisting of a discovery response packet string
|
102
|
+
# and the source ip address of the reciever.
|
103
|
+
|
104
|
+
def self.discover
|
105
|
+
sock = UDPSocket.new
|
106
|
+
sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_BROADCAST, true)
|
107
|
+
sock.send(ONKYO_MAGIC, 0, '<broadcast>', ONKYO_PORT)
|
108
|
+
data = []
|
109
|
+
while true
|
110
|
+
ready = IO.select([sock], nil, nil, 0.5)
|
111
|
+
if ready != nil
|
112
|
+
then readable = ready[0]
|
113
|
+
else
|
114
|
+
return data
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
readable.each do |socket|
|
119
|
+
begin
|
120
|
+
if socket == sock
|
121
|
+
msg, addr = sock.recvfrom_nonblock(1024)
|
122
|
+
data << [msg, addr[2]]
|
123
|
+
end
|
124
|
+
rescue IO::WaitReadable
|
125
|
+
retry
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Sends a packet string on the network
|
133
|
+
|
134
|
+
def send(eiscp_packet)
|
135
|
+
sock = TCPSocket.new @host, @port
|
136
|
+
sock.puts eiscp_packet
|
137
|
+
sock.close
|
138
|
+
end
|
139
|
+
|
140
|
+
# Send a packet string and return recieved data string.
|
141
|
+
|
142
|
+
def send_recv(eiscp_packet)
|
143
|
+
sock = TCPSocket.new @host, @port
|
144
|
+
sock.puts eiscp_packet
|
145
|
+
return Receiver.recv(sock, 0.5)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Open a TCP connection to the host and print all received messages until
|
149
|
+
# killed.
|
150
|
+
|
151
|
+
def connect(&block)
|
152
|
+
sock = TCPSocket.new @host, @port
|
153
|
+
while true
|
154
|
+
ready = IO.select([sock], nil, nil, nil)
|
155
|
+
if ready != nil
|
156
|
+
then readable = ready[0]
|
157
|
+
else
|
158
|
+
return
|
159
|
+
end
|
160
|
+
|
161
|
+
readable.each do |socket|
|
162
|
+
begin
|
163
|
+
if socket == sock
|
164
|
+
data = sock.recv_nonblock(1024).chomp
|
165
|
+
if block_given?
|
166
|
+
yield data
|
167
|
+
else
|
168
|
+
puts data
|
169
|
+
end
|
170
|
+
end
|
171
|
+
rescue IO::WaitReadable
|
172
|
+
retry
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
end
|
180
|
+
end
|
data/lib/eiscp.rb
CHANGED
@@ -1,10 +1,9 @@
|
|
1
|
-
#
|
1
|
+
# Library for controlling Onkyo receivers over TCP/IP.
|
2
2
|
|
3
|
-
|
4
|
-
VERSION = '0.0.
|
3
|
+
module EISCP
|
4
|
+
VERSION = '0.0.3'
|
5
5
|
end
|
6
6
|
|
7
|
-
require 'eiscp/
|
8
|
-
require 'eiscp/
|
9
|
-
require 'eiscp/iscp_message'
|
7
|
+
require 'eiscp/receiver'
|
8
|
+
require 'eiscp/message'
|
10
9
|
require 'eiscp/command'
|
data/onkyo_eiscp_ruby.gemspec
CHANGED
@@ -11,7 +11,8 @@ Gem::Specification.new do |s|
|
|
11
11
|
s.homepage = "https://github.com/mikerodrigues/onkyo_eiscp_ruby"
|
12
12
|
|
13
13
|
s.description = %q(
|
14
|
-
Use the provided binary script or
|
14
|
+
Control Onkyo receivers over the network.Use the provided binary script or
|
15
|
+
require the library for use in your scripts.
|
15
16
|
)
|
16
17
|
|
17
18
|
s.author = "Michael Rodrigues"
|
data/test/tc_command.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require_relative "../lib/eiscp/command.rb"
|
2
|
+
require_relative "../lib/eiscp/message.rb"
|
2
3
|
require "test/unit"
|
3
4
|
|
4
5
|
class TestCommand < Test::Unit::TestCase
|
@@ -7,8 +8,8 @@ class TestCommand < Test::Unit::TestCase
|
|
7
8
|
assert_equal(Command.command_to_name("PWR"), "system-power")
|
8
9
|
end
|
9
10
|
|
10
|
-
def
|
11
|
-
assert_equal(Command.
|
11
|
+
def test_command_name_to_command
|
12
|
+
assert_equal(Command.command_name_to_command("system-power"), "PWR")
|
12
13
|
end
|
13
14
|
|
14
15
|
def test_command_value_to_value_name
|
@@ -19,12 +20,24 @@ class TestCommand < Test::Unit::TestCase
|
|
19
20
|
assert_equal(Command.command_value_name_to_value("PWR", "on"), "01")
|
20
21
|
end
|
21
22
|
|
22
|
-
def
|
23
|
-
assert_equal(Command.
|
23
|
+
def test_description_from_command_name
|
24
|
+
assert_equal(Command.description_from_command_name("system-power"), "System Power Command")
|
24
25
|
end
|
25
26
|
|
26
27
|
def test_description_from_command
|
27
28
|
assert_equal(Command.description_from_command("PWR"), "System Power Command")
|
28
29
|
end
|
29
30
|
|
31
|
+
def test_parse_system_power
|
32
|
+
assert_equal(Command.parse('system-power on'), EISCP::Message.parse('PWR01'))
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_parse_zone2_system_power
|
36
|
+
assert_equal(Command.parse('zone2 power on'), EISCP::Message.parse('ZPW01'))
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_parse_volume_as_integer
|
40
|
+
assert_equal(Command.parse('main-volume 25'), EISCP::Message.parse('MVL19'))
|
41
|
+
end
|
42
|
+
|
30
43
|
end
|
data/test/tc_message.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require_relative "../lib/eiscp/message"
|
2
|
+
require "test/unit"
|
3
|
+
|
4
|
+
class TestMessage < Test::Unit::TestCase
|
5
|
+
|
6
|
+
|
7
|
+
DISCOVERY_PACKET = EISCP::Message.new('ECN', 'QSTN', 'x', '!')
|
8
|
+
DISCOVERY_STRING = DISCOVERY_PACKET.to_eiscp
|
9
|
+
|
10
|
+
|
11
|
+
def test_create_discovery_iscp_message
|
12
|
+
assert_equal(EISCP::Message.new("ECN", "QSTN", "x", "!").to_iscp, "!xECNQSTN")
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_parse_discovery_iscp_message
|
16
|
+
assert_equal(EISCP::Message.parse("!xECNQSTN").to_iscp, "!xECNQSTN")
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_create_discovery_packet_string
|
20
|
+
assert_equal(DISCOVERY_PACKET.to_eiscp, DISCOVERY_STRING)
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_parse_discovery_packet_string
|
24
|
+
assert_equal(EISCP::Message.parse(DISCOVERY_STRING).to_eiscp, DISCOVERY_PACKET.to_eiscp)
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
metadata
CHANGED
@@ -1,18 +1,17 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: onkyo_eiscp_ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
5
|
-
prerelease:
|
4
|
+
version: 0.0.3
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Michael Rodrigues
|
9
8
|
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date: 2014-01-
|
11
|
+
date: 2014-01-20 00:00:00.000000000 Z
|
13
12
|
dependencies: []
|
14
|
-
description:
|
15
|
-
in your scripts.\n "
|
13
|
+
description: "\n Control Onkyo receivers over the network.Use the provided binary
|
14
|
+
script or\n require the library for use in your scripts.\n "
|
16
15
|
email: mikebrodrigues@gmail.com
|
17
16
|
executables:
|
18
17
|
- onkyo.rb
|
@@ -21,49 +20,44 @@ extensions: []
|
|
21
20
|
extra_rdoc_files:
|
22
21
|
- README.md
|
23
22
|
files:
|
23
|
+
- README.md
|
24
|
+
- VERSION
|
24
25
|
- bin/onkyo-server.rb
|
25
26
|
- bin/onkyo.rb
|
26
|
-
-
|
27
|
-
- lib/eiscp/eiscp.rb
|
28
|
-
- lib/eiscp/eiscp_packet.rb
|
29
|
-
- lib/eiscp/command.rb
|
30
|
-
- lib/eiscp/iscp_message.rb
|
27
|
+
- eiscp-commands.yaml
|
31
28
|
- lib/eiscp.rb
|
32
|
-
-
|
33
|
-
-
|
34
|
-
-
|
35
|
-
-
|
36
|
-
- VERSION
|
29
|
+
- lib/eiscp/command.rb
|
30
|
+
- lib/eiscp/message.rb
|
31
|
+
- lib/eiscp/mock_receiver.rb
|
32
|
+
- lib/eiscp/receiver.rb
|
37
33
|
- onkyo_eiscp_ruby.gemspec
|
38
|
-
-
|
39
|
-
-
|
34
|
+
- test/tc_command.rb
|
35
|
+
- test/tc_message.rb
|
36
|
+
- test/tc_receiver.rb
|
40
37
|
homepage: https://github.com/mikerodrigues/onkyo_eiscp_ruby
|
41
38
|
licenses: []
|
39
|
+
metadata: {}
|
42
40
|
post_install_message:
|
43
41
|
rdoc_options: []
|
44
42
|
require_paths:
|
45
43
|
- lib
|
46
44
|
required_ruby_version: !ruby/object:Gem::Requirement
|
47
|
-
none: false
|
48
45
|
requirements:
|
49
|
-
- -
|
46
|
+
- - ">="
|
50
47
|
- !ruby/object:Gem::Version
|
51
48
|
version: '0'
|
52
49
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
|
-
none: false
|
54
50
|
requirements:
|
55
|
-
- -
|
51
|
+
- - ">="
|
56
52
|
- !ruby/object:Gem::Version
|
57
53
|
version: '0'
|
58
54
|
requirements: []
|
59
55
|
rubyforge_project:
|
60
|
-
rubygems_version:
|
56
|
+
rubygems_version: 2.2.0
|
61
57
|
signing_key:
|
62
|
-
specification_version:
|
58
|
+
specification_version: 4
|
63
59
|
summary: Manipulate Onkyo stereos with the eISCP protocol
|
64
60
|
test_files:
|
61
|
+
- test/tc_message.rb
|
62
|
+
- test/tc_receiver.rb
|
65
63
|
- test/tc_command.rb
|
66
|
-
- test/tc_iscp_message.rb
|
67
|
-
- test/tc_eiscp.rb
|
68
|
-
- test/tc_eiscp_packet.rb
|
69
|
-
has_rdoc:
|
data/lib/eiscp/eiscp.rb
DELETED
@@ -1,125 +0,0 @@
|
|
1
|
-
require 'socket'
|
2
|
-
require 'eiscp/eiscp_packet.rb'
|
3
|
-
require 'eiscp/iscp_message.rb'
|
4
|
-
|
5
|
-
class EISCP
|
6
|
-
ONKYO_PORT = 60128
|
7
|
-
ONKYO_MAGIC = EISCPPacket.new("ECN", "QSTN", "x").to_s
|
8
|
-
|
9
|
-
# Create a new EISCP object to communicate with a receiver.
|
10
|
-
|
11
|
-
def initialize(host)
|
12
|
-
@host = host
|
13
|
-
end
|
14
|
-
|
15
|
-
# Internal method for receiving data with a timeout
|
16
|
-
|
17
|
-
def self.recv(sock, timeout = 0.5)
|
18
|
-
data = []
|
19
|
-
while true
|
20
|
-
ready = IO.select([sock], nil, nil, timeout)
|
21
|
-
if ready != nil
|
22
|
-
then readable = ready[0]
|
23
|
-
else
|
24
|
-
return data
|
25
|
-
end
|
26
|
-
|
27
|
-
|
28
|
-
readable.each do |socket|
|
29
|
-
begin
|
30
|
-
if socket == sock
|
31
|
-
data << sock.recv_nonblock(1024).chomp
|
32
|
-
end
|
33
|
-
rescue IO::WaitReadable
|
34
|
-
retry
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
# Returns an array of arrays consisting of a discovery response packet string
|
42
|
-
# and the source ip address of the reciever.
|
43
|
-
|
44
|
-
def self.discover
|
45
|
-
sock = UDPSocket.new
|
46
|
-
sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_BROADCAST, true)
|
47
|
-
sock.send(ONKYO_MAGIC, 0, '<broadcast>', ONKYO_PORT)
|
48
|
-
data = []
|
49
|
-
while true
|
50
|
-
ready = IO.select([sock], nil, nil, 0.5)
|
51
|
-
if ready != nil
|
52
|
-
then readable = ready[0]
|
53
|
-
else
|
54
|
-
return data
|
55
|
-
end
|
56
|
-
|
57
|
-
|
58
|
-
readable.each do |socket|
|
59
|
-
begin
|
60
|
-
if socket == sock
|
61
|
-
msg, addr = sock.recvfrom_nonblock(1024)
|
62
|
-
data << [msg, addr[2]]
|
63
|
-
end
|
64
|
-
rescue IO::WaitReadable
|
65
|
-
retry
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
# Sends a packet string on the network
|
73
|
-
|
74
|
-
def send(eiscp_packet)
|
75
|
-
sock = TCPSocket.new @host, ONKYO_PORT
|
76
|
-
sock.puts eiscp_packet
|
77
|
-
sock.close
|
78
|
-
end
|
79
|
-
|
80
|
-
# Send a packet string and return recieved data string.
|
81
|
-
|
82
|
-
def send_recv(eiscp_packet)
|
83
|
-
sock = TCPSocket.new @host, ONKYO_PORT
|
84
|
-
sock.puts eiscp_packet
|
85
|
-
puts EISCP.recv(sock, 0.5)
|
86
|
-
end
|
87
|
-
|
88
|
-
# Open a TCP connection to the host and print all received messages until
|
89
|
-
# killed.
|
90
|
-
|
91
|
-
def connect(&block)
|
92
|
-
sock = TCPSocket.new @host, ONKYO_PORT
|
93
|
-
while true
|
94
|
-
ready = IO.select([sock], nil, nil, nil)
|
95
|
-
if ready != nil
|
96
|
-
then readable = ready[0]
|
97
|
-
else
|
98
|
-
return
|
99
|
-
end
|
100
|
-
|
101
|
-
readable.each do |socket|
|
102
|
-
begin
|
103
|
-
if socket == sock
|
104
|
-
data = sock.recv_nonblock(1024).chomp
|
105
|
-
if block_given?
|
106
|
-
yield data
|
107
|
-
else
|
108
|
-
puts data
|
109
|
-
end
|
110
|
-
end
|
111
|
-
rescue IO::WaitReadable
|
112
|
-
retry
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
end
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
data/lib/eiscp/eiscp_packet.rb
DELETED
@@ -1,51 +0,0 @@
|
|
1
|
-
require 'eiscp/iscp_message'
|
2
|
-
|
3
|
-
# Public: Encapsulates ISCP Messages in eISCP packets to send on the network.
|
4
|
-
# You can alsoe use the class method 'parse' to create objects from strings
|
5
|
-
# captured from the network using the EISCP class.
|
6
|
-
|
7
|
-
class EISCPPacket
|
8
|
-
|
9
|
-
MAGIC = "ISCP"
|
10
|
-
HEADER_SIZE = 16
|
11
|
-
VERSION = "\x01"
|
12
|
-
RESERVED = "\x00\x00\x00"
|
13
|
-
|
14
|
-
attr_accessor :header
|
15
|
-
attr_reader :iscp_message
|
16
|
-
|
17
|
-
# Create a new EISCPPacket object with a given command and parameter
|
18
|
-
# -+command+ - an ISCP command like "PWR" for "system-power"
|
19
|
-
# -+parameter+ - an ISCP command parameter like "01" for "on" for
|
20
|
-
# "system-power"
|
21
|
-
|
22
|
-
def initialize(command, parameter, unit_type = "1", start = "!")
|
23
|
-
@iscp_message = ISCPMessage.new(command, parameter, unit_type, start)
|
24
|
-
@header = { :magic => MAGIC, :header_size => HEADER_SIZE, :data_size => @iscp_message.to_s.length, :version => VERSION, :reserved => RESERVED }
|
25
|
-
end
|
26
|
-
|
27
|
-
# Returns a raw packet string for sending on the network using EISCP object.
|
28
|
-
|
29
|
-
def to_s
|
30
|
-
return [ @header[:magic], @header[:header_size], @header[:data_size], @header[:version], @header[:reserved], @iscp_message.to_s ].pack("A4NNAa3A*")
|
31
|
-
end
|
32
|
-
|
33
|
-
# Returns an EISCPPacket from a raw packet string
|
34
|
-
|
35
|
-
def self.parse(eiscp_message_string)
|
36
|
-
array = eiscp_message_string.unpack("A4NNAa3A*")
|
37
|
-
iscp_message = ISCPMessage.parse(array[5])
|
38
|
-
packet = EISCPPacket.new(iscp_message.command, iscp_message.parameter, iscp_message.unit_type, iscp_message.start)
|
39
|
-
packet.header = {
|
40
|
-
:magic => array[0],
|
41
|
-
:header_size => array[1],
|
42
|
-
:data_size => array[2],
|
43
|
-
:version => array[3],
|
44
|
-
:reserved => array[4]
|
45
|
-
}
|
46
|
-
return packet
|
47
|
-
end
|
48
|
-
|
49
|
-
end
|
50
|
-
|
51
|
-
|
data/lib/eiscp/eiscp_server.rb
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
require 'socket'
|
2
|
-
require 'eiscp/eiscp_packet'
|
3
|
-
require 'eiscp/eiscp'
|
4
|
-
|
5
|
-
# Mock server that only responds to ECNQSTN.
|
6
|
-
|
7
|
-
class EISCPServer
|
8
|
-
|
9
|
-
ONKYO_DISCOVERY_RESPONSE = EISCPPacket.new("ECN", "TX-NR609/60128/DX/001122334455")
|
10
|
-
|
11
|
-
# Create/start the server object.
|
12
|
-
|
13
|
-
def initialize
|
14
|
-
Socket.udp_server_loop("255.255.255.255", EISCP::ONKYO_PORT) do |msg, msg_src|
|
15
|
-
msg_src.reply ONKYO_DISCOVERY_RESPONSE.to_s
|
16
|
-
puts msg
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
end
|
21
|
-
|
data/lib/eiscp/iscp_message.rb
DELETED
@@ -1,24 +0,0 @@
|
|
1
|
-
class ISCPMessage
|
2
|
-
|
3
|
-
attr_accessor :start
|
4
|
-
attr_accessor :unit_type
|
5
|
-
attr_accessor :command
|
6
|
-
attr_accessor :parameter
|
7
|
-
|
8
|
-
def initialize(command, parameter, unit_type = "1", start = "!")
|
9
|
-
@unit_type = unit_type
|
10
|
-
@start = start
|
11
|
-
@command = command
|
12
|
-
@parameter = parameter
|
13
|
-
end
|
14
|
-
|
15
|
-
def self.parse(msg_string)
|
16
|
-
match = msg_string.match(/(?<start>!)(?<unit_type>\w)(?<command>[A-Z]{3})(?<parameter>\S+)/)
|
17
|
-
ISCPMessage.new(match[:command], match[:parameter], match[:unit_type], match[:start])
|
18
|
-
end
|
19
|
-
|
20
|
-
def to_s
|
21
|
-
return "#{@start + @unit_type + @command + @parameter}\r"
|
22
|
-
end
|
23
|
-
|
24
|
-
end
|
data/test/tc_eiscp_packet.rb
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
require_relative "../lib/eiscp/eiscp_packet.rb"
|
2
|
-
require "test/unit"
|
3
|
-
|
4
|
-
class TestEISCPPacket < Test::Unit::TestCase
|
5
|
-
|
6
|
-
DISCOVERY_STRING = "ISCP\x00\x00\x00\x10\x00\x00\x00\n\x01\x00\x00\x00!xECNQSTN\r"
|
7
|
-
DISCOVERY_PACKET = EISCPPacket.new('ECN', 'QSTN', 'x', '!')
|
8
|
-
|
9
|
-
def test_create_discovery_packet_string
|
10
|
-
assert_equal(DISCOVERY_PACKET.to_s, DISCOVERY_STRING)
|
11
|
-
end
|
12
|
-
|
13
|
-
def test_parse_discovery_packet_string
|
14
|
-
assert_equal(EISCPPacket.parse(DISCOVERY_STRING).to_s, DISCOVERY_PACKET.to_s)
|
15
|
-
end
|
16
|
-
|
17
|
-
end
|
data/test/tc_iscp_message.rb
DELETED
@@ -1,14 +0,0 @@
|
|
1
|
-
require_relative "../lib/eiscp/iscp_message.rb"
|
2
|
-
require "test/unit"
|
3
|
-
|
4
|
-
class TestISCPMessage < Test::Unit::TestCase
|
5
|
-
|
6
|
-
def test_create_discovery_iscp_message
|
7
|
-
assert_equal(ISCPMessage.new("ECN", "QSTN", "x", "!").to_s, "!xECNQSTN\r")
|
8
|
-
end
|
9
|
-
|
10
|
-
def test_parse_discovery_iscp_message
|
11
|
-
assert_equal(ISCPMessage.parse("!xECNQSTN\r").to_s, "!xECNQSTN\r")
|
12
|
-
end
|
13
|
-
|
14
|
-
end
|