onkyo_eiscp_ruby 0.0.2 → 2.1.1
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 +7 -0
- data/README.md +213 -29
- data/VERSION +1 -1
- data/bin/mock_receiver.rb +25 -0
- data/bin/onkyo.rb +85 -23
- data/bin/onkyo_server.rb +8 -0
- data/eiscp-commands.yaml +3911 -3900
- data/lib/eiscp.rb +9 -7
- data/lib/eiscp/dictionary.rb +54 -0
- data/lib/eiscp/dictionary/dictionary_generators.rb +63 -0
- data/lib/eiscp/dictionary/dictionary_helpers.rb +116 -0
- data/lib/eiscp/message.rb +123 -0
- 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 +203 -0
- data/lib/eiscp/receiver/command_methods.rb +28 -0
- data/lib/eiscp/receiver/discovery.rb +49 -0
- data/onkyo_eiscp_ruby.gemspec +16 -13
- data/test/tc_dictionary.rb +45 -0
- data/test/tc_message.rb +26 -0
- data/test/tc_parser.rb +34 -0
- data/test/tc_receiver.rb +7 -0
- metadata +37 -32
- data/bin/onkyo-server.rb +0 -7
- data/lib/eiscp/command.rb +0 -80
- 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_command.rb +0 -30
- data/test/tc_eiscp.rb +0 -6
- data/test/tc_eiscp_packet.rb +0 -17
- data/test/tc_iscp_message.rb +0 -14
data/lib/eiscp.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
# Library for controlling Onkyo receivers over TCP/IP.
|
4
|
+
#
|
5
|
+
module EISCP
|
6
|
+
VERSION = '2.1.1'
|
5
7
|
end
|
6
8
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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,116 @@
|
|
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
|
+
@commands[zone][command][:values][value][:name]
|
55
|
+
rescue StandardError
|
56
|
+
nil
|
57
|
+
end
|
58
|
+
|
59
|
+
# Return a value from a command and value name
|
60
|
+
def command_value_name_to_value(command, value_name)
|
61
|
+
zone = zone_from_command(command)
|
62
|
+
@commands[zone][command][:values].each_pair do |k, v|
|
63
|
+
if v[:name].class == String
|
64
|
+
return k if v[:name] == value_name.to_s
|
65
|
+
elsif v[:name].class == Array
|
66
|
+
return k if v[:name].first == value_name.to_s
|
67
|
+
end
|
68
|
+
end
|
69
|
+
return nil
|
70
|
+
rescue StandardError
|
71
|
+
nil
|
72
|
+
end
|
73
|
+
|
74
|
+
# Return a command description from a command name and zone
|
75
|
+
def description_from_command_name(name, zone)
|
76
|
+
@commands[zone].each_pair do |command, attrs|
|
77
|
+
return @commands[zone][command][:description] if attrs[:name] == name
|
78
|
+
end
|
79
|
+
nil
|
80
|
+
end
|
81
|
+
|
82
|
+
# Return a command description from a command
|
83
|
+
def description_from_command(command)
|
84
|
+
zone = zone_from_command(command)
|
85
|
+
@commands[zone][command][:description]
|
86
|
+
end
|
87
|
+
|
88
|
+
# Return a value description from a command and value
|
89
|
+
def description_from_command_value(command, value)
|
90
|
+
zone = zone_from_command(command)
|
91
|
+
@commands[zone][command][:values].select do |k, v|
|
92
|
+
return v[:description] if k == value
|
93
|
+
end
|
94
|
+
nil
|
95
|
+
end
|
96
|
+
|
97
|
+
# Return a list of commands compatible with a given model
|
98
|
+
def list_compatible_commands(modelstring)
|
99
|
+
sets = []
|
100
|
+
@modelsets.each_pair do |set, array|
|
101
|
+
sets << set if array.include? modelstring
|
102
|
+
end
|
103
|
+
sets
|
104
|
+
end
|
105
|
+
|
106
|
+
# Checks to see if the command is in the Dictionary
|
107
|
+
#
|
108
|
+
def known_command?(command)
|
109
|
+
zone = zone_from_command(command)
|
110
|
+
@commands[zone].include? command
|
111
|
+
rescue StandardError
|
112
|
+
nil
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './dictionary'
|
4
|
+
require_relative './parser'
|
5
|
+
|
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
|
+
#
|
15
|
+
class Message
|
16
|
+
# EISCP header
|
17
|
+
attr_accessor :header
|
18
|
+
# ISCP "magic" indicates the start of an eISCP message.
|
19
|
+
MAGIC = 'ISCP'
|
20
|
+
# eISCP header size, fixed length.
|
21
|
+
HEADER_SIZE = 16
|
22
|
+
# ISCP protocol version.
|
23
|
+
ISCP_VERSION = 1
|
24
|
+
# Reserved for future protocol updates.
|
25
|
+
RESERVED = "\x00\x00\x00"
|
26
|
+
|
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}"
|
59
|
+
end
|
60
|
+
|
61
|
+
raise 'No value specified.' if value.nil?
|
62
|
+
|
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"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Check if two messages are equivalent comparing their ISCP messages.
|
81
|
+
#
|
82
|
+
def ==(other)
|
83
|
+
to_iscp == other.to_iscp
|
84
|
+
end
|
85
|
+
|
86
|
+
# Return ISCP Message string
|
87
|
+
#
|
88
|
+
def to_iscp
|
89
|
+
(@start + @unit_type + @command + @value).to_s
|
90
|
+
end
|
91
|
+
|
92
|
+
# Return EISCP Message string
|
93
|
+
#
|
94
|
+
def to_eiscp
|
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}"
|
110
|
+
end
|
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
|
122
|
+
end
|
123
|
+
end
|
data/lib/eiscp/parser.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './parser/eiscp_parser'
|
4
|
+
require_relative './parser/iscp_parser'
|
5
|
+
require_relative './parser/human_readable_parser'
|
6
|
+
|
7
|
+
module EISCP
|
8
|
+
# This module provides an interface to the other parser modules.
|
9
|
+
#
|
10
|
+
module Parser
|
11
|
+
# Passes the string to the proper parser module's #parse method.
|
12
|
+
#
|
13
|
+
def self.parse(string)
|
14
|
+
case string
|
15
|
+
when /^ISCP/
|
16
|
+
EISCPParser.parse(string)
|
17
|
+
when ISCPParser::REGEX
|
18
|
+
ISCPParser.parse(string)
|
19
|
+
else
|
20
|
+
HumanReadableParser.parse(string)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './iscp_parser'
|
4
|
+
|
5
|
+
module EISCP
|
6
|
+
module Parser
|
7
|
+
# This module parses an eISCP string and returns a Message object
|
8
|
+
#
|
9
|
+
module EISCPParser
|
10
|
+
def self.parse(string)
|
11
|
+
array = string.unpack('A4NNCa3A*')
|
12
|
+
msg = ISCPParser.parse(array[5])
|
13
|
+
packet = Message.new(
|
14
|
+
command: msg.command,
|
15
|
+
value: msg.value,
|
16
|
+
terminator: msg.terminator,
|
17
|
+
unit_type: msg.unit_type,
|
18
|
+
start: msg.start
|
19
|
+
)
|
20
|
+
packet.header = {
|
21
|
+
magic: array[0],
|
22
|
+
header_size: array[1],
|
23
|
+
data_size: array[2],
|
24
|
+
version: array[3],
|
25
|
+
reserved: array[4]
|
26
|
+
}
|
27
|
+
packet
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.validate(packet)
|
31
|
+
packet.header.header_size.size == packet.command.size
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class EISCPParserException < RuntimeError; end
|
36
|
+
end
|
37
|
+
end
|