@nagisa~/node-red-systemair-save
Version:
Node-RED nodes to interact with SystemAIR’s SAVE line of products
163 lines • 8.87 kB
JavaScript
;
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