onkyo_eiscp_ruby 0.0.3 → 1.0.4
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 +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
|