plc_access 0.1.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 +7 -0
- data/.gitignore +8 -0
- data/.travis.yml +6 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +53 -0
- data/LICENSE.txt +21 -0
- data/README.md +69 -0
- data/Rakefile +11 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/plc_access/protocol/keyence/keyence.rb +33 -0
- data/lib/plc_access/protocol/keyence/kv_device.rb +68 -0
- data/lib/plc_access/protocol/keyence/kv_protocol.rb +181 -0
- data/lib/plc_access/protocol/mitsubishi/fx_device.rb +13 -0
- data/lib/plc_access/protocol/mitsubishi/fx_protocol.rb +265 -0
- data/lib/plc_access/protocol/mitsubishi/mc_protocol.rb +281 -0
- data/lib/plc_access/protocol/mitsubishi/mitsubishi.rb +36 -0
- data/lib/plc_access/protocol/mitsubishi/qdevice.rb +113 -0
- data/lib/plc_access/protocol/omron/c_mode_protocol.rb +218 -0
- data/lib/plc_access/protocol/omron/fins_tcp_protocol.rb +353 -0
- data/lib/plc_access/protocol/omron/omron.rb +30 -0
- data/lib/plc_access/protocol/omron/omron_device.rb +134 -0
- data/lib/plc_access/protocol/plc_device.rb +225 -0
- data/lib/plc_access/protocol/protocol.rb +219 -0
- data/lib/plc_access/version.rb +5 -0
- data/lib/plc_access.rb +30 -0
- data/plc_access.gemspec +31 -0
- metadata +94 -0
@@ -0,0 +1,265 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# The MIT License (MIT)
|
4
|
+
#
|
5
|
+
# Copyright (c) 2016 ITO SOFT DESIGN Inc.
|
6
|
+
#
|
7
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
8
|
+
# a copy of this software and associated documentation files (the
|
9
|
+
# "Software"), to deal in the Software without restriction, including
|
10
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
11
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
12
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
13
|
+
# the following conditions:
|
14
|
+
#
|
15
|
+
# The above copyright notice and this permission notice shall be
|
16
|
+
# included in all copies or substantial portions of the Software.
|
17
|
+
#
|
18
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
19
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
20
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
21
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
22
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
23
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
24
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
25
|
+
|
26
|
+
module PlcAccess
|
27
|
+
module Protocol
|
28
|
+
module Mitsubishi
|
29
|
+
class FxProtocol < Protocol
|
30
|
+
attr_accessor :pc_no, :baudrate, :station_no, :wait_time
|
31
|
+
|
32
|
+
STX = "\u0002"
|
33
|
+
ETX = "\u0003"
|
34
|
+
EOT = "\u0004"
|
35
|
+
ENQ = "\u0005"
|
36
|
+
ACK = "\u0006"
|
37
|
+
LF = "\u000a"
|
38
|
+
CR = "\u000d"
|
39
|
+
NAK = "\u0015"
|
40
|
+
|
41
|
+
DELIMITER = "\r\n"
|
42
|
+
|
43
|
+
def initialize(options = {})
|
44
|
+
super
|
45
|
+
@port = options[:port] || `ls /dev/tty.usb*`.split("\n").map(&:chomp).first
|
46
|
+
@pc_no = 0xff
|
47
|
+
@baudrate = 19_200
|
48
|
+
@station_no = 0
|
49
|
+
@wait_time = 0
|
50
|
+
@comm = nil
|
51
|
+
end
|
52
|
+
|
53
|
+
def open
|
54
|
+
open!
|
55
|
+
rescue StandardError
|
56
|
+
nil
|
57
|
+
end
|
58
|
+
|
59
|
+
def open!
|
60
|
+
return false unless @port
|
61
|
+
|
62
|
+
begin
|
63
|
+
# port, baudrate, bits, stop bits, parity(0:none, 1:even, 2:odd)
|
64
|
+
@comm ||= SerialPort.new(@port, @baudrate, 7, 1, 2).tap do |s|
|
65
|
+
s.read_timeout = (TIMEOUT * 1000.0).to_i
|
66
|
+
end
|
67
|
+
raise StandardError.new("invalid port #{@port}") unless @comm
|
68
|
+
rescue StandardError => e
|
69
|
+
p e
|
70
|
+
nil
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def close
|
75
|
+
@comm&.close
|
76
|
+
@comm = nil
|
77
|
+
end
|
78
|
+
|
79
|
+
def get_bits_from_device(count, device)
|
80
|
+
unless available_bits_range.include? count
|
81
|
+
raise ArgumentError,
|
82
|
+
"A count #{count} must be between #{available_bits_range.first} and #{available_bits_range.last} for #{__method__}"
|
83
|
+
end
|
84
|
+
|
85
|
+
device = device_by_name device
|
86
|
+
packet = body_for_get_bits_from_device(count, device) + DELIMITER
|
87
|
+
@logger.debug("> #{dump_packet packet}")
|
88
|
+
open
|
89
|
+
@comm.write(packet)
|
90
|
+
@comm.flush
|
91
|
+
res = receive
|
92
|
+
bits = []
|
93
|
+
|
94
|
+
if res && (check_sum(res[0..-5]) == res[-4, 2])
|
95
|
+
bits =
|
96
|
+
res[5..-6].each_char.map do |c|
|
97
|
+
c == '1'
|
98
|
+
end
|
99
|
+
end
|
100
|
+
@logger.debug("> #{dump_packet ack_packet}")
|
101
|
+
@comm.write(ack_packet)
|
102
|
+
@logger.debug("get #{device.name} => #{bits}")
|
103
|
+
|
104
|
+
bits
|
105
|
+
end
|
106
|
+
|
107
|
+
def set_bits_to_device(bits, device)
|
108
|
+
unless available_bits_range.include? bits.size
|
109
|
+
raise ArgumentError,
|
110
|
+
"A count #{count} must be between #{available_bits_range.first} and #{available_bits_range.last} for #{__method__}"
|
111
|
+
end
|
112
|
+
|
113
|
+
device = device_by_name device
|
114
|
+
packet = body_for_set_bits_to_device(bits, device)
|
115
|
+
@logger.debug("> #{dump_packet packet}")
|
116
|
+
open
|
117
|
+
@comm.write(packet)
|
118
|
+
@comm.flush
|
119
|
+
res = receive
|
120
|
+
@logger.debug("set #{bits} to:#{device.name}")
|
121
|
+
|
122
|
+
# error checking
|
123
|
+
raise "ERROR: return #{res} for set_bits_to_device(#{bits}, #{device.name})" unless res == ack_packet
|
124
|
+
end
|
125
|
+
|
126
|
+
def get_words_from_device(count, device)
|
127
|
+
unless available_words_range.include? count
|
128
|
+
raise ArgumentError,
|
129
|
+
"A count #{count} must be between #{available_words_range.first} and #{available_words_range.last} for #{__method__}"
|
130
|
+
end
|
131
|
+
|
132
|
+
device = device_by_name device
|
133
|
+
packet = body_for_get_words_from_device(count, device) + DELIMITER
|
134
|
+
@logger.debug("> #{dump_packet packet}")
|
135
|
+
open
|
136
|
+
@comm.write(packet)
|
137
|
+
@comm.flush
|
138
|
+
res = receive
|
139
|
+
words = []
|
140
|
+
|
141
|
+
if res && (check_sum(res[0..-5]) == res[-4, 2])
|
142
|
+
words =
|
143
|
+
res[5..-6].scan(/.{4}/).map do |v|
|
144
|
+
v.to_i(16)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
@logger.debug("> #{dump_packet ack_packet}")
|
148
|
+
@comm.write(ack_packet)
|
149
|
+
@logger.debug("get #{device.name} => #{words}")
|
150
|
+
|
151
|
+
words
|
152
|
+
end
|
153
|
+
|
154
|
+
def set_words_to_device(words, device)
|
155
|
+
unless available_bits_range.include? words.size
|
156
|
+
raise ArgumentError,
|
157
|
+
"A count of words #{words.size} must be between #{available_words_range.first} and #{available_words_range.last} for #{__method__}"
|
158
|
+
end
|
159
|
+
|
160
|
+
device = device_by_name device
|
161
|
+
packet = body_for_set_words_to_device(words, device)
|
162
|
+
@logger.debug("> #{dump_packet packet}")
|
163
|
+
open
|
164
|
+
@comm.write(packet)
|
165
|
+
@comm.flush
|
166
|
+
res = receive
|
167
|
+
@logger.debug("set #{words} to: #{device.name}")
|
168
|
+
|
169
|
+
# error checking
|
170
|
+
raise "ERROR: return #{res} for set_bits_to_device(#{words}, #{device.name})" unless res == ack_packet
|
171
|
+
end
|
172
|
+
|
173
|
+
def device_by_name(name)
|
174
|
+
case name
|
175
|
+
when String
|
176
|
+
d = FxDevice.new name
|
177
|
+
d.valid? ? d : nil
|
178
|
+
else
|
179
|
+
# it may be already QDevice
|
180
|
+
name
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def receive
|
185
|
+
res = ''
|
186
|
+
begin
|
187
|
+
Timeout.timeout(TIMEOUT) do
|
188
|
+
res = @comm.gets
|
189
|
+
end
|
190
|
+
res
|
191
|
+
rescue Timeout::Error
|
192
|
+
puts "*** ERROR: TIME OUT : #{res} ***"
|
193
|
+
end
|
194
|
+
@logger.debug("< #{dump_packet res}")
|
195
|
+
res
|
196
|
+
end
|
197
|
+
|
198
|
+
def available_bits_range(_device = nil)
|
199
|
+
1..256
|
200
|
+
end
|
201
|
+
|
202
|
+
def available_words_range(_device = nil)
|
203
|
+
1..64
|
204
|
+
end
|
205
|
+
|
206
|
+
def body_for_get_bit_from_deivce(device)
|
207
|
+
body_for_get_bits_from_device 1, device
|
208
|
+
end
|
209
|
+
|
210
|
+
def body_for_get_bits_from_device(count, device)
|
211
|
+
body = header_with_command 'BR'
|
212
|
+
body += "#{device.name}#{count.to_s(16).rjust(2, '0')}"
|
213
|
+
body += check_sum(body)
|
214
|
+
body += DELIMITER
|
215
|
+
body.upcase
|
216
|
+
end
|
217
|
+
|
218
|
+
def body_for_get_words_from_device(count, device)
|
219
|
+
body = header_with_command 'WR'
|
220
|
+
body += "#{device.name}#{count.to_s(16).rjust(2, '0')}"
|
221
|
+
body += check_sum(body)
|
222
|
+
body += DELIMITER
|
223
|
+
body.upcase
|
224
|
+
end
|
225
|
+
|
226
|
+
def body_for_set_bits_to_device(bits, device)
|
227
|
+
body = header_with_command 'BW'
|
228
|
+
body += "#{device.name}#{bits.count.to_s(16).rjust(2, '0')}"
|
229
|
+
body += bits.map { |b| b ? '1' : '0' }.join('')
|
230
|
+
body += check_sum(body)
|
231
|
+
body += DELIMITER
|
232
|
+
body.upcase
|
233
|
+
end
|
234
|
+
alias body_for_set_bit_to_device body_for_set_bits_to_device
|
235
|
+
|
236
|
+
def body_for_set_words_to_device(words, device)
|
237
|
+
body = header_with_command 'WW'
|
238
|
+
body += "#{device.name}#{words.count.to_s(16).rjust(2, '0')}"
|
239
|
+
body += words.map { |w| w.to_s(16).rjust(4, '0') }.join('')
|
240
|
+
body += check_sum(body)
|
241
|
+
body += DELIMITER
|
242
|
+
body.upcase
|
243
|
+
end
|
244
|
+
|
245
|
+
def dump_packet(packet)
|
246
|
+
packet.inspect
|
247
|
+
end
|
248
|
+
|
249
|
+
private
|
250
|
+
|
251
|
+
def check_sum(packet)
|
252
|
+
packet[1..].upcase.unpack('C*').inject(0) { |s, c| s + c }.to_s(16).rjust(2, '0')[-2, 2].upcase
|
253
|
+
end
|
254
|
+
|
255
|
+
def header_with_command(command)
|
256
|
+
"#{ENQ}#{station_no.to_s.rjust(2, '0')}#{pc_no.to_s(16).rjust(2, '0')}#{command}#{wait_time}".upcase
|
257
|
+
end
|
258
|
+
|
259
|
+
def ack_packet
|
260
|
+
"#{ACK}#{station_no.to_s.rjust(2, '0')}#{pc_no.to_s(16).rjust(2, '0')}#{DELIMITER}".upcase
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
@@ -0,0 +1,281 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# The MIT License (MIT)
|
4
|
+
#
|
5
|
+
# Copyright (c) 2016 ITO SOFT DESIGN Inc.
|
6
|
+
#
|
7
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
8
|
+
# a copy of this software and associated documentation files (the
|
9
|
+
# "Software"), to deal in the Software without restriction, including
|
10
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
11
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
12
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
13
|
+
# the following conditions:
|
14
|
+
#
|
15
|
+
# The above copyright notice and this permission notice shall be
|
16
|
+
# included in all copies or substantial portions of the Software.
|
17
|
+
#
|
18
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
19
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
20
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
21
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
22
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
23
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
24
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
25
|
+
|
26
|
+
module PlcAccess
|
27
|
+
module Protocol
|
28
|
+
module Mitsubishi
|
29
|
+
class McProtocol < Protocol
|
30
|
+
def initialize(options = {})
|
31
|
+
super
|
32
|
+
@socket = nil
|
33
|
+
@host = options[:host] || '192.168.0.10'
|
34
|
+
@port = options[:port] || 5010
|
35
|
+
end
|
36
|
+
|
37
|
+
def open
|
38
|
+
open!
|
39
|
+
rescue StandardError
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
|
43
|
+
def open!
|
44
|
+
@socket ||= TCPSocket.open(@host, @port)
|
45
|
+
end
|
46
|
+
|
47
|
+
def close
|
48
|
+
@socket&.close
|
49
|
+
@socket = nil
|
50
|
+
end
|
51
|
+
|
52
|
+
def get_bits_from_device(count, device)
|
53
|
+
unless available_bits_range.include? count
|
54
|
+
raise ArgumentError,
|
55
|
+
"A count #{count} must be between #{available_bits_range.first} and #{available_bits_range.last} for #{__method__}"
|
56
|
+
end
|
57
|
+
|
58
|
+
device = device_by_name device
|
59
|
+
packet = make_packet(body_for_get_bits_from_device(count, device))
|
60
|
+
@logger.debug("> #{dump_packet packet}")
|
61
|
+
open
|
62
|
+
@socket.write(packet.pack('C*'))
|
63
|
+
@socket.flush
|
64
|
+
res = receive
|
65
|
+
|
66
|
+
# error checking
|
67
|
+
end_code = res[9, 2].pack('C*').unpack1('v')
|
68
|
+
unless end_code.zero?
|
69
|
+
error = res[11, 2].pack('C*').unpack1('v')
|
70
|
+
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})"
|
71
|
+
end
|
72
|
+
|
73
|
+
# get results
|
74
|
+
bits = []
|
75
|
+
count.times do |i|
|
76
|
+
v = res[11 + i / 2]
|
77
|
+
bits << if i.even?
|
78
|
+
((v >> 4) != 0)
|
79
|
+
else
|
80
|
+
((v & 0xf) != 0)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
@logger.debug("get #{device.name} => #{bits}")
|
84
|
+
bits
|
85
|
+
end
|
86
|
+
|
87
|
+
def set_bits_to_device(bits, device)
|
88
|
+
unless available_bits_range.include? bits.size
|
89
|
+
raise ArgumentError,
|
90
|
+
"A count #{count} must be between #{available_bits_range.first} and #{available_bits_range.last} for #{__method__}"
|
91
|
+
end
|
92
|
+
|
93
|
+
device = device_by_name device
|
94
|
+
packet = make_packet(body_for_set_bits_to_device(bits, device))
|
95
|
+
@logger.debug("> #{dump_packet packet}")
|
96
|
+
open
|
97
|
+
@socket.write(packet.pack('C*'))
|
98
|
+
@socket.flush
|
99
|
+
res = receive
|
100
|
+
@logger.debug("set #{bits} to:#{device.name}")
|
101
|
+
|
102
|
+
# error checking
|
103
|
+
end_code = res[9, 2].pack('C*').unpack1('v')
|
104
|
+
unless end_code.zero?
|
105
|
+
error = res[11, 2].pack('C*').unpack1('v')
|
106
|
+
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})"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def get_words_from_device(count, device)
|
111
|
+
unless available_bits_range.include? count
|
112
|
+
raise ArgumentError,
|
113
|
+
"A count #{count} must be between #{available_words_range.first} and #{available_words_range.last} for #{__method__}"
|
114
|
+
end
|
115
|
+
|
116
|
+
device = device_by_name device
|
117
|
+
packet = make_packet(body_for_get_words_from_device(count, device))
|
118
|
+
@logger.debug("> #{dump_packet packet}")
|
119
|
+
open
|
120
|
+
@socket.write(packet.pack('C*'))
|
121
|
+
@socket.flush
|
122
|
+
res = receive
|
123
|
+
|
124
|
+
# error checking
|
125
|
+
end_code = res[9, 2].pack('C*').unpack1('v')
|
126
|
+
unless end_code.zero?
|
127
|
+
error = res[11, 2].pack('C*').unpack1('v')
|
128
|
+
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})"
|
129
|
+
end
|
130
|
+
|
131
|
+
# get result
|
132
|
+
words = []
|
133
|
+
res[11, 2 * count].each_slice(2) do |pair|
|
134
|
+
words << pair.pack('C*').unpack1('v')
|
135
|
+
end
|
136
|
+
@logger.debug("get from: #{device.name} => #{words}")
|
137
|
+
words
|
138
|
+
end
|
139
|
+
|
140
|
+
def set_words_to_device(words, device)
|
141
|
+
unless available_bits_range.include? words.size
|
142
|
+
raise ArgumentError,
|
143
|
+
"A count of words #{words.size} must be between #{available_words_range.first} and #{available_words_range.last} for #{__method__}"
|
144
|
+
end
|
145
|
+
|
146
|
+
device = device_by_name device
|
147
|
+
packet = make_packet(body_for_set_words_to_device(words, device))
|
148
|
+
@logger.debug("> #{dump_packet packet}")
|
149
|
+
open
|
150
|
+
@socket.write(packet.pack('C*'))
|
151
|
+
@socket.flush
|
152
|
+
res = receive
|
153
|
+
@logger.debug("set #{words} to: #{device.name}")
|
154
|
+
|
155
|
+
# error checking
|
156
|
+
end_code = res[9, 2].pack('C*').unpack1('v')
|
157
|
+
unless end_code.zero?
|
158
|
+
error = res[11, 2].pack('C*').unpack1('v')
|
159
|
+
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})"
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def device_by_name(name)
|
164
|
+
case name
|
165
|
+
when String
|
166
|
+
d = QDevice.new name
|
167
|
+
d.valid? ? d : nil
|
168
|
+
else
|
169
|
+
# it may be already QDevice
|
170
|
+
name
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def receive
|
175
|
+
res = []
|
176
|
+
len = 0
|
177
|
+
begin
|
178
|
+
Timeout.timeout(TIMEOUT) do
|
179
|
+
loop do
|
180
|
+
c = @socket.read(1)
|
181
|
+
next if c.nil? || c == ''
|
182
|
+
|
183
|
+
res << c.bytes.first
|
184
|
+
len = res[7, 2].pack('C*').unpack1('v*') if res.length >= 9
|
185
|
+
break if len + 9 == res.length
|
186
|
+
end
|
187
|
+
end
|
188
|
+
rescue Timeout::Error
|
189
|
+
puts '*** ERROR: TIME OUT ***'
|
190
|
+
end
|
191
|
+
@logger.debug("< #{dump_packet res}")
|
192
|
+
res
|
193
|
+
end
|
194
|
+
|
195
|
+
def available_bits_range(_device = nil)
|
196
|
+
1..(960 * 16)
|
197
|
+
end
|
198
|
+
|
199
|
+
def available_words_range(_device = nil)
|
200
|
+
1..960
|
201
|
+
end
|
202
|
+
|
203
|
+
private
|
204
|
+
|
205
|
+
def make_packet(body)
|
206
|
+
header = [0x50, 0x00, 0x00, 0xff, 0xff, 0x03, 0x00, 0x00, 0x00, 0x10, 0x00]
|
207
|
+
header[7..8] = data_for_short(body.length + 2)
|
208
|
+
header + body
|
209
|
+
end
|
210
|
+
|
211
|
+
def body_for_get_bit_from_deivce(device)
|
212
|
+
body_for_get_bits_from_device 1, device
|
213
|
+
end
|
214
|
+
|
215
|
+
def body_for_get_bits_from_device(count, device)
|
216
|
+
body_for_get_words_from_device count, device, false
|
217
|
+
end
|
218
|
+
|
219
|
+
def body_for_get_words_from_device(count, device, word = true)
|
220
|
+
body = [0x01, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0x01, 0x00]
|
221
|
+
body[2] = 1 unless word
|
222
|
+
body[4..7] = data_for_device(device)
|
223
|
+
body[8..9] = data_for_short count
|
224
|
+
body
|
225
|
+
end
|
226
|
+
|
227
|
+
def body_for_set_bits_to_device(bits, device)
|
228
|
+
body = [0x01, 0x14, 0x01, 0x00, 0x00, 0x00, 0x00, 0x90, 0x01, 0x00]
|
229
|
+
d = device
|
230
|
+
bits = [bits] unless bits.is_a? Array
|
231
|
+
bits.each_slice(2) do |pair|
|
232
|
+
body << (pair.first ? 0x10 : 0x00)
|
233
|
+
body[-1] |= (pair.last ? 0x1 : 0x00) if pair.size == 2
|
234
|
+
d = d.next_device
|
235
|
+
end
|
236
|
+
body[4..7] = data_for_device(device)
|
237
|
+
body[8..9] = data_for_short bits.size
|
238
|
+
body
|
239
|
+
end
|
240
|
+
alias body_for_set_bit_to_device body_for_set_bits_to_device
|
241
|
+
|
242
|
+
def body_for_set_words_to_device(words, device)
|
243
|
+
body = [0x01, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0x01, 0x00]
|
244
|
+
d = device
|
245
|
+
words = [words] unless words.is_a? Array
|
246
|
+
words.each do |v|
|
247
|
+
body += data_for_short v
|
248
|
+
d = d.next_device
|
249
|
+
end
|
250
|
+
body[4..7] = data_for_device(device)
|
251
|
+
body[8..9] = data_for_short words.size
|
252
|
+
body
|
253
|
+
end
|
254
|
+
|
255
|
+
def data_for_device(device)
|
256
|
+
a = data_for_int device.number
|
257
|
+
a[3] = device.suffix_code
|
258
|
+
a
|
259
|
+
end
|
260
|
+
|
261
|
+
def data_for_short(value)
|
262
|
+
[value].pack('v').unpack('C*')
|
263
|
+
end
|
264
|
+
|
265
|
+
def data_for_int(value)
|
266
|
+
[value].pack('V').unpack('C*')
|
267
|
+
end
|
268
|
+
|
269
|
+
def dump_packet(packet)
|
270
|
+
a = []
|
271
|
+
len = packet.length
|
272
|
+
bytes = packet.dup
|
273
|
+
len.times do |i|
|
274
|
+
a << "0#{bytes[i].to_s(16)}"[-2, 2]
|
275
|
+
end
|
276
|
+
"[#{a.join(', ')}]"
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# The MIT License (MIT)
|
4
|
+
#
|
5
|
+
# Copyright (c) 2016 ITO SOFT DESIGN Inc.
|
6
|
+
#
|
7
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
8
|
+
# a copy of this software and associated documentation files (the
|
9
|
+
# "Software"), to deal in the Software without restriction, including
|
10
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
11
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
12
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
13
|
+
# the following conditions:
|
14
|
+
#
|
15
|
+
# The above copyright notice and this permission notice shall be
|
16
|
+
# included in all copies or substantial portions of the Software.
|
17
|
+
#
|
18
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
19
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
20
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
21
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
22
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
23
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
24
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
25
|
+
|
26
|
+
$LOAD_PATH.unshift File.dirname(__FILE__)
|
27
|
+
|
28
|
+
require 'socket'
|
29
|
+
require 'logger'
|
30
|
+
require 'timeout'
|
31
|
+
|
32
|
+
require 'qdevice'
|
33
|
+
require 'mc_protocol'
|
34
|
+
|
35
|
+
require 'fx_device'
|
36
|
+
require 'fx_protocol'
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# The MIT License (MIT)
|
4
|
+
#
|
5
|
+
# Copyright (c) 2016 ITO SOFT DESIGN Inc.
|
6
|
+
#
|
7
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
8
|
+
# a copy of this software and associated documentation files (the
|
9
|
+
# "Software"), to deal in the Software without restriction, including
|
10
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
11
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
12
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
13
|
+
# the following conditions:
|
14
|
+
#
|
15
|
+
# The above copyright notice and this permission notice shall be
|
16
|
+
# included in all copies or substantial portions of the Software.
|
17
|
+
#
|
18
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
19
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
20
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
21
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
22
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
23
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
24
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
25
|
+
|
26
|
+
module PlcAccess
|
27
|
+
module Protocol
|
28
|
+
module Mitsubishi
|
29
|
+
class QDevice < PlcDevice
|
30
|
+
attr_reader :suffix, :number
|
31
|
+
|
32
|
+
SUFFIXES = %w[SM SD X Y M L F V B D W TS TC TN SS SC SN CS CC CN SB SW S DX DY Z R ZR].freeze
|
33
|
+
SUFFIX_CODES = [0x91, 0xa9, 0x9c, 0x9d, 0x90, 0x92, 0x93, 0x94, 0xa0, 0xa8, 0xb4, 0xc1, 0xc0, 0xc2, 0xc7, 0xc6,
|
34
|
+
0xc8, 0xc4, 0xc3, 0xc5, 0xa1, 0xb5, 0x98, 0xa2, 0xa3, 0xcc, 0xaf, 0xb0].freeze
|
35
|
+
|
36
|
+
def initialize(a, b = nil)
|
37
|
+
case a
|
38
|
+
when Array
|
39
|
+
case a.size
|
40
|
+
when 4
|
41
|
+
@suffix = suffix_for_code(a[3])
|
42
|
+
@number = ((a[2] << 8 | a[1]) << 8) | a[0]
|
43
|
+
end
|
44
|
+
when String
|
45
|
+
if b
|
46
|
+
@suffix = a.upcase
|
47
|
+
@number = b
|
48
|
+
elsif a.length == 12
|
49
|
+
@suffix = [a[0, 2].to_i(16), a[2, 2].to_i(16)].pack 'C*'
|
50
|
+
@suffix.strip!
|
51
|
+
@number = a[4, 8].to_i(16)
|
52
|
+
elsif /(X|Y|B|W|SB|SW|DX|DY)([0-9a-f]+)/i =~ a
|
53
|
+
@suffix = ::Regexp.last_match(1).upcase
|
54
|
+
@number = ::Regexp.last_match(2).to_i(p_adic_number)
|
55
|
+
elsif /(M|L|S|F|T|C|D|R|ZR)([0-9]+)/i =~ a
|
56
|
+
@suffix = ::Regexp.last_match(1).upcase
|
57
|
+
@number = ::Regexp.last_match(2).to_i(p_adic_number)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def valid?
|
63
|
+
!!@suffix && !!@number
|
64
|
+
end
|
65
|
+
|
66
|
+
def p_adic_number
|
67
|
+
case @suffix
|
68
|
+
when 'X', 'Y', 'B', 'W', 'SB', 'SW', 'DX', 'DY'
|
69
|
+
16
|
70
|
+
else
|
71
|
+
10
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def name
|
76
|
+
@suffix + @number.to_s(p_adic_number).upcase
|
77
|
+
end
|
78
|
+
|
79
|
+
def next_device
|
80
|
+
self.class.new @suffix, @number + 1
|
81
|
+
end
|
82
|
+
|
83
|
+
def bit_device?
|
84
|
+
case @suffix
|
85
|
+
when 'SM', 'X', 'Y', 'M', 'L', 'F', 'V', 'B',
|
86
|
+
'TS', 'TC', 'SS', 'SC', 'CS', 'CC', 'SB', 'S', 'DX', 'DY'
|
87
|
+
true
|
88
|
+
else
|
89
|
+
false
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def suffix_for_code(code)
|
94
|
+
index = SUFFIX_CODES.index code
|
95
|
+
index ? SUFFIXES[index] : nil
|
96
|
+
end
|
97
|
+
|
98
|
+
def suffix_code
|
99
|
+
index = SUFFIXES.index suffix
|
100
|
+
index ? SUFFIX_CODES[index] : 0
|
101
|
+
end
|
102
|
+
|
103
|
+
def +(other)
|
104
|
+
self.class.new suffix, number + other
|
105
|
+
end
|
106
|
+
|
107
|
+
def -(other)
|
108
|
+
self.class.new suffix, [number - other, 0].max
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|