onkyo_eiscp_ruby 0.0.3 → 2.1.2

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.
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