ladder_drive 0.5.2 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,155 @@
1
+ #
2
+ # Copyright (c) 2018 ITO SOFT DESIGN Inc.
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ dir = Dir.pwd
24
+ $:.unshift dir unless $:.include? dir
25
+
26
+ require 'active_support'
27
+ require 'active_support/core_ext'
28
+
29
+ module PlcPlugins
30
+
31
+ #def self.included(klass)
32
+ # load_plugins
33
+ #end
34
+
35
+ private
36
+
37
+ def plugins
38
+ @plugins ||= []
39
+ end
40
+
41
+ def load_plugins
42
+ return unless plugins.empty?
43
+ seen = {}
44
+
45
+ Dir.glob("plugins/*_plugin.rb").each do |plugin_path|
46
+ name = File.basename plugin_path, "_plugin.rb"
47
+ next if seen[name]
48
+ seen[name] = true
49
+
50
+ require plugin_path.gsub(/\.rb$/, "")
51
+ plugins << name
52
+ end
53
+ init_plugins
54
+ end
55
+
56
+ def init_plugins
57
+ send_message_plugins "init", self
58
+ end
59
+
60
+ def exec_plugins
61
+ send_message_plugins "exec", self
62
+ end
63
+
64
+ def send_message_plugins method, arg
65
+ plugins.each do |plugin|
66
+ msg = "plugin_#{plugin}_#{method}"
67
+ unless arg
68
+ send msg if Object.respond_to?(msg, true)
69
+ else
70
+ send msg, arg if Object.respond_to?(msg, true)
71
+ end
72
+ end
73
+ end
74
+
75
+ end
76
+
77
+ module LadderDrive
78
+ module Emulator
79
+
80
+ class Plugin
81
+
82
+ attr_reader :plc
83
+ attr_reader :config
84
+
85
+ class << self
86
+
87
+ def devices_with_plc_from_str plc, dev_str
88
+ devices = dev_str.split(",").map{|e| e.split("-")}.map do |devs|
89
+ devs = devs.map{|d| plc.device_by_name d.strip}
90
+ d1 = devs.first
91
+ d2 = devs.last
92
+ d = d1
93
+ [d2.number - d1.number + 1, 1].max.times.inject([]){|a, i| a << d1; d1 += 1; a}
94
+ end.flatten
95
+ end
96
+
97
+ def device_names_with_plc_from_str plc, dev_str
98
+ devices_with_plc_from_str.map{|d| d.name}
99
+ end
100
+
101
+ end
102
+
103
+ def devices_with_plc_from_str plc, dev_str
104
+ self.class.devices_with_plc_from_str plc, dev_str
105
+ end
106
+
107
+ def device_names_with_plc_from_str plc, dev_str
108
+ self.class.device_names_with_plc_from_str plc, dev_str
109
+ end
110
+
111
+ def initialize plc
112
+ @config = load_config
113
+ @plc = plc
114
+ end
115
+
116
+ def name
117
+ @name ||= self.class.name.split(":").last.underscore.scan(/(.*)_plugin$/).first.first
118
+ end
119
+
120
+ def disabled?
121
+ config[:disable]
122
+ end
123
+
124
+ def run_cycle plc
125
+ return false unless self.plc == plc
126
+ end
127
+
128
+ private
129
+
130
+ def load_config
131
+ h = {}
132
+ path = File.join("config", "plugins", "#{name}.yml")
133
+ if File.exist?(path)
134
+ h = YAML.load(File.read(path))
135
+ h = JSON.parse(h.to_json, symbolize_names: true)
136
+ end
137
+ h
138
+ end
139
+
140
+ end
141
+
142
+ end
143
+ end
144
+
145
+
146
+ # @deprecated use LadderDrive::Emulator::Plugin class instead of this.
147
+ def load_plugin_config name
148
+ h = {}
149
+ path = File.join("config", "plugins", "#{name}.yml")
150
+ if File.exist?(path)
151
+ h = YAML.load(File.read(path))
152
+ h = JSON.parse(h.to_json, symbolize_names: true)
153
+ end
154
+ h
155
+ end
@@ -0,0 +1,11 @@
1
+ # Execute it when this file was loaded.
2
+ def plugin_blank_init plc
3
+ # puts your code here.
4
+ #puts "Blank#init"
5
+ end
6
+
7
+ # Execute it each cycle.
8
+ def plugin_blank_exec plc
9
+ # puts your code here.
10
+ #puts "Blank#exec"
11
+ end
@@ -0,0 +1,191 @@
1
+ #
2
+ # Copyright (c) 2018 ITO SOFT DESIGN Inc.
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ <<-DOC
24
+ Here is a sample configuration.
25
+ Puts your configuration to config/plugins/google_drive.yml
26
+
27
+ client_id: your_auth_2_0_client_id
28
+ client_secret: your_client_secret
29
+ refresh_token: your_refresh_token
30
+
31
+ loggings:
32
+ - name: temperature
33
+ trigger:
34
+ device: M0
35
+ type: raise_and_fall
36
+ value_type: bool
37
+ columns: D0,D1,D2,D3,D4,D5,D6,D7,D8,D9,D20,M0
38
+ devices:
39
+ - device: D0-D9
40
+ type: value
41
+ - device: D20
42
+ type: value
43
+ - device: M0
44
+ type: bool
45
+ spread_sheet:
46
+ spread_sheet_key: 1LGiGzMUVj_NcdWRpzv9ivjtNKSWQa_0RXtK_4bch3bY
47
+ sheet_no: 0
48
+ - name: voltages
49
+ trigger:
50
+ type: interval
51
+ interval: 30.0
52
+ columns: D0,D1,D2,D3,D4,D5,D6,D7,D8,D9,D20,M0
53
+ devices:
54
+ - device: D0-D9
55
+ type: value
56
+ - device: D20
57
+ type: value
58
+ - device: M0
59
+ type: bool
60
+ spread_sheet:
61
+ spread_sheet_key: 1LGiGzMUVj_NcdWRpzv9ivjtNKSWQa_0RXtK_4bch3bY
62
+ sheet_name: Sheet2
63
+ DOC
64
+
65
+ require 'net/https'
66
+ require 'google_drive'
67
+
68
+ def plugin_google_drive_init plc
69
+ @plugin_google_drive_config = load_plugin_config 'google_drive'
70
+ return if @plugin_google_drive_config[:disable]
71
+
72
+ @plugin_google_drive_values = {}
73
+ @plugin_google_drive_times = {}
74
+ @plugin_google_drive_worker_queue = Queue.new
75
+
76
+ begin
77
+ # generate config file for google drive session
78
+ tmp_dir = File.expand_path "tmp"
79
+ session_path = File.join(tmp_dir, "google_drive_session.json")
80
+ unless File.exist? session_path
81
+ mkdir_p tmp_dir
82
+ conf =
83
+ [:client_id, :client_secret, :refresh_token].inject({}) do |h, key|
84
+ v = @plugin_google_drive_config[key]
85
+ h[key] = v if v
86
+ h
87
+ end
88
+ File.write session_path, JSON.generate(conf)
89
+ end
90
+
91
+ # create google drive session
92
+ @plugin_google_drive_session = GoogleDrive::Session.from_config(session_path)
93
+
94
+ # start worker thread
95
+ Thread.start {
96
+ plugin_google_drive_worker_loop
97
+ }
98
+ rescue => e
99
+ p e
100
+ @plugin_google_drive_session = nil
101
+ end
102
+ end
103
+
104
+ def plugin_google_drive_exec plc
105
+ return if @plugin_google_drive_config[:disable]
106
+ return unless @plugin_google_drive_session
107
+
108
+ @plugin_google_drive_config[:loggings].each do |logging|
109
+ begin
110
+ # check triggered or not
111
+ triggered = false
112
+ case logging[:trigger][:type]
113
+ when "interval"
114
+ now = Time.now
115
+ t = @plugin_google_drive_times[logging.object_id] || now
116
+ triggered = t <= now
117
+ if triggered
118
+ t += logging[:trigger][:interval] || 300
119
+ @plugin_google_drive_times[logging.object_id] = t
120
+ end
121
+ else
122
+ d = plc.device_by_name logging[:trigger][:device]
123
+ v = d.send logging[:trigger][:value_type], logging[:trigger][:text_length] || 8
124
+ unless @plugin_google_drive_values[logging.object_id] == v
125
+ @plugin_google_drive_values[logging.object_id] = v
126
+ case logging[:trigger][:type]
127
+ when "raise"
128
+ triggered = !!v
129
+ when "fall"
130
+ triggered = !v
131
+ else
132
+ triggered = true
133
+ end
134
+ end
135
+ end
136
+
137
+ next unless triggered
138
+
139
+ # gether values
140
+ values = logging[:devices].map do |config|
141
+ d1, d2 = config[:device].split("-").map{|d| plc.device_by_name d}
142
+ devices = [d1]
143
+ if d2
144
+ d = d1 + 1
145
+ devices += [d2.number - d1.number, 0].max.times.inject([]){|a, i| a << d; d += 1; a}
146
+ end
147
+ devices.map{|d| d.send config[:type], config[:length] || 8}
148
+ end.flatten
149
+ @plugin_google_drive_worker_queue.push logging:logging, values:values, time:Time.now
150
+ rescue => e
151
+ p e
152
+ end
153
+ end if @plugin_google_drive_config[:loggings]
154
+ end
155
+
156
+ def plugin_google_drive_worker_loop
157
+ while arg = @plugin_google_drive_worker_queue.pop
158
+ begin
159
+ logging = arg[:logging]
160
+ spread_sheet = @plugin_google_drive_session.spreadsheet_by_key(logging[:spread_sheet][:spread_sheet_key])
161
+
162
+ # get worksheet
163
+ worksheet = begin
164
+ if logging[:spread_sheet][:sheet_name]
165
+ spread_sheet.worksheet_by_title logging[:spread_sheet][:sheet_name]
166
+ else
167
+ spread_sheet.worksheets[logging[:spread_sheet][:sheet_no] || 0]
168
+ end
169
+ end
170
+
171
+ # write columns if needs
172
+ if worksheet.num_rows == 0
173
+ worksheet[1, 1] = "Time"
174
+ logging[:columns].split(",").each_with_index do |t, i|
175
+ worksheet[1, i + 2] = t
176
+ end
177
+ end
178
+
179
+ # write values
180
+ r = worksheet.num_rows + 1
181
+ worksheet[r, 1] = arg[:time]
182
+ arg[:values].each_with_index do |v, i|
183
+ worksheet[r, i + 2] = v
184
+ end if arg[:values]
185
+ worksheet.save
186
+ rescue => e
187
+ # TODO: Resend if it fails.
188
+ p e
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,134 @@
1
+ #
2
+ # Copyright (c) 2018 ITO SOFT DESIGN Inc.
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ <<-DOC
24
+ Here is a sample configuration.
25
+ Puts your configuration to config/plugins/ifttt.yml
26
+
27
+ web_hook_key: your_web_hook_key
28
+ events:
29
+ - name: event1
30
+ trigger:
31
+ device: M0
32
+ type: raise_and_fall
33
+ value_type: bool
34
+ params:
35
+ value1: error
36
+ value2: unit1
37
+ - name: event2
38
+ trigger:
39
+ device: D0
40
+ value: word
41
+ type: changed
42
+ params:
43
+ value1: temperature
44
+ value2: 値2
45
+ value3: 値3
46
+ - name: event3
47
+ trigger:
48
+ device: D2
49
+ value: dword
50
+ type: interval
51
+ time: 10.0
52
+ params:
53
+ value1: @value
54
+ value2: 値2
55
+ value3: 値3
56
+ DOC
57
+
58
+ require 'net/https'
59
+
60
+ def plugin_ifttt_init plc
61
+ @plugin_ifttt_config = load_plugin_config 'ifttt'
62
+ return if @plugin_ifttt_config[:disable]
63
+
64
+ @plugin_ifttt_values = {}
65
+ @plugin_ifttt_times = {}
66
+ @plugin_ifttt_worker_queue = Queue.new
67
+ Thread.start {
68
+ plugin_ifttt_worker_loop
69
+ }
70
+ end
71
+
72
+ def plugin_ifttt_exec plc
73
+ return if @plugin_ifttt_config[:disable]
74
+ return unless @plugin_ifttt_config[:web_hook_key]
75
+
76
+ @plugin_ifttt_config[:events].each do |event|
77
+ begin
78
+ triggered = false
79
+ case event[:trigger][:type]
80
+ when "interval"
81
+ now = Time.now
82
+ t = @plugin_ifttt_times[event.object_id] || now
83
+ triggered = t <= now
84
+ if triggered
85
+ t += event[:trigger][:interval] || 300
86
+ @plugin_ifttt_times[event.object_id] = t
87
+ end
88
+ else
89
+ d = plc.device_by_name event[:trigger][:device]
90
+ v = d.send event[:trigger][:value_type], event[:trigger][:text_length] || 8
91
+ unless @plugin_ifttt_values[event.object_id] == v
92
+ @plugin_ifttt_values[event.object_id] = v
93
+ case event[:trigger][:type]
94
+ when "raise"
95
+ triggered = !!v
96
+ when "fall"
97
+ triggered = !v
98
+ else
99
+ triggered = true
100
+ end
101
+ end
102
+ end
103
+
104
+ next unless triggered
105
+
106
+ @plugin_ifttt_worker_queue.push event:event[:name], payload:event[:params].dup || {}, value:v
107
+ rescue => e
108
+ p e
109
+ end
110
+ end if @plugin_ifttt_config[:events]
111
+ end
112
+
113
+ def plugin_ifttt_worker_loop
114
+ while arg = @plugin_ifttt_worker_queue.pop
115
+ begin
116
+ uri = URI.parse("https://maker.ifttt.com/trigger/#{arg[:event]}/with/key/#{@plugin_ifttt_config[:web_hook_key]}")
117
+ http = Net::HTTP.new(uri.host, uri.port)
118
+ http.use_ssl = true
119
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
120
+
121
+ req = Net::HTTP::Post.new(uri.path)
122
+ payload = arg[:payload]
123
+ payload.keys.each do |key|
124
+ payload[key] = arg[:value] if payload[key] == "__value__"
125
+ end
126
+ req.set_form_data(payload)
127
+
128
+ http.request(req)
129
+ rescue => e
130
+ # TODO: Resend if it fails.
131
+ p e
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,186 @@
1
+ #
2
+ # Copyright (c) 2018 ITO SOFT DESIGN Inc.
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ <<-DOC
24
+ Here is a sample configuration.
25
+ Puts your configuration to config/plugins/plc_mapper.yml
26
+
27
+ plcs:
28
+ - description: Machine A-123
29
+ protocol: mc_protocol
30
+ host: 192.168.0.1
31
+ port: 5010
32
+ interval: 60
33
+ mapping:
34
+ read:
35
+ - plc: M1000-M1099
36
+ ld: M0
37
+ - plc: D1000-D1099
38
+ ld: D0
39
+ write:
40
+ - plc: M100-M199
41
+ ld: M1100
42
+ - plc: D100-D199
43
+ ld: D1100
44
+ DOC
45
+
46
+ module LadderDrive
47
+ module Emulator
48
+
49
+ class PlcMapperPlugin < Plugin
50
+
51
+ attr_reader :lock
52
+ attr_reader :values_for_reading
53
+ attr_reader :values_for_writing
54
+
55
+ def initialize plc
56
+ super #plc
57
+ @lock = Mutex.new
58
+ @values_for_reading = {}
59
+ @values_for_writing = {}
60
+ setup
61
+ end
62
+
63
+ def run_cycle plc
64
+ return false unless super
65
+ @lock.synchronize {
66
+ # set values from plcs to ladder drive.
67
+ values_for_reading.each do |d, v|
68
+ plc.device_by_name(d).value = v
69
+ end
70
+ values_for_reading.clear
71
+
72
+ # set values from ladder drive to values_for_writing.
73
+ # then set it to plc at #sync_with_plc
74
+ values_for_writing.each do |d, v|
75
+ values_for_writing[d] = plc.device_by_name(d).value
76
+ end
77
+ }
78
+ end
79
+
80
+ private
81
+
82
+ def setup
83
+ config[:plcs].each do |plc_config|
84
+ Thread.start(plc_config) {|plc_config|
85
+ mapping_thread_proc plc_config
86
+ }
87
+ end
88
+ end
89
+
90
+ def protocol_with_config config
91
+ begin
92
+ eval("#{config[:protocol].camelize}.new").tap do |protocol|
93
+ protocol.host = config[:host] if config[:host]
94
+ protocol.port = config[:port] if config[:port]
95
+ end
96
+ rescue
97
+ nil
98
+ end
99
+ end
100
+
101
+ def mapping_devices protocol, mappings
102
+ mappings.map do |h|
103
+ a = []
104
+ h.each do |k, v|
105
+ devs = v.split("-").map{|d| protocol.device_by_name d.strip}
106
+ d1 = devs.first
107
+ d2 = devs.last
108
+ a << k
109
+ a << [d1, [d2.number - d1.number + 1, 1].max]
110
+ end
111
+ Hash[*a]
112
+ end
113
+ end
114
+
115
+ def mapping_thread_proc config
116
+ protocol = protocol_with_config config
117
+
118
+ read_mappings = mapping_devices protocol, config[:mapping][:read]
119
+ write_mappings = mapping_devices protocol, config[:mapping][:write]
120
+
121
+ interval = config[:interval]
122
+ next_time = begin
123
+ t = Time.now.to_f
124
+ t = t - t % interval
125
+ Time.at t
126
+ end
127
+
128
+ loop do
129
+ begin
130
+ now = Time.now
131
+ if next_time <= now
132
+ sync_with_plc protocol, read_mappings, write_mappings
133
+ next_time += interval
134
+ end
135
+ sleep next_time - Time.now
136
+ rescue => e
137
+ puts e, caller
138
+ end
139
+ end
140
+ end
141
+
142
+ def sync_with_plc protocol, read_mappings, write_mappings
143
+ # set values from plc to values_for_reading.
144
+ read_mappings.each do |mapping|
145
+ src_d, c = mapping[:plc]
146
+ values = protocol[src_d.name, c]
147
+ dst_d = plc.device_by_name mapping[:ld].first.name
148
+ lock.synchronize {
149
+ values.each do |v|
150
+ values_for_reading[dst_d.name] = v
151
+ dst_d = dst_d.next_device
152
+ end
153
+ }
154
+ end
155
+
156
+ # set values form ladder drive (values_for_writing) to plc
157
+ # values_for_writing was set at run_cycle
158
+ # but for the first time, it's not known what device it should take.
159
+ # after running below, devices for need is listed to values_for_writing.
160
+ write_mappings.each do |mapping|
161
+ dst_d, c = mapping[:plc]
162
+ src_d = plc.device_by_name mapping[:ld].first.name
163
+ values = []
164
+ lock.synchronize {
165
+ # It may not get the value for the first time, set zero instead of it.
166
+ values_for_writing[src_d.name] ||= 0
167
+ values << values_for_writing[src_d.name]
168
+ src_d = src_d.next_device
169
+ }
170
+ protocol[dst_d.name, c] = values
171
+ end
172
+ end
173
+
174
+ end
175
+
176
+ end
177
+ end
178
+
179
+
180
+ def plugin_plc_mapper_init plc
181
+ @plugin_plc_mapper = LadderDrive::Emulator::PlcMapperPlugin.new plc
182
+ end
183
+
184
+ def plugin_plc_mapper_exec plc
185
+ @plugin_plc_mapper.run_cycle plc
186
+ end