sabato 4 febbraio 2017

Comunicare con un PLC Ethernet

Il python dispone di una libreria per la gestione della comunicazione con i PLC.

Nel nostro caso si trattava di dialogare con un Siemens 1500, non ho idea se la cosa funzioni anche per altri PLC o se sono necessarie librerie differenti.

Si tratta del package snap7, non è distributito nella installazione standard di python, pertanto si deve procedere ad una semplice installazione.


Installazione della libreria snap7:

dalla command line nella cartella di installazione di python lanciare

python -m pip install python-snap7

questi i messaggi ricevuti:

Downloading/unpacking python-snap7
  Downloading python-snap7-0.5.tar.gz
  Running setup.py (path:C:\...python-snap7\setup.py) egg_info for package python-snap7

Installing collected packages: python-snap7
  Running setup.py install for python-snap7

Successfully installed python-snap7
Cleaning up...

scaricare le dll da sourceforge

Nel mio caso l'ambiente è Windows 7 64 bit

ho copiato da \snap7-full-1.4.1\release\Windows\Win64

snap7.dll
snap7.lib

nella cartella C:\Windows\System32

Bene ora siamo pronti per iniziare.

Vi commento brevemente le funzioni che ho usato e sotto trovate un sorgente completo per leggere e scrivere su un PLC.

Per attivare la connessione con il PLC: connect(address, rack, slot) 

i parametri sono:

        address: IP address del PLC
        rack   : rack del PLC
        slot   : slot del PLC

Per leggere da PLC : read_area(area, dbnumber, start, size)

area: nome del DB
dbnumber: The DB number, only used when area= S7AreaDB
        start: offset dove inizia la lettura
        size: numero di byte da leggere

ritorna un array di bytes che deve essere decodificato in una stringa

Per scrivere su PLC: write_area(area, dbnumber, start, data)

area: nome del DB
        dbnumber: The DB number, only used when area= S7AreaDB
        start: offset dove inizia la scrittura
        data: dati da scrivere

si deve fornire un array di bytes (la stringa di partenza deve essere decodificata)

Ho avuto un po' di problemi a capire come indirizzare le aree su cui lavorare e a come convertire le stringhe in lettura/scrittura (è richiesto infatti di lavorare con un array di byte). Mi sono costruito un paio di funzioni che operano le conversioni necessarie alle chiamate standard.

Il programma chiede all'operatore se vuole leggere oppure scrivere, poi chiede l'offset di partenza (da quale byte iniziare a leggere/scrivere), quindi il numero di byte da leggere oppure la stringa di caratteri da scrivere.
Nel caso di scrittura il programma mostra il contenuto prima dell'aggiornamento ed esegue una lettura dopo l'aggiornamento.

questo è un esempio di funzionamento del programma da riga di comando:

prima ho chiesto di leggere 10 bytes da offset 0
poi ho chiesto di scrivere una stringa 112233 da offset 0

evidenziati in azzurro gli inserimenti dell'operatore

+--------------------------------------------------+
|           Avvio Lettura/scrittura PLC            |
+--------------------------------------------------+

IP PLC:  nnn.nnn.nnn.nnn

Premere: W per scrittura, R per lettura, S per fermarsi
>> r
Inserire offset iniziale      >> 0
Inserire num. byte da leggere >> 10
Lettura= 1234567890
Premere: W per scrittura, R per lettura, S per fermarsi
>> w
Inserire offset iniziale      >> 0
Inserire stringa da scrivere  >> 112233
Lettura prima write= 123456
Lettura dopo  write= 112233
Premere: W per scrittura, R per lettura, S per fermarsi
>>

Sorgente completo:

evidenziato in verde il punto dove inserire l'indirizzo IP del vostro PLC; potrebbero cambiare anche riferimenti all'area con nome e numero DB.

#----------------------------------------------------------------------+
# Programma       : Test_PLC                                           |
# Python versione : 3.6.0                                              |
#----------------------------------------------------------------------+
# Modifiche  :            by                                           |
# Note       :                                                         |
#----------------------------------------------------------------------+

#-----------------------------------------
#           Import moduli 
#-----------------------------------------
import os
import sys

# librerie di comunicazione con PLC siemens
import snap7.client as c
from snap7.util import *
from snap7.snap7types import *

#---- Definizione variabili globali - Costanti fisse ------

# Dati di comunicazione con PLC 
HOST_PLC = "<indirizzoIP>"      # Indirizzo IP del PLC
DB_PLC = "DB"                   # Nome DB nel PLC
NUM_DB = 8                      # Numero DB nel PLC

#------------------------------------------------------------------------------
# aggiorna_schermo : Aggiorna dati in elaborazione
#------------------------------------------------------------------------------
# input : nulla
# return: nulla
#------------------------------------------------------------------------------
def aggiorna_schermo():

    if os.name == "posix":                  # Unix/Linux/MacOS/BSD/etc
        os.system('clear')
    elif os.name in ("nt", "dos", "ce"):    # DOS/Windows
        os.system('CLS')

# mostro all'utente avvio e indirizzo IP del PLC
    print ("+--------------------------------------------------+")
    print ("|           Avvio Lettura/scrittura PLC            |")
    print ("+--------------------------------------------------+")
    print (" ")
    print ("IP PLC: ", HOST_PLC)
    print (" ")
    
