ladder_drive 0.3.1 → 0.4.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 +4 -4
- data/Gemfile.lock +3 -3
- data/README.md +21 -0
- data/README_jp.md +20 -0
- data/ladder_drive.gemspec +2 -2
- data/lib/ladder_drive/plc_device.rb +1 -1
- data/lib/ladder_drive/protocol/keyence/kv_protocol.rb +42 -2
- data/lib/ladder_drive/protocol/mitsubishi/mc_protocol.rb +56 -8
- data/lib/ladder_drive/protocol/mitsubishi/qdevice.rb +3 -3
- data/lib/ladder_drive/protocol/protocol.rb +95 -7
- data/lib/ladder_drive/uploader.rb +1 -1
- data/lib/ladder_drive/version.rb +1 -1
- data/lib/plc/emulator/emu_plc.rb +2 -2
- metadata +10 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 11f39560e094ef9fc61411b71fc952aebbca6051
|
4
|
+
data.tar.gz: 3bedd76a016c2f5bd8848bbf1ff8610a569c13be
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ff72c950069f9b7bd6606547b4769b1956738b4c1f450782d3e0dc8f7b98c40769e8517c8cec93fead79ae65ae2e130ef795bb409efd05e1435c8d58124a5268
|
7
|
+
data.tar.gz: c498bcbc93242716a1fb697d0ee5ec5f6b2d46943b3e870a046312c720248003217dd557aebe3fdbb500ec2b99e8186c3ab78bbb98b98c4d3e705ab37712097d
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -180,6 +180,27 @@ OUT M1
|
|
180
180
|
<!-- [](https://youtu.be/qGbicGLB7Gs) -->
|
181
181
|
|
182
182
|
|
183
|
+
# Accessing a device values
|
184
|
+
|
185
|
+
You can use LadderDrive as a accessing tool for the PLC device.
|
186
|
+
|
187
|
+
You can read/write device like this.
|
188
|
+
|
189
|
+
```
|
190
|
+
require 'ladder_drive'
|
191
|
+
|
192
|
+
plc = LadderDrive::Protocol::Mitsubishi::McProtocol.new host:"192.168.0.10"
|
193
|
+
|
194
|
+
plc["M0"] = true
|
195
|
+
plc["M0"] # => true
|
196
|
+
plc["M0", 10] # => [true, false, ..., false]
|
197
|
+
|
198
|
+
plc["D0"] = 123
|
199
|
+
plc["D0"] # => 123
|
200
|
+
plc["D0", 10] = [0, 1, 2, ..., 9]
|
201
|
+
plc["D0".."D9"] => [0, 1, 2, ..., 9]
|
202
|
+
```
|
203
|
+
|
183
204
|
# Information related ladder_drive
|
184
205
|
|
185
206
|
- [My japanese diary [ladder_drive]](http://diary.itosoft.com/?category=ladder_drive)
|
data/README_jp.md
CHANGED
@@ -183,6 +183,26 @@ OUT M1
|
|
183
183
|
|
184
184
|
<!-- [](https://youtu.be/qGbicGLB7Gs) -->
|
185
185
|
|
186
|
+
## PLCデバイスへのアクセスツールとしての利用
|
187
|
+
|
188
|
+
LadderDriveはPLCデバイスの読み書きツールとしての利用もできます。
|
189
|
+
下の様にとても簡単に読み書きできます。
|
190
|
+
|
191
|
+
```
|
192
|
+
require 'ladder_drive'
|
193
|
+
|
194
|
+
plc = LadderDrive::Protocol::Mitsubishi::McProtocol.new host:"192.168.0.10"
|
195
|
+
|
196
|
+
plc["M0"] = true
|
197
|
+
plc["M0"] # => true
|
198
|
+
plc["M0", 10] # => [true, false, ..., false]
|
199
|
+
|
200
|
+
plc["D0"] = 123
|
201
|
+
plc["D0"] # => 123
|
202
|
+
plc["D0", 10] = [0, 1, 2, ..., 9]
|
203
|
+
plc["D0".."D9"] => [0, 1, 2, ..., 9]
|
204
|
+
```
|
205
|
+
|
186
206
|
## エスカレーターに関する情報
|
187
207
|
|
188
208
|
- [一往確認日記 [ladder_drive]](http://diary.itosoft.com/?category=ladder_drive)
|
data/ladder_drive.gemspec
CHANGED
@@ -14,8 +14,8 @@ Gem::Specification.new do |spec|
|
|
14
14
|
spec.homepage = "https://github.com/ito-soft-design/ladder_drive"
|
15
15
|
spec.license = "MIT"
|
16
16
|
|
17
|
-
spec.
|
18
|
-
spec.add_runtime_dependency
|
17
|
+
spec.add_runtime_dependency 'thor', '~> 0'
|
18
|
+
spec.add_runtime_dependency 'activesupport', '~> 4.2', '>= 4.2.7'
|
19
19
|
|
20
20
|
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
21
21
|
spec.bindir = "exe"
|
@@ -59,7 +59,7 @@ module Keyence
|
|
59
59
|
values = values.map{|v| v == 0 ? false : true}
|
60
60
|
values.each do |v|
|
61
61
|
device.bool = v
|
62
|
-
device
|
62
|
+
device += 1
|
63
63
|
end
|
64
64
|
values
|
65
65
|
end
|
@@ -80,7 +80,7 @@ module Keyence
|
|
80
80
|
@socket.puts(packet)
|
81
81
|
res = receive
|
82
82
|
raise res unless /OK/i =~ res
|
83
|
-
device
|
83
|
+
device += 1
|
84
84
|
end
|
85
85
|
end
|
86
86
|
alias :set_bit_to_device :set_bits_to_device
|
@@ -144,6 +144,46 @@ module Keyence
|
|
144
144
|
packet.dup.chomp
|
145
145
|
end
|
146
146
|
|
147
|
+
def available_bits_range suffix=nil
|
148
|
+
case suffix
|
149
|
+
when "TM"
|
150
|
+
1..512
|
151
|
+
when "TM"
|
152
|
+
1..12
|
153
|
+
when "T", "TC", "TS", "C", "CC", "CS"
|
154
|
+
1..120
|
155
|
+
when "CTH"
|
156
|
+
1..2
|
157
|
+
when "CTC"
|
158
|
+
1..4
|
159
|
+
when "AT"
|
160
|
+
1..8
|
161
|
+
else
|
162
|
+
1..1000
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def available_words_range suffix=nil
|
167
|
+
case suffix
|
168
|
+
when "TM"
|
169
|
+
1..256
|
170
|
+
when "TM"
|
171
|
+
1..12
|
172
|
+
when "T", "TC", "TS", "C", "CC", "CS"
|
173
|
+
1..120
|
174
|
+
1..120
|
175
|
+
when "CTH"
|
176
|
+
1..2
|
177
|
+
when "CTC"
|
178
|
+
1..4
|
179
|
+
when "AT"
|
180
|
+
1..8
|
181
|
+
else
|
182
|
+
1..500
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
|
147
187
|
private
|
148
188
|
|
149
189
|
def device_class
|
@@ -55,13 +55,24 @@ module Mitsubishi
|
|
55
55
|
end
|
56
56
|
|
57
57
|
def get_bits_from_device count, device
|
58
|
+
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
|
59
|
+
|
58
60
|
device = device_by_name device
|
59
61
|
packet = make_packet(body_for_get_bits_from_deivce(count, device))
|
60
62
|
@logger.debug("> #{dump_packet packet}")
|
61
63
|
open
|
62
|
-
@socket.write(packet.pack("
|
64
|
+
@socket.write(packet.pack("C*"))
|
63
65
|
@socket.flush
|
64
66
|
res = receive
|
67
|
+
|
68
|
+
# error checking
|
69
|
+
end_code = res[9,2].pack("C*").unpack("v").first
|
70
|
+
unless end_code == 0
|
71
|
+
error = res[11,2].pack("C*").unpack("v").first
|
72
|
+
raise "return end code 0x#{end_code.to_s(16)} error code 0x#{error.to_s(16)} for get_bits_from_device(#{count}, #{device.name})"
|
73
|
+
end
|
74
|
+
|
75
|
+
# get results
|
65
76
|
bits = []
|
66
77
|
count.times do |i|
|
67
78
|
v = res[11 + i / 2]
|
@@ -76,14 +87,23 @@ module Mitsubishi
|
|
76
87
|
end
|
77
88
|
|
78
89
|
def set_bits_to_device bits, device
|
90
|
+
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? bits.size
|
91
|
+
|
79
92
|
device = device_by_name device
|
80
93
|
packet = make_packet(body_for_set_bits_to_device(bits, device))
|
81
94
|
@logger.debug("> #{dump_packet packet}")
|
82
95
|
open
|
83
|
-
@socket.write(packet.pack("
|
96
|
+
@socket.write(packet.pack("C*"))
|
84
97
|
@socket.flush
|
85
98
|
res = receive
|
86
99
|
@logger.debug("set #{bits} to:#{device.name}")
|
100
|
+
|
101
|
+
# error checking
|
102
|
+
end_code = res[9,2].pack("C*").unpack("v").first
|
103
|
+
unless end_code == 0
|
104
|
+
error = res[11,2].pack("C*").unpack("v").first
|
105
|
+
raise "return end code 0x#{end_code.to_s(16)} error code 0x#{error.to_s(16)} for set_bits_to_device(#{bits}, #{device.name})"
|
106
|
+
end
|
87
107
|
end
|
88
108
|
|
89
109
|
|
@@ -93,30 +113,50 @@ module Mitsubishi
|
|
93
113
|
end
|
94
114
|
|
95
115
|
def get_words_from_device(count, device)
|
116
|
+
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
|
117
|
+
|
96
118
|
device = device_by_name device
|
97
119
|
packet = make_packet(body_for_get_words_from_deivce(count, device))
|
98
120
|
@logger.debug("> #{dump_packet packet}")
|
99
121
|
open
|
100
|
-
@socket.write(packet.pack("
|
122
|
+
@socket.write(packet.pack("C*"))
|
101
123
|
@socket.flush
|
102
124
|
res = receive
|
125
|
+
|
126
|
+
# error checking
|
127
|
+
end_code = res[9,2].pack("C*").unpack("v").first
|
128
|
+
unless end_code == 0
|
129
|
+
error = res[11,2].pack("C*").unpack("v").first
|
130
|
+
raise "return end code 0x#{end_code.to_s(16)} error code 0x#{error.to_s(16)} for get_words_from_device(#{count}, #{device.name})"
|
131
|
+
end
|
132
|
+
|
133
|
+
# get result
|
103
134
|
words = []
|
104
135
|
res[11, 2 * count].each_slice(2) do |pair|
|
105
|
-
words << pair.pack("
|
136
|
+
words << pair.pack("C*").unpack("v").first
|
106
137
|
end
|
107
138
|
@logger.debug("get from: #{device.name} => #{words}")
|
108
139
|
words
|
109
140
|
end
|
110
141
|
|
111
142
|
def set_words_to_device words, device
|
143
|
+
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? words.size
|
144
|
+
|
112
145
|
device = device_by_name device
|
113
146
|
packet = make_packet(body_for_set_words_to_device(words, device))
|
114
147
|
@logger.debug("> #{dump_packet packet}")
|
115
148
|
open
|
116
|
-
@socket.write(packet.pack("
|
149
|
+
@socket.write(packet.pack("C*"))
|
117
150
|
@socket.flush
|
118
151
|
res = receive
|
119
152
|
@logger.debug("set #{words} to: #{device.name}")
|
153
|
+
|
154
|
+
# error checking
|
155
|
+
end_code = res[9,2].pack("C*").unpack("v").first
|
156
|
+
unless end_code == 0
|
157
|
+
error = res[11,2].pack("C*").unpack("v").first
|
158
|
+
raise "return end code 0x#{end_code.to_s(16)} error code 0x#{error.to_s(16)} for set_words_to_device(#{words}, #{device.name})"
|
159
|
+
end
|
120
160
|
end
|
121
161
|
|
122
162
|
|
@@ -143,7 +183,7 @@ module Mitsubishi
|
|
143
183
|
next if c.nil? || c == ""
|
144
184
|
|
145
185
|
res << c.bytes.first
|
146
|
-
len = res[7,2].pack("
|
186
|
+
len = res[7,2].pack("C*").unpack("v*").first if res.length >= 9
|
147
187
|
break if (len + 9 == res.length)
|
148
188
|
end
|
149
189
|
end
|
@@ -154,6 +194,14 @@ module Mitsubishi
|
|
154
194
|
res
|
155
195
|
end
|
156
196
|
|
197
|
+
def available_bits_range device=nil
|
198
|
+
1..(960 * 16)
|
199
|
+
end
|
200
|
+
|
201
|
+
def available_words_range device=nil
|
202
|
+
1..960
|
203
|
+
end
|
204
|
+
|
157
205
|
private
|
158
206
|
|
159
207
|
def make_packet body
|
@@ -214,11 +262,11 @@ module Mitsubishi
|
|
214
262
|
end
|
215
263
|
|
216
264
|
def data_for_short value
|
217
|
-
[value].pack("v").unpack("
|
265
|
+
[value].pack("v").unpack("C*")
|
218
266
|
end
|
219
267
|
|
220
268
|
def data_for_int value
|
221
|
-
[value].pack("V").unpack("
|
269
|
+
[value].pack("V").unpack("C*")
|
222
270
|
end
|
223
271
|
|
224
272
|
def dump_packet packet
|
@@ -46,14 +46,14 @@ module Mitsubishi
|
|
46
46
|
@number = b
|
47
47
|
else
|
48
48
|
if a.length == 12
|
49
|
-
@suffix = [a[0,2].to_i(16), a[2,2].to_i(16)].pack "
|
49
|
+
@suffix = [a[0,2].to_i(16), a[2,2].to_i(16)].pack "C*"
|
50
50
|
@suffix.strip!
|
51
51
|
@number = a[4,8].to_i(16)
|
52
52
|
elsif /(X|Y)(.+)/i =~ a
|
53
53
|
@suffix = $1.upcase
|
54
54
|
@number = $2.to_i(p_adic_number)
|
55
55
|
else
|
56
|
-
/(M|L|S|B|F|T|C|D|W|R)(.+)/i =~ a
|
56
|
+
/(M|L|S|B|F|T|C|D|W|R|ZR)(.+)/i =~ a
|
57
57
|
@suffix = $1.upcase
|
58
58
|
@number = $2.to_i(p_adic_number)
|
59
59
|
end
|
@@ -63,7 +63,7 @@ module Mitsubishi
|
|
63
63
|
|
64
64
|
def p_adic_number
|
65
65
|
case @suffix
|
66
|
-
when "X", "Y", "B", "W", "SB", "SW", "DX", "DY"
|
66
|
+
when "X", "Y", "B", "W", "SB", "SW", "DX", "DY"
|
67
67
|
16
|
68
68
|
else
|
69
69
|
10
|
@@ -24,11 +24,6 @@
|
|
24
24
|
dir = File.expand_path(File.dirname(__FILE__))
|
25
25
|
$:.unshift dir unless $:.include? dir
|
26
26
|
|
27
|
-
module LadderDrive
|
28
|
-
module Protocol
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
27
|
module LadderDrive
|
33
28
|
module Protocol
|
34
29
|
|
@@ -67,12 +62,12 @@ module Protocol
|
|
67
62
|
def get_bit_from_device device; end
|
68
63
|
def get_bits_from_device count, device; end
|
69
64
|
def set_bits_to_device bits, device; end
|
70
|
-
def set_bit_to_device bit, device; set_bits_to_device bit, device; end
|
65
|
+
def set_bit_to_device bit, device; set_bits_to_device [bit], device; end
|
71
66
|
|
72
67
|
def get_word_from_device device; end
|
73
68
|
def get_words_from_device(count, device); end
|
74
69
|
def set_words_to_device words, device; end
|
75
|
-
def set_word_to_device word, device; set_words_to_device word, device; end
|
70
|
+
def set_word_to_device word, device; set_words_to_device [word], device; end
|
76
71
|
|
77
72
|
def device_by_name name; nil; end
|
78
73
|
|
@@ -96,6 +91,99 @@ module Protocol
|
|
96
91
|
end
|
97
92
|
end
|
98
93
|
|
94
|
+
def available_bits_range device=nil
|
95
|
+
-Float::INFINITY..Float::INFINITY
|
96
|
+
end
|
97
|
+
|
98
|
+
def available_words_range device=nil
|
99
|
+
-Float::INFINITY..Float::INFINITY
|
100
|
+
end
|
101
|
+
|
102
|
+
def [] *args
|
103
|
+
case args.size
|
104
|
+
when 1
|
105
|
+
# protocol["DM0"]
|
106
|
+
# protocol["DM0".."DM9"]
|
107
|
+
case args[0]
|
108
|
+
when String
|
109
|
+
self[args[0], 1].first
|
110
|
+
when Range
|
111
|
+
self[args[0].first, args[0].count]
|
112
|
+
else
|
113
|
+
raise ArgumentError.new("#{args[0]} must be String or Range.")
|
114
|
+
end
|
115
|
+
when 2
|
116
|
+
# protocol["DM0", 10]
|
117
|
+
d = device_by_name args[0]
|
118
|
+
c = args[1]
|
119
|
+
if d.bit_device?
|
120
|
+
a = []
|
121
|
+
b = available_bits_range(d).last
|
122
|
+
until c == 0
|
123
|
+
n_c = [b, c].min
|
124
|
+
a += get_bits_from_device(n_c, d)
|
125
|
+
d += n_c
|
126
|
+
c -= n_c
|
127
|
+
end
|
128
|
+
a
|
129
|
+
else
|
130
|
+
a = []
|
131
|
+
b = available_words_range(d).last
|
132
|
+
until c == 0
|
133
|
+
n_c = [b, c].min
|
134
|
+
a += get_words_from_device(n_c, d)
|
135
|
+
d += n_c
|
136
|
+
c -= n_c
|
137
|
+
end
|
138
|
+
a
|
139
|
+
end
|
140
|
+
else
|
141
|
+
raise ArgumentError.new("wrong number of arguments (given #{args.size}, expected 1 or 2)")
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def []= *args
|
146
|
+
case args.size
|
147
|
+
when 2
|
148
|
+
# protocol["DM0"] = 0
|
149
|
+
# protocol["DM0".."DM9"] = [0, 1, .., 9]
|
150
|
+
v = args[1]
|
151
|
+
v = [v] unless v.is_a? Array
|
152
|
+
case args[0]
|
153
|
+
when String
|
154
|
+
self[args[0], 1] = v
|
155
|
+
when Range
|
156
|
+
self[args[0].first, args[0].count] = v
|
157
|
+
else
|
158
|
+
raise ArgumentError.new("#{args[1]} must be String or Array.")
|
159
|
+
end
|
160
|
+
when 3
|
161
|
+
# protocol["DM0", 10] = [0, 1, .., 9]
|
162
|
+
d = device_by_name args[0]
|
163
|
+
c = args[1]
|
164
|
+
values = args[2]
|
165
|
+
values = [values] unless values.is_a? Array
|
166
|
+
raise ArgumentError.new("Count #{c} is not match #{args[2].size}.") unless c == values.size
|
167
|
+
if d.bit_device?
|
168
|
+
a = []
|
169
|
+
values.each_slice(available_bits_range(d).last) do |sv|
|
170
|
+
set_bits_to_device(values, d)
|
171
|
+
d += sv.size
|
172
|
+
end
|
173
|
+
a
|
174
|
+
else
|
175
|
+
a = []
|
176
|
+
values.each_slice(available_words_range(d).last) do |sv|
|
177
|
+
set_words_to_device(values, d)
|
178
|
+
d += sv.size
|
179
|
+
end
|
180
|
+
a
|
181
|
+
end
|
182
|
+
else
|
183
|
+
raise ArgumentError.new("wrong number of arguments (given #{args.size}, expected 2 or 3)")
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
99
187
|
end
|
100
188
|
|
101
189
|
end
|
data/lib/ladder_drive/version.rb
CHANGED
data/lib/plc/emulator/emu_plc.rb
CHANGED
@@ -152,7 +152,7 @@ module Emulator
|
|
152
152
|
case d.suffix
|
153
153
|
when "PRG"
|
154
154
|
c.times do
|
155
|
-
r << program_data[d.number * 2, 2].pack("
|
155
|
+
r << program_data[d.number * 2, 2].pack("C*").unpack("n").first
|
156
156
|
d = device_by_name (d+1).name
|
157
157
|
end
|
158
158
|
else
|
@@ -169,7 +169,7 @@ module Emulator
|
|
169
169
|
case d.suffix
|
170
170
|
when "PRG"
|
171
171
|
a[3, c].each do |v|
|
172
|
-
program_data[d.number * 2, 2] = [v.to_i].pack("n").unpack("
|
172
|
+
program_data[d.number * 2, 2] = [v.to_i].pack("n").unpack("C*")
|
173
173
|
d = device_by_name (d+1).name
|
174
174
|
end
|
175
175
|
else
|
metadata
CHANGED
@@ -1,33 +1,36 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ladder_drive
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Katsuyoshi Ito
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-05-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: activesupport
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '4.2'
|
31
34
|
- - ">="
|
32
35
|
- !ruby/object:Gem::Version
|
33
36
|
version: 4.2.7
|
@@ -35,6 +38,9 @@ dependencies:
|
|
35
38
|
prerelease: false
|
36
39
|
version_requirements: !ruby/object:Gem::Requirement
|
37
40
|
requirements:
|
41
|
+
- - "~>"
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '4.2'
|
38
44
|
- - ">="
|
39
45
|
- !ruby/object:Gem::Version
|
40
46
|
version: 4.2.7
|