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.
data/lib/eiscp.rb CHANGED
@@ -1,9 +1,12 @@
1
- # Library for controlling Onkyo receivers over TCP/IP.
1
+ # frozen_string_literal: true
2
2
 
3
+ # Library for controlling Onkyo receivers over TCP/IP.
4
+ #
3
5
  module EISCP
4
- VERSION = '0.0.3'
6
+ VERSION = '2.1.2'
5
7
  end
6
8
 
7
- require 'eiscp/receiver'
8
- require 'eiscp/message'
9
- require 'eiscp/command'
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
- MAGIC = "ISCP"
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
- ISCP_VERSION = "\x01"
22
+ # ISCP protocol version.
23
+ ISCP_VERSION = 1
24
+ # Reserved for future protocol updates.
9
25
  RESERVED = "\x00\x00\x00"
10
26
 
11
-
12
- # ISCP attrs
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)?/
22
-
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
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
- # Check if two messages send the same command
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
- 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."
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
- # ISCP Message string parser
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
- return "#{@start + @unit_type + @command + @parameter}"
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
- return [ @header[:magic], @header[:header_size], @header[:data_size], @header[:version], @header[:reserved], @iscp_message.to_s ].pack("A4NNAa3A*")
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