Nello scorso articolo abbiamo avuto modo di introdurre l’OPC UA discutendo della sua storia e delle caratteristiche fondamentali.
Oggi facciamo un passo avanti verso lo sviluppo di un server OPC UA e, in particolare, ci concentriamo sulla definizione di un modello di dati che descriva una semplice linea produttiva composta da alcuni componenti industriali.
Al termine di questo articolo, avremo tutte le carte in regola per dedicarci alla creazione del nostro server OPC UA.
La Linea
Per cominciare bene il nostro viaggio, è necessario innanzitutto avere chiaro cosa si vuole andare a descrivere.
Nel nostro caso, immaginiamo di avere un semplice macchinario composto da un motore per muovere un nastro trasportatore, un sensore di temperatura e uno di prossimità:
Vogliamo descrivere questa architettura così da avere un modello usabile poi per sviluppare il nostro server.
Modellazione Dati
Per descrivere il modello appena presentato, secondo lo standard OPC, è necessario produrre un file XML denominato NodeSet2 che agisce da interfaccia ultima con i server OPC UA. Esistono diversi modi per raggiungere questo obiettivo:
- Produrre il file Nodeset2.xml manualmente;
- Utilizzare un tool grafico come ModelDesign;
- Utilizzare il tool UAModelCompiler fornito dalla OPC Foundation.
Partendo dal primo, non è da considerarsi una buona pratica quella di scrivere file NodeSet2.xml in autonomia, in quanto questo prevede una profonda conoscenza dello standard oltre ad essere un metodo soggetto ad errore.
L’utilizzo di tool grafici, quindi, potrebbe sembrare un ottimo compromesso, il problema è che al momento della scrittura di questo articolo non mi sento di poter dire che ne esistano di così evoluti da poter essere presi come riferimento.
Per completezza di informazione ti riporto i link dove poter scaricare i principali strumenti di sviluppo:
- UAModeler: versione completa scaricabile previa registrazione al sito ufficiale.
- UA Model eXcelerator Pro: versione free scaricabile previa registrazione al sito ufficiale.
Non ci resta quindi che approfondire il terzo punto che sarà inevitabilmente la nostra scelta.
L’OPC Foundation negli anni ha definito uno standard per la descrizione dei modelli chiamato Model Design.
Questo si basa sempre sulla redazione di un file XML molto più intuitivo, però, della controparte NodeSet2.
Per scrivere questo file, possiamo tranquillamente utilizzare un editor di testo. Una volta terminata la scrittura, potremo far uso del tool UAModelCompiler per compilare il modello e generare il file NodeSet2.xml insieme a tutta una serie di dati importanti quali classi C# e metadati necessari alla creazione del server.
XSD Editor Setup
Arrivati a questo punto vale però la pena soffermarsi un attimo per preparare alcuni tool che ci renderanno la vita più semplice. Possiamo pensare di scrivere il nostro XML senza alcun aiuto, ma quanto sarebbe meglio poter avere un editor che ci guidi durante la digitazione dei vari elementi?
A tal proposito, quando si parla di XML esiste uno standard che permette di estendere rapidamente le funzionalità dei più diffusi editor di sviluppo, ovvero i file XSD (XML Schema Definition).
Un file XSD non è altro che un insieme di regole che possono essere importate all’interno di un editor. Una volta fatto, sarà l’editor stesso a suggerirci i comandi da scrivere mentre sviluppiamo il nostro modello e a segnalarci eventuali errori commessi.
In questo articolo vediamo come importare l’XSD fornito dalla OPC Foundation in Visual Studio, l’editor che utilizzerò per sviluppare questo progetto. E’ ovviamente possibile importare questo file anche in altri editor quali IntelliJ, Notepad++ e altri.
Dunque, l’XSD di nostro interesse va copiato dal repository github ufficiale raggiungibile qui.
Una volta scaricato e salvato in locale (UAModelDesign.xsd), apriamo Visual Studio, scegliamo di proseguire senza codice:
Una volta aperto l’editor, clicchiamo su File -> Open -> File.
Nella finestra che si aprirà selezioniamo UAModelDesign.xsd e carichiamolo.
Creiamo adesso un nuovo file XML cliccando su File -> Nuovo -> File -> File XML.
A questo punto è possibile scrivere modelli in modo guidato.
OPC UA Model Design
Bene, abbiamo detto che vogliamo descrivere il modello precedentemente discusso.
Il Template
Un buon punto di partenza è il template che ti metto qui sotto e che rappresenta la base da cui cominciare qualsiasi sviluppo futuro.
Nel template, innanzitutto partiamo con la definizione del tipo di file (righe 1-9) che abbiamo già detto essere un XML.
Si passa poi a definire il namespace dell’applicazione, ovvero lo spazio di lavoro del nostro server, che come avrai visto chiameremo OPCUAServer. Ciò viene definito nelle righe 10-13.
Mi raccomando, copia il contenuto del template all’interno del file XML creato precedentemente in Visual Studio.
I Tipi Generici
Come detto nello scorso articolo, il modello di descrizione dei dati di OPC UA permette di trattare i dati con gli stessi concetti della programmazione ad oggetti.
Vogliamo quindi descrivere un tipo di sensore generico che possa fare da classe base per i sensori effettivamente installati nella nostra macchina.
Questo sensore padre, avrà un valore in sola lettura che rappresenterà il dato rilevato.
Aggiungere un tipo generico è un’operazione molto semplice. Si deve definire un ObjectType e specificarne poi il nome e la classe di appartenenza.
In OPCUA tutti gli oggetti derivano dal nodo base BaseObjectType.
Gli attributi di un nodo vengono specificati all’interno dei tag Children, in questo caso abbiamo una variabile in sola lettura di tipo double chiamata Value.
Ottimo, adesso possiamo passare a creare le due tipologie di sensori installati in macchina: temperatura e prossimità.
I Metodi Remoti
Terminiamo la definizione dei nostri nodi di partenza, definendo il motore che muoverà il nastro trasportatore.
In questo caso, ci piace poter sia leggere che scrivere la velocità con cui il motore sta ruotando, sia poterne comandare l’accensione e lo spegnimento.
Sarà dunque necessario aggiungere dei metodi remoti. Andiamo a vedere come si fa:
Questa volta, oltre alla classica notazione usata per creare un nodo con degli attributi, abbiamo sia specificato un livello di accesso in lettura e scrittura, sia aggiunto due metodi remoti visibili nelle righe 5 e 6.
Le Istanze
Ottimo, finora abbiamo definito dei tipi generici che descrivono dei possibili sensori e motori. Adesso, vogliamo creare un contenitore che rappresenterà la nostra macchina.
A differenza dei nodi precedenti, il BaseType di questo sarà un FolderType che verrà poi visualizzato nel nostro server come una cartella contenitore.
All’interno di questa cartella avremo i nostri sensori e il nostro motore. Andiamo a vedere come scrivere l’XML:
Dentro MachineType, abbiamo inserito 3 nodi di tipo Object che stavolta non sono più definizioni astratte di tipi, bensì sono delle verie e proprie istanze dei nodi prima definiti.
Nella riga 3 abbiamo creato un sensore di temperatura, nella 6 quello di prossimità e nella 9 il motore.
Tutti e tre hanno il campo SupportEvents impostato a true. Questo valore permette di associare degli eventi ogni qual volta uno degli attributi cambia.
Per terminare il nostro modello, è tempo adesso di creare un nodo generico che rappresenterà la nostra macchina ed, infine, il nodo da cui istanzieremo a cascata tutti gli oggetti finora definiti:
Con le righe dalla 1 alla 8, creiamo un nodo che rappresenterà il server una volta creato.
Il server sarà composto da una macchina di tipo MachineType (riga 4).
Definito ciò, nelle righe 10 e 11, creiamo finalmente un’istanza del nodo OPCUAServerType chiamata OPCUAServer1.
Infine nelle righe 12-17, linkiamo questa istanza alla cartella ObjectFolder del server OPC UA.
Puoi scaricare il modello XML appena discusso, a questo link.
UA-ModelCompiler
Adesso che abbiamo il modello pronto, non ci resta che compilarlo.
La compilazione ci permette sia di verificare che il modello sia corretto, sia di produrre i file necessari allo sviluppo successivo del server.
Come introdotto in precedenza, la compilazione del file XML è possibile grazie al tool della OPC Foundation chiamata UA-ModelCompiler.
Per poterlo utilizzare in Windows, è necessario innanzitutto clonare il codice dal repository ufficiale:
Se avete necessità di utilizzare il ModelCompiler in ambiente Linux o Mac, fate riferimento a questa guida.
Prima di aprire il progetto Visual Studio, dobbiamo scaricare ed installare due .NET Developer Pack:
Terminata l’installazione, apriamo il progetto UA-ModelCompiler dalla cartella precedentemente clonata, cliccando sul file ModelCompiler Solution.sln e procediamo al build della soluzione.
Perfetto, se tutto è andato a buon fine dovresti avere adesso un eseguibile nella cartella Debug o Release.
Apriamo una finestra di powershell in questa cartella e lanciamo il seguente comando:
Così facendo stiamo chiedendo al ModelCompiler ti prendere l’xml salvato nel path specificato come primo parametro (-d2), validarne la correttezza e creare i file nei percorsi di output specificati nei due parametri successivi.
Conclusioni
In questo articolo ci siamo soffermati su come creare un modello da compilare poi con il tool sviluppato dalla OPC Foundation.
L’output di questa fase sono una serie di file che ci serviranno per progettare, nel prossimo articolo, il nostro server.
Sono nuovo nel mondo opc-ua e sto cercando di realizzare una piccola applicazione client-server custom.
Nella costruzione dell’xml ho due “problemi”:
– non so come poter avere Description diverse in base a differenti Locale (sulla reference parla di ListOfDescription come array di Description direttamente nel modello, mentre sull’UAModelDesign.xsd parla di un attributo key per una lookup table in un database)
– negli AnalogItemType, come Speed del MotorConveyor e Value del GenericSensorType, non riesco a creare il campo EUInformation per poi valorizzarlo nel NodeManager.cs (invece EURange appare e riesco poi a valorizzarlo. Entrambi sono non mandatory, quindi non capisco perché uno appare e possa valorizzarlo mentre l’altro non si crea nemmeno).
Come posso superare questi problemi? Grazie in anticipo!
Ciao Matf4,
purtroppo non riesco a capire esattamente cosa ti serve.
Potresti cortesemente spiegarmi meglio?
Nello specifico:
– Cosa intendi per “non so come poter avere Description diverse in base a differenti Locale”? Puoi farmi qualche esempio più tecnico?
– Quanto citi la reference, intendi la documentazione online? Puoi passarmi qualche link così da poter capire cosa intendi?
– I campi EUInformation e EURange che tipi di campi sono? Stai facendo riferimento anche qui alla documentazione ufficiale? Che processo stai adottando per “creare” questi campi?
Purtroppo l’articolo tratta un argomento base, non mi sono spinto ad ora più avanti di quello che leggi. Ad ogni modo se mi fai capire per bene qual è il problema ti aiuto volentieri.
Grazie,
Macheronte
Ciao Macheronte, innanzitutto grazie della risposta e del tempo dedicatomi.
I riferimenti che faccio sono legati alla Reference e all’SDK ufficiali.
Per quanto riguarda la Description :
nella creazione dell’xml quando ho
<opc:ObjectType SymbolicName="GenericSensorType" BaseType="ua:BaseObjectType">
<opc:Description>A generic sensor.</opc:Description>
[...]
</opc:ObjectType>
vorrei poter mettere una Description in inglese “A generic sensor” , una in italiano “Un sensore generico”, ad esempio:
<opc:Description Locale="en-US">A generic sensor.</opc:Description>
<opc:Description Locale="it-IT">Un sensore generico.</opc:Description>
Come riferimento avevo preso questi:
– https://reference.opcfoundation.org/v104/Core/docs/Part6/F.3/
– https://reference.opcfoundation.org/v104/Core/docs/Part6/F.10/
Ho trovato anche questa discussione nel forum ufficiale ma non è stata particolarmente d’aiuto (anche perché parlano di un tool di Unified Automation):
– https://opcfoundation.org/forum/opc-ua-standard/add-multiple-locales-to-the-model/
In generale si parla di un array di Descriprion comandata magari da una lingua di default impostata ad inizio xml come:
<opc:ModelDesign>
[...]
DefaultLocale="it-IT">
Guardando poi il file UA Model Design.xsd, per la Description dice (righe 202-204): “”The Description the value of the Description attribute for the Node. This element includes an optional key that can be used to look up the Description for other locales in a resource DB. The validator automatically creates a generic Description from the BrowseName and NodeClass.” (analogo discorso per il DisplayName) e non capisco come effettivamente usare il parametro key e l’eventuale db esterno.
(qui ho messo ulteriori dettagli e codice: https://github.com/OPCFoundation/UA-ModelCompiler/issues/95)
Per quanto riguarda EURange ed EUInformation:
Ci sono due proprietà (EURange ed EngineeringUnits, rispettivamente di tipo Range ed EUInformation) che sono proprietà dell’AnalogItemType (sottotipo di BaseAnalogType):
– https://reference.opcfoundation.org/Core/docs/Part8/5.3.2/#5.3.2.2
– https://reference.opcfoundation.org/Core/docs/Part8/5.3.2/#5.3.2.3
entrambe opzionali per il tipo base mentre EURange obbligatorio per il tipo specifico.
Una volta creati i file per la creazione del server, l’EURange è inizializzato ed è possibile assegnarne il Value.
L’EngineeringUnit invece è null e non riesco a crearlo/valorizzarlo. Speravo di poterlo “abilitare” direttamente dal modello xml ma non sono riuscito a capire come.
Ho provato ad aggirare il problema aggiungendo una proprietà come figlio della variabile:
<opc:ObjectType SymbolicName="Motor" BaseType="ua:BaseObjectType">
<opc:Description>A motor.</opc:Description>
<opc:Children>
<opc:Variable SymbolicName="Speed" DataType="ua:Double" ValueRank="Scalar" TypeDefinition="ua:AnalogItemType" AccessLevel="ReadWrite">
<opc:Children>
<opc:Property SymbolicName="EngUnits1" DataType="ua:String" AccessLevel="ReadWrite" />
<!--<opc:Property SymbolicName="EngUnits2" DataType="ua:EUInformation" />-->
</opc:Children>
</opc:Variable>
<opc:Method SymbolicName="Start" ModellingRule="Mandatory"></opc:Method>
<opc:Method SymbolicName="Stop" ModellingRule="Mandatory"></opc:Method>
<opc:Method SymbolicName="Step" ModellingRule="Mandatory"></opc:Method>
<!--\<opc:Property SymbolicName="EngUnits3" DataType="ua:EUInformation" />-->
</opc:Children>
</opc:ObjectType>
Anche se il model compiler compila senza errori (se la proprietà la metto con DataType=”ua:EUInformation” invece genera errore, cosa che non accade se la proprietà la assegno a Motor anziché a Speed) in C# la proprietà EngUnits non riesco nemmeno a vederla (Mentre EngineeringUnits si ma, come detto prima, è null).
(qui ci sono altre info ma non tanto dettagliate: https://stackoverflow.com/questions/69808541/how-to-use-euinformation-on-a-opc-ua-server-c)
Spero di essere stato chiaro e non troppo dispersivo.
Ti ringrazio nuovamente per l’aiuto!
Ti chiedo scusa ma tutto il codice xml inserito nel commento è nascosto per via delle parentesi angolari. provo a riscrivere il commento diversamente?
Ciao Matf4,
Non preoccuparti, figurati! Sì grazie!
Se puoi riscrivere il commento con il codice visibile sarebbe meglio, così resta consultabile anche da altri utenti, caso mai servisse!
Ti ringrazio,
Macheronte
Ciao Matf4,
per ora ho avuto tempo per dedicarmi al secondo problema, per cui ad ora ti rispondo relativamente a quello soltanto.
Per poter attivare la proprietà EngineeringUnits, in modo da non averla null lato C# io procederei così:
<opc:ObjectType SymbolicName="MotorConveyor" BaseType="ua:BaseObjectType">
<opc:Description>A motor.</opc:Description>
<opc:Children>
<opc:Variable SymbolicName="Speed" DataType="ua:Double" ValueRank="Scalar" TypeDefinition="ua:AnalogItemType" AccessLevel="ReadWrite">
<opc:Children>
<opc:Property SymbolicName="EngineeringUnits" DataType="ua:EUInformation" AccessLevel="ReadWrite" ModellingRule="Mandatory" />
</opc:Children>
</opc:Variable>
</opc:Children>
</opc:ObjectType>
Questo dovrebbe risolvere il tuo problema e permetterti sia di vedere la proprietà, sia di poterla valorizzare lato C# evitando di incorrere nell’eccezione che lamenti.
Approccio il primo problema in un secondo momento e appena ho notizie ti faccio sapere.
Un caro saluto,
Macheronte
Ciao Macheronte, grazie molto per l’aiuto e la tua gentilezza.
Il problema è così in parte risolto.
I miei errori erano consistevano in questo:
– provare a nominare la proprietà in maniera differente (EngUnits anziché EngineeringUnits) nel modello e di conseguenza lato C# non riuscivo a visualizzarla
– non aver inserito la ModellingRule=”Mandatory” in quanto pensavo fosse sufficiente inserire la proprietà affinché venisse valorizzata
Tuttavia la parte che ancora “non torna” è questa:
la proprietà EngineeringUnits dichiarata in questo modo assume (non so perché) BrowseName.NamespaceIndex = 2, mentre la proprietà EURange ha BrowseName.NamespaceIndex = 0.
Questo si ripercuote a livello visivo usando il ReferenceClient dell’SDK ufficiale (https://github.com/OPCFoundation/UA-.NETStandard) in quanto la nuova proprietà EngineeringUnits viene visualizzata solo tra i figli di Speed nell’albero di ricerca presente nella parte sinistra dell’applicazione; a differenza di ciò, la proprietà EURange viene visualizzata non solo tra i figli dell’albero di ricerca ma anche tra le proprietà (con i relativi valori) dell’oggetto nella parte destra dell’applicazione.
Provando invece ad usare al posto di AnalogItemType il DataType=”ua:AnalogUnitType” (che stando alla Reference ufficiale è figlio, come anche AnalogItemType, di BaseAnalogType ma con EURange optional e EngineeringUnits mandatory) la proprietà EngineeringUnits viene visualizzata in entrambe le sezioni dell’applicazione avendo BrowseName.SpaceIndex = 0.
Questa differenza non crea un grosso problema in quanto sto cercando di sviluppare un mio client personalizzato ma rimane una differenza, secondo me, concettuale importante.
Intanto ti ringrazio nuovamente per il grande aiuto e la tua disponibilità!
Tornerò a controllare se sei riuscito a risolvere anche l’altro problema. A presto!
Scusa ho fatto un errore io: mi era rimasta nel codice la parola Variable anziché Property, quindi il “problema” di visualizzazione non c’è.
Il NamespaceIndex differente rimane, per quanto sia una questione marginale (probabilmente dovuto alla definizione “a mano”.
Ciao Matf4,
sono contento che ti sia stato di aiuto e che ora ti funziona. Sì, concordo con te a riguardo del NamespaceIndex. Credo di poter dire che quello dipende molto dal UAModelCompiler che genera e assegna i vari attributi dei nodi OPC, tra cui anche il NamespaceIndex. Evidentemente EURange viene definita di default come proprietà del nodo e quindi assume indice a 0. Eventuali altre, appunto come dici tu specificate a mano, vengono poi accodate e numerate di conseguenza.
Per quanto riguarda il primo problema che mi hai menzionato, ancora non ho avuto tempo di guardarci ma appena mi è possibile ti farò sapere!
Un saluto e a presto,
Macheronte
Ciao Macheronte,
ho un altro dubbio sempre sul modello xml.
Nel momento in cui utilizzo il Model Compiler mi viene restituito un file csv contenente la definizione di tutti i tipi e gli oggetti presenti nel modello con i relativi identificatori numerici.
Come posso impostare questi id direttamente dal modello xml?
Grazie ancora della tua disponibilità!
Ciao Matf4,
in passato, se non ricordo male, ho fatto una cosa del genere e ho risolto cambiando direttamente i file con estensione .xml. Non ricordo ora se il PredefinedNodes.xml o i NodeSet.xml, ti consiglierei di fare un test su un nodo e vedere quali tra questi file hanno effetto.
Il file csv non ha utilità, almeno per quelle che sono le cose che ho sperimentato io. Credo sia semplicemente un file di riepilogo per chi dovrà interagire con il tuo server.
Colgo l’occasione anche per dirti che non ho ancora avuto modo di affrontare il problema delle Description che mi accennavi nel primo commento. Per caso l’hai risolto?
A presto,
Macheronte
Il fatto che il csv non abbia una vera e propria utilità lo so, però può essere utile da fornire ad un eventuale client prodotto da terzi., con la possibilità di fissare i type id degli elementi comuni a diversi server, senza dover modificare i file autogenerati ogni volta).
Speravo di poterlo indicare nel modello tramite un tag tipo (uso le parentesi tonde al posto delle angolari per questioni di editing)
(uax:Identifier)112233(/uax:Identifier)
Avevo trovato un riferimento in rete ma non lo riesco a ritrovare.
Per quanto riguarda il problema del multilingua non ho ancora risolto.
Invece per quanto riguarda la questione dei namespace a cui facevamo riferimento (per il problema delle unità di misura), ho notato che c’era della confusione per via di 3 namespace index differenti: uno per il browse name, uno per il node id e uno per il node type.
Grazie dell’aiuto!