Scambio dati con PLC tramite protocollo OPC-UA

Dalla versione 2021.06+10 la piattaforma di sviluppo QWay consente di scambiare informazioni con PLC collegati a macchine di produzione, utilizzando il protocollo standard OPC-UA.

La piattaforma incorpora infatti la libreria QWOpc che mette a disposizione primitive per scrivere e leggere i registri di cui sopra. Di seguito riportiamo alcuni esempi.

Per facilitare il test è anche disponibile un semplice client da utilizzare per “esplorare” le interfacce del PLC, scaricabile da qui

Tramite questo programma è possibile vedere quali “variabili” sono disponibili per la lettura o scrittura. Per collegarsi, è necessario specificare, nel campo di fianco al pulsante “Connect”, l’indirizzo del PLC, nella forma “opc.tcp://<indirizzo ip>:<porta”, e poi premere il pulsante stesso. Le “variabili” che possono essere oggetto di lettura/scritture tramite le primitive possono essere visibili nel ramo Objects–>ServerInterfaces, oppure in un ramo a parte. Nel caso dell’immagine sopra riportata, è nel ramo “Tags”. Il nome del ramo, se non immediatamente determinabile, deve essere richiesto a chi ha programmato il PLC.

Per potersi collegare, è necessario determinare il NodeId del suddetto ramo, che è composto da una coppia di valori, che può essere (<ns>,<i>) oppure (<ns>, <g>) oppure (<ns>, <s>), dove <ns> e <i> sono interi, <g> è una stringa in formato GUID e <s> è una semplice stringa che identifica il nodo.

Tali numeri si possono determinare usando il client, cliccando con il tasto destro sul ramo e scegliendo “View Attributes”.

Si presenterà la seguente scheda, dove sono facilmente reperibili i valori <ns> e, in questo caso, <g>.

I valori <ns> e <g> (oppure <ns> e <i> oppure <ns> e <s>) vanno riportati rispettivamente come terzo e secondo parametro nel metodo “Connect” di un oggetto di tipo QWOPC, essendo il primo parametro l’indirizzo del PLC.

Esempio:

opc.Connect("opc.tcp://192.168.0.2:4840",1,4)

oppure

opc.Connect("opc.tcp://192.168.0.2:4840","ecef81dg-c834-4379ab79-c8fa31338311",4)

Il metodo opc.Connect accetta anche un quarto parametro opzionale boolean, che, se posto a “true” attiva una connessione sicura criptata, modalità di cui parleremo più avanti, mentre se non specificato o posto a “false” attiva una connessione non protetta.

Per leggere e scrivere le variabili, l’oggetto QWOPC mette a disposizione i metodi ReadValue e WriteValue.

err = opc.WriteValue("nome variabile", val)

val = opc.ReadValue("nome variabile", err)

“nome variabile” è il nome della variabile che si vuole leggere o scrivere, reperibile dal client.

La variabile “err” conterrà l’eventuale messaggio d’errore rilevato in lettura o scrittura, mentre la variabile “val” conterrà il valore letto o da scrivere.

Sicurezza e configurazione del server

Come anticipato, il protocollo OPC-UA consente connessioni protette e non protette, attraverso l’opportuna configurazione di due parametri chiamati Security Mode e, Security Policy, definiti a livello di configurazione del PLC.

Il client che si collega, in questo caso QualiWare, deve rispettare questi parametri in fase di attivazione della connessione.

Se il PLC accetta connessioni non protette, il che corrisponde a Security Mode=None e Security Policy=None, il quarto parametro del metodo opc.Connect può non essere specificato o essere lasciato al valore “false”. Con questa modalità, non è necessaria alcuna configurazione particolare del server sul quale è installato QualiWare Web Server.

A partire dalla versione 2023.00.05, è possibile utilizzare anche una connessione protetta, che corrisponde a Security Mode=SignAndEncrypt e SecurityPolicy=Basic256Sha256, specificando il quarto parametro di opc.Connect al valore “true”. La sicurezza è garantita dall’utilizzo di certificati, uno lato PLC e uno lato QualiWare, per criptare la comunicazione. Questi certificati, vengono scambiati fra i due sistemi. Lato QualiWare, il certificato è autogenerato e viene inserito nella cartella UA_MachineDefault del repository dei certificati associato all’utente utilizzato per l’esecuzione di QualiWare Web Server (quello inserito al primo passaggio della configurazione dell’applicativo). Questo certificato deve poi essere abilitato fra quelli accettati anche lato PLC, come spiegato nel seguito.

NOTA: è possibile anche configurare sul PLC l’autenticazione tramite utente e password. Al momento questa modalità non è supportata da QualiWare, ed è quindi possibile solo la connessione anonima.

Affinchè la comunicazione protetta funzioni, è necessario effettuare scrupolosamente le seguenti configurazioni sul server e sul PLC:

  1. Configurare il pool di applicazioni affinchè utilizzi come utente quello specificato nella configurazione di QualiWare anzichè “NetworkService”
  2. Effettuare un tentativo di connessione, che fallirà riportando l’errore “Error received from remote host: Certificate is not trusted.”. Questo avviene perchè di default il certificato inviato da QualiWare non viene riconosciuto come affidabile e viene posto in una lista di certificati “Rejected”.
  3. Sul programma di configurazione del PLC, spostare i certificati che risultano “Rejected” fra gli “Accepted”. Di seguito un esempio di come si può presentare tale programma.

