plc_access 0.1.0

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.
@@ -0,0 +1,265 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The MIT License (MIT)
4
+ #
5
+ # Copyright (c) 2016 ITO SOFT DESIGN Inc.
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining
8
+ # a copy of this software and associated documentation files (the
9
+ # "Software"), to deal in the Software without restriction, including
10
+ # without limitation the rights to use, copy, modify, merge, publish,
11
+ # distribute, sublicense, and/or sell copies of the Software, and to
12
+ # permit persons to whom the Software is furnished to do so, subject to
13
+ # the following conditions:
14
+ #
15
+ # The above copyright notice and this permission notice shall be
16
+ # included in all copies or substantial portions of the Software.
17
+ #
18
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25
+
26
+ module PlcAccess
27
+ module Protocol
28
+ module Mitsubishi
29
+ class FxProtocol < Protocol
30
+ attr_accessor :pc_no, :baudrate, :station_no, :wait_time
31
+
32
+ STX = "\u0002"
33
+ ETX = "\u0003"
34
+ EOT = "\u0004"
35
+ ENQ = "\u0005"
36
+ ACK = "\u0006"
37
+ LF = "\u000a"
38
+ CR = "\u000d"
39
+ NAK = "\u0015"
40
+
41
+ DELIMITER = "\r\n"
42
+
43
+ def initialize(options = {})
44
+ super
45
+ @port = options[:port] || `ls /dev/tty.usb*`.split("\n").map(&:chomp).first
46
+ @pc_no = 0xff
47
+ @baudrate = 19_200
48
+ @station_no = 0
49
+ @wait_time = 0
50
+ @comm = nil
51
+ end
52
+
53
+ def open
54
+ open!
55
+ rescue StandardError
56
+ nil
57
+ end
58
+
59
+ def open!
60
+ return false unless @port
61
+
62
+ begin
63
+ # port, baudrate, bits, stop bits, parity(0:none, 1:even, 2:odd)
64
+ @comm ||= SerialPort.new(@port, @baudrate, 7, 1, 2).tap do |s|
65
+ s.read_timeout = (TIMEOUT * 1000.0).to_i
66
+ end
67
+ raise StandardError.new("invalid port #{@port}") unless @comm
68
+ rescue StandardError => e
69
+ p e
70
+ nil
71
+ end
72
+ end
73
+
74
+ def close
75
+ @comm&.close
76
+ @comm = nil
77
+ end
78
+
79
+ def get_bits_from_device(count, device)
80
+ unless available_bits_range.include? count
81
+ raise ArgumentError,
82
+ "A count #{count} must be between #{available_bits_range.first} and #{available_bits_range.last} for #{__method__}"
83
+ end
84
+
85
+ device = device_by_name device
86
+ packet = body_for_get_bits_from_device(count, device) + DELIMITER
87
+ @logger.debug("> #{dump_packet packet}")
88
+ open
89
+ @comm.write(packet)
90
+ @comm.flush
91
+ res = receive
92
+ bits = []
93
+
94
+ if res && (check_sum(res[0..-5]) == res[-4, 2])
95
+ bits =
96
+ res[5..-6].each_char.map do |c|
97
+ c == '1'
98
+ end
99
+ end
100
+ @logger.debug("> #{dump_packet ack_packet}")
101
+ @comm.write(ack_packet)
102
+ @logger.debug("get #{device.name} => #{bits}")
103
+
104
+ bits
105
+ end
106
+
107
+ def set_bits_to_device(bits, device)
108
+ unless available_bits_range.include? bits.size
109
+ raise ArgumentError,
110
+ "A count #{count} must be between #{available_bits_range.first} and #{available_bits_range.last} for #{__method__}"
111
+ end
112
+
113
+ device = device_by_name device
114
+ packet = body_for_set_bits_to_device(bits, device)
115
+ @logger.debug("> #{dump_packet packet}")
116
+ open
117
+ @comm.write(packet)
118
+ @comm.flush
119
+ res = receive
120
+ @logger.debug("set #{bits} to:#{device.name}")
121
+
122
+ # error checking
123
+ raise "ERROR: return #{res} for set_bits_to_device(#{bits}, #{device.name})" unless res == ack_packet
124
+ end
125
+
126
+ def get_words_from_device(count, device)
127
+ unless available_words_range.include? count
128
+ raise ArgumentError,
129
+ "A count #{count} must be between #{available_words_range.first} and #{available_words_range.last} for #{__method__}"
130
+ end
131
+
132
+ device = device_by_name device
133
+ packet = body_for_get_words_from_device(count, device) + DELIMITER
134
+ @logger.debug("> #{dump_packet packet}")
135
+ open
136
+ @comm.write(packet)
137
+ @comm.flush
138
+ res = receive
139
+ words = []
140
+
141
+ if res && (check_sum(res[0..-5]) == res[-4, 2])
142
+ words =
143
+ res[5..-6].scan(/.{4}/).map do |v|
144
+ v.to_i(16)
145
+ end
146
+ end
147
+ @logger.debug("> #{dump_packet ack_packet}")
148
+ @comm.write(ack_packet)
149
+ @logger.debug("get #{device.name} => #{words}")
150
+
151
+ words
152
+ end
153
+
154
+ def set_words_to_device(words, device)
155
+ unless available_bits_range.include? words.size
156
+ raise ArgumentError,
157
+ "A count of words #{words.size} must be between #{available_words_range.first} and #{available_words_range.last} for #{__method__}"
158
+ end
159
+
160
+ device = device_by_name device
161
+ packet = body_for_set_words_to_device(words, device)
162
+ @logger.debug("> #{dump_packet packet}")
163
+ open
164
+ @comm.write(packet)
165
+ @comm.flush
166
+ res = receive
167
+ @logger.debug("set #{words} to: #{device.name}")
168
+
169
+ # error checking
170
+ raise "ERROR: return #{res} for set_bits_to_device(#{words}, #{device.name})" unless res == ack_packet
171
+ end
172
+
173
+ def device_by_name(name)
174
+ case name
175
+ when String
176
+ d = FxDevice.new name
177
+ d.valid? ? d : nil
178
+ else
179
+ # it may be already QDevice
180
+ name
181
+ end
182
+ end
183
+
184
+ def receive
185
+ res = ''
186
+ begin
187
+ Timeout.timeout(TIMEOUT) do
188
+ res = @comm.gets
189
+ end
190
+ res
191
+ rescue Timeout::Error
192
+ puts "*** ERROR: TIME OUT : #{res} ***"
193
+ end
194
+ @logger.debug("< #{dump_packet res}")
195
+ res
196
+ end
197
+
198
+ def available_bits_range(_device = nil)
199
+ 1..256
200
+ end
201
+
202
+ def available_words_range(_device = nil)
203
+ 1..64
204
+ end
205
+
206
+ def body_for_get_bit_from_deivce(device)
207
+ body_for_get_bits_from_device 1, device
208
+ end
209
+
210
+ def body_for_get_bits_from_device(count, device)
211
+ body = header_with_command 'BR'
212
+ body += "#{device.name}#{count.to_s(16).rjust(2, '0')}"
213
+ body += check_sum(body)
214
+ body += DELIMITER
215
+ body.upcase
216
+ end
217
+
218
+ def body_for_get_words_from_device(count, device)
219
+ body = header_with_command 'WR'
220
+ body += "#{device.name}#{count.to_s(16).rjust(2, '0')}"
221
+ body += check_sum(body)
222
+ body += DELIMITER
223
+ body.upcase
224
+ end
225
+
226
+ def body_for_set_bits_to_device(bits, device)
227
+ body = header_with_command 'BW'
228
+ body += "#{device.name}#{bits.count.to_s(16).rjust(2, '0')}"
229
+ body += bits.map { |b| b ? '1' : '0' }.join('')
230
+ body += check_sum(body)
231
+ body += DELIMITER
232
+ body.upcase
233
+ end
234
+ alias body_for_set_bit_to_device body_for_set_bits_to_device
235
+
236
+ def body_for_set_words_to_device(words, device)
237
+ body = header_with_command 'WW'
238
+ body += "#{device.name}#{words.count.to_s(16).rjust(2, '0')}"
239
+ body += words.map { |w| w.to_s(16).rjust(4, '0') }.join('')
240
+ body += check_sum(body)
241
+ body += DELIMITER
242
+ body.upcase
243
+ end
244
+
245
+ def dump_packet(packet)
246
+ packet.inspect
247
+ end
248
+
249
+ private
250
+
251
+ def check_sum(packet)
252
+ packet[1..].upcase.unpack('C*').inject(0) { |s, c| s + c }.to_s(16).rjust(2, '0')[-2, 2].upcase
253
+ end
254
+
255
+ def header_with_command(command)
256
+ "#{ENQ}#{station_no.to_s.rjust(2, '0')}#{pc_no.to_s(16).rjust(2, '0')}#{command}#{wait_time}".upcase
257
+ end
258
+
259
+ def ack_packet
260
+ "#{ACK}#{station_no.to_s.rjust(2, '0')}#{pc_no.to_s(16).rjust(2, '0')}#{DELIMITER}".upcase
261
+ end
262
+ end
263
+ end
264
+ end
265
+ end
@@ -0,0 +1,281 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The MIT License (MIT)
4
+ #
5
+ # Copyright (c) 2016 ITO SOFT DESIGN Inc.
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining
8
+ # a copy of this software and associated documentation files (the
9
+ # "Software"), to deal in the Software without restriction, including
10
+ # without limitation the rights to use, copy, modify, merge, publish,
11
+ # distribute, sublicense, and/or sell copies of the Software, and to
12
+ # permit persons to whom the Software is furnished to do so, subject to
13
+ # the following conditions:
14
+ #
15
+ # The above copyright notice and this permission notice shall be
16
+ # included in all copies or substantial portions of the Software.
17
+ #
18
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25
+
26
+ module PlcAccess
27
+ module Protocol
28
+ module Mitsubishi
29
+ class McProtocol < Protocol
30
+ def initialize(options = {})
31
+ super
32
+ @socket = nil
33
+ @host = options[:host] || '192.168.0.10'
34
+ @port = options[:port] || 5010
35
+ end
36
+
37
+ def open
38
+ open!
39
+ rescue StandardError
40
+ nil
41
+ end
42
+
43
+ def open!
44
+ @socket ||= TCPSocket.open(@host, @port)
45
+ end
46
+
47
+ def close
48
+ @socket&.close
49
+ @socket = nil
50
+ end
51
+
52
+ def get_bits_from_device(count, device)
53
+ unless available_bits_range.include? count
54
+ raise ArgumentError,
55
+ "A count #{count} must be between #{available_bits_range.first} and #{available_bits_range.last} for #{__method__}"
56
+ end
57
+
58
+ device = device_by_name device
59
+ packet = make_packet(body_for_get_bits_from_device(count, device))
60
+ @logger.debug("> #{dump_packet packet}")
61
+ open
62
+ @socket.write(packet.pack('C*'))
63
+ @socket.flush
64
+ res = receive
65
+
66
+ # error checking
67
+ end_code = res[9, 2].pack('C*').unpack1('v')
68
+ unless end_code.zero?
69
+ error = res[11, 2].pack('C*').unpack1('v')
70
+ raise "return end code 0x#{end_code.to_s(16)} error code 0x#{error.to_s(16)} for get_bits_from_device(#{count}, #{device.name})"
71
+ end
72
+
73
+ # get results
74
+ bits = []
75
+ count.times do |i|
76
+ v = res[11 + i / 2]
77
+ bits << if i.even?
78
+ ((v >> 4) != 0)
79
+ else
80
+ ((v & 0xf) != 0)
81
+ end
82
+ end
83
+ @logger.debug("get #{device.name} => #{bits}")
84
+ bits
85
+ end
86
+
87
+ def set_bits_to_device(bits, device)
88
+ unless available_bits_range.include? bits.size
89
+ raise ArgumentError,
90
+ "A count #{count} must be between #{available_bits_range.first} and #{available_bits_range.last} for #{__method__}"
91
+ end
92
+
93
+ device = device_by_name device
94
+ packet = make_packet(body_for_set_bits_to_device(bits, device))
95
+ @logger.debug("> #{dump_packet packet}")
96
+ open
97
+ @socket.write(packet.pack('C*'))
98
+ @socket.flush
99
+ res = receive
100
+ @logger.debug("set #{bits} to:#{device.name}")
101
+
102
+ # error checking
103
+ end_code = res[9, 2].pack('C*').unpack1('v')
104
+ unless end_code.zero?
105
+ error = res[11, 2].pack('C*').unpack1('v')
106
+ raise "return end code 0x#{end_code.to_s(16)} error code 0x#{error.to_s(16)} for set_bits_to_device(#{bits}, #{device.name})"
107
+ end
108
+ end
109
+
110
+ def get_words_from_device(count, device)
111
+ unless available_bits_range.include? count
112
+ raise ArgumentError,
113
+ "A count #{count} must be between #{available_words_range.first} and #{available_words_range.last} for #{__method__}"
114
+ end
115
+
116
+ device = device_by_name device
117
+ packet = make_packet(body_for_get_words_from_device(count, device))
118
+ @logger.debug("> #{dump_packet packet}")
119
+ open
120
+ @socket.write(packet.pack('C*'))
121
+ @socket.flush
122
+ res = receive
123
+
124
+ # error checking
125
+ end_code = res[9, 2].pack('C*').unpack1('v')
126
+ unless end_code.zero?
127
+ error = res[11, 2].pack('C*').unpack1('v')
128
+ raise "return end code 0x#{end_code.to_s(16)} error code 0x#{error.to_s(16)} for get_words_from_device(#{count}, #{device.name})"
129
+ end
130
+
131
+ # get result
132
+ words = []
133
+ res[11, 2 * count].each_slice(2) do |pair|
134
+ words << pair.pack('C*').unpack1('v')
135
+ end
136
+ @logger.debug("get from: #{device.name} => #{words}")
137
+ words
138
+ end
139
+
140
+ def set_words_to_device(words, device)
141
+ unless available_bits_range.include? words.size
142
+ raise ArgumentError,
143
+ "A count of words #{words.size} must be between #{available_words_range.first} and #{available_words_range.last} for #{__method__}"
144
+ end
145
+
146
+ device = device_by_name device
147
+ packet = make_packet(body_for_set_words_to_device(words, device))
148
+ @logger.debug("> #{dump_packet packet}")
149
+ open
150
+ @socket.write(packet.pack('C*'))
151
+ @socket.flush
152
+ res = receive
153
+ @logger.debug("set #{words} to: #{device.name}")
154
+
155
+ # error checking
156
+ end_code = res[9, 2].pack('C*').unpack1('v')
157
+ unless end_code.zero?
158
+ error = res[11, 2].pack('C*').unpack1('v')
159
+ raise "return end code 0x#{end_code.to_s(16)} error code 0x#{error.to_s(16)} for set_words_to_device(#{words}, #{device.name})"
160
+ end
161
+ end
162
+
163
+ def device_by_name(name)
164
+ case name
165
+ when String
166
+ d = QDevice.new name
167
+ d.valid? ? d : nil
168
+ else
169
+ # it may be already QDevice
170
+ name
171
+ end
172
+ end
173
+
174
+ def receive
175
+ res = []
176
+ len = 0
177
+ begin
178
+ Timeout.timeout(TIMEOUT) do
179
+ loop do
180
+ c = @socket.read(1)
181
+ next if c.nil? || c == ''
182
+
183
+ res << c.bytes.first
184
+ len = res[7, 2].pack('C*').unpack1('v*') if res.length >= 9
185
+ break if len + 9 == res.length
186
+ end
187
+ end
188
+ rescue Timeout::Error
189
+ puts '*** ERROR: TIME OUT ***'
190
+ end
191
+ @logger.debug("< #{dump_packet res}")
192
+ res
193
+ end
194
+
195
+ def available_bits_range(_device = nil)
196
+ 1..(960 * 16)
197
+ end
198
+
199
+ def available_words_range(_device = nil)
200
+ 1..960
201
+ end
202
+
203
+ private
204
+
205
+ def make_packet(body)
206
+ header = [0x50, 0x00, 0x00, 0xff, 0xff, 0x03, 0x00, 0x00, 0x00, 0x10, 0x00]
207
+ header[7..8] = data_for_short(body.length + 2)
208
+ header + body
209
+ end
210
+
211
+ def body_for_get_bit_from_deivce(device)
212
+ body_for_get_bits_from_device 1, device
213
+ end
214
+
215
+ def body_for_get_bits_from_device(count, device)
216
+ body_for_get_words_from_device count, device, false
217
+ end
218
+
219
+ def body_for_get_words_from_device(count, device, word = true)
220
+ body = [0x01, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0x01, 0x00]
221
+ body[2] = 1 unless word
222
+ body[4..7] = data_for_device(device)
223
+ body[8..9] = data_for_short count
224
+ body
225
+ end
226
+
227
+ def body_for_set_bits_to_device(bits, device)
228
+ body = [0x01, 0x14, 0x01, 0x00, 0x00, 0x00, 0x00, 0x90, 0x01, 0x00]
229
+ d = device
230
+ bits = [bits] unless bits.is_a? Array
231
+ bits.each_slice(2) do |pair|
232
+ body << (pair.first ? 0x10 : 0x00)
233
+ body[-1] |= (pair.last ? 0x1 : 0x00) if pair.size == 2
234
+ d = d.next_device
235
+ end
236
+ body[4..7] = data_for_device(device)
237
+ body[8..9] = data_for_short bits.size
238
+ body
239
+ end
240
+ alias body_for_set_bit_to_device body_for_set_bits_to_device
241
+
242
+ def body_for_set_words_to_device(words, device)
243
+ body = [0x01, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0x01, 0x00]
244
+ d = device
245
+ words = [words] unless words.is_a? Array
246
+ words.each do |v|
247
+ body += data_for_short v
248
+ d = d.next_device
249
+ end
250
+ body[4..7] = data_for_device(device)
251
+ body[8..9] = data_for_short words.size
252
+ body
253
+ end
254
+
255
+ def data_for_device(device)
256
+ a = data_for_int device.number
257
+ a[3] = device.suffix_code
258
+ a
259
+ end
260
+
261
+ def data_for_short(value)
262
+ [value].pack('v').unpack('C*')
263
+ end
264
+
265
+ def data_for_int(value)
266
+ [value].pack('V').unpack('C*')
267
+ end
268
+
269
+ def dump_packet(packet)
270
+ a = []
271
+ len = packet.length
272
+ bytes = packet.dup
273
+ len.times do |i|
274
+ a << "0#{bytes[i].to_s(16)}"[-2, 2]
275
+ end
276
+ "[#{a.join(', ')}]"
277
+ end
278
+ end
279
+ end
280
+ end
281
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The MIT License (MIT)
4
+ #
5
+ # Copyright (c) 2016 ITO SOFT DESIGN Inc.
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining
8
+ # a copy of this software and associated documentation files (the
9
+ # "Software"), to deal in the Software without restriction, including
10
+ # without limitation the rights to use, copy, modify, merge, publish,
11
+ # distribute, sublicense, and/or sell copies of the Software, and to
12
+ # permit persons to whom the Software is furnished to do so, subject to
13
+ # the following conditions:
14
+ #
15
+ # The above copyright notice and this permission notice shall be
16
+ # included in all copies or substantial portions of the Software.
17
+ #
18
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25
+
26
+ $LOAD_PATH.unshift File.dirname(__FILE__)
27
+
28
+ require 'socket'
29
+ require 'logger'
30
+ require 'timeout'
31
+
32
+ require 'qdevice'
33
+ require 'mc_protocol'
34
+
35
+ require 'fx_device'
36
+ require 'fx_protocol'
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The MIT License (MIT)
4
+ #
5
+ # Copyright (c) 2016 ITO SOFT DESIGN Inc.
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining
8
+ # a copy of this software and associated documentation files (the
9
+ # "Software"), to deal in the Software without restriction, including
10
+ # without limitation the rights to use, copy, modify, merge, publish,
11
+ # distribute, sublicense, and/or sell copies of the Software, and to
12
+ # permit persons to whom the Software is furnished to do so, subject to
13
+ # the following conditions:
14
+ #
15
+ # The above copyright notice and this permission notice shall be
16
+ # included in all copies or substantial portions of the Software.
17
+ #
18
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25
+
26
+ module PlcAccess
27
+ module Protocol
28
+ module Mitsubishi
29
+ class QDevice < PlcDevice
30
+ attr_reader :suffix, :number
31
+
32
+ SUFFIXES = %w[SM SD X Y M L F V B D W TS TC TN SS SC SN CS CC CN SB SW S DX DY Z R ZR].freeze
33
+ SUFFIX_CODES = [0x91, 0xa9, 0x9c, 0x9d, 0x90, 0x92, 0x93, 0x94, 0xa0, 0xa8, 0xb4, 0xc1, 0xc0, 0xc2, 0xc7, 0xc6,
34
+ 0xc8, 0xc4, 0xc3, 0xc5, 0xa1, 0xb5, 0x98, 0xa2, 0xa3, 0xcc, 0xaf, 0xb0].freeze
35
+
36
+ def initialize(a, b = nil)
37
+ case a
38
+ when Array
39
+ case a.size
40
+ when 4
41
+ @suffix = suffix_for_code(a[3])
42
+ @number = ((a[2] << 8 | a[1]) << 8) | a[0]
43
+ end
44
+ when String
45
+ if b
46
+ @suffix = a.upcase
47
+ @number = b
48
+ elsif a.length == 12
49
+ @suffix = [a[0, 2].to_i(16), a[2, 2].to_i(16)].pack 'C*'
50
+ @suffix.strip!
51
+ @number = a[4, 8].to_i(16)
52
+ elsif /(X|Y|B|W|SB|SW|DX|DY)([0-9a-f]+)/i =~ a
53
+ @suffix = ::Regexp.last_match(1).upcase
54
+ @number = ::Regexp.last_match(2).to_i(p_adic_number)
55
+ elsif /(M|L|S|F|T|C|D|R|ZR)([0-9]+)/i =~ a
56
+ @suffix = ::Regexp.last_match(1).upcase
57
+ @number = ::Regexp.last_match(2).to_i(p_adic_number)
58
+ end
59
+ end
60
+ end
61
+
62
+ def valid?
63
+ !!@suffix && !!@number
64
+ end
65
+
66
+ def p_adic_number
67
+ case @suffix
68
+ when 'X', 'Y', 'B', 'W', 'SB', 'SW', 'DX', 'DY'
69
+ 16
70
+ else
71
+ 10
72
+ end
73
+ end
74
+
75
+ def name
76
+ @suffix + @number.to_s(p_adic_number).upcase
77
+ end
78
+
79
+ def next_device
80
+ self.class.new @suffix, @number + 1
81
+ end
82
+
83
+ def bit_device?
84
+ case @suffix
85
+ when 'SM', 'X', 'Y', 'M', 'L', 'F', 'V', 'B',
86
+ 'TS', 'TC', 'SS', 'SC', 'CS', 'CC', 'SB', 'S', 'DX', 'DY'
87
+ true
88
+ else
89
+ false
90
+ end
91
+ end
92
+
93
+ def suffix_for_code(code)
94
+ index = SUFFIX_CODES.index code
95
+ index ? SUFFIXES[index] : nil
96
+ end
97
+
98
+ def suffix_code
99
+ index = SUFFIXES.index suffix
100
+ index ? SUFFIX_CODES[index] : 0
101
+ end
102
+
103
+ def +(other)
104
+ self.class.new suffix, number + other
105
+ end
106
+
107
+ def -(other)
108
+ self.class.new suffix, [number - other, 0].max
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end