simatic 0.0.1
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 +10 -0
- data/Gemfile +4 -0
- data/README.md +91 -0
- data/Rakefile +1 -0
- data/bin/read +39 -0
- data/lib/simatic.rb +7 -0
- data/lib/simatic/memory_mapper.rb +191 -0
- data/lib/simatic/plc.rb +100 -0
- data/lib/simatic/sessions.rb +9 -0
- data/lib/simatic/sessions/exchange_session.rb +50 -0
- data/lib/simatic/sessions/open_session.rb +27 -0
- data/lib/simatic/sessions/read_session.rb +83 -0
- data/lib/simatic/sessions/session.rb +44 -0
- data/lib/simatic/sessions/setup_session.rb +32 -0
- data/lib/simatic/sessions/write_session.rb +80 -0
- data/lib/simatic/types.rb +134 -0
- data/lib/simatic/types/bool.rb +18 -0
- data/lib/simatic/types/byte.rb +10 -0
- data/lib/simatic/types/char.rb +15 -0
- data/lib/simatic/types/date_and_time.rb +43 -0
- data/lib/simatic/types/dint.rb +10 -0
- data/lib/simatic/types/dword.rb +10 -0
- data/lib/simatic/types/iec_date.rb +20 -0
- data/lib/simatic/types/iec_time.rb +16 -0
- data/lib/simatic/types/int.rb +10 -0
- data/lib/simatic/types/real.rb +10 -0
- data/lib/simatic/types/s5_time.rb +47 -0
- data/lib/simatic/types/s7_string.rb +38 -0
- data/lib/simatic/types/simatic_simple_type.rb +20 -0
- data/lib/simatic/types/simatic_type.rb +34 -0
- data/lib/simatic/types/time_of_day.rb +8 -0
- data/lib/simatic/types/word.rb +10 -0
- data/lib/simatic/version.rb +3 -0
- data/simatic.gemspec +27 -0
- metadata +94 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: cb39fba52aa7abd65f8a1f837b6a8bb1e5572ffa
|
|
4
|
+
data.tar.gz: 0e88119b704b2d9c95d6f4744163ac073e5cebc4
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b140bdef40f8ad4a3ffc43ddb4ed828703984fea3c5a5044f40e24dcaa728f63b78c774c55ed006dd7dd2e8ce0a2ebae54485bdd526666d152942f7fbed5efa3
|
|
7
|
+
data.tar.gz: cac43153f5aebb686b11ed143b9e0edf6d0e052b7138232ec2e70fd48f9dd8e56be2b1337a6cdf9f6834a709bc0a16132fa63cd78baf2ac80ec910ae0b85c290
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# ruby_s7plc
|
|
2
|
+
Ruby library for Siemens Simatic S7-300 PLC data exchange.
|
|
3
|
+
|
|
4
|
+
## Quick start
|
|
5
|
+
```ruby
|
|
6
|
+
require 'simatic'
|
|
7
|
+
Simatic::Plc.exchange('192.168.0.1') do |plc|
|
|
8
|
+
plc.write('db1.dbw2'=> 0xffff)
|
|
9
|
+
res = plc.read('db1.dbw2')
|
|
10
|
+
puts "#{res.verbal} = #{Simatic::Types::Word.parse(res.value)}"
|
|
11
|
+
end
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Reference
|
|
15
|
+
### Requires
|
|
16
|
+
```ruby
|
|
17
|
+
require 'simatic'
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Client creation
|
|
21
|
+
```ruby
|
|
22
|
+
plc = Simatic::Plc.new('192.168.0.1')
|
|
23
|
+
plc.connect
|
|
24
|
+
# Add you code here
|
|
25
|
+
plc.disconnect
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or with block:
|
|
29
|
+
```ruby
|
|
30
|
+
Simatic::Plc.exchange('192.168.0.1') do |plc|
|
|
31
|
+
# Add you code here
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
You can specify rack and slot parametrs by adding a hash args to functions <new> or <exchange>, defaults rack: 0, slot: 2
|
|
36
|
+
```ruby
|
|
37
|
+
Simatic::Plc.new('192.168.0.1', rack: 0, slot: 2) ...
|
|
38
|
+
Simatic::Plc.exchange('192.168.0.1', rack: 0, slot: 2) do |plc| ...
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Exchange functions
|
|
42
|
+
```ruby
|
|
43
|
+
plc.read('db1.dbx0.0')
|
|
44
|
+
```
|
|
45
|
+
This function can understand verbal adressing in Simatic Step 7 notation, like M0.0, DB1.DBB23, C0, T1 and so on.
|
|
46
|
+
Result of this funtion is a MemoryMapper object that can give you ```raw_data``` and ```value``` of plc response.
|
|
47
|
+
After this function you should use SymaticTypes module to convert data into ruby undestandable types.
|
|
48
|
+
```ruby
|
|
49
|
+
plc.write('db1.dbw2' => 0xffff)
|
|
50
|
+
```
|
|
51
|
+
This function take a hash with verbal addresses and values to write pairs. Values can be simple types like Integer, Float, one char String, Date, Time or it can be special types of SimaticTypes Module.
|
|
52
|
+
```ruby
|
|
53
|
+
plc.write('db1.dbb18[2]' => Simatic::Types::S5time.new(711.3))
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Convertation functions
|
|
57
|
+
```ruby
|
|
58
|
+
Simatic::Types::Bool # use TrueClass, FalseClass
|
|
59
|
+
Simatic::Types::Byte # use Numeric
|
|
60
|
+
Simatic::Types::Word # use Numeric
|
|
61
|
+
Simatic::Types::Dword # use Numeric
|
|
62
|
+
Simatic::Types::Int # use Numeric
|
|
63
|
+
Simatic::Types::Dint # use Numeric
|
|
64
|
+
Simatic::Types::Real # use Numeric
|
|
65
|
+
Simatic::Types::S5time # use Numeric as a time in seconds
|
|
66
|
+
Simatic::Types::IECTime # use Numeric as a time in seconds
|
|
67
|
+
Simatic::Types::IECDate # use Numeric as a days
|
|
68
|
+
Simatic::Types::TimeOfDay # use Numeric as a seconds from start of day 0:00 a.m.
|
|
69
|
+
Simatic::Types::DateAndTime # use Time
|
|
70
|
+
Simatic::Types::S7String # use String with length of buffer param
|
|
71
|
+
```
|
|
72
|
+
Each of this objects take Ruby types with ```new``` method and gives you with ```parse``` method:
|
|
73
|
+
```ruby
|
|
74
|
+
Simatic::Types::S5time.new(10.2)
|
|
75
|
+
Simatic::Types::S5time.parse(res.raw_data)
|
|
76
|
+
Simatic::Types::S7String.new("hello world", 15) # 15 is a length of buffer (address for this string to read or write must be [17])
|
|
77
|
+
```
|
|
78
|
+
For special types like S5time, IECTime, IECDate, TimeOfDay, DateAndTime, S7String you should use ```raw_data``` method instead of `value`, because if you write an adress with [] notation of bytes ```value``` will return a array of chars.
|
|
79
|
+
|
|
80
|
+
## Waring
|
|
81
|
+
This is beta code! Do not use it in danger manufactory!
|
|
82
|
+
|
|
83
|
+
## See also
|
|
84
|
+
Father and oldest of all open Simatic communication libraries is Libnodave project:
|
|
85
|
+
* http://libnodave.sourceforge.net/
|
|
86
|
+
|
|
87
|
+
Another interesting projects:
|
|
88
|
+
* https://github.com/kprovost/libs7comm
|
|
89
|
+
* https://github.com/kirilk/simatic
|
|
90
|
+
* https://github.com/killnine/s7netplus
|
|
91
|
+
* https://github.com/plcpeople/nodeS7
|
data/Rakefile
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/read
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "simatic"
|
|
5
|
+
|
|
6
|
+
ip = ARGV.shift
|
|
7
|
+
address = ARGV.shift
|
|
8
|
+
|
|
9
|
+
type = nil
|
|
10
|
+
slot = 2
|
|
11
|
+
rack = 0
|
|
12
|
+
|
|
13
|
+
while param = ARGV.shift do
|
|
14
|
+
case param
|
|
15
|
+
when '-t', '--type'
|
|
16
|
+
type = ARGV.shift
|
|
17
|
+
when '--slot'
|
|
18
|
+
slot = ARGV.shift
|
|
19
|
+
when '--rack'
|
|
20
|
+
rack = ARGV.shift
|
|
21
|
+
when '-h', '--help'
|
|
22
|
+
puts "USAGE"
|
|
23
|
+
puts "read <ip> <address> (-t|--type <type>) [--rack <rack>] [--slot <slot>] [-h|--help]"
|
|
24
|
+
puts "avaliable types #{Simatic::Types.avaliable}"
|
|
25
|
+
puts "EXAMPLE"
|
|
26
|
+
puts "read 192.168.0.1 db0.dbw4 -t int"
|
|
27
|
+
exit
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
#puts "address #{address}"
|
|
32
|
+
#puts "type #{type}"
|
|
33
|
+
#puts "rack #{rack}"
|
|
34
|
+
#puts "slot #{slot}"
|
|
35
|
+
|
|
36
|
+
Simatic::Plc.exchange(ip, rack: rack, slot: slot) do |plc|
|
|
37
|
+
value = plc.read(address).value
|
|
38
|
+
puts "#{address}: #{Simatic::Types.parse value, type}"
|
|
39
|
+
end
|
data/lib/simatic.rb
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
module Simatic
|
|
2
|
+
class MemoryMapper
|
|
3
|
+
attr_reader :verbal
|
|
4
|
+
attr_reader :raw_values
|
|
5
|
+
attr_accessor :raw_data
|
|
6
|
+
|
|
7
|
+
attr_reader :length
|
|
8
|
+
attr_reader :count
|
|
9
|
+
|
|
10
|
+
attr_reader :area
|
|
11
|
+
attr_reader :db
|
|
12
|
+
attr_reader :address
|
|
13
|
+
attr_reader :bit
|
|
14
|
+
|
|
15
|
+
def initialize verbal, args={}
|
|
16
|
+
@verbal = verbal
|
|
17
|
+
|
|
18
|
+
parse_address @verbal
|
|
19
|
+
# puts args[:value]
|
|
20
|
+
self.value = args[:value] unless args[:value].nil?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def parse_address verbal = @verbal
|
|
24
|
+
/^(DB\s*(?<db>\d+)(\.|\s*))?(?<area>PI|PQ|I|Q|M|DB|T|C)(?<type>X|B|W|D)?\s*?(?<adr>\d+)(\.(?<bit>\d+))?\s*?(\[(?<count>\d+)\])?$/i =~ verbal
|
|
25
|
+
|
|
26
|
+
@db = db ? db.to_i : 0
|
|
27
|
+
@count = count ? count.to_i : 1
|
|
28
|
+
@address = adr ? adr.to_i : nil
|
|
29
|
+
@bit = bit ? bit.to_i : nil
|
|
30
|
+
|
|
31
|
+
unless @address
|
|
32
|
+
raise "Adressing by #{verbal} is impossible. Cant parse it."
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
case area.upcase
|
|
36
|
+
when 'PI','PQ'
|
|
37
|
+
@area = AreaP
|
|
38
|
+
when 'I'
|
|
39
|
+
@area = AreaI
|
|
40
|
+
when 'Q'
|
|
41
|
+
@area = AreaQ
|
|
42
|
+
when 'M'
|
|
43
|
+
@area = AreaM
|
|
44
|
+
when 'DB'
|
|
45
|
+
@area = AreaDB
|
|
46
|
+
when 'T'
|
|
47
|
+
@area = AreaT
|
|
48
|
+
when 'C'
|
|
49
|
+
@area = AreaC
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
case type ? type.upcase : 'X'
|
|
53
|
+
when 'B'
|
|
54
|
+
@length = 1
|
|
55
|
+
when 'W'
|
|
56
|
+
@length = 2
|
|
57
|
+
when 'D'
|
|
58
|
+
@length = 4
|
|
59
|
+
else
|
|
60
|
+
@length = 1
|
|
61
|
+
unless @bit
|
|
62
|
+
raise "Adressing by #{verbal} is impossible. Cant parse it. Is bit expected?"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def value= new_value
|
|
68
|
+
val_array = (new_value.kind_of? Array) ? new_value : [new_value]
|
|
69
|
+
|
|
70
|
+
@raw_values = []
|
|
71
|
+
val_array.each do |v|
|
|
72
|
+
|
|
73
|
+
val = case v
|
|
74
|
+
when Simatic::Types::SimaticType
|
|
75
|
+
v
|
|
76
|
+
when Integer
|
|
77
|
+
case @length
|
|
78
|
+
when 1
|
|
79
|
+
Simatic::Types::Byte.new(v)
|
|
80
|
+
when 2
|
|
81
|
+
Simatic::Types::Int.new(v)
|
|
82
|
+
when 4
|
|
83
|
+
Simatic::Types::Dint.new(v)
|
|
84
|
+
else
|
|
85
|
+
raise "Cannot convert #{v.class} to SimaticType class"
|
|
86
|
+
end
|
|
87
|
+
when Float
|
|
88
|
+
Simatic::Types::Real.new(v)
|
|
89
|
+
when TrueClass, FalseClass
|
|
90
|
+
Simatic::Types::Bool.new(v)
|
|
91
|
+
when String
|
|
92
|
+
raise "Cannot write string to plc use SimaticType::S7String class" if length > 1
|
|
93
|
+
Simatic::Types::Char.new(v) if v.length == 1
|
|
94
|
+
when Date
|
|
95
|
+
Simatic::Types::IECDate.new(v)
|
|
96
|
+
when Time
|
|
97
|
+
Simatic::Types::DateAndTime.new(v)
|
|
98
|
+
else
|
|
99
|
+
raise "Unknoun type of write value <#{v.class}> #{v}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
@raw_values << val.serialize
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
@raw_data = @raw_values.inject { |sum, v| sum+v }
|
|
106
|
+
|
|
107
|
+
if @raw_data.length != @length*@count
|
|
108
|
+
raise "This values #{val_array} can not be writed because adress #{verbal} mean #{@length*@count} byte(s) instead #{@raw_data.length}"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def value
|
|
113
|
+
@raw_values = []
|
|
114
|
+
|
|
115
|
+
byte = 0
|
|
116
|
+
while byte < @count*@length
|
|
117
|
+
@raw_values << @raw_data[byte, @length]
|
|
118
|
+
byte += @length
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
result = @raw_values
|
|
122
|
+
|
|
123
|
+
# .map do |raw|
|
|
124
|
+
|
|
125
|
+
# case type
|
|
126
|
+
# when :bool
|
|
127
|
+
|
|
128
|
+
# bit = @bit ? 0 : (args[:bit] || nil)
|
|
129
|
+
|
|
130
|
+
# binary_string = raw.unpack('b*').first
|
|
131
|
+
|
|
132
|
+
# if bit
|
|
133
|
+
# binary_string[bit] == '1' ? true : false
|
|
134
|
+
# else
|
|
135
|
+
# binary_string.index('1') ? true : false
|
|
136
|
+
# end
|
|
137
|
+
|
|
138
|
+
# when :int8
|
|
139
|
+
# raw.unpack('c').first
|
|
140
|
+
# when :uint8
|
|
141
|
+
# raw.unpack('C').first
|
|
142
|
+
# when :int16
|
|
143
|
+
# raw.unpack('s>').first
|
|
144
|
+
# when :uint16
|
|
145
|
+
# raw.unpack('S>').first # try n
|
|
146
|
+
# when :int32
|
|
147
|
+
# raw.unpack('l>').first
|
|
148
|
+
# when :uint32
|
|
149
|
+
# raw.unpack('L>').first # try N
|
|
150
|
+
# when :float32
|
|
151
|
+
# raw.unpack('g').first
|
|
152
|
+
# when :float64
|
|
153
|
+
# raw.unpack('G').first
|
|
154
|
+
# when :text8
|
|
155
|
+
# raw.unpack('A*').first
|
|
156
|
+
# when :text16
|
|
157
|
+
# raw.unpack('u*').first
|
|
158
|
+
# else
|
|
159
|
+
# raw
|
|
160
|
+
# end
|
|
161
|
+
# end
|
|
162
|
+
|
|
163
|
+
result = result.first if result.count == 1
|
|
164
|
+
result
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def as_bool raw
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def as_int raw
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def as_uint raw
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def as_real raw_data
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def as_s5time raw_data
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def as_time raw_data
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def as_date raw_data
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def as_string raw_data
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
data/lib/simatic/plc.rb
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
require 'socket'
|
|
2
|
+
require 'simatic/sessions'
|
|
3
|
+
require 'simatic/memory_mapper'
|
|
4
|
+
|
|
5
|
+
module Simatic
|
|
6
|
+
BUFFER_SIZE = 1000
|
|
7
|
+
|
|
8
|
+
class Plc
|
|
9
|
+
def initialize address, args = {}
|
|
10
|
+
@address = address
|
|
11
|
+
@rack = args[:rack] || 0
|
|
12
|
+
@slot = args[:slot] || 2
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.exchange address, args = {}, timeout = 500
|
|
16
|
+
plc = self.new address, args
|
|
17
|
+
|
|
18
|
+
plc.connect timeout
|
|
19
|
+
yield plc
|
|
20
|
+
plc.disconnect
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def read *args
|
|
24
|
+
if @socket.nil?
|
|
25
|
+
raise "Plc #{@address} is not connected"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
read = Sessions::ReadSession.new
|
|
29
|
+
memory_mappers = []
|
|
30
|
+
args.each do |verbal|
|
|
31
|
+
memory_mappers << MemoryMapper.new(verbal)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
request = read.make_request memory_mappers
|
|
35
|
+
@socket.send request, 0
|
|
36
|
+
|
|
37
|
+
# debug_print request
|
|
38
|
+
|
|
39
|
+
response = @socket.recv BUFFER_SIZE
|
|
40
|
+
|
|
41
|
+
# debug_print response
|
|
42
|
+
|
|
43
|
+
result = read.parse_response response
|
|
44
|
+
|
|
45
|
+
# Hash[result.map { |memory_mapper| [memory.verbal, memory.value] }]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def write args
|
|
49
|
+
if @socket.nil?
|
|
50
|
+
raise "Plc #{@address} is not connected"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
memory_mappers = []
|
|
54
|
+
args.each do |verbal, value|
|
|
55
|
+
memory_mappers << MemoryMapper.new(verbal, value: value)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
write = Sessions::WriteSession.new
|
|
59
|
+
request = write.make_request memory_mappers
|
|
60
|
+
@socket.send request, 0
|
|
61
|
+
|
|
62
|
+
# debug_print request
|
|
63
|
+
|
|
64
|
+
response = @socket.recv BUFFER_SIZE
|
|
65
|
+
|
|
66
|
+
# debug_print response
|
|
67
|
+
|
|
68
|
+
result = write.parse_response response
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def connect timeout = 500
|
|
72
|
+
@socket = Socket.tcp @address, 102, connect_timeout: timeout / 1000.0
|
|
73
|
+
|
|
74
|
+
setup = Sessions::SetupSession.new @rack, @slot
|
|
75
|
+
@socket.send setup.make_request, 0
|
|
76
|
+
setup.parse_response @socket.recv BUFFER_SIZE
|
|
77
|
+
|
|
78
|
+
open = Sessions::OpenSession.new
|
|
79
|
+
@socket.send open.make_request, 0
|
|
80
|
+
open.parse_response @socket.recv BUFFER_SIZE
|
|
81
|
+
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def disconnect
|
|
85
|
+
@socket.close
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def debug_print raw
|
|
91
|
+
puts '-'*80
|
|
92
|
+
raw.bytes.each_with_index do |byte, i|
|
|
93
|
+
ends = (i+1)%16 == 0 ? "\n" : ' '
|
|
94
|
+
printf '%02X '+ ends, byte
|
|
95
|
+
end
|
|
96
|
+
puts ''
|
|
97
|
+
puts '-'*80
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|