onkyo_eiscp_ruby 2.0.0 → 2.1.5
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 +5 -5
- data/README.md +22 -8
- data/VERSION +1 -1
- data/bin/mock_receiver.rb +4 -1
- data/bin/onkyo.rb +22 -26
- data/bin/onkyo_server.rb +1 -0
- data/lib/eiscp.rb +3 -1
- data/lib/eiscp/dictionary.rb +12 -13
- data/lib/eiscp/dictionary/dictionary_generators.rb +17 -21
- data/lib/eiscp/dictionary/dictionary_helpers.rb +28 -25
- data/lib/eiscp/message.rb +11 -12
- data/lib/eiscp/parser.rb +3 -1
- data/lib/eiscp/parser/dynamic_value_parser.rb +2 -0
- data/lib/eiscp/parser/eiscp_parser.rb +10 -4
- data/lib/eiscp/parser/human_readable_parser.rb +10 -10
- data/lib/eiscp/parser/iscp_parser.rb +4 -5
- data/lib/eiscp/receiver.rb +27 -39
- data/lib/eiscp/receiver/command_methods.rb +7 -7
- data/lib/eiscp/receiver/discovery.rb +10 -11
- data/onkyo_eiscp_ruby.gemspec +8 -7
- data/test/tc_dictionary.rb +3 -1
- data/test/tc_message.rb +6 -1
- data/test/tc_parser.rb +3 -2
- data/test/tc_receiver.rb +2 -0
- metadata +5 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d35b01beb072ac7058ef54920b734aff8d4d098ecc702eb9edd21ee818ad7ca0
|
4
|
+
data.tar.gz: f1112ac834d03f1dea2e50aeed301ac8f0e802b55b00b0f317d7802515ffa84e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f3a50446a0df21efd8dc6b2397daeb0cf791a997e7f0c27bb9d01c98b9e08fdc99cd68176b464882b29acc64a5f9cbe0748da690da51269f10f8f41f3d75dfc7
|
7
|
+
data.tar.gz: 65bafa48d392b8578435f006de9b27b8e58b9f16d7ccd7e2d527b38f23c0e2db187204c79f9c2dfe121021907735b51f73b82cef8fb82789ac362b5c3fec228f
|
data/README.md
CHANGED
@@ -3,19 +3,24 @@ onkyo_eiscp_ruby
|
|
3
3
|
[](http://badge.fury.io/rb/onkyo_eiscp_ruby)
|
4
4
|
[](http://badge.fury.io/gh/mikerodrigues%2Fonkyo_eiscp_ruby)
|
5
5
|
|
6
|
-
*A Ruby implementation of eISCP for controlling Onkyo receivers.*
|
6
|
+
*A Ruby implementation of eISCP (ethernet Integra Serial Control Protocol) for controlling Onkyo receivers.*
|
7
7
|
|
8
|
-
**
|
8
|
+
**I'm still sort of updating this code. Please feel free to reach out if there's something you need that it doesn't do, I may be willing to help.**
|
9
9
|
|
10
|
-
|
10
|
+
**The python version linked below sees much more activity.**
|
11
11
|
|
12
|
-
Send commands to receivers and parse returned messages
|
13
12
|
|
14
|
-
|
13
|
+
Features
|
14
|
+
---------------
|
15
|
+
* Automatically discover receivers in the broadcast domain
|
16
|
+
|
17
|
+
* Send commands to receivers and parse returned messages
|
18
|
+
|
19
|
+
* Open a TCP socket to receive solicited and non-solicited status updates.
|
15
20
|
|
16
|
-
Mock reciever (currently only responds to discovery)
|
21
|
+
* Mock reciever (currently only responds to discovery)
|
17
22
|
|
18
|
-
Human-readable commands
|
23
|
+
* Human-readable commands
|
19
24
|
|
20
25
|
**Inspired by https://github.com/miracle2k/onkyo-eiscp
|
21
26
|
|
@@ -38,7 +43,11 @@ What's missing?
|
|
38
43
|
|
39
44
|
Using the Library
|
40
45
|
-----------------
|
41
|
-
*
|
46
|
+
* Install the library
|
47
|
+
|
48
|
+
gem install onkyo_eiscp_ruby
|
49
|
+
|
50
|
+
* Require the library
|
42
51
|
|
43
52
|
require 'eiscp'
|
44
53
|
|
@@ -160,6 +169,11 @@ Using the Library
|
|
160
169
|
|
161
170
|
# Set the master volume to 45
|
162
171
|
receiver.master_volume "45"
|
172
|
+
|
173
|
+
# Change the input to TV/CD
|
174
|
+
# Note: when a command value has more than one name (an array in the YAML file)
|
175
|
+
# we default to using the first entry. So for `['cd', 'tv', 'cd']` you get:
|
176
|
+
receiver.input_selector "cd"
|
163
177
|
```
|
164
178
|
|
165
179
|
* Parse ISCP and human readable strings:
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.
|
1
|
+
2.1.5
|
data/bin/mock_receiver.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'socket'
|
2
4
|
require_relative './receiver'
|
3
5
|
require_relative './message'
|
@@ -9,7 +11,8 @@ module EISCP
|
|
9
11
|
#
|
10
12
|
class MockReceiver
|
11
13
|
DISCOVERY_IP = '255.255.255.255'
|
12
|
-
ONKYO_DISCOVERY_RESPONSE =
|
14
|
+
ONKYO_DISCOVERY_RESPONSE = Message.new('ECN',
|
15
|
+
"TX-NR609/60128/DX/14DAE9E967C8\x19\r\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")
|
13
16
|
|
14
17
|
# Create/start the server object.
|
15
18
|
|
data/bin/onkyo.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
4
|
require 'eiscp'
|
4
5
|
require 'optparse'
|
@@ -7,14 +8,13 @@ require 'ostruct'
|
|
7
8
|
# This object parses ARGV and returns an @option hash
|
8
9
|
#
|
9
10
|
class Options
|
10
|
-
DEFAULT_OPTIONS = { verbose: true, all: false }
|
11
|
+
DEFAULT_OPTIONS = { verbose: true, all: false }.freeze
|
11
12
|
USAGE = ' Usage: onkyo_rb [options]'
|
12
13
|
|
13
14
|
def self.parse(args)
|
14
15
|
@options = OpenStruct.new
|
15
16
|
|
16
17
|
options = OptionParser.new do |opts|
|
17
|
-
|
18
18
|
opts.banner = USAGE
|
19
19
|
|
20
20
|
opts.on '-d', '--discover', 'Find Onkyo Receivers on the local broadcast domain' do |d|
|
@@ -38,17 +38,16 @@ class Options
|
|
38
38
|
end
|
39
39
|
|
40
40
|
opts.on '-m', '--monitor', 'Connect to the first discovered reciever and monitor updates' do |m|
|
41
|
-
@options.monitor = m
|
41
|
+
@options.monitor = m
|
42
42
|
end
|
43
|
-
|
44
43
|
end
|
45
44
|
|
46
45
|
options.parse!(args)
|
47
46
|
|
48
|
-
if @options.nil? && ARGV == []
|
47
|
+
puts options if @options.nil? && ARGV == []
|
49
48
|
|
50
49
|
if @options.discover
|
51
|
-
EISCP::Receiver.discover.each {|rec| puts "#{rec.host}:#{rec.port} - #{rec.model} - #{rec.mac_address}"}
|
50
|
+
EISCP::Receiver.discover.each { |rec| puts "#{rec.host}:#{rec.port} - #{rec.model} - #{rec.mac_address}" }
|
52
51
|
exit 0
|
53
52
|
end
|
54
53
|
|
@@ -59,7 +58,7 @@ class Options
|
|
59
58
|
|
60
59
|
if @options.monitor
|
61
60
|
begin
|
62
|
-
rec = EISCP::Receiver.new do |reply|
|
61
|
+
rec = EISCP::Receiver.new do |reply|
|
63
62
|
puts "#{Time.now} #{rec.host} "\
|
64
63
|
"#{reply.zone}: "\
|
65
64
|
"#{reply.command_description || reply.command} "\
|
@@ -67,9 +66,9 @@ class Options
|
|
67
66
|
end
|
68
67
|
rec.thread.join
|
69
68
|
rescue Interrupt
|
70
|
-
|
69
|
+
raise 'Exiting...'
|
71
70
|
rescue Exception => e
|
72
|
-
puts
|
71
|
+
puts 'bummer...'
|
73
72
|
puts e
|
74
73
|
end
|
75
74
|
end
|
@@ -79,28 +78,25 @@ class Options
|
|
79
78
|
modelsets = []
|
80
79
|
EISCP::Receiver.discover.each do |rec|
|
81
80
|
models << rec.model
|
82
|
-
end
|
81
|
+
end
|
83
82
|
models.each do |model|
|
84
83
|
EISCP::Dictionary.modelsets.each do |modelset, list|
|
85
|
-
|
86
|
-
modelsets << modelset
|
87
|
-
end
|
84
|
+
modelsets << modelset unless list.select { |x| x.match model }.empty?
|
88
85
|
end
|
89
86
|
end
|
90
87
|
EISCP::Dictionary.zones.each do |zone|
|
91
88
|
EISCP::Dictionary.commands[zone].each do |command, command_hash|
|
92
|
-
puts
|
89
|
+
puts 'Command - Description'
|
93
90
|
puts "\n"
|
94
91
|
puts " '#{Dictionary.command_to_name(command)}' - "\
|
95
92
|
"#{Dictionary.description_from_command(command)}"
|
96
93
|
puts "\n"
|
97
|
-
puts
|
94
|
+
puts ' Value - Description>'
|
98
95
|
puts "\n"
|
99
|
-
command_hash[:values].each do |
|
100
|
-
if
|
101
|
-
|
102
|
-
|
103
|
-
else
|
96
|
+
command_hash[:values].each do |_value, attr_hash|
|
97
|
+
if modelsets.include? attr_hash[:models]
|
98
|
+
puts " '#{EISCP::Dictionary.command_value_to_value_name(command, _value)}' - "\
|
99
|
+
" #{attr_hash[:description]}"
|
104
100
|
end
|
105
101
|
end
|
106
102
|
puts "\n"
|
@@ -111,16 +107,16 @@ class Options
|
|
111
107
|
if @options.list_all
|
112
108
|
EISCP::Dictionary.zones.each do |zone|
|
113
109
|
EISCP::Dictionary.commands[zone].each do |command, command_hash|
|
114
|
-
puts
|
110
|
+
puts 'Command - Description'
|
115
111
|
puts "\n"
|
116
112
|
puts " '#{Dictionary.command_to_name(command)}' - "\
|
117
113
|
"#{Dictionary.description_from_command(command)}"
|
118
114
|
puts "\n"
|
119
|
-
puts
|
115
|
+
puts ' Value - Description>'
|
120
116
|
puts "\n"
|
121
|
-
command_hash[:values].each do |
|
122
|
-
puts " '#{
|
123
|
-
|
117
|
+
command_hash[:values].each do |_value, attr_hash|
|
118
|
+
puts " '#{EISCP::Dictionary.command_value_to_value_name(command, _value)}' - "\
|
119
|
+
" #{attr_hash[:description]}"
|
124
120
|
end
|
125
121
|
puts "\n"
|
126
122
|
end
|
@@ -143,7 +139,7 @@ receiver = EISCP::Receiver.discover[0]
|
|
143
139
|
receiver.connect
|
144
140
|
begin
|
145
141
|
command = EISCP::Parser.parse(ARGV.join(' '))
|
146
|
-
rescue
|
142
|
+
rescue StandardError
|
147
143
|
raise "Couldn't parse command"
|
148
144
|
end
|
149
145
|
reply = receiver.send_recv(command)
|
data/bin/onkyo_server.rb
CHANGED
data/lib/eiscp.rb
CHANGED
data/lib/eiscp/dictionary.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative './dictionary/dictionary_generators'
|
2
4
|
require_relative './dictionary/dictionary_helpers'
|
3
5
|
|
@@ -12,14 +14,12 @@ module EISCP
|
|
12
14
|
extend DictionaryHelpers
|
13
15
|
|
14
16
|
class << self
|
15
|
-
attr_reader :zones
|
16
|
-
attr_reader :modelsets
|
17
|
-
attr_reader :commands
|
17
|
+
attr_reader :zones, :modelsets, :commands
|
18
18
|
end
|
19
19
|
|
20
20
|
DEFAULT_ZONE = 'main'
|
21
|
-
@yaml_file_path = File.join(
|
22
|
-
@commands = YAML.
|
21
|
+
@yaml_file_path = File.join(__dir__, '../../eiscp-commands.yaml')
|
22
|
+
@commands = YAML.safe_load(File.read(@yaml_file_path), permitted_classes: [Symbol])
|
23
23
|
@modelsets = @commands[:modelsets]
|
24
24
|
@commands.delete(:modelsets)
|
25
25
|
@zones = @commands.map { |k, _| k }
|
@@ -30,11 +30,12 @@ module EISCP
|
|
30
30
|
command = command[0]
|
31
31
|
@commands[zone][command][:values].each do |value|
|
32
32
|
value = value[0]
|
33
|
-
|
33
|
+
case value
|
34
|
+
when Array
|
34
35
|
@additions << [zone, command, value, create_range_commands(zone, command, value)]
|
35
|
-
|
36
|
+
when /^(B|T){xx}$/
|
36
37
|
@additions << [zone, command, value, create_treble_bass_commands(zone, command, value)]
|
37
|
-
|
38
|
+
when /^{xx}$/
|
38
39
|
@additions << [zone, command, value, create_balance_commands(zone, command, value)]
|
39
40
|
else
|
40
41
|
next
|
@@ -44,11 +45,9 @@ module EISCP
|
|
44
45
|
end
|
45
46
|
|
46
47
|
@additions.each do |zone, command, value, hash|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
puts "Failed to add #{hash} to #{zone}:#{command}:#{value}"
|
51
|
-
end
|
48
|
+
@commands[zone][command][:values].merge! hash
|
49
|
+
rescue StandardError
|
50
|
+
puts "Failed to add #{hash} to #{zone}:#{command}:#{value}"
|
52
51
|
end
|
53
52
|
end
|
54
53
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'yaml'
|
2
4
|
|
3
5
|
module EISCP
|
@@ -19,14 +21,12 @@ module EISCP
|
|
19
21
|
range.each do |number|
|
20
22
|
tmp.merge!(number.to_s(16).rjust(2, '0').upcase =>
|
21
23
|
{
|
22
|
-
:
|
23
|
-
:description
|
24
|
-
|
25
|
-
|
26
|
-
}
|
27
|
-
)
|
24
|
+
name: number.to_s,
|
25
|
+
description: @commands[zone][command][:values][value][:description].gsub(/\d - \d+/, number.to_s),
|
26
|
+
models: @commands[zone][command][:values][value][:models]
|
27
|
+
})
|
28
28
|
end
|
29
|
-
|
29
|
+
tmp
|
30
30
|
end
|
31
31
|
|
32
32
|
# Creates hash object for treble and bass commands
|
@@ -36,14 +36,12 @@ module EISCP
|
|
36
36
|
['-A', '-8', '-6', '-4', '-2', '00', '+2', '+4', '+6', '+8', '+A'].each do |v|
|
37
37
|
tmp.merge!((value[0] + v.to_s) =>
|
38
38
|
{
|
39
|
-
:
|
40
|
-
:description
|
41
|
-
|
42
|
-
|
43
|
-
}
|
44
|
-
)
|
39
|
+
name: value[0].downcase + v,
|
40
|
+
description: @commands[zone][command][:values]["#{value[0]}{xx}"][:description].gsub(/\(.*[\]|)]$/, v),
|
41
|
+
models: @commands[zone][command][:values]["#{value[0]}{xx}"][:models]
|
42
|
+
})
|
45
43
|
end
|
46
|
-
|
44
|
+
tmp
|
47
45
|
end
|
48
46
|
|
49
47
|
# Creates hash object for balance commands
|
@@ -53,14 +51,12 @@ module EISCP
|
|
53
51
|
['-A', '-8', '-6', '-4', '-2', '00', '+2', '+4', '+6', '+8', '+A'].each do |v|
|
54
52
|
tmp.merge!(v.to_s =>
|
55
53
|
{
|
56
|
-
:
|
57
|
-
:description
|
58
|
-
|
59
|
-
|
60
|
-
}
|
61
|
-
)
|
54
|
+
name: v.downcase,
|
55
|
+
description: @commands[zone][command][:values]['{xx}'][:description].gsub(/\(.*[\]|)]$/, v),
|
56
|
+
models: @commands[zone][command][:values]['{xx}'][:models]
|
57
|
+
})
|
62
58
|
end
|
63
|
-
|
59
|
+
tmp
|
64
60
|
end
|
65
61
|
end
|
66
62
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module EISCP
|
2
4
|
module Dictionary
|
3
5
|
# This module provides methods to get information from the Dictionary about
|
@@ -19,9 +21,9 @@ module EISCP
|
|
19
21
|
command = command.upcase
|
20
22
|
begin
|
21
23
|
zone = zone_from_command(command)
|
22
|
-
|
23
|
-
rescue
|
24
|
-
|
24
|
+
@commands[zone][command][:name]
|
25
|
+
rescue StandardError
|
26
|
+
nil
|
25
27
|
end
|
26
28
|
end
|
27
29
|
|
@@ -34,46 +36,50 @@ module EISCP
|
|
34
36
|
return command if attrs[:name] == name
|
35
37
|
end
|
36
38
|
end
|
37
|
-
|
39
|
+
nil
|
38
40
|
|
39
41
|
else
|
40
42
|
|
41
43
|
@commands[command_zone].each_pair do |command, attrs|
|
42
44
|
return command if attrs[:name] == name
|
43
45
|
end
|
44
|
-
|
46
|
+
nil
|
45
47
|
|
46
48
|
end
|
47
49
|
end
|
48
50
|
|
49
51
|
# Return a value name from a command and a value
|
50
52
|
def command_value_to_value_name(command, value)
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
53
|
+
zone = zone_from_command(command)
|
54
|
+
command_value = @commands[zone][command][:values][value][:name]
|
55
|
+
if command_value.instance_of?(String)
|
56
|
+
command_value
|
57
|
+
elsif command_value.instance_of?(Array)
|
58
|
+
command_value.first
|
56
59
|
end
|
60
|
+
rescue StandardError
|
61
|
+
nil
|
57
62
|
end
|
58
63
|
|
59
64
|
# Return a value from a command and value name
|
60
65
|
def command_value_name_to_value(command, value_name)
|
61
|
-
|
62
|
-
|
63
|
-
|
66
|
+
zone = zone_from_command(command)
|
67
|
+
@commands[zone][command][:values].each_pair do |k, v|
|
68
|
+
if v[:name].instance_of?(String)
|
64
69
|
return k if v[:name] == value_name.to_s
|
70
|
+
elsif v[:name].instance_of?(Array)
|
71
|
+
return k if v[:name].first == value_name.to_s
|
65
72
|
end
|
66
|
-
rescue
|
67
|
-
nil
|
68
73
|
end
|
74
|
+
nil
|
75
|
+
rescue StandardError
|
76
|
+
nil
|
69
77
|
end
|
70
78
|
|
71
79
|
# Return a command description from a command name and zone
|
72
80
|
def description_from_command_name(name, zone)
|
73
81
|
@commands[zone].each_pair do |command, attrs|
|
74
|
-
if attrs[:name] == name
|
75
|
-
return @commands[zone][command][:description]
|
76
|
-
end
|
82
|
+
return @commands[zone][command][:description] if attrs[:name] == name
|
77
83
|
end
|
78
84
|
nil
|
79
85
|
end
|
@@ -105,14 +111,11 @@ module EISCP
|
|
105
111
|
# Checks to see if the command is in the Dictionary
|
106
112
|
#
|
107
113
|
def known_command?(command)
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
return nil
|
113
|
-
end
|
114
|
+
zone = zone_from_command(command)
|
115
|
+
@commands[zone].include? command
|
116
|
+
rescue StandardError
|
117
|
+
nil
|
114
118
|
end
|
115
119
|
end
|
116
|
-
|
117
120
|
end
|
118
121
|
end
|
data/lib/eiscp/message.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require_relative './dictionary'
|
3
4
|
require_relative './parser'
|
4
5
|
|
@@ -14,6 +15,7 @@ module EISCP
|
|
14
15
|
class Message
|
15
16
|
# EISCP header
|
16
17
|
attr_accessor :header
|
18
|
+
|
17
19
|
# ISCP "magic" indicates the start of an eISCP message.
|
18
20
|
MAGIC = 'ISCP'
|
19
21
|
# eISCP header size, fixed length.
|
@@ -52,41 +54,38 @@ module EISCP
|
|
52
54
|
# @param [String] value variable length ISCP command value
|
53
55
|
# @param [String] unit_type_character override default unit type character, optional
|
54
56
|
# @param [String] start_character override default start character, optional
|
55
|
-
def initialize(command: nil, value:
|
57
|
+
def initialize(command: nil, value: '', terminator: "\r\n", unit_type: '1', start: '!')
|
56
58
|
unless Dictionary.known_command?(command)
|
57
|
-
#STDERR.puts "Unknown command #{command}"
|
59
|
+
# STDERR.puts "Unknown command #{command}"
|
58
60
|
end
|
59
61
|
|
60
|
-
fail 'No value specified.' if value.nil?
|
61
|
-
|
62
62
|
@command = command
|
63
63
|
@value = value
|
64
64
|
@terminator = terminator
|
65
65
|
@unit_type = unit_type
|
66
66
|
@start = start
|
67
67
|
@header = { magic: MAGIC,
|
68
|
-
header_size:
|
68
|
+
header_size: HEADER_SIZE,
|
69
69
|
data_size: to_iscp.length,
|
70
70
|
version: ISCP_VERSION,
|
71
|
-
reserved: RESERVED
|
72
|
-
}
|
71
|
+
reserved: RESERVED }
|
73
72
|
begin
|
74
73
|
get_human_readable_attrs
|
75
|
-
rescue
|
76
|
-
#STDERR.puts"Couldn't get all human readable attrs"
|
74
|
+
rescue StandardError
|
75
|
+
# STDERR.puts"Couldn't get all human readable attrs"
|
77
76
|
end
|
78
77
|
end
|
79
78
|
|
80
79
|
# Check if two messages are equivalent comparing their ISCP messages.
|
81
80
|
#
|
82
81
|
def ==(other)
|
83
|
-
to_iscp == other.to_iscp
|
82
|
+
to_iscp == other.to_iscp
|
84
83
|
end
|
85
84
|
|
86
85
|
# Return ISCP Message string
|
87
86
|
#
|
88
87
|
def to_iscp
|
89
|
-
|
88
|
+
(@start + @unit_type + @command + @value).to_s
|
90
89
|
end
|
91
90
|
|
92
91
|
# Return EISCP Message string
|
data/lib/eiscp/parser.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative './parser/eiscp_parser'
|
2
4
|
require_relative './parser/iscp_parser'
|
3
5
|
require_relative './parser/human_readable_parser'
|
@@ -17,6 +19,6 @@ module EISCP
|
|
17
19
|
else
|
18
20
|
HumanReadableParser.parse(string)
|
19
21
|
end
|
20
|
-
end
|
22
|
+
end
|
21
23
|
end
|
22
24
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative './iscp_parser'
|
2
4
|
|
3
5
|
module EISCP
|
@@ -15,22 +17,26 @@ module EISCP
|
|
15
17
|
unit_type: msg.unit_type,
|
16
18
|
start: msg.start
|
17
19
|
)
|
18
|
-
packet.header =
|
20
|
+
packet.header = create_header(array)
|
21
|
+
packet
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.create_header(array)
|
25
|
+
{
|
19
26
|
magic: array[0],
|
20
27
|
header_size: array[1],
|
21
28
|
data_size: array[2],
|
22
29
|
version: array[3],
|
23
30
|
reserved: array[4]
|
24
31
|
}
|
25
|
-
packet
|
26
32
|
end
|
27
33
|
|
28
34
|
def self.validate(packet)
|
29
35
|
packet.header.header_size.size == packet.command.size
|
30
36
|
end
|
37
|
+
|
31
38
|
end
|
32
39
|
|
33
|
-
class EISCPParserException <
|
34
|
-
|
40
|
+
class EISCPParserException < RuntimeError; end
|
35
41
|
end
|
36
42
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative '../dictionary'
|
2
4
|
|
3
5
|
module EISCP
|
@@ -8,22 +10,20 @@ module EISCP
|
|
8
10
|
def self.parse(string)
|
9
11
|
array = string.split(' ')
|
10
12
|
|
11
|
-
if Dictionary.zones.include? array[0]
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
parsed_zone = if Dictionary.zones.include? array[0]
|
14
|
+
array.shift
|
15
|
+
else
|
16
|
+
Dictionary::DEFAULT_ZONE
|
17
|
+
end
|
16
18
|
|
17
19
|
command_name = array.shift
|
18
|
-
value_name = array.join(
|
20
|
+
value_name = array.join(' ')
|
19
21
|
command = Dictionary.command_name_to_command(command_name, parsed_zone)
|
20
22
|
value = Dictionary.command_value_name_to_value(command, value_name)
|
21
|
-
if
|
22
|
-
|
23
|
-
end
|
23
|
+
return nil if command.nil? || value.nil?
|
24
|
+
|
24
25
|
Message.new(command: command, value: value)
|
25
26
|
end
|
26
|
-
|
27
27
|
end
|
28
28
|
end
|
29
29
|
end
|
@@ -1,10 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module EISCP
|
2
4
|
module Parser
|
3
5
|
# This module parses an ISCP string and returns a Message object
|
4
6
|
#
|
5
7
|
module ISCPParser
|
6
8
|
# Regexp for parsing ISCP messages
|
7
|
-
REGEX = /(?<start>!)?(?<unit_type>(
|
9
|
+
REGEX = /(?<start>!)?(?<unit_type>(?:\d|x))?(?<command>[A-Z]{3})\s?(?<value>.*?)(?<terminator>[[:cntrl:]]*$)/
|
8
10
|
def self.parse(string)
|
9
11
|
match = string.match(REGEX)
|
10
12
|
|
@@ -15,10 +17,7 @@ module EISCP
|
|
15
17
|
hash.delete_if { |_, v| v.nil? || v == '' }
|
16
18
|
|
17
19
|
# Convert keys to symbols
|
18
|
-
hash = hash.
|
19
|
-
memo[k.to_sym] = v
|
20
|
-
memo
|
21
|
-
end
|
20
|
+
hash = hash.transform_keys(&:to_sym)
|
22
21
|
|
23
22
|
Message.new(**hash)
|
24
23
|
end
|
data/lib/eiscp/receiver.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'resolv'
|
2
4
|
require_relative './receiver/discovery'
|
3
5
|
require_relative './receiver/command_methods'
|
@@ -40,7 +42,6 @@ module EISCP
|
|
40
42
|
# Default Onkyo eISCP port
|
41
43
|
ONKYO_PORT = 60_128
|
42
44
|
|
43
|
-
|
44
45
|
# Create a new EISCP::Receiver object to communicate with a receiver.
|
45
46
|
# If no host is given, use auto discovery and create a
|
46
47
|
# receiver object using the first host to respond.
|
@@ -53,7 +54,7 @@ module EISCP
|
|
53
54
|
# with the Message object that results from a CommandMethod being called.
|
54
55
|
# All we're doing here is calling #send_recv
|
55
56
|
#
|
56
|
-
command_method_proc =
|
57
|
+
command_method_proc = proc { |msg| send_recv msg }
|
57
58
|
CommandMethods.generate(&command_method_proc)
|
58
59
|
|
59
60
|
# This proc sets the four ECN attributes and initiates a connection to the
|
@@ -64,9 +65,7 @@ module EISCP
|
|
64
65
|
@port = hash[:port]
|
65
66
|
@area = hash[:area]
|
66
67
|
@mac_address = hash[:mac_address]
|
67
|
-
if block_given?
|
68
|
-
connect(&block)
|
69
|
-
end
|
68
|
+
connect(&block) if block_given?
|
70
69
|
end
|
71
70
|
|
72
71
|
# This lambda sets the host IP after resolving it
|
@@ -83,12 +82,11 @@ module EISCP
|
|
83
82
|
# Else, use the given host and hash to create a new Receiver object.
|
84
83
|
# This is how ::discover creates Receivers.
|
85
84
|
#
|
86
|
-
|
87
|
-
when host.nil?
|
85
|
+
if host.nil?
|
88
86
|
first_found = Receiver.discover[0]
|
89
87
|
set_host.call first_found.host
|
90
88
|
set_attrs.call first_found.ecn_hash
|
91
|
-
|
89
|
+
elsif info_hash.empty?
|
92
90
|
set_host.call host
|
93
91
|
Receiver.discover.each do |receiver|
|
94
92
|
receiver.host == @host && set_attrs.call(receiver.ecn_hash)
|
@@ -99,7 +97,7 @@ module EISCP
|
|
99
97
|
end
|
100
98
|
end
|
101
99
|
|
102
|
-
# Manages the thread and uses the same block passed to
|
100
|
+
# Manages the thread and uses the same block passed to through #connect.
|
103
101
|
#
|
104
102
|
def update_thread
|
105
103
|
# Kill thread if it exists
|
@@ -114,16 +112,14 @@ module EISCP
|
|
114
112
|
end
|
115
113
|
private :update_thread
|
116
114
|
|
117
|
-
# This creates a socket conection to the receiver if one doesn't exist,
|
115
|
+
# This creates a socket conection to the receiver if one doesn't exist,
|
118
116
|
# and updates or sets the callback block if one is passed.
|
119
117
|
#
|
120
118
|
def connect(&block)
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
puts e
|
126
|
-
end
|
119
|
+
@socket ||= TCPSocket.new(@host, @port)
|
120
|
+
update_thread(&block)
|
121
|
+
rescue StandardError => e
|
122
|
+
puts e
|
127
123
|
end
|
128
124
|
|
129
125
|
# Disconnect from the receiver by closing the socket and killing the
|
@@ -137,9 +133,11 @@ module EISCP
|
|
137
133
|
# Sends an EISCP::Message object or string on the network
|
138
134
|
#
|
139
135
|
def send(eiscp)
|
140
|
-
if
|
136
|
+
connect if @socket.nil? || @socket.closed?
|
137
|
+
case eiscp
|
138
|
+
when EISCP::Message
|
141
139
|
@socket.puts(eiscp.to_eiscp)
|
142
|
-
|
140
|
+
when String
|
143
141
|
@socket.puts eiscp
|
144
142
|
end
|
145
143
|
end
|
@@ -147,18 +145,15 @@ module EISCP
|
|
147
145
|
# Reads the socket and returns and EISCP::Message
|
148
146
|
#
|
149
147
|
def recv
|
150
|
-
data =
|
148
|
+
data = String.new
|
151
149
|
data << @socket.gets until data.match(/\r\n$/)
|
152
|
-
|
153
|
-
message
|
150
|
+
Parser.parse(data)
|
154
151
|
end
|
155
152
|
|
156
153
|
# Sends an EISCP::Message object or string on the network and returns recieved data string.
|
157
154
|
#
|
158
155
|
def send_recv(eiscp)
|
159
|
-
if eiscp.is_a? String
|
160
|
-
eiscp = Parser.parse(eiscp)
|
161
|
-
end
|
156
|
+
eiscp = Parser.parse(eiscp) if eiscp.is_a? String
|
162
157
|
send eiscp
|
163
158
|
sleep DEFAULT_TIMEOUT
|
164
159
|
Parser.parse("#{eiscp.command}#{@state[eiscp.command]}")
|
@@ -170,16 +165,15 @@ module EISCP
|
|
170
165
|
{ model: @model,
|
171
166
|
port: @port,
|
172
167
|
area: @area,
|
173
|
-
mac_address: @mac_address
|
174
|
-
}
|
168
|
+
mac_address: @mac_address }
|
175
169
|
end
|
176
170
|
|
177
|
-
# This will return a human-readable represantion of the receiver's state.
|
171
|
+
# This will return a human-readable represantion of the receiver's state.
|
178
172
|
#
|
179
173
|
def human_readable_state
|
180
174
|
hash = {}
|
181
|
-
@state.each do |c, v|
|
182
|
-
hash[
|
175
|
+
@state.dup.each do |c, v|
|
176
|
+
hash[Dictionary.command_to_name(c).to_s] = (Dictionary.command_value_to_value_name(c, v) || v.to_s).to_s
|
183
177
|
end
|
184
178
|
hash
|
185
179
|
end
|
@@ -189,18 +183,12 @@ module EISCP
|
|
189
183
|
#
|
190
184
|
def update_state
|
191
185
|
Thread.new do
|
192
|
-
Dictionary.commands.each do |zone,
|
186
|
+
Dictionary.commands.each do |zone, _commands|
|
193
187
|
Dictionary.commands[zone].each do |command, info|
|
194
188
|
info[:values].each do |value, _|
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
# A dropped reply is not necessarily indicative of the
|
199
|
-
# receiver's failure to receive the command and change state
|
200
|
-
# accordingly. In this case, we're only making queries, so we do
|
201
|
-
# want to capture every reply.
|
202
|
-
sleep DEFAULT_TIMEOUT
|
203
|
-
end
|
189
|
+
next unless value == 'QSTN'
|
190
|
+
|
191
|
+
send_recv(Parser.parse("#{command}QSTN"))
|
204
192
|
end
|
205
193
|
end
|
206
194
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative '../parser'
|
2
4
|
require_relative '../dictionary'
|
3
5
|
|
@@ -12,14 +14,12 @@ module EISCP
|
|
12
14
|
def self.generate(&block)
|
13
15
|
Dictionary.zones.each do |zone|
|
14
16
|
Dictionary.commands[zone].each do |command, _values|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
self.instance_exec Parser.parse(command_name.gsub(/_/, '-') + ' ' + v), &block
|
19
|
-
end
|
20
|
-
rescue => e
|
21
|
-
puts e
|
17
|
+
command_name = Dictionary.command_to_name(command).to_s.gsub(/-/, '_')
|
18
|
+
define_method(command_name) do |v|
|
19
|
+
instance_exec Parser.parse("#{command_name.gsub(/_/, '-')} #{v}"), &block
|
22
20
|
end
|
21
|
+
rescue StandardError => e
|
22
|
+
puts e
|
23
23
|
end
|
24
24
|
end
|
25
25
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'socket'
|
2
4
|
require_relative '../message'
|
3
5
|
require_relative '../parser'
|
@@ -31,17 +33,14 @@ module EISCP
|
|
31
33
|
sock.send(ONKYO_MAGIC, 0, '<broadcast>', discovery_port)
|
32
34
|
data = []
|
33
35
|
loop do
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
else
|
43
|
-
retry
|
44
|
-
end
|
36
|
+
msg, addr = sock.recvfrom_nonblock(1024)
|
37
|
+
data << Receiver.new(addr[2], ecn_string_to_ecn_array(msg))
|
38
|
+
rescue IO::WaitReadable
|
39
|
+
io = IO.select([sock], nil, nil, 0.5)
|
40
|
+
if io.nil?
|
41
|
+
return data
|
42
|
+
else
|
43
|
+
retry
|
45
44
|
end
|
46
45
|
end
|
47
46
|
end
|
data/onkyo_eiscp_ruby.gemspec
CHANGED
@@ -1,28 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
Gem::Specification.new do |s|
|
2
4
|
s.name = 'onkyo_eiscp_ruby'
|
3
5
|
s.version = File.read(File.expand_path('VERSION', File.dirname(__FILE__))).strip
|
4
|
-
s.licenses
|
6
|
+
s.licenses = ['MIT']
|
5
7
|
s.platform = Gem::Platform::RUBY
|
6
8
|
s.summary = 'Manipulate Onkyo stereos with the eISCP protocol'
|
7
9
|
s.files = Dir.glob('{bin,config,lib,test,doc}/**/*') +
|
8
|
-
|
10
|
+
['VERSION', 'onkyo_eiscp_ruby.gemspec', 'eiscp-commands.yaml']
|
9
11
|
s.extra_rdoc_files = ['README.md']
|
10
12
|
s.require_path = 'lib'
|
11
13
|
|
12
14
|
s.homepage = 'https://github.com/mikerodrigues/onkyo_eiscp_ruby'
|
13
15
|
|
14
|
-
s.description =
|
16
|
+
s.description = '
|
15
17
|
Control Onkyo receivers over the network.Use the provided binary or
|
16
18
|
require the library for use in your scripts.
|
17
|
-
|
19
|
+
'
|
18
20
|
|
19
21
|
s.author = 'Michael Rodrigues'
|
20
22
|
s.email = 'mikebrodrigues@gmail.com'
|
21
23
|
|
22
24
|
s.test_files = Dir['test/tc*.rb']
|
23
|
-
s.executables = %w
|
25
|
+
s.executables = %w[
|
24
26
|
onkyo.rb
|
25
27
|
onkyo_server.rb
|
26
|
-
|
27
|
-
|
28
|
+
]
|
28
29
|
end
|
data/test/tc_dictionary.rb
CHANGED
data/test/tc_message.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative '../lib/eiscp/message'
|
2
4
|
require 'minitest/autorun'
|
3
5
|
|
@@ -6,7 +8,10 @@ class TestMessage < MiniTest::Test
|
|
6
8
|
DISCOVERY_STRING = DISCOVERY_PACKET.to_eiscp
|
7
9
|
|
8
10
|
def test_create_discovery_iscp_message
|
9
|
-
assert_equal(
|
11
|
+
assert_equal(
|
12
|
+
EISCP::Message.new(command: 'ECN', value: 'QSTN', terminator: "\r\n", unit_type: 'x',
|
13
|
+
start: '!').to_iscp, '!xECNQSTN'
|
14
|
+
)
|
10
15
|
end
|
11
16
|
|
12
17
|
def test_create_messages
|
data/test/tc_parser.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative '../lib/eiscp/parser'
|
2
4
|
require_relative '../lib/eiscp/message'
|
3
5
|
require 'minitest/autorun'
|
@@ -27,7 +29,6 @@ class TestParser < MiniTest::Test
|
|
27
29
|
end
|
28
30
|
|
29
31
|
def test_return_nil_for_fake_human_readable
|
30
|
-
|
32
|
+
assert_nil(EISCP::Parser.parse('fake-command value'))
|
31
33
|
end
|
32
|
-
|
33
34
|
end
|
data/test/tc_receiver.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: onkyo_eiscp_ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.1.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Rodrigues
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-03-30 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: "\n Control Onkyo receivers over the network.Use the provided binary
|
14
14
|
or\n require the library for use in your scripts.\n "
|
@@ -63,13 +63,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
63
63
|
- !ruby/object:Gem::Version
|
64
64
|
version: '0'
|
65
65
|
requirements: []
|
66
|
-
|
67
|
-
rubygems_version: 2.4.5
|
66
|
+
rubygems_version: 3.2.3
|
68
67
|
signing_key:
|
69
68
|
specification_version: 4
|
70
69
|
summary: Manipulate Onkyo stereos with the eISCP protocol
|
71
70
|
test_files:
|
72
|
-
- test/tc_parser.rb
|
73
|
-
- test/tc_receiver.rb
|
74
71
|
- test/tc_dictionary.rb
|
75
72
|
- test/tc_message.rb
|
73
|
+
- test/tc_parser.rb
|
74
|
+
- test/tc_receiver.rb
|