#------------------------------------------------------------------------------
# loop_plc : Ciclo principale
#------------------------------------------------------------------------------
# input : nulla
# return: nulla
#------------------------------------------------------------------------------
def loop_plc():

    aggiorna_schermo()    

    continua = 1

    while continua == 1:

        print ("Premere: W per scrittura, R per lettura, S per fermarsi")
        
        # input operatore
        comando=input('>> ')
        comando=str.lower(comando)

        if comando == "s":
            continua = 0

        elif comando == "r":

            esegui_plc("r")
            
        elif comando == "w":

            esegui_plc("w")

#------------------------------------------------------------------------------
# esegui_plc : Collegamento con PLC
#------------------------------------------------------------------------------
# input : tipo operazione r=read w=write
# return: nulla
#------------------------------------------------------------------------------
def esegui_plc(tipo):

    plc = c.Client()
     
    try:

       # Apro canale di comunicazione con PLC 
        plc.connect(HOST_PLC,0,1)
        
    except:
        print("Errore di connessione!!!")
        exit(1)

    continua = 1

    while continua == 1:
        
        # inserire offset 
        try:
            offset=int(input("Inserire offset iniziale      >> "))
            continua = 0
        except ValueError:
            print("Inserire un numero intero!!!")

    if tipo == 'r':

        continua = 1

        while continua == 1:
        
            # inserire byte da leggere
            try:
                nbytes=int(input("Inserire num. byte da leggere >> "))
                continua = 0
            except ValueError:
                print("Inserire un numero intero!!!")

        stringa = ReadMemory(plc,offset,nbytes,"Lettura PLC")

        print("Lettura=", stringa)

    elif tipo == 'w':
        
       # inserire stringa caratteri
        s=input('Inserire stringa da scrivere  >> ')
        l=len(s)

       # Leggo dati PLC prima di scrivere
        stringa = ReadMemory(plc,offset,l,"Lettura PLC prima Write")
        print("Lettura prima write=", stringa)
        
        WriteMemory(plc,offset,s,"Write PLC")

       # Verifica dati scritti
        stringa = ReadMemory(plc,offset,l,"Lettura PLC dopo Write")
        print("Lettura dopo  write=", stringa)
    
    plc.disconnect()

#------------------------------------------------------------------------------
# ReadMemory : Legge area di memoria PLC
#------------------------------------------------------------------------------
# input : plc    = riferimento alla connessione PLC
#         offset = offset iniziale di lettura
#         L      = lunghezza byte da leggere
#         text   = testo che indica fase di lettura
# return: stringa di lettura
#------------------------------------------------------------------------------
def ReadMemory(plc,offset,L,text):

    try:
        result = plc.read_area(areas[DB_PLC],NUM_DB,offset,L)
    except:
        print("Errore di lettura PLC in fase:",text,"!!!")
        exit(1)

    return result.decode('utf-8')

#------------------------------------------------------------------------------
# WriteMemory : Scrive area di memoria PLC
#------------------------------------------------------------------------------
# input : plc    = riferimento alla connessione PLC
#         offset = offset iniziale di lettura
#         s      = stringa da scrivere
#         text   = testo che indica fase di scrittura
# return: nulla
#------------------------------------------------------------------------------
def WriteMemory(plc,offset,s,text):
    
   # Trasformo la stringa in un array di byte
    b = bytearray()
    b.extend(map(ord, s))

   # Lancio la scrittura nel PLC 
    try:
        plc.write_area(areas[DB_PLC],NUM_DB,offset,b)
    except:
        print("Errore di scrittura PLC in fase:",text,"!!!")
        exit(1)
    
#------------------------------------------------------------------------------
# INIZIO PROGRAMMA
#------------------------------------------------------------------------------
if __name__=='__main__':

    loop_plc()

2 commenti:

  1. Ciao Fabio,
    ho installato SNAP7 su raspberry riesco a comunicare correttamente in TCP con il plc siemens. Dovrei ora indirizzare le mie chiamate sui registri del DB, ma non saprei come reperire questi indirizzi (AREA DI MEMORIA, ecc.). Esiste uno script PYTHON SNAP7 in GET che te li potrebbe elencare?

    Se volessi puntare all'area degli ingressi I1,I2,I3,ecc. su che area di memoria,lenght,bit mi dovrei spostare-dirottare la mia chiamata python???


    Se potresti aiutarmi te ne sarei grato

    Purtroppo le sorgenti del software caricato sul plc LOGO!

    Intanto grazie.

    RispondiElimina
    Risposte
    1. Ciao Mike55,

      scusami ma il primo dicembre ero in cina e vedo solo adesso la tua domanda (mi è sfuggita la mail di avviso!...in cina google è del tutto bannato).

      Hai già risolto?

      L'area di memoria, a suo tempo, me l'ha indicata la persona che ha installato l'impianto, hai modo di contattarli?
      Altrimenti prova ad andare per tentativi usando la funzione indicata:

      plc.write_area(areas[DB_PLC],NUM_DB,offset,b)

      Nel mio caso il DB_PLC era DB e il NUM_DB era 8, fai qualche tentativo.

      ciao

      fabio

      Elimina