How to make a simple Siemens data logger in Python

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.

A real-life use case

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.

Enter the data logger

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.

Let’s build the data logger

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

IP = '192.168.73.1' # the IP address of the S7 network card
RACK = 0 # the rack number and...
SLOT = 2 # ...the slot where the CPU is located

client = snap7.client.Client()
client.connect(IP, RACK, SLOT)

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

db = 9
start_address = 0 # let's read from the beginning of the DB
end_address = 10 # we are happy to read just the 10 first bytes

bytedata = client.db_read(db, start_address, end_address)

boiler_pressure = util.get_real(bytedata, 2)

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:

feeder_pressure = util.get_real(bytedata, 6)

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:

bytedata = client.db_read(9, 2, 10)

Then indeed, we can still extract the data that we want, but we need to adjust the byte number:

boiler_pressure = util.get_real(bytedata, 0)
feeder_pressure = util.get_real(bytedata, 4)

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.