opcua read e write
This commit is contained in:
1 parent
6750251703
commit
289e09ddba
4 files changed
+4062
-13
No files matched your search
@@ -0,0 +1,97 @@
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('opcua-briq-write', {
|
||||
category: 'plc',
|
||||
color: '#ffaaff',
|
||||
defaults: {
|
||||
name: { value: "" }
|
||||
},
|
||||
inputs: 1,
|
||||
outputs: 1,
|
||||
icon: "serial.png",
|
||||
label: function () {
|
||||
return this.name || "opcua write";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/html" data-template-name="opcua-briq-write">
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/html" data-help-name="opcua-briq-write">
|
||||
<p>Scrive variabili multiple su opcua</p>
|
||||
<code>msg.opcua_server</code> deve contenere
|
||||
<ul>
|
||||
<li><code>id</code>: identifica questa connessione al server (se già aperta in precedenza, viene riutilizzata). Esempio: <code>"plc_macchina_1"</code>.</li>
|
||||
<li><code>url</code>: url connessione, per esempio <code>opc.tcp://127.0.0.1:4840/</code></li>
|
||||
<li><code>client_settings</code>: dizionario di impostazioni per libreria node-opcua, in <a href="https://node-opcua.github.io/api_doc/latest/classes/node-opcua.OPCUAClient.html">questo formato</a>. Può essere vuoto.</li>
|
||||
</ul>
|
||||
<code>msg.nodesToWrite</code> deve essere una lista di oggetti fatti così:
|
||||
<ul>
|
||||
<li><code>nodeId</code>: indirizzo del nodo (per esempio <code>ns=3;s="DB"."Varname"</code>)</li>
|
||||
<li><code>dataType</code>: tipo di dato del nodo, per esempio <code>"String"</code>. Tutti i valori <a href="https://node-opcua.github.io/api_doc/latest/enums/node-opcua.DataType.html">sono elencati qui</a>.</li>
|
||||
<li><code>value</code>: il valore da scrivere, coerente con il <code>dataType</code> indicato.</li>
|
||||
<li><code>label</code>: se fornito, sostituisce <code>nodeId</code> in eventuali messaggi di errore</li>
|
||||
</ul>
|
||||
<code>msg.verifyWrittenValues</code>: se <code>true</code>, richiede al nodo di rileggere subito le variabili e verificare che i valori siano quelli appena scritti.
|
||||
<p>
|
||||
Il messaggio di uscita aggiunge a quello d'ingresso le chiavi:
|
||||
<li><code>msg.payload</code>: <code>"write ok"</code> oppure <code>"write ok & verified"</code></li>
|
||||
<li><code>msg.raw_response</code>: risposta grezza della libreria node-opcua</li>
|
||||
<li><code>msg.error</code>: errore, se presente</li>
|
||||
</p>
|
||||
<p>
|
||||
Il messaggio di uscita toglie a quello d'ingresso le chiavi:
|
||||
<li><code>msg.error</code>: se non c'è errore</li>
|
||||
</p>
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('opcua-briq-read', {
|
||||
category: 'plc',
|
||||
color: '#ffaaff',
|
||||
defaults: {
|
||||
name: { value: "" }
|
||||
},
|
||||
inputs: 1,
|
||||
outputs: 1,
|
||||
icon: "serial.png",
|
||||
label: function () {
|
||||
return this.name || "s7 read";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/html" data-template-name="opcua-briq-read">
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/html" data-help-name="opcua-briq-read">
|
||||
<p>Legge variabili multiple da opcua</p>
|
||||
<code>msg.opcua_server</code> deve contenere
|
||||
<ul>
|
||||
<li><code>id</code>: identifica questa connessione al server (se già aperta in precedenza, viene riutilizzata). Esempio: <code>"plc_macchina_1"</code>.</li>
|
||||
<li><code>url</code>: url connessione, per esempio <code>opc.tcp://127.0.0.1:4840/</code></li>
|
||||
<li><code>client_settings</code>: dizionario di impostazioni per libreria node-opcua, in <a href="https://node-opcua.github.io/api_doc/latest/classes/node-opcua.OPCUAClient.html">questo formato</a>. Può essere vuoto.</li>
|
||||
</ul>
|
||||
<code>msg.nodesToRead</code> deve essere una lista di oggetti fatti così:
|
||||
<ul>
|
||||
<li><code>nodeId</code>: indirizzo del nodo (per esempio <code>ns=3;s="DB"."Varname"</code>)</li>
|
||||
<li><code>label</code>: se fornito, sostituisce <code>nodeId</code> come chiave di questo valore nel dizionario <code>msg.payload</code></li>
|
||||
</ul>
|
||||
<p>
|
||||
Il messaggio di uscita aggiunge a quello d'ingresso le chiavi:
|
||||
<li><code>msg.payload</code>: dizionario label -> valore</li>
|
||||
<li><code>msg.raw_response</code>: risposta grezza della libreria node-opcua</li>
|
||||
<li><code>msg.error</code>: errore, se presente</li>
|
||||
</p>
|
||||
<p>
|
||||
Il messaggio di uscita toglie a quello d'ingresso le chiavi:
|
||||
<li><code>msg.error</code>: se non c'è errore</li>
|
||||
</p>
|
||||
</script>
|
||||
@@ -0,0 +1,280 @@
|
||||
const { OPCUAClient, AttributeIds, ReadRequest, ReadResponse, promoteOpaqueStructure, WriteRequest, WriteResponse, StatusCodes, DataType } = require("node-opcua");
|
||||
|
||||
|
||||
let connection_registry = new Map();
|
||||
let client_is_closed = new WeakSet();
|
||||
|
||||
class Queue {
|
||||
constructor(single_consumer) {
|
||||
this.q = [];
|
||||
this.promises = [];
|
||||
this.single_consumer = false;
|
||||
this.hangup_done = false;
|
||||
}
|
||||
async _get() {
|
||||
if (this.hangup_done) {
|
||||
throw new Error('hangup');
|
||||
}
|
||||
if (this.q.length > 0) {
|
||||
return this.q.shift()
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
this.promises.push({ resolve, reject })
|
||||
});
|
||||
}
|
||||
async process(func) {
|
||||
let q_el = await this._get();
|
||||
try {
|
||||
let out = await func(q_el.element);
|
||||
q_el.resolve(out);
|
||||
} catch (e) {
|
||||
q_el.reject(e);
|
||||
}
|
||||
}
|
||||
async submit_and_wait_processed(element) {
|
||||
return await new Promise((resolve, reject) => {
|
||||
let queue_el = {
|
||||
element,
|
||||
resolve,
|
||||
reject
|
||||
}
|
||||
this._put(queue_el)
|
||||
})
|
||||
}
|
||||
_put(element) {
|
||||
if (this.promises.length > 0) {
|
||||
this.promises.shift().resolve(element);
|
||||
} else {
|
||||
this.q.push(element);
|
||||
}
|
||||
}
|
||||
hangup() {
|
||||
this.hangup_done = true;
|
||||
for (let prom of this.promises) {
|
||||
prom.reject('hangp');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function perform_request_loop(queue, session) {
|
||||
try {
|
||||
while (true) {
|
||||
await queue.process(async (request) => {
|
||||
return await new Promise((resolve, reject) => {
|
||||
session.performMessageTransaction(request, (err, response) => {
|
||||
/* c8 ignore next */
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.message == 'hangup') {
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function get_client_and_session(opcua_conn_data) {
|
||||
if (!opcua_conn_data?.id) {
|
||||
throw new Error("'id' missing in opcua connection settings");
|
||||
}
|
||||
let client_id = opcua_conn_data.id;
|
||||
if (connection_registry.has(client_id)) {
|
||||
let entry = connection_registry.get(client_id);
|
||||
let { client, } = entry;
|
||||
if (!client_is_closed.has(client)) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
const client_settings = {
|
||||
endpointMustExist: false,
|
||||
connectionStrategy: {
|
||||
initialDelay: 1,
|
||||
maxRetry: 3,
|
||||
...(opcua_conn_data?.client_settings?.connectionStrategy || {})
|
||||
},
|
||||
reconnectOnFailure: false,
|
||||
...(opcua_conn_data?.client_settings || {})
|
||||
};
|
||||
const client = OPCUAClient.create(client_settings);
|
||||
let queue = new Queue();
|
||||
client.on("connection_lost", async () => {
|
||||
client_is_closed.add(client);
|
||||
try {
|
||||
await session.close()
|
||||
} catch (e) {
|
||||
} finally {
|
||||
await client.disconnect();
|
||||
}
|
||||
});
|
||||
await client.connect(opcua_conn_data.url);
|
||||
const session = await client.createSession();
|
||||
perform_request_loop(queue, session);
|
||||
let entry = { session, client, queue };
|
||||
connection_registry.set(client_id, entry)
|
||||
return entry;
|
||||
}
|
||||
|
||||
function mapAttributeIds(list_in) {
|
||||
let list_out = [];
|
||||
for (let n of list_in) {
|
||||
let attrid = n?.attributeId;
|
||||
if (typeof attrid == 'string') {
|
||||
attrid = AttributeIds[n.attributeId];
|
||||
if (!attrid) {
|
||||
throw new Error("unknown attribute id: " + n?.attributeId + " for nodeId: " + n?.nodeId)
|
||||
}
|
||||
}
|
||||
if (attrid === false || attrid === undefined || attrid === null) {
|
||||
attrid = AttributeIds.Value;
|
||||
}
|
||||
list_out.push({
|
||||
...n,
|
||||
attributeId: attrid
|
||||
})
|
||||
}
|
||||
return list_out;
|
||||
}
|
||||
function mapDataTypes(list_in) {
|
||||
let list_out = [];
|
||||
for (let n of list_in) {
|
||||
if (
|
||||
typeof (n?.value) === 'object' &&
|
||||
!Array.isArray(n?.value) &&
|
||||
n?.value !== null) {
|
||||
// se è già dizionario, facciamo conto che sia
|
||||
// nel formato {value: {value: x, dataType:"lkdsjflks"}}
|
||||
list_out.push(n)
|
||||
} else {
|
||||
// altrimenti leggiamo dataType e lo convertiamo.
|
||||
let dtype = n?.dataType;
|
||||
|
||||
if (typeof dtype == 'string') {
|
||||
dtype = DataType[dtype];
|
||||
if (!dtype) {
|
||||
throw new Error("unknown datatype id: " + dtype)
|
||||
}
|
||||
}
|
||||
if (dtype === false || dtype === undefined || dtype === null) {
|
||||
throw new Error("missing datatype for nodeId: " + n?.nodeId);
|
||||
}
|
||||
list_out.push({
|
||||
...n,
|
||||
value: { value: { dataType: dtype, value: n?.value } },
|
||||
})
|
||||
}
|
||||
}
|
||||
return list_out;
|
||||
}
|
||||
|
||||
module.exports = function (RED) {
|
||||
function OpcUaBriqRead(config) {
|
||||
RED.nodes.createNode(this, config);
|
||||
var node = this;
|
||||
node.on('input', async function (msg) {
|
||||
msg.payload = msg.payload || {};
|
||||
try {
|
||||
let conn = await get_client_and_session(msg.opcua_server);
|
||||
let nodesToRead = mapAttributeIds(msg?.nodesToRead || []);
|
||||
let response = await conn.queue.submit_and_wait_processed(
|
||||
new ReadRequest({
|
||||
nodesToRead: nodesToRead,
|
||||
})
|
||||
)
|
||||
if (!response || !(response instanceof ReadResponse)) {
|
||||
throw new Error("Internal Error");
|
||||
}
|
||||
|
||||
if (nodesToRead.length !== response.results.length) {
|
||||
throw new Error("wrong response length");
|
||||
}
|
||||
msg.raw_response = response;
|
||||
let parsed = {};
|
||||
for (let i = 0; i < nodesToRead.length; i++) {
|
||||
let label = nodesToRead[i]?.label;
|
||||
if (!label) {
|
||||
label = nodesToRead[i]?.nodeId;
|
||||
}
|
||||
parsed[label] = response.results[i].value.value;
|
||||
}
|
||||
msg.payload = parsed;
|
||||
msg.error = undefined;
|
||||
node.send(msg);
|
||||
} catch (e) {
|
||||
msg.error = e.message + '' + e.stack;
|
||||
node.send(msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("opcua-briq-read", OpcUaBriqRead);
|
||||
|
||||
|
||||
function OpcUaBriqWrite(config) {
|
||||
RED.nodes.createNode(this, config);
|
||||
var node = this;
|
||||
node.on('input', async function (msg) {
|
||||
msg.payload = msg.payload || {};
|
||||
try {
|
||||
let conn = await get_client_and_session(msg.opcua_server);
|
||||
let nodesToWrite = mapAttributeIds(msg?.nodesToWrite || []);
|
||||
nodesToWrite = mapDataTypes(nodesToWrite);
|
||||
let response = await conn.queue.submit_and_wait_processed(
|
||||
new WriteRequest({
|
||||
nodesToWrite: nodesToWrite,
|
||||
})
|
||||
)
|
||||
if (!response || !(response instanceof WriteResponse)) {
|
||||
throw new Error("Internal Error");
|
||||
}
|
||||
|
||||
if (response.responseHeader.serviceResult.isNot(StatusCodes.Good)) {
|
||||
throw new Error(response.responseHeader.serviceResult.toString());
|
||||
}
|
||||
|
||||
if (nodesToWrite.length !== response.results.length) {
|
||||
throw new Error("wrong response length");
|
||||
}
|
||||
msg.raw_response = response;
|
||||
if (msg?.verifyWrittenValues) {
|
||||
// verifica valori appena scritti
|
||||
let response = await conn.queue.submit_and_wait_processed(
|
||||
new ReadRequest({
|
||||
nodesToRead: nodesToWrite,
|
||||
})
|
||||
)
|
||||
if (!response || !(response instanceof ReadResponse)) {
|
||||
throw new Error("Internal Error");
|
||||
}
|
||||
if (nodesToWrite.length !== response.results.length) {
|
||||
throw new Error("wrong response length");
|
||||
}
|
||||
for (let i = 0; i < nodesToWrite.length; i++) {
|
||||
let left = nodesToWrite[i].value.value.value; // Lol
|
||||
let right = response.results[i].value.value;
|
||||
if (left !== right) {
|
||||
throw new Error(`verify failed: ${left} != ${right}`);
|
||||
}
|
||||
}
|
||||
msg.payload = "write ok & verified";
|
||||
} else {
|
||||
msg.payload = "write ok";
|
||||
}
|
||||
msg.error = undefined;
|
||||
node.send(msg);
|
||||
} catch (e) {
|
||||
msg.error = e.message + '' + e.stack;
|
||||
node.send(msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("opcua-briq-write", OpcUaBriqWrite);
|
||||
}
|
||||
Reference in new issue
Block a user