ladder_drive 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
<!-- [![](http://img.youtube.com/vi/qGbicGLB7Gs/0.jpg)](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
|
<!-- [![](http://img.youtube.com/vi/qGbicGLB7Gs/0.jpg)](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
|