PLC interacting with Algorand Blockchain
Overview
Although the industry’s demand for intelligent software solutions is growing, Industry 4.0 remains merely a buzzword in many areas. The reason is often not a lack of readiness, but the extremely long life cycles of the machines. Because of these long operating times, old control units (PLCs) must also be married to future technologies. In the following, a simple solution will be shown that allows Simatic S7 PLCs to communicate with the algorand blockchain. Siemens control systems are the most widely used PLCs. This solution shows how s7 controllers can communicate with the Algorand blockchain in just a few steps, for example to trigger transactions.
Requirements
-
A PureStake Account and the corresponding API key
-
An Algorand wallet for the TESTNet
-
Siemens Simatic S7 PLC
This example is tested with an SIMATIC S7-300, more specific a CPU315-2-PN/DP. The solution is not specific for this type of S7 and should work with any S7 PLC.
-
A PC. Note that the Siemens Software is only available for windows
-
Siemens Simatic Manager and python 3.6+
-
At least three TestNet Accounts. A tutorial for the generation can be found here.
-
A PureStake API key. For more details visit this tutorial. For this solution the code for the transaction was adapted from this tutorial.
Use Case
The solution is to show how materials are paid for directly where they are used. In the automotive industry, the manufacturing process for producing parts is often fully automated. For example, in a common injection moulding process, pre-materials such as pins are fed directly to the line on a stamping grid and then they get separated and overmoulded in the process. When a stampings grid runs out, the line operator must change it. The operator is informed of the stamping grid to be changed and the change is then checked via a QR code to ensure that the proper spool of stamping grid has been used. As soon as the machine operator confirms the change, the transaction is triggered and the operator is immediately informed about the result of the transaction.
Communication with the PLC
Hardware and Data Configuration of the PLC
The S7-300 is designed to perform control tasks and does not provide the infrastructure to communicate directly with the algorand blockchain. The S7-300 provides Industrial Ethernet protocols and in this solution we will use the S7 communication protocol. A short description of this protocol can be found here. To handle the protocol communications we will use the python library python-snap7. An installation guide can be found here.
The S7-300 controller is configured at the factory to communicate with other devices via industrial Ethernet using s7 communication. To establish communication we have to know the IP-address, the rack and the slot of the PLC. This information is available in the hardware tab of the SIMATIC manager:
Via double tab on the PN-IO module, another window pops up, where you can access the IP-address of your PLC:
The snap7 library enables simple communication to data blocks within the PLC. Therefore a global data block (DB) is created on the PLC. The S7 protocol is always enabled and everyone with the right IP-address, rack und slot information can read and write directly from the PLC. Therefore, make sure that the communication between your PC and your PLC is within a safe local network.
Data Communication between PLC and PC
The purpose of this solution is to trigger an transaction. For the communication from the PLC to the PC a DB gdALGO
is used. This DB is created in the SIMATIC Manager and its data structure is
The handshake for this is defined as:
The DBs with which you want to communicate are defined in file db_layouts.py. The data block of the PLC is now mirrored:
"""
Define DB blocks used.
"""
step7toALGO = """
0.0 SendRequest BOOL
2 SendResponse INT
4 PartID INT
6 PartName STRING[30]
"""
Python Program
The following imports are necessary to perform this solution:
#Snap 7
import snap7
from snap7.util import *
import struct
from db_layouts import step7toALGO
#ALgorand
import json
import time
import base64
from algosdk.v2client import algod
from algosdk import mnemonic
from algosdk import transaction
#Selfmade constants
import constants
The used constants for error codes are defined in the file constants.py:
# constants.py
TRANSACTION_GOOD = 10
TRANSACTION_BAD = 20
UNKNOWN_PART = 30
In the main function of our script the connection to the PLC and the data block of interest is defined by the following lines of code. If the PLC is not reachable already at the start of the communication the program is terminated.
def main():
plc = snap7.client.Client()
# Connect to PLC:
try:
plc.connect("192.168.0.1",0,2)
except Exception as e:
print('No connection possible - program is terminated')
return None
#Specify DB number and length
db_ALGO = 444
dblength = 38
The rest of the main function consists of a while loop, which repeats 3 functions permanently and can only be terminated with the key combination ctrl+c:
try:
while True:
# check if there is a payment request from the machine
PartID, PartName = checkSendReq(plc, db_ALGO, dblength)
#Perform the payment via Algorand
transaction_res = payMaterial(PartID, PartName)
#Tell the machine if payment was an success or not
Result2PLC(plc, db_ALGO, transaction_res)
except KeyboardInterrupt:
pass
print("###### Ending Communication ######")
plc.disconnect();
In checkSendReq
we listen to the Bit SendRequest
of gdALGO
. If it is true we return the PartID
and the PartName
sent by the PLC. In industry, hundreds of data blocks are often retrieved from a multitude of systems per production line. Therefore, unnecessary traffic should be prevented and 2 seconds are fast enough for our purposes.
def checkSendReq(plc, db_ALGO, dblength):
sendIT = False
while sendIT == False:
try:
print("####### Read DB Data #######")
all_data = plc.db_read(db_ALGO,0,dblength)
print("Bytearray of DB" + str(db_ALGO) + ":\n" + str(all_data) + ":\n")
# Format the bytearray to the defined struct, which represents the DB structure
db1 = snap7.util.DB(
db_ALGO, # the db we use
all_data, # bytearray from the plc
step7toALGO, # layout specification DB variable data
dblength, #row size
1, #size
id_field=None, # field we can use to identify a row.
db_offset=0,
layout_offset=0,
row_offset=0
)
print("Read data of DB" + str(db_ALGO) + str(db1[0]) + ":\n")
if db1[0]['SendRequest'] == True:
sendIT = True
PartID = db1[0]['PartID']
PartName = db1[0]['PartName']
except Exception as e:
try:
#Try to reconnect
plc.disconnect()
plc.connect("192.168.0.1",0,2)
except Exception as e:
pass
time.sleep(2.0)
return PartID, PartName
The function runs until a request for a transaction is recognized. Then the part ID and the part name are returned. Afterwards, the function payMaterial
is called in the main.
def payMaterial(PartID, PartName):
print("####### Start Sending #######")
print("Send Request from PLC for Part" + str(PartID))
if PartID == 1:
send_to_address = 'Your recipient address Nr.1'
amount = 777
TransactionNote = PartName + " has arrived"
elif PartID == 2:
send_to_address = 'Your recipient address Nr.2'
amount = 555
TransactionNote = PartName + " has arrived"
else:
send_to_address = ' '
amount = 0
if send_to_address != ' ':
transaction_res = ALGOtransaction(send_to_address, amount, TransactionNote)
else:
print("Error: Unknown PartID")
transaction_res = constants.UNKNOWN_PART
return transaction_res
The following snippet only has minor modification to the tutorial outlined in the algorand developer documentation.
def ALGOtransaction(send_to_address, amount, TransactionNote):
# setup http client w/guest key provided by purestake
ip_address = "https://testnet-algorand.api.purestake.io/ps2"
token = "YOUR TOKEN"
headers = {
"X-API-Key": token,
}
mnemonic1 = "your mnemonic"
account_private_key = mnemonic.to_private_key(mnemonic1)
account_public_key = 'YOUR PUPLIC KEY'
algodclient = algod.AlgodClient(token, ip_address, headers)
# get suggested parameters from algod
params = algodclient.suggested_params()
gh = params.gh
first_valid_round = params.first
last_valid_round = params.last
fee = params.min_fee
send_amount = amount
existing_account = account_public_key
# create and sign transaction
tx = transaction.PaymentTxn(existing_account, fee, first_valid_round, last_valid_round, gh, send_to_address, send_amount, flat_fee=True,note=TransactionNote.encode())
signed_tx = tx.sign(account_private_key)
try:
tx_confirm = algodclient.send_transaction(signed_tx)
print('transaction sent with id', signed_tx.transaction.get_txid())
wait_for_confirmation(algodclient, txid=signed_tx.transaction.get_txid())
return constants.TRANSACTION_GOOD
except Exception as e:
print(e)
return constants.TRANSACTION_BAD
Last but not least, after the algorand transaction, the result is sent back to the PLC. The logic behind is implemented using the function Result2PLC
:
def Result2PLC(plc, db_ALGO, transaction_res):
print("Write Result to PLC")
bytearrayResult = bytearray(2)
snap7.util.set_int(bytearrayResult, 0, transaction_res)
confirmed = False
while (confirmed == False):
try:
plc.db_write(db_ALGO, 2, bytearrayResult)
confirmed = True
print("Result handover was successful \n")
except Exception as e:
try:
plc.disconnect()
plc.connect("192.168.0.1",0,2)
except Exception as e:
pass
PLC Program
A PLC is operated with a real-time capable operating system and has some limitations in programming. With Siemens controllers, the main function corresponds to OB1, which is called cyclically. For this solution we need only a one liner in the OB1: CALL "funALGO"
When programming PLCs, the programming language Structured Text, which is defined by the IEC 61131-3 standard, is often used. Siemens calls this language Structured Control Language (SCL), which is also defined by the same norm. You can find a detailed description of this language here. Our solution is also written in this language. In order not to hinder the cyclic call by the OB1, it is usual to work with function blocks in which the current state is stored with static variables to jump back to the correct step when called again. Therefore we call the function block funblockALGO
in funALGO
.
//Function Call
FUNCTION funALGO : VOID
VAR_TEMP
END_VAR
funblockALGO.idbfunblockALGO(
DieCuttingTapeChangedID := gdALGOUserInterface.DieCuttingTapeChangedID
,DieCuttingTapeQRCode := gdALGOUserInterface.DieCuttingTapeQRCode
,userBookingRequest := gdALGOUserInterface.userBookingRequest
,returnCode := gdALGOUserInterface.returnCode
,returnText := gdALGOUserInterface.returnText
,SendRequest := gdALGO.SendRequest
,SendResponse := gdALGO.SendResponse
,PartID := gdALGO.PartID
,PartName := gdALGO.PartName);
END_FUNCTION
The datablock gdALGOUserInterface
is used for a simple graphical visualization, which will be shown later.
The variables we will use in the function block are:
//Define the variables of the function block
FUNCTION_BLOCK funblockALGO
CONST
STATE_WAIT4REQ := 0;
STATE_START_TRANSACTION := 1;
STATE_WAIT4RESULT := 2;
TRANSACTION_GOOD := 10;
TRANSACTION_BAD := 20;
TRANSACTION_WRONG_PART := 30;
WRONG_DIE_CAST := 40;
END_CONST
VAR_INPUT //Call by value
DieCuttingTapeChangedID : INT;
DieCuttingTapeQRCode : INT;
END_VAR
VAR_IN_OUT //Call by reference
userBookingRequest : BOOL;
SendRequest : BOOL;
SendResponse : INT;
PartID : INT;
returnCode : INT;
returnText : STRING[50];
PartName : STRING[30];
END_VAR
VAR // Internal Variables
state : INT;
END_VAR
The function itself implements a simple state machine to perform the handshake to the PC:
BEGIN
//Set part Name
CASE DieCuttingTapeQRCode OF
1:
PartName := 'Die-Cast Nr.657';
2:
PartName := 'Die-Cast Nr.647';
ELSE
PartName := ' ';
END_CASE;
//State of Transaction Request
CASE state OF
STATE_WAIT4REQ:
IF userBookingRequest THEN
SendResponse := 0;
IF DieCuttingTapeChangedID = DieCuttingTapeQRCode THEN
PartID := DieCuttingTapeChangedID;
state := STATE_START_TRANSACTION;
ELSE
returnCode := WRONG_DIE_CAST;
returnText := 'Wrong Die-Cast';
END_IF;
userBookingRequest := false;
END_IF;
STATE_START_TRANSACTION:
SendRequest := true;
state := STATE_WAIT4RESULT;
STATE_WAIT4RESULT:
IF SendResponse <> 0 THEN
returnCode := SendResponse;
IF SendResponse = TRANSACTION_GOOD THEN
returnText := 'Success';
ELSIF SendResponse = TRANSACTION_BAD THEN
returnText := 'Failed ';
ELSIF SendResponse = TRANSACTION_WRONG_PART then
returnText := 'Unknown Part';
END_IF;
SendRequest := false;
state := STATE_WAIT4REQ;
END_IF;
END_CASE;
END_FUNCTION_BLOCK
User Interface
For this solution a simple user interface was made with WinCC, which is the siemens program for visualizations in the context of PLCs. The graphical creation via WinCC is not explained in detail here, because a graphical interface is not important for the core of this solution. It serves in this document mainly for illustrative purposes and looks like this:
Usage
Set PLC to RUN
Now the cyclic operation of OB1 starts.
Run the python script
Run the script S7ToALGO.py
. The output should look like this:
Start transaction via interface
Simply by pressing the button, the machine operator confirms the change and starts the transaction
Transaction
The transaction is performed through the python script. The output should look like this:
PLC Confirmation
The user gets informed about the transaction status on the PLC visualization:
Testnet Explorer
The transaction can be viewed via the Algorand Testnet Explorer:
The transaction is visible with the right note, to match it with the paid Die-Cast.
Video-Tutorial
Conclusion
This solution is intended to show how even old industrial controllers can communicate with the algorand blockchain with just a few lines of code. Due to the long lifetimes of machines and systems in industry, it is important that they can be integrated into new systems with little effort. The simple integration shown makes innovative approaches possible in the area of the supply chain. In further projects, we therefore aim to pursue these approaches and define additional use cases in the field of industry 4.0. In addition, the integration is to be extended to other PLCs.