onkyo_eiscp_ruby 0.0.3 → 2.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +182 -32
- data/VERSION +1 -1
- data/bin/mock_receiver.rb +25 -0
- data/bin/onkyo.rb +85 -25
- data/bin/{onkyo-server.rb → onkyo_server.rb} +2 -1
- data/eiscp-commands.yaml +3911 -3900
- data/lib/eiscp.rb +8 -5
- data/lib/eiscp/dictionary.rb +54 -0
- data/lib/eiscp/dictionary/dictionary_generators.rb +63 -0
- data/lib/eiscp/dictionary/dictionary_helpers.rb +121 -0
- data/lib/eiscp/message.rb +98 -74
- data/lib/eiscp/parser.rb +24 -0
- data/lib/eiscp/parser/dynamic_value_parser.rb +9 -0
- data/lib/eiscp/parser/eiscp_parser.rb +37 -0
- data/lib/eiscp/parser/human_readable_parser.rb +29 -0
- data/lib/eiscp/parser/iscp_parser.rb +28 -0
- data/lib/eiscp/receiver.rb +158 -135
- data/lib/eiscp/receiver/command_methods.rb +28 -0
- data/lib/eiscp/receiver/discovery.rb +49 -0
- data/onkyo_eiscp_ruby.gemspec +15 -13
- data/test/tc_dictionary.rb +45 -0
- data/test/tc_message.rb +11 -12
- data/test/tc_parser.rb +34 -0
- data/test/tc_receiver.rb +4 -3
- metadata +23 -12
- data/lib/eiscp/command.rb +0 -99
- data/lib/eiscp/mock_receiver.rb +0 -22
- data/test/tc_command.rb +0 -43
data/lib/eiscp.rb
CHANGED
@@ -1,9 +1,12 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# Library for controlling Onkyo receivers over TCP/IP.
|
4
|
+
#
|
3
5
|
module EISCP
|
4
|
-
VERSION = '
|
6
|
+
VERSION = '2.1.2'
|
5
7
|
end
|
6
8
|
|
7
|
-
|
8
|
-
|
9
|
-
|
9
|
+
require_relative './eiscp/receiver'
|
10
|
+
require_relative './eiscp/message'
|
11
|
+
require_relative './eiscp/dictionary'
|
12
|
+
require_relative './eiscp/parser'
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './dictionary/dictionary_generators'
|
4
|
+
require_relative './dictionary/dictionary_helpers'
|
5
|
+
|
6
|
+
module EISCP
|
7
|
+
# This module provides an interface to the information from the yaml file. It
|
8
|
+
# uses DictionaryGenerators to add commands specified by ranges in the yaml
|
9
|
+
# file. It uses DictionaryHelpers to convert commands and values to and from
|
10
|
+
# their human readable form.
|
11
|
+
#
|
12
|
+
module Dictionary
|
13
|
+
extend DictionaryGenerators
|
14
|
+
extend DictionaryHelpers
|
15
|
+
|
16
|
+
class << self
|
17
|
+
attr_reader :zones
|
18
|
+
attr_reader :modelsets
|
19
|
+
attr_reader :commands
|
20
|
+
end
|
21
|
+
|
22
|
+
DEFAULT_ZONE = 'main'
|
23
|
+
@yaml_file_path = File.join(__dir__, '../../eiscp-commands.yaml')
|
24
|
+
@commands = YAML.load(File.read(@yaml_file_path))
|
25
|
+
@modelsets = @commands[:modelsets]
|
26
|
+
@commands.delete(:modelsets)
|
27
|
+
@zones = @commands.map { |k, _| k }
|
28
|
+
|
29
|
+
@additions = []
|
30
|
+
@commands.each_key do |zone|
|
31
|
+
@commands[zone].each do |command|
|
32
|
+
command = command[0]
|
33
|
+
@commands[zone][command][:values].each do |value|
|
34
|
+
value = value[0]
|
35
|
+
if value.is_a? Array
|
36
|
+
@additions << [zone, command, value, create_range_commands(zone, command, value)]
|
37
|
+
elsif value.match(/^(B|T){xx}$/)
|
38
|
+
@additions << [zone, command, value, create_treble_bass_commands(zone, command, value)]
|
39
|
+
elsif value.match(/^{xx}$/)
|
40
|
+
@additions << [zone, command, value, create_balance_commands(zone, command, value)]
|
41
|
+
else
|
42
|
+
next
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
@additions.each do |zone, command, value, hash|
|
49
|
+
@commands[zone][command][:values].merge! hash
|
50
|
+
rescue StandardError
|
51
|
+
puts "Failed to add #{hash} to #{zone}:#{command}:#{value}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module EISCP
|
6
|
+
module Dictionary
|
7
|
+
# This module provides methods that can be used to generate command entries
|
8
|
+
# that are specified as ranges in the yaml file.
|
9
|
+
#
|
10
|
+
module DictionaryGenerators
|
11
|
+
# Creates a hash object for range commands like master-volume
|
12
|
+
#
|
13
|
+
def create_range_commands(zone, command, value)
|
14
|
+
case value.count
|
15
|
+
when 3
|
16
|
+
range = Range.new(value[0], value[2])
|
17
|
+
when 2
|
18
|
+
range = Range.new(*value)
|
19
|
+
end
|
20
|
+
tmp = {}
|
21
|
+
range.each do |number|
|
22
|
+
tmp.merge!(number.to_s(16).rjust(2, '0').upcase =>
|
23
|
+
{
|
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
|
+
end
|
29
|
+
tmp
|
30
|
+
end
|
31
|
+
|
32
|
+
# Creates hash object for treble and bass commands
|
33
|
+
#
|
34
|
+
def create_treble_bass_commands(zone, command, value)
|
35
|
+
tmp = {}
|
36
|
+
['-A', '-8', '-6', '-4', '-2', '00', '+2', '+4', '+6', '+8', '+A'].each do |v|
|
37
|
+
tmp.merge!((value[0] + v.to_s) =>
|
38
|
+
{
|
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
|
+
})
|
43
|
+
end
|
44
|
+
tmp
|
45
|
+
end
|
46
|
+
|
47
|
+
# Creates hash object for balance commands
|
48
|
+
#
|
49
|
+
def create_balance_commands(zone, command, _value)
|
50
|
+
tmp = {}
|
51
|
+
['-A', '-8', '-6', '-4', '-2', '00', '+2', '+4', '+6', '+8', '+A'].each do |v|
|
52
|
+
tmp.merge!(v.to_s =>
|
53
|
+
{
|
54
|
+
name: v.downcase,
|
55
|
+
description: @commands[zone][command][:values]['{xx}'][:description].gsub(/\(.*[\]|\)]$/, v),
|
56
|
+
models: @commands[zone][command][:values]['{xx}'][:models]
|
57
|
+
})
|
58
|
+
end
|
59
|
+
tmp
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module EISCP
|
4
|
+
module Dictionary
|
5
|
+
# This module provides methods to get information from the Dictionary about
|
6
|
+
# commands, values, zones, and models.
|
7
|
+
#
|
8
|
+
module DictionaryHelpers
|
9
|
+
# Return the zone that includes the given command
|
10
|
+
def zone_from_command(command)
|
11
|
+
@zones.each do |zone|
|
12
|
+
@commands[zone].each_pair do |k, _|
|
13
|
+
return zone if command == k
|
14
|
+
end
|
15
|
+
end
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
|
19
|
+
# Return the human readable name of a command
|
20
|
+
def command_to_name(command)
|
21
|
+
command = command.upcase
|
22
|
+
begin
|
23
|
+
zone = zone_from_command(command)
|
24
|
+
@commands[zone][command][:name]
|
25
|
+
rescue StandardError
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Return the command from a given command name
|
31
|
+
def command_name_to_command(name, command_zone = nil)
|
32
|
+
if command_zone.nil?
|
33
|
+
|
34
|
+
@zones.each do |zone|
|
35
|
+
@commands[zone].each_pair do |command, attrs|
|
36
|
+
return command if attrs[:name] == name
|
37
|
+
end
|
38
|
+
end
|
39
|
+
nil
|
40
|
+
|
41
|
+
else
|
42
|
+
|
43
|
+
@commands[command_zone].each_pair do |command, attrs|
|
44
|
+
return command if attrs[:name] == name
|
45
|
+
end
|
46
|
+
nil
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Return a value name from a command and a value
|
52
|
+
def command_value_to_value_name(command, value)
|
53
|
+
zone = zone_from_command(command)
|
54
|
+
command_value = @commands[zone][command][:values][value][:name]
|
55
|
+
if command_value.class == String
|
56
|
+
command_value
|
57
|
+
elsif command_value.class == Array
|
58
|
+
command_value.first
|
59
|
+
end
|
60
|
+
rescue StandardError
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
|
64
|
+
# Return a value from a command and value name
|
65
|
+
def command_value_name_to_value(command, value_name)
|
66
|
+
zone = zone_from_command(command)
|
67
|
+
@commands[zone][command][:values].each_pair do |k, v|
|
68
|
+
if v[:name].class == String
|
69
|
+
return k if v[:name] == value_name.to_s
|
70
|
+
elsif v[:name].class == Array
|
71
|
+
return k if v[:name].first == value_name.to_s
|
72
|
+
end
|
73
|
+
end
|
74
|
+
return nil
|
75
|
+
rescue StandardError
|
76
|
+
nil
|
77
|
+
end
|
78
|
+
|
79
|
+
# Return a command description from a command name and zone
|
80
|
+
def description_from_command_name(name, zone)
|
81
|
+
@commands[zone].each_pair do |command, attrs|
|
82
|
+
return @commands[zone][command][:description] if attrs[:name] == name
|
83
|
+
end
|
84
|
+
nil
|
85
|
+
end
|
86
|
+
|
87
|
+
# Return a command description from a command
|
88
|
+
def description_from_command(command)
|
89
|
+
zone = zone_from_command(command)
|
90
|
+
@commands[zone][command][:description]
|
91
|
+
end
|
92
|
+
|
93
|
+
# Return a value description from a command and value
|
94
|
+
def description_from_command_value(command, value)
|
95
|
+
zone = zone_from_command(command)
|
96
|
+
@commands[zone][command][:values].select do |k, v|
|
97
|
+
return v[:description] if k == value
|
98
|
+
end
|
99
|
+
nil
|
100
|
+
end
|
101
|
+
|
102
|
+
# Return a list of commands compatible with a given model
|
103
|
+
def list_compatible_commands(modelstring)
|
104
|
+
sets = []
|
105
|
+
@modelsets.each_pair do |set, array|
|
106
|
+
sets << set if array.include? modelstring
|
107
|
+
end
|
108
|
+
sets
|
109
|
+
end
|
110
|
+
|
111
|
+
# Checks to see if the command is in the Dictionary
|
112
|
+
#
|
113
|
+
def known_command?(command)
|
114
|
+
zone = zone_from_command(command)
|
115
|
+
@commands[zone].include? command
|
116
|
+
rescue StandardError
|
117
|
+
nil
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
data/lib/eiscp/message.rb
CHANGED
@@ -1,99 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './dictionary'
|
4
|
+
require_relative './parser'
|
5
|
+
|
1
6
|
module EISCP
|
7
|
+
# The EISCP::Message class is used to handle commands and responses.
|
8
|
+
#
|
9
|
+
# Messages can be parsed directly from raw data or created with values:
|
10
|
+
# receiver = Receiver.new
|
11
|
+
#
|
12
|
+
# command = EISCP::Message.new('PWR', 'QSTN')
|
13
|
+
# response = EISCP::Parser.parse(receiver.send_recv(command))
|
14
|
+
#
|
2
15
|
class Message
|
3
|
-
|
4
16
|
# EISCP header
|
5
17
|
attr_accessor :header
|
6
|
-
|
18
|
+
# ISCP "magic" indicates the start of an eISCP message.
|
19
|
+
MAGIC = 'ISCP'
|
20
|
+
# eISCP header size, fixed length.
|
7
21
|
HEADER_SIZE = 16
|
8
|
-
|
22
|
+
# ISCP protocol version.
|
23
|
+
ISCP_VERSION = 1
|
24
|
+
# Reserved for future protocol updates.
|
9
25
|
RESERVED = "\x00\x00\x00"
|
10
26
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
27
|
+
# ISCP Start character, usually "!"
|
28
|
+
attr_reader :start
|
29
|
+
# ISCP Unit Type character, usually "1"
|
30
|
+
attr_reader :unit_type
|
31
|
+
# ISCP Command
|
32
|
+
attr_reader :command
|
33
|
+
# Human readable command name
|
34
|
+
attr_reader :command_name
|
35
|
+
# Command description
|
36
|
+
attr_reader :command_description
|
37
|
+
# ISCP Command Value
|
38
|
+
attr_reader :value
|
39
|
+
# Human readable value name
|
40
|
+
attr_reader :value_name
|
41
|
+
# Value description
|
42
|
+
attr_reader :value_description
|
43
|
+
# ISCP Zone
|
44
|
+
attr_reader :zone
|
45
|
+
# Differentiates parsed messages from command messages
|
46
|
+
attr_reader :parsed
|
47
|
+
|
48
|
+
# Terminator character for eISCP packets
|
49
|
+
attr_reader :terminator
|
50
|
+
|
51
|
+
# Create an ISCP message
|
52
|
+
# @param [String] command three-character length ISCP command
|
53
|
+
# @param [String] value variable length ISCP command value
|
54
|
+
# @param [String] unit_type_character override default unit type character, optional
|
55
|
+
# @param [String] start_character override default start character, optional
|
56
|
+
def initialize(command: nil, value: nil, terminator: "\r\n", unit_type: '1', start: '!')
|
57
|
+
unless Dictionary.known_command?(command)
|
58
|
+
# STDERR.puts "Unknown command #{command}"
|
33
59
|
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
60
|
|
46
|
-
|
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.
|
61
|
+
raise 'No value specified.' if value.nil?
|
52
62
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
63
|
+
@command = command
|
64
|
+
@value = value
|
65
|
+
@terminator = terminator
|
66
|
+
@unit_type = unit_type
|
67
|
+
@start = start
|
68
|
+
@header = { magic: MAGIC,
|
69
|
+
header_size: HEADER_SIZE,
|
70
|
+
data_size: to_iscp.length,
|
71
|
+
version: ISCP_VERSION,
|
72
|
+
reserved: RESERVED }
|
73
|
+
begin
|
74
|
+
get_human_readable_attrs
|
75
|
+
rescue StandardError
|
76
|
+
# STDERR.puts"Couldn't get all human readable attrs"
|
61
77
|
end
|
62
78
|
end
|
63
79
|
|
64
|
-
|
65
|
-
|
66
|
-
|
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
|
80
|
+
# Check if two messages are equivalent comparing their ISCP messages.
|
81
|
+
#
|
82
|
+
def ==(other)
|
83
|
+
to_iscp == other.to_iscp
|
86
84
|
end
|
87
85
|
|
88
86
|
# Return ISCP Message string
|
87
|
+
#
|
89
88
|
def to_iscp
|
90
|
-
|
89
|
+
(@start + @unit_type + @command + @value).to_s
|
91
90
|
end
|
92
91
|
|
93
92
|
# Return EISCP Message string
|
93
|
+
#
|
94
94
|
def to_eiscp
|
95
|
-
|
95
|
+
[
|
96
|
+
@header[:magic],
|
97
|
+
@header[:header_size].to_i,
|
98
|
+
@header[:data_size].to_i,
|
99
|
+
@header[:version].to_i,
|
100
|
+
@header[:reserved],
|
101
|
+
to_iscp.to_s,
|
102
|
+
@terminator
|
103
|
+
].pack('A4NNCa3A*A*')
|
104
|
+
end
|
105
|
+
|
106
|
+
# Return human readable description.
|
107
|
+
#
|
108
|
+
def to_s
|
109
|
+
"#{@zone} - #{@command_name}:#{@value_name}"
|
96
110
|
end
|
97
111
|
|
112
|
+
private
|
113
|
+
|
114
|
+
# Retrieves human readable attributes from the yaml file via Dictionary
|
115
|
+
def get_human_readable_attrs
|
116
|
+
@zone = Dictionary.zone_from_command(@command)
|
117
|
+
@command_name = Dictionary.command_to_name(@command)
|
118
|
+
@command_description = Dictionary.description_from_command(@command)
|
119
|
+
@value_name = Dictionary.command_value_to_value_name(@command, @value)
|
120
|
+
@value_description = Dictionary.description_from_command_value(@command, @value)
|
121
|
+
end
|
98
122
|
end
|
99
123
|
end
|