ladder_drive 0.6.3 → 0.6.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +5 -5
  2. data/Gemfile +7 -2
  3. data/Gemfile.lock +107 -62
  4. data/README.md +1 -0
  5. data/README_jp.md +2 -2
  6. data/ladder_drive.gemspec +12 -8
  7. data/lib/ladder_drive/cli.rb +16 -4
  8. data/lib/ladder_drive/config.rb +4 -1
  9. data/lib/ladder_drive/config_target.rb +1 -0
  10. data/lib/ladder_drive/plc_device.rb +11 -0
  11. data/lib/ladder_drive/protocol/keyence/kv_protocol.rb +1 -11
  12. data/lib/ladder_drive/protocol/mitsubishi/fx_protocol.rb +0 -11
  13. data/lib/ladder_drive/protocol/mitsubishi/mc_protocol.rb +1 -11
  14. data/lib/ladder_drive/protocol/omron/c_mode_protocol.rb +224 -0
  15. data/lib/ladder_drive/protocol/omron/fins_tcp_protocol.rb +380 -0
  16. data/lib/ladder_drive/protocol/omron/omron.rb +28 -0
  17. data/lib/ladder_drive/protocol/omron/omron_device.rb +139 -0
  18. data/lib/ladder_drive/protocol/protocol.rb +16 -5
  19. data/lib/ladder_drive/tasks/build.rb +1 -1
  20. data/lib/ladder_drive/version.rb +1 -1
  21. data/lib/ladder_drive.rb +3 -0
  22. data/lib/plc/emulator/emu_plc_server.rb +3 -1
  23. data/lib/plc/emulator/plc_plugins.rb +20 -13
  24. data/lib/plc/emulator/plugin_trigger_state.rb +152 -0
  25. data/plugins/ambient_plugin.rb +155 -0
  26. data/plugins/config/ambient.yaml.example +34 -0
  27. data/plugins/config/google_drive.yaml.example +39 -0
  28. data/plugins/config/ifttt.yaml.example +24 -0
  29. data/plugins/config/plc_mapper.yaml.example +36 -0
  30. data/plugins/config/slack.yaml.example +36 -0
  31. data/plugins/config/trello.yaml.example +41 -0
  32. data/plugins/ifttt_plugin.rb +4 -27
  33. data/plugins/plc_mapper_plugin.rb +27 -15
  34. data/plugins/slack_plugin.rb +14 -3
  35. data/plugins/trello_plugin.rb +30 -42
  36. metadata +72 -15
@@ -50,11 +50,6 @@ module Mitsubishi
50
50
  @socket = nil
51
51
  end
52
52
 
53
- def get_bit_from_device device
54
- device = device_by_name device
55
- get_bits_from_device(1, device).first
56
- end
57
-
58
53
  def get_bits_from_device count, device
59
54
  raise ArgumentError.new("A count #{count} must be between #{available_bits_range.first} and #{available_bits_range.last} for #{__method__}") unless available_bits_range.include? count
60
55
 
@@ -108,11 +103,6 @@ module Mitsubishi
108
103
  end
109
104
 
110
105
 
111
- def get_word_from_device device
112
- device = device_by_name device
113
- get_words_from_device(1, device).first
114
- end
115
-
116
106
  def get_words_from_device(count, device)
117
107
  raise ArgumentError.new("A count #{count} must be between #{available_words_range.first} and #{available_words_range.last} for #{__method__}") unless available_bits_range.include? count
118
108
 
@@ -179,7 +169,7 @@ module Mitsubishi
179
169
  res = []
180
170
  len = 0
181
171
  begin
182
- Timeout.timeout(1.0) do
172
+ Timeout.timeout(TIMEOUT) do
183
173
  loop do
184
174
  c = @socket.read(1)
185
175
  next if c.nil? || c == ""
