ladder_drive 0.5.2 → 0.6.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,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