onkyo_eiscp_ruby 0.0.3 → 1.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +154 -33
- data/VERSION +1 -1
- data/bin/mock_receiver.rb +23 -0
- data/bin/onkyo.rb +50 -23
- data/bin/{onkyo-server.rb → onkyo_server.rb} +1 -1
- data/eiscp-commands.yaml +10329 -10318
- data/lib/eiscp/dictionary/dictionary_generators.rb +67 -0
- data/lib/eiscp/dictionary/dictionary_helpers.rb +118 -0
- data/lib/eiscp/dictionary.rb +54 -0
- data/lib/eiscp/message.rb +97 -73
- data/lib/eiscp/parser/dynamic_value_parser.rb +7 -0
- data/lib/eiscp/parser/eiscp_parser.rb +37 -0
- data/lib/eiscp/parser/human_readable_parser.rb +30 -0
- data/lib/eiscp/parser/iscp_parser.rb +29 -0
- data/lib/eiscp/parser.rb +22 -0
- data/lib/eiscp/receiver/command_methods.rb +29 -0
- data/lib/eiscp/receiver/connection.rb +83 -0
- data/lib/eiscp/receiver/discovery.rb +50 -0
- data/lib/eiscp/receiver.rb +71 -153
- data/lib/eiscp.rb +6 -5
- data/onkyo_eiscp_ruby.gemspec +9 -8
- data/test/tc_dictionary.rb +43 -0
- data/test/tc_message.rb +10 -13
- data/test/tc_parser.rb +33 -0
- data/test/tc_receiver.rb +2 -3
- metadata +27 -14
- data/lib/eiscp/command.rb +0 -99
- data/lib/eiscp/mock_receiver.rb +0 -22
- data/test/tc_command.rb +0 -43
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module EISCP
|
4
|
+
module Dictionary
|
5
|
+
# This module provides methods that can be used to generate command entries
|
6
|
+
# that are specified as ranges in the yaml file.
|
7
|
+
#
|
8
|
+
module DictionaryGenerators
|
9
|
+
# Creates a hash object for range commands like master-volume
|
10
|
+
#
|
11
|
+
def create_range_commands(zone, command, value)
|
12
|
+
case value.count
|
13
|
+
when 3
|
14
|
+
range = Range.new(value[0], value[2])
|
15
|
+
when 2
|
16
|
+
range = Range.new(*value)
|
17
|
+
end
|
18
|
+
tmp = {}
|
19
|
+
range.each do |number|
|
20
|
+
tmp.merge!(number.to_s(16).rjust(2, '0').upcase =>
|
21
|
+
{
|
22
|
+
:name => number.to_s,
|
23
|
+
:description =>
|
24
|
+
@commands[zone][command][:values][value][:description].gsub(/\d - \d+/, number.to_s),
|
25
|
+
:models => @commands[zone][command][:values][value][:models]
|
26
|
+
}
|
27
|
+
)
|
28
|
+
end
|
29
|
+
return 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 =>
|
41
|
+
@commands[zone][command][:values][value[0] + '{xx}'][:description].gsub(/\(.*[\]|\)]$/, v),
|
42
|
+
:models => @commands[zone][command][:values][value[0] + '{xx}'][:models]
|
43
|
+
}
|
44
|
+
)
|
45
|
+
end
|
46
|
+
return tmp
|
47
|
+
end
|
48
|
+
|
49
|
+
# Creates hash object for balance commands
|
50
|
+
#
|
51
|
+
def create_balance_commands(zone, command, _value)
|
52
|
+
tmp = {}
|
53
|
+
['-A', '-8', '-6', '-4', '-2', '00', '+2', '+4', '+6', '+8', '+A'].each do |v|
|
54
|
+
tmp.merge!(v.to_s =>
|
55
|
+
{
|
56
|
+
:name => v.downcase,
|
57
|
+
:description =>
|
58
|
+
@commands[zone][command][:values]['{xx}'][:description].gsub(/\(.*[\]|\)]$/, v),
|
59
|
+
:models => @commands[zone][command][:values]['{xx}'][:models]
|
60
|
+
}
|
61
|
+
)
|
62
|
+
end
|
63
|
+
return tmp
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
module EISCP
|
2
|
+
module Dictionary
|
3
|
+
# This module provides methods to get information from the Dictionary about
|
4
|
+
# commands, values, zones, and models.
|
5
|
+
#
|
6
|
+
module DictionaryHelpers
|
7
|
+
# Return the zone that includes the given command
|
8
|
+
def zone_from_command(command)
|
9
|
+
@zones.each do |zone|
|
10
|
+
@commands[zone].each_pair do |k, _|
|
11
|
+
return zone if command == k
|
12
|
+
end
|
13
|
+
end
|
14
|
+
nil
|
15
|
+
end
|
16
|
+
|
17
|
+
# Return the human readable name of a command
|
18
|
+
def command_to_name(command)
|
19
|
+
command = command.upcase
|
20
|
+
begin
|
21
|
+
zone = zone_from_command(command)
|
22
|
+
return @commands[zone][command][:name]
|
23
|
+
rescue
|
24
|
+
return nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Return the command from a given command name
|
29
|
+
def command_name_to_command(name, command_zone = nil)
|
30
|
+
if command_zone.nil?
|
31
|
+
|
32
|
+
@zones.each do |zone|
|
33
|
+
@commands[zone].each_pair do |command, attrs|
|
34
|
+
return command if attrs[:name] == name
|
35
|
+
end
|
36
|
+
end
|
37
|
+
return nil
|
38
|
+
|
39
|
+
else
|
40
|
+
|
41
|
+
@commands[command_zone].each_pair do |command, attrs|
|
42
|
+
return command if attrs[:name] == name
|
43
|
+
end
|
44
|
+
return nil
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Return a value name from a command and a value
|
50
|
+
def command_value_to_value_name(command, value)
|
51
|
+
begin
|
52
|
+
zone = zone_from_command(command)
|
53
|
+
@commands[zone][command][:values][value][:name]
|
54
|
+
rescue
|
55
|
+
nil
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Return a value from a command and value name
|
60
|
+
def command_value_name_to_value(command, value_name)
|
61
|
+
begin
|
62
|
+
zone = zone_from_command(command)
|
63
|
+
@commands[zone][command][:values].each_pair do |k, v|
|
64
|
+
return k if v[:name] == value_name.to_s
|
65
|
+
end
|
66
|
+
rescue
|
67
|
+
nil
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Return a command description from a command name and zone
|
72
|
+
def description_from_command_name(name, zone)
|
73
|
+
@commands[zone].each_pair do |command, attrs|
|
74
|
+
if attrs[:name] == name
|
75
|
+
return @commands[zone][command][:description]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
nil
|
79
|
+
end
|
80
|
+
|
81
|
+
# Return a command description from a command
|
82
|
+
def description_from_command(command)
|
83
|
+
zone = zone_from_command(command)
|
84
|
+
@commands[zone][command][:description]
|
85
|
+
end
|
86
|
+
|
87
|
+
# Return a value description from a command and value
|
88
|
+
def description_from_command_value(command, value)
|
89
|
+
zone = zone_from_command(command)
|
90
|
+
@commands[zone][command][:values].select do |k, v|
|
91
|
+
return v[:description] if k == value
|
92
|
+
end
|
93
|
+
nil
|
94
|
+
end
|
95
|
+
|
96
|
+
# Return a list of commands compatible with a given model
|
97
|
+
def list_compatible_commands(modelstring)
|
98
|
+
sets = []
|
99
|
+
@modelsets.each_pair do |set, array|
|
100
|
+
sets << set if array.include? modelstring
|
101
|
+
end
|
102
|
+
sets
|
103
|
+
end
|
104
|
+
|
105
|
+
# Checks to see if the command is in the Dictionary
|
106
|
+
#
|
107
|
+
def known_command?(command)
|
108
|
+
begin
|
109
|
+
zone = zone_from_command(command)
|
110
|
+
@commands[zone].include? command
|
111
|
+
rescue
|
112
|
+
return nil
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require_relative './dictionary/dictionary_generators'
|
2
|
+
require_relative './dictionary/dictionary_helpers'
|
3
|
+
|
4
|
+
module EISCP
|
5
|
+
# This module provides an interface to the information from the yaml file. It
|
6
|
+
# uses DictionaryGenerators to add commands specified by ranges in the yaml
|
7
|
+
# file. It uses DictionaryHelpers to convert commands and values to and from
|
8
|
+
# their human readable form.
|
9
|
+
#
|
10
|
+
module Dictionary
|
11
|
+
extend DictionaryGenerators
|
12
|
+
extend DictionaryHelpers
|
13
|
+
|
14
|
+
class << self
|
15
|
+
attr_reader :zones
|
16
|
+
attr_reader :modelsets
|
17
|
+
attr_reader :commands
|
18
|
+
end
|
19
|
+
|
20
|
+
DEFAULT_ZONE = 'main'
|
21
|
+
@yaml_file_path = File.join(File.expand_path(File.dirname(__FILE__)), '../../eiscp-commands.yaml')
|
22
|
+
@commands = YAML.load(File.read(@yaml_file_path))
|
23
|
+
@modelsets = @commands[:modelsets]
|
24
|
+
@commands.delete(:modelsets)
|
25
|
+
@zones = @commands.map { |k, _| k }
|
26
|
+
|
27
|
+
@additions = []
|
28
|
+
@commands.each_key do |zone|
|
29
|
+
@commands[zone].each do |command|
|
30
|
+
command = command[0]
|
31
|
+
@commands[zone][command][:values].each do |value|
|
32
|
+
value = value[0]
|
33
|
+
if value.is_a? Array
|
34
|
+
@additions << [zone, command, value, create_range_commands(zone, command, value)]
|
35
|
+
elsif value.match(/^(B|T){xx}$/)
|
36
|
+
@additions << [zone, command, value, create_treble_bass_commands(zone, command, value)]
|
37
|
+
elsif value.match(/^{xx}$/)
|
38
|
+
@additions << [zone, command, value, create_balance_commands(zone, command, value)]
|
39
|
+
else
|
40
|
+
next
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
@additions.each do |zone, command, value, hash|
|
47
|
+
begin
|
48
|
+
@commands[zone][command][:values].merge! hash
|
49
|
+
rescue
|
50
|
+
puts "Failed to add #{hash} to #{zone}:#{command}:#{value}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/eiscp/message.rb
CHANGED
@@ -1,99 +1,123 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require_relative './dictionary'
|
3
|
+
require_relative './parser'
|
4
|
+
|
1
5
|
module EISCP
|
6
|
+
# The EISCP::Message class is used to handle commands and responses.
|
7
|
+
#
|
8
|
+
# Messages can be parsed directly from raw data or created with values:
|
9
|
+
# receiver = Receiver.new
|
10
|
+
#
|
11
|
+
# command = EISCP::Message.new('PWR', 'QSTN')
|
12
|
+
# response = EISCP::Parser.parse(receiver.send_recv(command))
|
13
|
+
#
|
2
14
|
class Message
|
3
|
-
|
4
15
|
# EISCP header
|
5
16
|
attr_accessor :header
|
6
|
-
|
17
|
+
# ISCP "magic" indicates the start of an eISCP message.
|
18
|
+
MAGIC = 'ISCP'
|
19
|
+
# eISCP header size, fixed length.
|
7
20
|
HEADER_SIZE = 16
|
8
|
-
|
21
|
+
# ISCP protocol version.
|
22
|
+
ISCP_VERSION = 1
|
23
|
+
# Reserved for future protocol updates.
|
9
24
|
RESERVED = "\x00\x00\x00"
|
10
25
|
|
26
|
+
# ISCP Start character, usually "!"
|
27
|
+
attr_reader :start
|
28
|
+
# ISCP Unit Type character, usually "1"
|
29
|
+
attr_reader :unit_type
|
30
|
+
# ISCP Command
|
31
|
+
attr_reader :command
|
32
|
+
# Human readable command name
|
33
|
+
attr_reader :command_name
|
34
|
+
# Command description
|
35
|
+
attr_reader :command_description
|
36
|
+
# ISCP Command Value
|
37
|
+
attr_reader :value
|
38
|
+
# Human readable value name
|
39
|
+
attr_reader :value_name
|
40
|
+
# Value description
|
41
|
+
attr_reader :value_description
|
42
|
+
# ISCP Zone
|
43
|
+
attr_reader :zone
|
44
|
+
# Differentiates parsed messages from command messages
|
45
|
+
attr_reader :parsed
|
46
|
+
|
47
|
+
# Terminator character for eISCP packets
|
48
|
+
attr_reader :terminator
|
49
|
+
|
50
|
+
# Create an ISCP message
|
51
|
+
# @param [String] command three-character length ISCP command
|
52
|
+
# @param [String] value variable length ISCP command value
|
53
|
+
# @param [String] unit_type_character override default unit type character, optional
|
54
|
+
# @param [String] start_character override default start character, optional
|
55
|
+
def initialize(command: nil, value: nil, terminator: "\r\n", unit_type: '1', start: '!')
|
56
|
+
unless Dictionary.known_command?(command)
|
57
|
+
warn "Unknown command #{command}"
|
58
|
+
end
|
11
59
|
|
12
|
-
|
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)?/
|
60
|
+
fail 'No value specified.' if value.nil?
|
22
61
|
|
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
62
|
@command = command
|
35
|
-
@
|
36
|
-
@
|
37
|
-
@
|
38
|
-
|
39
|
-
|
40
|
-
:
|
41
|
-
:
|
63
|
+
@value = value
|
64
|
+
@terminator = terminator
|
65
|
+
@unit_type = unit_type
|
66
|
+
@start = start
|
67
|
+
@header = { magic: MAGIC,
|
68
|
+
header_size: HEADER_SIZE,
|
69
|
+
data_size: to_iscp.length,
|
70
|
+
version: ISCP_VERSION,
|
71
|
+
reserved: RESERVED
|
42
72
|
}
|
43
|
-
|
44
|
-
|
45
|
-
|
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.
|
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."
|
73
|
+
begin
|
74
|
+
get_human_readable_attrs
|
75
|
+
rescue
|
76
|
+
warn "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 ? true : false
|
86
84
|
end
|
87
85
|
|
88
86
|
# Return ISCP Message string
|
87
|
+
#
|
89
88
|
def to_iscp
|
90
|
-
|
89
|
+
"#{@start + @unit_type + @command + @value}"
|
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
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require_relative '../message'
|
2
|
+
require_relative './iscp_parser'
|
3
|
+
|
4
|
+
module EISCP
|
5
|
+
module Parser
|
6
|
+
# This module parses an eISCP string and returns a Message object
|
7
|
+
#
|
8
|
+
module EISCPParser
|
9
|
+
def self.parse(string)
|
10
|
+
array = string.unpack('A4NNCa3A*')
|
11
|
+
msg = ISCPParser.parse(array[5])
|
12
|
+
packet = Message.new(
|
13
|
+
command: msg.command,
|
14
|
+
value: msg.value,
|
15
|
+
terminator: msg.terminator,
|
16
|
+
unit_type: msg.unit_type,
|
17
|
+
start: msg.start
|
18
|
+
)
|
19
|
+
packet.header = {
|
20
|
+
magic: array[0],
|
21
|
+
header_size: array[1],
|
22
|
+
data_size: array[2],
|
23
|
+
version: array[3],
|
24
|
+
reserved: array[4]
|
25
|
+
}
|
26
|
+
packet
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.validate(packet)
|
30
|
+
packet.header.header_size.size == packet.command.size
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class EISCPParserException < Exception; end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require_relative '../message'
|
2
|
+
require_relative '../dictionary'
|
3
|
+
|
4
|
+
module EISCP
|
5
|
+
module Parser
|
6
|
+
# This module parses a human readable command and returns a Message object
|
7
|
+
#
|
8
|
+
module HumanReadableParser
|
9
|
+
def self.parse(string)
|
10
|
+
array = string.split(' ')
|
11
|
+
|
12
|
+
if Dictionary.zones.include? array[0]
|
13
|
+
parsed_zone = array.shift
|
14
|
+
else
|
15
|
+
parsed_zone = Dictionary::DEFAULT_ZONE
|
16
|
+
end
|
17
|
+
|
18
|
+
command_name = array.shift
|
19
|
+
value_name = array.join(" ")
|
20
|
+
command = Dictionary.command_name_to_command(command_name, parsed_zone)
|
21
|
+
value = Dictionary.command_value_name_to_value(command, value_name)
|
22
|
+
if (command.nil? || value.nil?)
|
23
|
+
return nil
|
24
|
+
end
|
25
|
+
Message.new(command: command, value: value)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require_relative '../message'
|
2
|
+
|
3
|
+
module EISCP
|
4
|
+
module Parser
|
5
|
+
# This module parses an ISCP string and returns a Message object
|
6
|
+
#
|
7
|
+
module ISCPParser
|
8
|
+
# Regexp for parsing ISCP messages
|
9
|
+
REGEX = /(?<start>!)?(?<unit_type>(\d|x))?(?<command>[A-Z]{3})\s?(?<value>.*?)(?<terminator>[[:cntrl:]]*$)/
|
10
|
+
def self.parse(string)
|
11
|
+
match = string.match(REGEX)
|
12
|
+
|
13
|
+
# Convert MatchData to Hash
|
14
|
+
hash = Hash[match.names.zip(match.captures)]
|
15
|
+
|
16
|
+
# Remove nil and blank values
|
17
|
+
hash.delete_if { |_, v| v.nil? || v == '' }
|
18
|
+
|
19
|
+
# Convert keys to symbols
|
20
|
+
hash = hash.inject({}) do |memo, (k, v)|
|
21
|
+
memo[k.to_sym] = v
|
22
|
+
memo
|
23
|
+
end
|
24
|
+
|
25
|
+
Message.new(**hash)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/eiscp/parser.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require_relative './parser/eiscp_parser'
|
2
|
+
require_relative './parser/iscp_parser'
|
3
|
+
require_relative './parser/human_readable_parser'
|
4
|
+
|
5
|
+
module EISCP
|
6
|
+
# This module provides an interface to the other parser modules. It identifies
|
7
|
+
# the type of string to be parsed and then passes the string off to the
|
8
|
+
# appropriate parser module.
|
9
|
+
#
|
10
|
+
module Parser
|
11
|
+
def self.parse(string)
|
12
|
+
case string
|
13
|
+
when /^ISCP/
|
14
|
+
EISCPParser.parse(string)
|
15
|
+
when ISCPParser::REGEX
|
16
|
+
ISCPParser.parse(string)
|
17
|
+
else
|
18
|
+
HumanReadableParser.parse(string)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require_relative '../parser'
|
2
|
+
require_relative '../dictionary'
|
3
|
+
|
4
|
+
module EISCP
|
5
|
+
class Receiver
|
6
|
+
# Iterates through every available command and defines a method to call that
|
7
|
+
# command. It's intended to be used through Receiver and uses methods included
|
8
|
+
# by Receiver::Connection. Each method accepts a string that should match the
|
9
|
+
# human readable name of a valid value for that command.
|
10
|
+
#
|
11
|
+
module CommandMethods
|
12
|
+
def self.generate
|
13
|
+
Dictionary.zones.each do |zone|
|
14
|
+
Dictionary.commands[zone].each do |command, _values|
|
15
|
+
begin
|
16
|
+
command_name = Dictionary.command_to_name(command).to_s.gsub(/-/, '_')
|
17
|
+
define_method(command_name) do |v|
|
18
|
+
yield Parser.parse(command_name.gsub(/_/, '-') + ' ' + v)
|
19
|
+
end
|
20
|
+
rescue => e
|
21
|
+
puts e
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require_relative '../parser'
|
3
|
+
|
4
|
+
module EISCP
|
5
|
+
class Receiver
|
6
|
+
# This module handles connecting, sending, and receiving for Receivers.
|
7
|
+
#
|
8
|
+
module Connection
|
9
|
+
# Receiver's connection socket
|
10
|
+
attr_reader :socket
|
11
|
+
# Receiver's connection thread
|
12
|
+
attr_reader :thread
|
13
|
+
# Most recent message received
|
14
|
+
attr_reader :last
|
15
|
+
|
16
|
+
# Default connection timeout value in seconds
|
17
|
+
DEFAULT_TIMEOUT = 0.5
|
18
|
+
|
19
|
+
# Default Onkyo eISCP port
|
20
|
+
ONKYO_PORT = 60_128
|
21
|
+
|
22
|
+
# Create a new connection thread. Also accepts a block that will run
|
23
|
+
# whenver a message is received. You can pass the Message object in with
|
24
|
+
# your block. This is the method #new uses to create the initial thread.
|
25
|
+
#
|
26
|
+
def update_thread
|
27
|
+
@thread && @thread.kill
|
28
|
+
@thread = Thread.new do
|
29
|
+
loop do
|
30
|
+
recv
|
31
|
+
yield(@last) if block_given?
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# This handles the background thread for monitoring messages from the
|
37
|
+
# receiver.
|
38
|
+
#
|
39
|
+
# If a block is given, it can be used to setup a callback when a message
|
40
|
+
# is received.
|
41
|
+
#
|
42
|
+
def connect(host, port = ONKYO_PORT, &block)
|
43
|
+
begin
|
44
|
+
@socket = TCPSocket.new(host, port)
|
45
|
+
update_thread(&block)
|
46
|
+
rescue => e
|
47
|
+
puts e
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Sends an EISCP::Message object or string on the network
|
52
|
+
#
|
53
|
+
def send(eiscp)
|
54
|
+
if eiscp.is_a? EISCP::Message
|
55
|
+
@socket.puts(eiscp.to_eiscp)
|
56
|
+
elsif eiscp.is_a? String
|
57
|
+
@socket.puts eiscp
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Reads the socket and returns and EISCP::Message
|
62
|
+
#
|
63
|
+
def recv
|
64
|
+
message = ''
|
65
|
+
message << @socket.gets until message.match(/\r\n$/) do
|
66
|
+
@last = Parser.parse(message)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Sends an EISCP::Message object or string on the network and returns recieved data string.
|
71
|
+
#
|
72
|
+
def send_recv(eiscp)
|
73
|
+
if eiscp.is_a? EISCP::Message
|
74
|
+
@socket.puts(eiscp.to_eiscp)
|
75
|
+
elsif eiscp.is_a? String
|
76
|
+
@socket.puts(eiscp)
|
77
|
+
end
|
78
|
+
sleep DEFAULT_TIMEOUT
|
79
|
+
last
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|