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,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The MIT License (MIT)
4
+ #
5
+ # Copyright (c) 2019 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 Omron
29
+ class CModeProtocol < Protocol
30
+ attr_accessor :baudrate, :unit_no
31
+
32
+ DELIMITER = "\r"
33
+ TERMINATOR = "*\r"
34
+ TIMEOUT = 1.0
35
+
36
+ def initialize(options = {})
37
+ super
38
+ @port = options[:port] || `ls /dev/tty.usb*`.split("\n").map(&:chomp).first
39
+ @baudrate = 38_400
40
+ @unit_no = 0
41
+ @comm = nil
42
+ end
43
+
44
+ def open
45
+ open!
46
+ rescue StandardError
47
+ nil
48
+ end
49
+
50
+ def open!
51
+ return false unless @port
52
+
53
+ begin
54
+ # port, baudrate, bits, stop bits, parity(0:none, 1:even, 2:odd)
55
+ @comm ||= SerialPort.new(@port, @baudrate, 7, 2, 1).tap do |s|
56
+ s.read_timeout = (TIMEOUT * 1000.0).to_i
57
+ end
58
+ rescue StandardError => e
59
+ p e
60
+ nil
61
+ end
62
+ end
63
+
64
+ def close
65
+ @comm&.close
66
+ @comm = nil
67
+ end
68
+
69
+ def unit_no=(no)
70
+ @unit_no = [[no, 0].max, 31].min
71
+ end
72
+
73
+ def get_bits_from_device(count, device)
74
+ device = device_by_name device
75
+
76
+ # convert to the channel device
77
+ from = device.channel_device
78
+ to = (device + count).channel_device
79
+ c = [to - from, 1].max
80
+
81
+ # get value as words
82
+ words = get_words_from_device(c, device)
83
+
84
+ # convert to bit devices
85
+ index = device.bit
86
+ bits = []
87
+ count.times do
88
+ i = index / 16
89
+ b = index % 16
90
+ f = 1 << b
91
+ bits << ((words[i] & f) == f)
92
+ index += 1
93
+ end
94
+ bits
95
+ end
96
+
97
+ def get_words_from_device(count, device)
98
+ device = device_by_name(device).channel_device
99
+
100
+ # make read packet
101
+ packet = read_packet_with device
102
+ packet << "#{device.channel.to_s.rjust(4, '0')}#{count.to_s.rjust(4, '0')}"
103
+ packet << fcs_for(packet).to_s(16).upcase.rjust(2, '0')
104
+ packet << TERMINATOR
105
+ @logger.debug("> #{dump_packet packet}")
106
+
107
+ # send command
108
+ open
109
+ send packet
110
+
111
+ # receive response
112
+ words = []
113
+ terminated = false
114
+ loop do
115
+ res = receive
116
+ data = ''
117
+ if res
118
+ ec = error_code(res)
119
+ raise "Error response: #{ec.to_i(16).rjust(2, '0')}" unless ec.zero?
120
+
121
+ if res[-2, 2] == TERMINATOR
122
+ fcs = fcs_for(res[0..-5])
123
+ raise "Not matched FCS expected #{fcs.to_s(16).rjust(2, '0')}" unless fcs == res[-4, 2].to_i(16)
124
+
125
+ data = res[7..-5]
126
+ terminated = true
127
+ elsif res[-1, 1] == DELIMITER
128
+ fcs = fcs_for(res[0..-4])
129
+ raise "Not matched FCS expected #{fcs.to_s(16).rjust(2, '0')}" unless fcs == res[-3, 2].to_i(16)
130
+
131
+ data = res[7..-4]
132
+ end
133
+ len = data.length
134
+ index = 0
135
+ while index < len
136
+ words << data[index, 4].to_i(16)
137
+ index += 4
138
+ end
139
+ return words if terminated
140
+ else
141
+ break
142
+ end
143
+ end
144
+ []
145
+ end
146
+
147
+ private
148
+
149
+ def device_by_name(name)
150
+ case name
151
+ when String
152
+ d = OmronDevice.new name
153
+ d.valid? ? d : nil
154
+ else
155
+ # it may be already OmronDevice
156
+ name
157
+ end
158
+ end
159
+
160
+ def send(packet)
161
+ @comm.write(packet)
162
+ @comm.flush
163
+ end
164
+
165
+ def receive
166
+ res = ''
167
+ begin
168
+ Timeout.timeout(TIMEOUT) do
169
+ res = @comm.gets DELIMITER
170
+ # loop do
171
+ # res << @comm.getc# '\r' #gets
172
+ # break if res[-1] == '\r'
173
+ # end
174
+ end
175
+ # res
176
+ rescue Timeout::Error
177
+ puts "*** ERROR: TIME OUT : #{res} ***"
178
+ end
179
+ @logger.debug("< #{dump_packet res}")
180
+ res
181
+ end
182
+
183
+ def read_packet_with(device)
184
+ packet = "@#{unit_no.to_s.rjust(2, '0')}R"
185
+ packet << case device.suffix
186
+ when 'HR'
187
+ 'H'
188
+ when 'AL'
189
+ 'L'
190
+ when 'DM', 'D'
191
+ 'D'
192
+ when 'AR'
193
+ 'J'
194
+ when 'EM', 'E'
195
+ 'E'
196
+ else
197
+ 'R'
198
+ end
199
+ end
200
+
201
+ def fcs_for(packet)
202
+ fcs = packet.bytes.inject(0) do |a, b|
203
+ a ^ b
204
+ end
205
+ fcs & 0xff
206
+ end
207
+
208
+ def error_code(packet)
209
+ packet[1 + 2 + 2, 2].to_i(16)
210
+ end
211
+
212
+ def dump_packet(packet)
213
+ packet.inspect
214
+ end
215
+ end
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,353 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The MIT License (MIT)
4
+ #
5
+ # Copyright (c) 2019 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 Omron
29
+ class FinsTcpProtocol < Protocol
30
+ attr_accessor :gateway_count, :destination_network, :destination_node, :destination_unit, :source_network,
31
+ :source_node, :source_unit, :ethernet_module, :tcp_error_code
32
+
33
+ IOFINS_DESTINATION_NODE_FROM_IP = 0
34
+ IOFINS_SOURCE_AUTO_NODE = 0
35
+
36
+ # Available ethernet module.
37
+ ETHERNET_ETN21 = 0
38
+ ETHERNET_CP1E = 1
39
+ ETHERNET_CP1L = 2
40
+ ETHERNET_CP1H = 3
41
+
42
+ TIMEOUT = 5.0
43
+
44
+ def initialize(options = {})
45
+ super
46
+ @socket = nil
47
+ @host = options[:host] || '192.168.250.1'
48
+ @port = options[:port] || 9600
49
+ @gateway_count = 3
50
+ @destination_network = 0
51
+ @destination_node = 0
52
+ @destination_unit = 0
53
+ @source_network = 0
54
+ @source_node = IOFINS_SOURCE_AUTO_NODE
55
+ @source_unit = 0
56
+ @ethernet_module = ETHERNET_ETN21
57
+
58
+ @tcp_error_code = 0
59
+ end
60
+
61
+ def open
62
+ open!
63
+ rescue StandardError => e
64
+ p e
65
+ nil
66
+ end
67
+
68
+ def open!
69
+ if @socket.nil?
70
+ @socket = TCPSocket.open(@host, @port)
71
+ if @socket
72
+ self.source_node = IOFINS_SOURCE_AUTO_NODE
73
+ query_node
74
+ end
75
+ end
76
+ @socket
77
+ end
78
+
79
+ def close
80
+ @socket&.close
81
+ @socket = nil
82
+ end
83
+
84
+ def tcp_error?
85
+ tcp_error_code != 0
86
+ end
87
+
88
+ def create_query_node
89
+ header = ['FINS'.bytes.to_a, 0, 0, 0, 0xc, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0].flatten
90
+ header[19] = source_node == IOFINS_SOURCE_AUTO_NODE ? 0 : source_node
91
+ header
92
+ end
93
+
94
+ def create_fins_frame(packet)
95
+ packet = packet.flatten
96
+ header = ['FINS'.bytes.to_a, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0].flatten
97
+ header[4, 4] = int_to_a(packet.length + 8, 4)
98
+ header + packet
99
+ end
100
+
101
+ def get_bits_from_device(count, device)
102
+ open
103
+ unless available_bits_range.include? count
104
+ raise ArgumentError,
105
+ "A count #{count} must be between #{available_bits_range.first} and #{available_bits_range.last} for #{__method__}"
106
+ end
107
+
108
+ device = device_by_name device
109
+ raise ArgumentError, "#{device.name} is not bit device!" unless device.bit_device?
110
+
111
+ command = [1, 1]
112
+ command << device_to_a(device)
113
+ command << int_to_a(count, 2)
114
+
115
+ send_packet create_fins_frame(fins_header + command)
116
+ res = receive
117
+
118
+ count.times.each_with_object([]) do |i, a|
119
+ a << (res[16 + 10 + 4 + i] != 0)
120
+ end
121
+ end
122
+
123
+ def get_words_from_device(count, device)
124
+ open
125
+ unless available_words_range.include? count
126
+ raise ArgumentError,
127
+ "A count #{count} must be between #{available_words_range.first} and #{available_words_range.last} for #{__method__}"
128
+ end
129
+
130
+ device = device_by_name device
131
+ device = device.channel_device
132
+
133
+ command = [1, 1]
134
+ command << device_to_a(device)
135
+ command << int_to_a(count, 2)
136
+
137
+ send_packet create_fins_frame(fins_header + command)
138
+ res = receive
139
+ count.times.each_with_object([]) do |i, a|
140
+ a << to_int(res[16 + 10 + 4 + i * 2, 2])
141
+ end
142
+ end
143
+
144
+ def set_bits_to_device(bits, device)
145
+ open
146
+ count = bits.size
147
+ unless available_bits_range.include? count
148
+ raise ArgumentError,
149
+ "A count #{count} must be between #{available_bits_range.first} and #{available_bits_range.last} for #{__method__}"
150
+ end
151
+
152
+ device = device_by_name device
153
+ raise ArgumentError, "#{device.name} is not bit device!" unless device.bit_device?
154
+
155
+ command = [1, 2]
156
+ command << device_to_a(device)
157
+ command << int_to_a(count, 2)
158
+ bits.each do |b|
159
+ command << (b ? 1 : 0)
160
+ end
161
+
162
+ send_packet create_fins_frame(fins_header + command)
163
+ receive
164
+ end
165
+
166
+ def set_words_to_device(words, device)
167
+ open
168
+ count = words.size
169
+ unless available_words_range.include? count
170
+ raise ArgumentError,
171
+ "A count #{count} must be between #{available_words_range.first} and #{available_words_range.last} for #{__method__}"
172
+ end
173
+
174
+ device = device_by_name device
175
+ device = device.channel_device
176
+
177
+ command = [1, 2]
178
+ command << device_to_a(device)
179
+ command << int_to_a(count, 2)
180
+ words.each do |w|
181
+ command << int_to_a(w, 2)
182
+ end
183
+
184
+ send_packet create_fins_frame(fins_header + command)
185
+ receive
186
+ end
187
+
188
+ def query_node
189
+ send_packet create_query_node
190
+ res = receive
191
+ self.source_node = res[19]
192
+ end
193
+
194
+ def send_packet(packet)
195
+ @socket.write(packet.flatten.pack('c*'))
196
+ @socket.flush
197
+ @logger.debug("> #{dump_packet packet}")
198
+ end
199
+
200
+ def receive
201
+ res = []
202
+ len = 0
203
+ begin
204
+ Timeout.timeout(TIMEOUT) do
205
+ loop do
206
+ c = @socket.getc
207
+ next if c.nil? || c == ''
208
+
209
+ res << c.bytes.first
210
+ next if res.length < 8
211
+
212
+ len = to_int(res[4, 4])
213
+ next if res.length < 8 + len
214
+
215
+ tcp_command = to_int(res[8, 4])
216
+ case tcp_command
217
+ when 3 # ERROR
218
+ raise "Invalidate tcp header: #{res}"
219
+ end
220
+ break
221
+ end
222
+ end
223
+ raise "Response error code: #{res[15]}" unless (res[15]).zero?
224
+
225
+ res
226
+ end
227
+ @logger.debug("< #{dump_packet res}")
228
+ res
229
+ end
230
+
231
+ # max length:
232
+ # CS1W-ETN21, CJ1W-ETN21 : 2012
233
+ # CP1W-CIF41 option board : 540 (1004 if cpu is CP1L/H)
234
+
235
+ def available_bits_range(_device = nil)
236
+ case ethernet_module
237
+ when ETHERNET_ETN21
238
+ 1..(2012 - 8)
239
+ when ETHERNET_CP1E
240
+ 1..(540 - 8)
241
+ when ETHERNET_CP1L, ETHERNET_CP1H
242
+ 1..(1004 - 8)
243
+ else
244
+ 0..0
245
+ end
246
+ end
247
+
248
+ def available_words_range(_device = nil)
249
+ case ethernet_module
250
+ when ETHERNET_ETN21
251
+ 1..((2012 - 8) / 2)
252
+ when ETHERNET_CP1E
253
+ 1..((540 - 8) / 2)
254
+ when ETHERNET_CP1L, ETHERNET_CP1H
255
+ 1..((1004 - 8) / 2)
256
+ else
257
+ 0..0
258
+ end
259
+ end
260
+
261
+ def device_by_name(name)
262
+ case name
263
+ when String
264
+ d = OmronDevice.new name
265
+ d.valid? ? d : nil
266
+ else
267
+ # it may be already OmronDevice
268
+ name
269
+ end
270
+ end
271
+
272
+ private
273
+
274
+ def fins_header
275
+ buf = [
276
+ 0x80, # ICF
277
+ 0x00, # RSV
278
+ 0x02, # GCT
279
+ 0x00, # DNA
280
+ 0x01, # DA1
281
+ 0x00, # DA2
282
+ 0x00, # SNA
283
+ 0x01, # SA1
284
+ 0x00, # SA2
285
+ 0x00 # SID
286
+ ]
287
+ buf[2] = gateway_count - 1
288
+ buf[3] = destination_network
289
+ buf[4] = if destination_node == IOFINS_DESTINATION_NODE_FROM_IP
290
+ destination_ipv4.split('.').last.to_i
291
+ else
292
+ destination_node
293
+ end
294
+ buf[7] = source_node
295
+ buf[8] = source_unit
296
+
297
+ buf
298
+ end
299
+
300
+ def fins_tcp_cmnd_header
301
+ header = ['FINS'.bytes.to_a, 0, 0, 0, 0xc, 0, 0, 0, 2, 0, 0, 0, 0].flatten
302
+ header[19] = source_node == IOFINS_SOURCE_AUTO_NODE ? 0 : source_node
303
+ header
304
+ end
305
+
306
+ def device_code_of(device)
307
+ @@bit_codes ||= { nil => 0x30, '' => 0x30, 'W' => 0x31, 'H' => 0x32, 'A' => 0x33, 'T' => 0x09, 'C' => 0x09,
308
+ 'D' => 0x02, 'E' => 0x0a, 'TK' => 0x06 }
309
+ @@word_codes ||= { nil => 0xB0, '' => 0xB0, 'W' => 0xB1, 'H' => 0xB2, 'A' => 0xB3, 'TIM' => 0x89, 'CNT' => 0x89,
310
+ 'D' => 0x82, 'E' => 0x98, 'DR' => 0xbc }
311
+ if device.bit_device?
312
+ @@bit_codes[device.suffix]
313
+ else
314
+ @@word_codes[device.suffix]
315
+ end
316
+ end
317
+
318
+ def device_to_a(device)
319
+ a = []
320
+ a << device_code_of(device)
321
+ a << int_to_a(device.channel, 2)
322
+ a << (device.bit_device? ? (device.bit || 0) : 0)
323
+ a.flatten
324
+ end
325
+
326
+ def int_to_a(value, size)
327
+ a = []
328
+ (size - 1).downto 0 do |i|
329
+ a << ((value >> (i * 8)) & 0xff)
330
+ end
331
+ a
332
+ end
333
+
334
+ def to_int(a)
335
+ v = 0
336
+ a.each do |e|
337
+ v <<= 8
338
+ v += e
339
+ end
340
+ v
341
+ end
342
+
343
+ def dump_packet(packet)
344
+ a =
345
+ packet.map do |e|
346
+ e.to_s(16).rjust(2, '0')
347
+ end
348
+ "[#{a.join(', ')}]"
349
+ end
350
+ end
351
+ end
352
+ end
353
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The MIT License (MIT)
4
+ #
5
+ # Copyright (c) 2019 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 'omron_device'
29
+ require 'c_mode_protocol'
30
+ require 'fins_tcp_protocol'