ladder_drive 0.6.3 → 0.6.7

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