@@ -0,0 +1,224 @@
1
+ # The MIT License (MIT)
2
+ #
3
+ # Copyright (c) 2019 ITO SOFT DESIGN Inc.
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining
6
+ # a copy of this software and associated documentation files (the
7
+ # "Software"), to deal in the Software without restriction, including
8
+ # without limitation the rights to use, copy, modify, merge, publish,
9
+ # distribute, sublicense, and/or sell copies of the Software, and to
10
+ # permit persons to whom the Software is furnished to do so, subject to
11
+ # the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ module LadderDrive
25
+ module Protocol
26
+ module Omron
27
+
28
+ class CModeProtocol < Protocol
29
+
30
+ attr_accessor :baudrate
31
+ attr_accessor :unit_no
32
+
33
+ DELIMITER = "\r"
34
+ TERMINATOR = "*\r"
35
+ TIMEOUT = 1.0
36
+
37
+ def initialize options={}
38
+ super
39
+ @port = options[:port] || `ls /dev/tty.usb*`.split("\n").map{|l| l.chomp}.first
40
+ @baudrate = 38400
41
+ @unit_no = 0
42
+ @comm = nil
43
+ #prepare_device_map
44
+ end
45
+
46
+ def open
47
+ open!
48
+ rescue
49
+ nil
50
+ end
51
+
52
+ def open!
53
+ return false unless @port
54
+ begin
55
+ # port, baudrate, bits, stop bits, parity(0:none, 1:even, 2:odd)
56
+ @comm ||= SerialPort.new(@port, @baudrate, 7, 2, 1).tap do |s|
57
+ s.read_timeout = (TIMEOUT * 1000.0).to_i
58
+ end
59
+ rescue => e
60
+ p e
61
+ nil
62
+ end
63
+ end
64
+
65
+ def close
66
+ @comm.close if @comm
67
+ @comm = nil
68
+ end
69
+
70
+ def unit_no= no
71
+ @unit_no = [[no, 0].max, 31].min
72
+ end
73
+
74
+ def get_bits_from_device count, device
75
+ device = device_by_name device
76
+
77
+ # convert to the channel device
78
+ from = device.channel_device
79
+ to = (device + count).channel_device
80
+ c = [to - from, 1].max
81
+
82
+ # get value as words
83
+ words = get_words_from_device(c, device)
84
+
85
+ # convert to bit devices
86
+ index = device.bit
87
+ bits = []
88
+ count.times do
89
+ i = index / 16
90
+ b = index % 16
91
+ f = 1 << b
92
+ bits << ((words[i] & f) == f)
93
+ index += 1
94
+ end
95
+ bits
96
+ end
97
+
98
+ def get_words_from_device(count, device)
99
+ device = device_by_name(device).channel_device
100
+
101
+ # make read packet
102
+ packet = read_packet_with device
103
+ packet << "#{device.channel.to_s.rjust(4, '0')}#{count.to_s.rjust(4, '0')}"
104
+ packet << fcs_for(packet).to_s(16).upcase.rjust(2, "0")
105
+ packet << TERMINATOR
106
+ @logger.debug("> #{dump_packet packet}")
107
+
108
+ # send command
109
+ open
110
+ send packet
111
+
112
+ # receive response
113
+ words = []
114
+ terminated = false
115
+ loop do
116
+ res = receive
117
+ data = ""
118
+ if res
119
+ ec = error_code(res)
120
+ raise "Error response: #{ec.to_i(16).rjust(2, '0')}" unless ec == 0
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
+ data = res[7..-5]
125
+ terminated = true
126
+ else res[-1,1] == DELIMITER
127
+ fcs = fcs_for(res[0..-4])
128
+ raise "Not matched FCS expected #{fcs.to_s(16).rjust(2,'0')}" unless fcs == res[-3,2].to_i(16)
129
+ data = res[7..-4]
130
+ end
131
+ len = data.length
132
+ index = 0
133
+ while index < len
134
+ words << data[index,4].to_i(16)
135
+ index += 4
136
+ end
137
+ return words if terminated
138
+ else
139
+ break
140
+ end
141
+ end
142
+ []
143
+ end
144
+
145
+ private
146
+
147
+ def device_by_name name
148
+ case name
149
+ when String
150
+ d = OmronDevice.new name
151
+ d.valid? ? d : nil
152
+ when EscDevice
153
+ local_device_of name
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
+ =begin
171
+ loop do
172
+ res << @comm.getc# '\r' #gets
173
+ break if res[-1] == '\r'
174
+ end
175
+ =end
176
+ end
177
+ # res
178
+ rescue Timeout::Error
179
+ puts "*** ERROR: TIME OUT : #{res} ***"
180
+ end
181
+ @logger.debug("< #{dump_packet res}")
182
+ res
183
+ end
184
+
185
+
186
+ def read_packet_with device
187
+ packet = "@#{unit_no.to_s.rjust(2, '0')}R"
188
+ case device.suffix
189
+ when "HR"
190
+ packet << "H"
191
+ when "AL"
192
+ packet << "L"
193
+ when "DM", "D"
194
+ packet << "D"
195
+ when "AR"
196
+ packet << "J"
197
+ when "EM", "E"
198
+ packet << "E"
199
+ else
200
+ packet << "R"
201
+ end
202
+ end
203
+
204
+ def fcs_for packet
205
+ fcs = packet.bytes.inject(0) do |a, b|
206
+ a = a ^ b
207
+ end
208
+ fcs = fcs & 0xff
209
+ fcs
210
+ end
211
+
212
+ def error_code packet
213
+ packet[1 + 2 + 2, 2].to_i(16)
214
+ end
215
+
216
+ def dump_packet packet
217
+ packet.inspect
218
+ end
219
+
220
+ end
221
+
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,380 @@
1
+ # The MIT License (MIT)
2
+ #
3
+ # Copyright (c) 2019 ITO SOFT DESIGN Inc.
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining
6
+ # a copy of this software and associated documentation files (the
7
+ # "Software"), to deal in the Software without restriction, including
8
+ # without limitation the rights to use, copy, modify, merge, publish,
9
+ # distribute, sublicense, and/or sell copies of the Software, and to
10
+ # permit persons to whom the Software is furnished to do so, subject to
11
+ # the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ module LadderDrive
25
+ module Protocol
26
+ module Omron
27
+
28
+ class FinsTcpProtocol < Protocol
29
+
30
+ attr_accessor :gateway_count
31
+ attr_accessor :destination_network
32
+ attr_accessor :destination_node
33
+ attr_accessor :destination_unit
34
+ attr_accessor :source_network
35
+ attr_accessor :source_node
36
+ attr_accessor :source_unit
37
+
38
+ attr_accessor :ethernet_module
39
+
40
+ attr_accessor :tcp_error_code
41
+
42
+ IOFINS_DESTINATION_NODE_FROM_IP = 0
43
+ IOFINS_SOURCE_AUTO_NODE = 0
44
+
45
+ # Available ethernet module.
46
+ ETHERNET_ETN21 = 0
47
+ ETHERNET_CP1E = 1
48
+ ETHERNET_CP1L = 2
49
+ ETHERNET_CP1H = 3
50
+
51
+ TIMEOUT = 5.0
52
+
53
+ def initialize options={}
54
+ super
55
+ @socket = nil
56
+ @host = options[:host] || "192.168.250.1"
57
+ @port = options[:port] || 9600
58
+ @gateway_count = 3
59
+ @destination_network = 0
60
+ @destination_node = 0
61
+ @destination_unit = 0
62
+ @source_network = 0
63
+ @source_node = IOFINS_SOURCE_AUTO_NODE
64
+ @source_unit = 0
65
+ @ethernet_module = ETHERNET_ETN21
66
+
67
+ @tcp_error_code = 0
68
+
69
+ prepare_device_map
70
+ end
71
+
72
+ def open
73
+ open!
74
+ rescue =>e
75
+ p e
76
+ nil
77
+ end
78
+
79
+ def open!
80
+ if @socket.nil?
81
+ @socket = TCPSocket.open(@host, @port)
82
+ if @socket
83
+ source_node = IOFINS_SOURCE_AUTO_NODE
84
+ query_node
85
+ end
86
+ end
87
+ @socket
88
+ end
89
+
90
+ def close
91
+ @socket.close if @socket
92
+ @socket = nil
93
+ end
94
+
95
+ def tcp_error?
96
+ tcp_error_code != 0
97
+ end
98
+
99
+ def create_query_node
100
+ header = [ "FINS".bytes.to_a, 0, 0, 0, 0xc, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0].flatten
101
+ header[19] = source_node == IOFINS_SOURCE_AUTO_NODE ? 0 : source_node
102
+ header
103
+ end
104
+
105
+ def create_fins_frame packet
106
+ packet = packet.flatten
107
+ header = [ "FINS".bytes.to_a, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0].flatten
108
+ header[4, 4] = int_to_a(packet.length + 8, 4)
109
+ header + packet
110
+ end
111
+
112
+ def get_bits_from_device(count, device)
113
+ open
114
+ raise ArgumentError.new("A count #{count} must be between #{available_bits_range.first} and #{available_bits_range.last} for #{__method__}") unless available_bits_range.include? count
115
+
116
+ device = device_by_name device
117
+ raise ArgumentError.new("#{device.name} is not bit device!") unless device.bit_device?
118
+
119
+ command = [1, 1]
120
+ command << device_to_a(device)
121
+ command << int_to_a(count, 2)
122
+
123
+ send_packet create_fins_frame(fins_header + command)
124
+ res = receive
125
+
126
+ count.times.inject([]) do |a, i|
127
+ a << (res[16 + 10 + 4 + i] == 0 ? false : true)
128
+ a
129
+ end
130
+ end
131
+
132
+ def get_words_from_device(count, device)
133
+ open
134
+ raise ArgumentError.new("A count #{count} must be between #{available_words_range.first} and #{available_words_range.last} for #{__method__}") unless available_words_range.include? count
135
+
136
+ device = device_by_name device
137
+ device = device.channel_device
138
+
139
+ command = [1, 1]
140
+ command << device_to_a(device)
141
+ command << int_to_a(count, 2)
142
+
143
+ send_packet create_fins_frame(fins_header + command)
144
+ res = receive
145
+ count.times.inject([]) do |a, i|
146
+ a << to_int(res[16 + 10 + 4 + i * 2, 2])
147
+ a
148
+ end
149
+ end
150
+
151
+ def set_bits_to_device(bits, device)
152
+ open
153
+ count = bits.size
154
+ raise ArgumentError.new("A count #{count} must be between #{available_bits_range.first} and #{available_bits_range.last} for #{__method__}") unless available_bits_range.include? count
155
+
156
+ device = device_by_name device
157
+ raise ArgumentError.new("#{device.name} is not bit device!") unless device.bit_device?
158
+
159
+ command = [1, 2]
160
+ command << device_to_a(device)
161
+ command << int_to_a(count, 2)
162
+ bits.each do |b|
163
+ command << (b ? 1 : 0)
164
+ end
165
+
166
+ send_packet create_fins_frame(fins_header + command)
167
+ res = receive
168
+ end
169
+
170
+ def set_words_to_device(words, device)
171
+ open
172
+ count = words.size
173
+ raise ArgumentError.new("A count #{count} must be between #{available_words_range.first} and #{available_words_range.last} for #{__method__}") unless available_words_range.include? count
174
+
175
+ device = device_by_name device
176
+ device = device.channel_device
177
+
178
+ command = [1, 2]
179
+ command << device_to_a(device)
180
+ command << int_to_a(count, 2)
181
+ words.each do |w|
182
+ command << int_to_a(w, 2)
183
+ end
184
+
185
+ send_packet create_fins_frame(fins_header + command)
186
+ res = receive
187
+ end
188
+
189
+ def query_node
190
+ send_packet create_query_node
191
+ res = receive
192
+ self.source_node = res[19]
193
+ end
194
+
195
+
196
+ def send_packet packet
197
+ @socket.write(packet.flatten.pack("c*"))
198
+ @socket.flush
199
+ @logger.debug("> #{dump_packet packet}")
200
+ end
201
+
202
+ def receive
203
+ res = []
204
+ len = 0
205
+ begin
206
+ Timeout.timeout(TIMEOUT) do
207
+ loop do
208
+ c = @socket.getc
209
+ next if c.nil? || c == ""
210
+
211
+ res << c.bytes.first
212
+ next if res.length < 8
213
+
214
+ len = to_int(res[4, 4])
215
+ next if res.length < 8 + len
216
+
217
+ tcp_command = to_int(res[8, 4])
218
+ case tcp_command
219
+ when 3 # ERROR
220
+ raise "Invalidate tcp header: #{res}"
221
+ end
222
+ break
223
+ end
224
+ end
225
+ raise "Response error code: #{res[15]}" unless res[15] == 0
226
+ res
227
+ end
228
+ @logger.debug("< #{dump_packet res}")
229
+ res
230
+ end
231
+
232
+ # max length:
233
+ # CS1W-ETN21, CJ1W-ETN21 : 2012
234
+ # CP1W-CIF41 option board : 540 (1004 if cpu is CP1L/H)
235
+
236
+ def available_bits_range device=nil
237
+ case ethernet_module
238
+ when ETHERNET_ETN21
239
+ 1..(2012 - 8)
240
+ when ETHERNET_CP1E
241
+ 1..(540 - 8)
242
+ when ETHERNET_CP1L, ETHERNET_CP1H
243
+ 1..(1004 - 8)
244
+ else
245
+ 0..0
246
+ end
247
+ end
248
+
249
+ def available_words_range device=nil
250
+ case ethernet_module
251
+ when ETHERNET_ETN21
252
+ 1..((2012 - 8) / 2)
253
+ when ETHERNET_CP1E
254
+ 1..((540 - 8) / 2)
255
+ when ETHERNET_CP1L, ETHERNET_CP1H
256
+ 1..((1004 - 8) / 2)
257
+ else
258
+ 0..0
259
+ end
260
+ end
261
+
262
+ def device_by_name name
263
+ case name
264
+ when String
265
+ d = OmronDevice.new name
266
+ d.valid? ? d : nil
267
+ when EscDevice
268
+ local_device_of name
269
+ else
270
+ # it may be already OmronDevice
271
+ name
272
+ end
273
+ end
274
+
275
+ private
276
+
277
+ def fins_header
278
+ buf = [
279
+ 0x80, # ICF
280
+ 0x00, # RSV
281
+ 0x02, # GCT
282
+ 0x00, # DNA
283
+ 0x01, # DA1
284
+ 0x00, # DA2
285
+ 0x00, # SNA
286
+ 0x01, # SA1
287
+ 0x00, # SA2
288
+ 0x00, # SID
289
+ ]
290
+ buf[2] = gateway_count - 1
291
+ buf[3] = destination_network
292
+ if destination_node == IOFINS_DESTINATION_NODE_FROM_IP
293
+ buf[4] = destination_ipv4.split(".").last.to_i
294
+ else
295
+ buf[4] = destination_node
296
+ end
297
+ buf[7] = source_node
298
+ buf[8] = source_unit
299
+
300
+ buf
301
+ end
302
+
303
+ def fins_tcp_cmnd_header
304
+ header = [ "FINS".bytes.to_a, 0, 0, 0, 0xc, 0, 0, 0, 2, 0, 0, 0, 0].flatten
305
+ header[19] = source_node == IOFINS_SOURCE_AUTO_NODE ? 0 : source_node
306
+ header
307
+ end
308
+
309
+ def device_code_of device
310
+ @@bit_codes ||= { nil => 0x30, "" => 0x30, "W" => 0x31, "H" => 0x32, "A" => 0x33, "T" => 0x09, "C" => 0x09, "D" => 0x02, "E" => 0x0a, "TK" => 0x06 }
311
+ @@word_codes ||= { nil => 0xB0, "" => 0xB0, "W" => 0xB1, "H" => 0xB2, "A" => 0xB3, "TIM" => 0x89, "CNT" => 0x89, "D" => 0x82, "E" => 0x98, "DR" => 0xbc }
312
+ if device.bit_device?
313
+ @@bit_codes[device.suffix]
314
+ else
315
+ @@word_codes[device.suffix]
316
+ end
317
+ end
318
+
319
+ def device_to_a device
320
+ a = []
321
+ a << device_code_of(device)
322
+ a << int_to_a(device.channel, 2)
323
+ a << (device.bit_device? ? (device.bit || 0) : 0)
324
+ a.flatten
325
+ end
326
+
327
+
328
+ # FIXME: It's dummy currently.
329
+ def prepare_device_map
330
+ @conv_dev_dict ||= begin
331
+ h = {}
332
+ [
333
+ ["X", "0.0", 1024],
334
+ ["Y", "400.0", 1024],
335
+ ["M", "M0.0", 1024],
336
+ ["C", "C0", 256],
337
+ ["T", "T0", 256],
338
+ ["L", "H0.0", 1024],
339
+ ["SC", "M400.0", 1024],
340
+ ["D", "D0", 1024],
341
+ ["H", "D1024", 1024],
342
+ ["SD", "D2048", 1024],
343
+ ["PRG", "D3072", 1024] # ..D4095
344
+ ].each do |s,d,c|
345
+ h[s] = [OmronDevice.new(d), c]
346
+ end
347
+ h
348
+ end
349
+ end
350
+
351
+ def int_to_a value, size
352
+ a = []
353
+ (size - 1).downto 0 do |i|
354
+ a << ((value >> (i * 8)) & 0xff)
355
+ end
356
+ a
357
+ end
358
+
359
+ def to_int a
360
+ v = 0
361
+ a.each do |e|
362
+ v <<= 8
363
+ v += e
364
+ end
365
+ v
366
+ end
367
+
368
+ def dump_packet packet
369
+ a =
370
+ packet.map{|e|
371
+ e.to_s(16).rjust(2, '0')
372
+ }
373
+ "[#{a.join(', ')}]"
374
+ end
375
+
376
+ end
377
+
378
+ end
379
+ end
380
+ end