UNPKG

@nagisa~/node-red-systemair-save

Version:

Node-RED nodes to interact with SystemAIR’s SAVE line of products

163 lines • 8.87 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; const jsmodbus_1 = require("jsmodbus"); const net_1 = require("net"); const await_semaphore_1 = require("await-semaphore"); const user_request_error_1 = require("jsmodbus/dist/user-request-error"); const node_events_1 = require("node:events"); const init = (RED) => { const save_device = function (props) { RED.nodes.createNode(this, props); const semaphore = new await_semaphore_1.Semaphore(props.max_concurrency); const shutdown_ctl = new AbortController(); const shutdown_sig = shutdown_ctl.signal; let outstanding_requests = 0; const acquire_resources = () => __awaiter(this, void 0, void 0, function* () { shutdown_sig.throwIfAborted(); if (outstanding_requests > props.max_backlog) { throw new Error("too many outstanding requests, increase the limit or adjust your flows"); } let release = () => { }; try { outstanding_requests += 1; // This does not correctly handle Promise.race, so implement cancellation checks // manually... Ugh... release = yield semaphore.acquire(); return release; } finally { if (shutdown_sig.aborted) { release(); throw shutdown_sig.reason; } outstanding_requests -= 1; } }); const ready_client = (operation_timeout) => __awaiter(this, void 0, void 0, function* () { // NB: we create a socket for each distinct modbus operation. I have seen that IAM tends // to forget open sockets sometimes which can lead to timeouts. Having each read an // individual connection is slower and higher latency, but it also helps isolating the // faults. const socket = new net_1.Socket(); const client = new jsmodbus_1.client.TCP(socket, ~~props.device_id, ~~props.op_timeout); //@ts-ignore: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/65782 const connect_abort_signal = AbortSignal.any([ AbortSignal.timeout(~~props.op_timeout), operation_timeout ]); try { connect_abort_signal.throwIfAborted(); const connected = (0, node_events_1.once)(socket, 'connect', { signal: connect_abort_signal }); socket.connect({ host: props.address, port: props.port }); yield connected; return client; } catch (e) { socket.destroy(); throw e; } }); const operation_complete = new Error("operation complete"); const operation = (op) => __awaiter(this, void 0, void 0, function* () { var _a, _b; const release_resources = yield acquire_resources(); const operation_abort_ctl = new AbortController(); //@ts-ignore: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/65782 const operation_abort_signal = AbortSignal.any([ AbortSignal.timeout(~~props.timeout), shutdown_sig, operation_abort_ctl.signal ]); const operation_abort_future = new Promise((_ok, err) => { operation_abort_signal.addEventListener('abort', () => { if (operation_abort_signal.reason !== operation_complete) { err(operation_abort_signal.reason); } }, { once: true, passive: true }); }); let client = undefined; try { while (true) { client === null || client === void 0 ? void 0 : client.socket.destroy(); client = undefined; operation_abort_signal.throwIfAborted(); try { client = yield ready_client(operation_abort_signal); } catch (e) { // This might have been a single connection timeout. Retry. We will loop // back right away into the whole-operation timeout check at the beginning // of the loop. continue; } try { const result = yield Promise.race([operation_abort_future, op(client)]); return result; } catch (e) { // Ocassionally the device will respond with SLAVE_DEVICE_BUSY for a little // while. These are always retryable, so just implement the logic here. if ((0, user_request_error_1.isUserRequestError)(e)) { if (((_b = (_a = e.response) === null || _a === void 0 ? void 0 : _a._body) === null || _b === void 0 ? void 0 : _b._code) === 6) { continue; } // We implement a two-tier timeout mechanism. One is the overall request // timeout, which limits the maximum processing time of a message. // The other is per-operation timeout, such as read or write. These handle // the general flakyness of the IAM module. This condition is for the 2nd // kind of timeout. If it occurs, we loop back to start and attempt this // operation from scratch. Hopefully this time around IAM does not forget // the connection! if (e.err === 'Timeout') { continue; } } console.log(e); throw e; } } } finally { if (!operation_abort_signal.aborted) { operation_abort_ctl.abort(operation_complete); } client === null || client === void 0 ? void 0 : client.socket.destroy(); client = undefined; release_resources(); } }); this.read = (register_description) => __awaiter(this, void 0, void 0, function* () { const data_type = register_description.data_type; let buffers = []; for (const command of data_type.read_commands(register_description)) { const result = yield operation((client) => client.readHoldingRegisters(command.address - 1, command.count)); buffers.push(result.response.body.valuesAsBuffer); } return data_type.extract(buffers); }); this.write = (register_description, value) => __awaiter(this, void 0, void 0, function* () { const data_type = register_description.data_type; for (const command of data_type.encode_writes(register_description, value)) { yield operation((client) => client.writeMultipleRegisters(command.address - 1, command.payload)); } }); this.close = (removed) => __awaiter(this, void 0, void 0, function* () { shutdown_ctl.abort(new Error(`node has been ${removed ? "removed" : "stopped"}`)); // Grab all of the semaphores here. This will ensure that all operations have completed // and no new ones can start. In order to ensure that this is not soo slow the // `cancel` here will pop a cancellation fuse that's used in strategic locations // thoughout the code to hasten a release of any semaphores already acquired. let semas = Array.from({ length: props.max_concurrency }, () => semaphore.acquire()); yield Promise.allSettled(semas); return; }); }; RED.nodes.registerType("systemair save device", save_device); }; module.exports = init; //# sourceMappingURL=systemair%20save%20device.js.map