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.
- checksums.yaml +5 -5
- data/.gitignore +0 -1
- data/Gemfile +6 -2
- data/Gemfile.lock +93 -17
- data/README.md +47 -14
- data/README_jp.md +51 -2
- data/Rakefile +31 -0
- data/doc/jp/raspberrypi.md +183 -0
- data/ladder_drive.gemspec +2 -1
- data/lib/ladder_drive/asm.rb +62 -7
- data/lib/ladder_drive/cli.rb +11 -0
- data/lib/ladder_drive/plc_device.rb +33 -4
- data/lib/ladder_drive/protocol/mitsubishi/fx_device.rb +14 -0
- data/lib/ladder_drive/protocol/mitsubishi/fx_protocol.rb +330 -0
- data/lib/ladder_drive/protocol/mitsubishi/mc_protocol.rb +6 -6
- data/lib/ladder_drive/protocol/mitsubishi/mitsubishi.rb +4 -0
- data/lib/ladder_drive/protocol/protocol.rb +1 -0
- data/lib/ladder_drive/version.rb +1 -1
- data/lib/plc/emulator/emu_device.rb +7 -0
- data/lib/plc/emulator/emu_plc.rb +6 -2
- data/lib/plc/emulator/emu_plc_server.rb +9 -8
- data/lib/plc/emulator/plc_plugins.rb +155 -0
- data/plugins/blank_plugin.rb +11 -0
- data/plugins/google_drive_plugin.rb +191 -0
- data/plugins/ifttt_plugin.rb +134 -0
- data/plugins/plc_mapper_plugin.rb +186 -0
- data/plugins/slack_plugin.rb +155 -0
- data/plugins/trello_plugin.rb +156 -0
- metadata +41 -5
@@ -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,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
|