opcua read e write

This commit is contained in:
Pietro Brenna committed 2026-09-29 20:54:57 +02:00
1 parent 6750251703
commit 289e09ddba
4 files changed
+4062 -13

No files matched your search

+97
View File
@@ -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>
+280
View File
@@ -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);
}