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. Questa modalità è supportata dalla versione 2023.02.12 o successiva.
Affinchè la comunicazione protetta funzioni, è necessario effettuare scrupolosamente le seguenti configurazioni sul server e sul PLC:
- Configurare il pool di applicazioni affinchè utilizzi come utente quello specificato nella configurazione di QualiWare anzichè “NetworkService”
- 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”.
- 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()