2019-10-12
In this post I’m going to describe how to make a simple data logger for Siemens S7 PLC’s using only open source tools.
One day at work, we got some problems with a steam boiler. The boiler is a 5 MW electrical boiler that provides the factory with steam. It’s standalone and lives a quite still life in its own big hall. It is connected to the ethernet network but that is only for enabling remote programming (remote in the sense that I won’t have to walk the 100 long meters from my office if I need to check something, that I rarely do).
To make steam, you need water. And you need a proper supply of water that is treated to meet certain requirements. The means of creating steam in an electrical boiler is to stick in three electrodes in the water tank directly connected to the three-phase 400V grid. Since current is given by voltage and resistance, you need to control the resistivity in the water to control the current (since voltage is constant).
So, the water is under treatment (basically, a reverse osmosis process for purification and the usage of additives for controlling resistivity) and the treated water is buffered in a big tank. From the tank, it gets pumped to the boiler.
And here was the problem.
The supply of feed water was intermittently interrupted due to a bad pump or a bad minimum flow (recirculation) valve. We had a spare pump but this was rated at a lesser flow capacity, so we needed to make sure the consumption was within the capacity. And furthermore, we needed some data to correlate the fault with what was going on elsewhere.
But we didn’t have any data logger. So I had to come up with something.
The boiler’s control system is a Siemens S7-300 PLC and this was connected to ethernet. We did not have any OPC server set up for this PLC, which is a common way to send and receive data; but I recalled I had come across a C library what could talk natively to Siemens PLC’s, named libnodave, some years ago. I also found a Python port but this hasn’t been updated for many years and it seems to be a Win32 thing. So finally, I found the solution: a cross-platform library called Snap7 that’s available for lots of programming languages, including Python.
So, first thing that needs to be done in order to build a data logger is of course to get access to the data source. In the case of Siemens S7, you need to set up a connection to the CPU and retrieve the data stored in the DB’s. And as it turns out, this is actually very easy thanks to Snap7.
Step one is to install Snap7. Since it’s available on PyPI it’s just a matter of:
$ pip install python-snap7
Then you set up a connection in Python:
import snap7
= '192.168.73.1' # the IP address of the S7 network card
IP = 0 # the rack number and...
RACK SLOT = 2 # ...the slot where the CPU is located
= snap7.client.Client()
client connect(IP, RACK, SLOT) client.
Now you have an instance of a Snap7 client object that gives you the tools needed for retrieving data.
Next, we need to find out where the data points to be logged are stored. In my case, they were spread across a number of DB’s. Here’s a few of them:
DB | Address | Name | Comment |
---|---|---|---|
3 | dbd6 | PID6_TANK_KOND.PV_IN | Amount of condensate water |
9 | dbd2 | TR_PANNA | Pressure in boiler |
9 | dbd6 | TR_MAVA | Pressure in feeding tank |
All the above signals have datatype REAL. To retrieve the actual pressure in boiler, the third signal above, you can do like this:
from snap7 import util
= 9
db = 0 # let's read from the beginning of the DB
start_address = 10 # we are happy to read just the 10 first bytes
end_address
= client.db_read(db, start_address, end_address)
bytedata
= util.get_real(bytedata, 2)
boiler_pressure
print(boiler_pressure)
# 5.67231
The important lines here are line 7 and line 9. [TODO: Add line numbers in code block.]
In line 7 we use the function client.db_read to retrieve data from DB 9. We request all bytes starting from byte 0 and ending on byte 10. Since we only want the data from DB9.DBD2, we could give 2 as start address and 5 as end address (a REAL is 4 bytes), but I will explain further down why we start on byte 0.
So, now we have a bunch of bytes in our bytedata variable. These don’t tell us much, so we need to extract the actual value related to the signal, i.e. the pressure in the boiler. This is what we do on line 9. We use a function from the util module supplied with snap7, to extract the value of one REAL from bytedata starting on byte 2.
OK, so when we not print the output we can see the actual pressure in the boiler.
Let’s say we also want to see the pressure in the feeder tank. Since this data is in the same DB and that the address (DBD6) fits within the scope of bytes that we already has downloaded (remember, we read all bytes from 0 on to byte 10 and DBD6 is < 10), we can easily just extract the value from the same bunch of bytes:
= util.get_real(bytedata, 6)
feeder_pressure
print(feeder_pressure)
# 1.15123
Now, let’s say we didn’t read the bytes starting at address 0, but starting at 2 instead, since we don’t need any data from the first two bytes. Like this:
= client.db_read(9, 2, 10) bytedata
Then indeed, we can still extract the data that we want, but we need to adjust the byte number:
= util.get_real(bytedata, 0)
boiler_pressure = util.get_real(bytedata, 4) feeder_pressure
The boiler pressure is the third byte (byte 2) in the DB but the first byte (byte 0) in bytedata. Now you see it’s much simpler to read from the first byte so that the byte sequences match! Especially if you start to read lots of data from several DB’s.
Well, let’s stop here for now. In the next article, I will explain how to receive data periodically and save it to a csv file. Then, I will explain how to store it in InfluxDB and view it in Grafana.