NOTA: se il server è Domain Controller, si è notato che la connessione restituisce l’errore “Accesso Negato” in quanto l’utente utilizzato per eseguire QualiWare Web Server non riesce ad accedere al Certificate Store. Per risolvere questo problema, è sufficiente avviare una connessione remota al server con quell’utente, e poi chiuderla senza disconnettere l’utente stesso. Così facendo, l’utente rimarrà loggato al server e QualiWare potrà accedere al Certificate Store.

Esempi

Di seguito riportiamo l’esempio di un task per la lettura dei dati di produzione dal PLC di una macchina.

dim err as string=""
dim opc as new QWOpc()

err= opc.Connect("opc.tcp://192.168.0.2:4840",1,4)

if not empty(err)
  ' La macchina non è raggiungibile'
  return
End If
  
dim q as new QWTable()
q.database=DB
q.sql="select * from P_WBM25"
q.allowallrecords=false
q.active=true

err=opc.WriteValue("ERP_Connected",true)

if not empty(err)
  throw new exception(err)
  return
End If

q.beginappend()
q.replace("DATE",now())
q.replace("MACCHINA","01")

dim vars as new DBArray("toERP_commessa","toERP_recipeName","toERP_famigliaConn","toERP_recipe2.5VieConn1","toERP_recipe2.5VieConn2","toERP_recipe2.5VieConn3","toERP_recipe2.5VieConn4","toERP_recipe2.5VieConn5",
"toERP_recipe2.5VieConn6","toERP_recipe2.5VieConn7","toERP_recipe2.5VieConn8","toERP_recipeENVieConn","toERP_recipeENRipetizione","toERP_prodM_Rast2.5","toERP_prodH_Rast2.5","toERP_prodM_RastEN","toERP_prodH_RastEN",
  "toERP_LottoRichiesto","toERP_LottoProdotto")

dim i as integer
dim val as object
dim ok as boolean=true

for i=1 to vars.size
  val = opc.ReadValue(vars(i),err)

  if not empty(err)
    err="Errore leggendo la variabile: "+vars(i)+": "+err
    ok=false
    exit for
  Else
    q.replace(vars(i),val)
  End If
Next
    
if ok
  ok=q.saverecord(err)
Else
  throw new exception(err)
End If
q.active=false

opc.WriteValue("ERP_Connected",false)
    
opc.disconnect()

Di seguito riportiamo l’esempio di un evento associato ad un pulsante per l’invio di parametri di produzione al PLC di una macchina.

if empty(form.FindControl("QUANTITA").Value)
  form.alert("Specificare la quantità da assemblare")
  return
End If

dim err as string=""
dim opc as new QWOpc()
dim ok as boolean=true

err= opc.Connect("opc.tcp://192.168.0.2:4840",1,4)

if not empty(err)
  form.alert("Errore di connessione. "+vbcr+vbcr+err)
  return
End If
  
err=opc.WriteValue("ERP_Connected",true)
if not empty(err)
  form.alert("Errore assegnando ERP_Connected a true"+vbCr+vbCr+err)
  ok=false
End If	

if ok
  dim ready as boolean
  ready=opc.ReadValue("toERP_recipeReadyToReceive",err)
   
  if not empty(err)
    form.alert("Errore leggendo toERP_recipeReadyToReceive: "+vbCr+vbCr+err)
    ok=false
  Else
    if not ready
      form.alert("La macchina non è pronta a ricevere i dati")
      ok=false
    End If
  End If
End If		
  
if ok
  ' Verifica se deve mostrare il pulsante per inviare a WBM25
   dim ARTIC as new qwtable()
   ARTIC.database=form.GetDataBase()
   ARTIC.sql="select * from ARTIC"
   ARTIC.requestlive=false
   ARTIC.allowallrecords=false
   ARTIC.active=true
   
   if ARTIC.rowset.findkey(form.FindControl("CODART").value)
    err=opc.WriteValue("toWBM_recipe",ARTIC.rowset.fields("P_RICETTA_WBM25").value)
    
    if not empty(err)
      form.alert("Errore scrivendo toWBM_recipe: "+vbCr+vbCr+err)
      ok=false
    Else	
       err=opc.WriteValue("toWBM_famigliaConn",ARTIC.rowset.fields("P_FAMIGLIACONN_WBM25").value="2.5")

      if not empty(err)
        form.alert("Errore scrivendo toWBM_famigliaConn: "+vbCr+vbCr+err)
        ok=false
      Else	
        dim err_ricetta as boolean=opc.ReadValue("toERP_recipeError",err)
        
        if not empty(err)
          form.alert("Errore durante la lettura di toERP_recipeError: "+vbCr+vbCR+err)
          ok=false
        else
          if err_ricetta
            form.alert("Errore durante l'elaborazione della ricetta")
            ok=false
          End If
        End If
      End If
    End If			
    
    if ok
      err=opc.WriteValue("toWBM_commessa",form.FindControl("NCOMM").value+"/"+form.FindControl("NSOTTOCOMM").Value)
      if not empty(err)
        form.alert("Errore scrivendo toWBM_commessa: "+vbCr+vbCr+err)
        ok=false				
      End If
    End If
      
    if ok
      err=opc.WriteValue("toWBM_lottoRichiesto",form.FindControl("QUANTITA").Value*ARTIC.rowset.fields("P_CONNETTORI_WBM25").value)
      if not empty(err)
        form.alert("Errore scrivendo toWBM_lottoRichiesto: "+vbCr+vbCr+err)
        ok=false				
      End If		
    End If
  End If		
   
   ARTIC.active=false   
End If				
      
if ok
  form.alert("Dati trasmessi con successo")
End If
      
opc.disconnect()