modbus-connect
Version:
Modbus RTU over Web Serial and Node.js SerialPort
734 lines (733 loc) • 25.4 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var import_async_mutex = require("async-mutex");
var import_logger = __toESM(require("./logger.js"));
var import_errors = require("./errors.js");
class TaskController {
id;
priority;
name;
fn;
interval;
onData;
onError;
onStart;
onStop;
onFinish;
onBeforeEach;
onRetry;
shouldRun;
onSuccess;
onFailure;
maxRetries;
backoffDelay;
taskTimeout;
stopped;
paused;
executionInProgress;
stats;
logger;
manager;
timerId = null;
constructor(options, manager) {
const {
id,
priority = 0,
interval,
fn,
onData,
onError,
onStart,
onStop,
onFinish,
onBeforeEach,
onRetry,
shouldRun,
onSuccess,
onFailure,
name = null,
maxRetries = 3,
backoffDelay = 1e3,
taskTimeout = 5e3
} = options;
this.id = id;
this.priority = priority;
this.name = name;
this.fn = Array.isArray(fn) ? fn : [fn];
this.interval = interval;
this.onData = onData;
this.onError = onError;
this.onStart = onStart;
this.onStop = onStop;
this.onFinish = onFinish;
this.onBeforeEach = onBeforeEach;
this.onRetry = onRetry;
this.shouldRun = shouldRun;
this.onSuccess = onSuccess;
this.onFailure = onFailure;
this.maxRetries = maxRetries;
this.backoffDelay = backoffDelay;
this.taskTimeout = taskTimeout;
this.stopped = true;
this.paused = false;
this.executionInProgress = false;
this.manager = manager;
this.stats = {
totalRuns: 0,
totalErrors: 0,
lastError: null,
lastResult: null,
lastRunTime: null,
retries: 0,
successes: 0,
failures: 0
};
this.logger = manager.loggerInstance.createLogger(`Task:${id}`);
this.logger.setLevel("error");
this.logger.debug("TaskController created", {
id,
priority,
interval,
maxRetries,
backoffDelay,
taskTimeout
});
}
start() {
if (!this.stopped) {
this.logger.debug("Task already running");
return;
}
this.stopped = false;
this.logger.info("Task started", { id: this.id });
this.onStart?.();
this._scheduleNextRun(true);
}
stop() {
if (this.stopped) {
this.logger.debug("Task already stopped", { id: this.id });
return;
}
this.stopped = true;
if (this.timerId) {
clearTimeout(this.timerId);
this.timerId = null;
}
this.manager.removeFromQueue(this.id);
this.logger.info("Task stopped", { id: this.id });
this.onStop?.();
}
pause() {
if (this.paused) {
this.logger.debug("Task already paused", { id: this.id });
return;
}
this.paused = true;
this.logger.info("Task paused", { id: this.id });
}
resume() {
if (!this.stopped && this.paused) {
this.paused = false;
this.logger.info("Task resumed", { id: this.id });
if (!this.timerId && !this.executionInProgress) {
this._scheduleNextRun(true);
}
} else {
this.logger.debug("Cannot resume task - not paused or stopped", {
id: this.id
});
}
}
_scheduleNextRun(immediate = false) {
if (this.stopped) return;
if (this.timerId) {
clearTimeout(this.timerId);
this.timerId = null;
}
const delay = immediate ? 0 : this.interval;
this.timerId = setTimeout(() => {
this.timerId = null;
if (this.stopped) return;
this.manager.enqueueTask(this);
}, delay);
}
async execute() {
if (this.stopped || this.paused) {
this.logger.debug("Cannot execute - task is stopped or paused", {
id: this.id
});
this._scheduleNextRun();
return;
}
if (this.shouldRun && !this.shouldRun()) {
this.logger.debug("Task should not run according to shouldRun function", {
id: this.id
});
this._scheduleNextRun();
return;
}
this.onBeforeEach?.();
this.executionInProgress = true;
this.stats.totalRuns++;
this.logger.debug("Executing task", { id: this.id });
try {
let overallSuccess = false;
const results = [];
for (let fnIndex = 0; fnIndex < this.fn.length; fnIndex++) {
if (this.stopped) break;
if (this.paused) break;
let retryCount = 0;
let result = null;
let fnSuccess = false;
while (!this.stopped && retryCount <= this.maxRetries) {
if (this.paused) break;
try {
const fnToExecute = this.fn[fnIndex];
if (typeof fnToExecute !== "function") {
throw new import_errors.PollingManagerError(
`Task ${this.id} fn at index ${fnIndex} is not a function`
);
}
const promiseResult = fnToExecute();
if (!(promiseResult instanceof Promise)) {
throw new import_errors.PollingManagerError(
`Task ${this.id} fn ${fnIndex} did not return a Promise`
);
}
result = await this._withTimeout(promiseResult, this.taskTimeout);
fnSuccess = true;
this.stats.successes++;
this.stats.lastError = null;
break;
} catch (err) {
const error = err instanceof Error ? err : new import_errors.PollingManagerError(String(err));
this._logSpecificError(error);
retryCount++;
this.stats.totalErrors++;
this.stats.retries++;
this.stats.lastError = error;
this.onRetry?.(error, fnIndex, retryCount);
if (retryCount > this.maxRetries) {
this.stats.failures++;
this.onFailure?.(error);
this.onError?.(error, fnIndex, retryCount);
this.logger.warn("Max retries exhausted for fn[" + fnIndex + "]", {
id: this.id,
fnIndex,
retryCount,
error: error.message
});
} else {
const isFlushedError = error instanceof import_errors.ModbusFlushError;
const baseDelay = isFlushedError ? 50 : this.backoffDelay * Math.pow(2, retryCount - 1);
const jitter = Math.random() * baseDelay * 0.5;
const delay = baseDelay + jitter;
this.logger.debug("Retrying fn[" + fnIndex + "] with delay", {
id: this.id,
delay,
retryCount
});
await this._sleep(delay);
}
}
}
results.push(result);
overallSuccess = overallSuccess || fnSuccess;
}
this.stats.lastResult = results;
this.stats.lastRunTime = Date.now();
if (results.length > 0 && results.some((r) => r !== null && r !== void 0)) {
this.onData?.(results);
}
if (overallSuccess) {
this.onSuccess?.(results);
}
this.onFinish?.(overallSuccess, results);
this.logger.info("Task execution completed", {
id: this.id,
success: overallSuccess,
resultsCount: results.length
});
} catch (err) {
this.logger.error("Fatal error during task execution cycle", {
id: this.id,
error: err instanceof Error ? err.message : String(err)
});
} finally {
this.executionInProgress = false;
this._scheduleNextRun();
}
}
isRunning() {
return !this.stopped;
}
isPaused() {
return this.paused;
}
setInterval(ms) {
this.interval = ms;
this.logger.info("Interval updated", { id: this.id, interval: ms });
}
getState() {
return {
stopped: this.stopped,
paused: this.paused,
running: !this.stopped,
inProgress: this.executionInProgress
};
}
getStats() {
return { ...this.stats };
}
_logSpecificError(error) {
const logContext = { id: this.id, error: error.message };
if (error instanceof import_errors.ModbusTimeoutError) this.logger.error("Modbus timeout error", logContext);
else if (error instanceof import_errors.ModbusCRCError) this.logger.error("Modbus CRC error", logContext);
else if (error instanceof import_errors.ModbusParityError)
this.logger.error("Modbus parity error", logContext);
else if (error instanceof import_errors.ModbusNoiseError) this.logger.error("Modbus noise error", logContext);
else if (error instanceof import_errors.ModbusFramingError)
this.logger.error("Modbus framing error", logContext);
else if (error instanceof import_errors.ModbusOverrunError)
this.logger.error("Modbus overrun error", logContext);
else if (error instanceof import_errors.ModbusCollisionError)
this.logger.error("Modbus collision error", logContext);
else if (error instanceof import_errors.ModbusConfigError)
this.logger.error("Modbus config error", logContext);
else if (error instanceof import_errors.ModbusBaudRateError)
this.logger.error("Modbus baud rate error", logContext);
else if (error instanceof import_errors.ModbusSyncError) this.logger.error("Modbus sync error", logContext);
else if (error instanceof import_errors.ModbusFrameBoundaryError)
this.logger.error("Modbus frame boundary error", logContext);
else if (error instanceof import_errors.ModbusLRCError) this.logger.error("Modbus LRC error", logContext);
else if (error instanceof import_errors.ModbusChecksumError)
this.logger.error("Modbus checksum error", logContext);
else if (error instanceof import_errors.ModbusDataConversionError)
this.logger.error("Modbus data conversion error", logContext);
else if (error instanceof import_errors.ModbusBufferOverflowError)
this.logger.error("Modbus buffer overflow error", logContext);
else if (error instanceof import_errors.ModbusBufferUnderrunError)
this.logger.error("Modbus buffer underrun error", logContext);
else if (error instanceof import_errors.ModbusMemoryError)
this.logger.error("Modbus memory error", logContext);
else if (error instanceof import_errors.ModbusStackOverflowError)
this.logger.error("Modbus stack overflow error", logContext);
else if (error instanceof import_errors.ModbusResponseError)
this.logger.error("Modbus response error", logContext);
else if (error instanceof import_errors.ModbusInvalidAddressError)
this.logger.error("Modbus invalid address error", logContext);
else if (error instanceof import_errors.ModbusInvalidFunctionCodeError)
this.logger.error("Modbus invalid function code error", logContext);
else if (error instanceof import_errors.ModbusInvalidQuantityError)
this.logger.error("Modbus invalid quantity error", logContext);
else if (error instanceof import_errors.ModbusIllegalDataAddressError)
this.logger.error("Modbus illegal data address error", logContext);
else if (error instanceof import_errors.ModbusIllegalDataValueError)
this.logger.error("Modbus illegal data value error", logContext);
else if (error instanceof import_errors.ModbusSlaveBusyError)
this.logger.error("Modbus slave busy error", logContext);
else if (error instanceof import_errors.ModbusAcknowledgeError)
this.logger.error("Modbus acknowledge error", logContext);
else if (error instanceof import_errors.ModbusSlaveDeviceFailureError)
this.logger.error("Modbus slave device failure error", logContext);
else if (error instanceof import_errors.ModbusMalformedFrameError)
this.logger.error("Modbus malformed frame error", logContext);
else if (error instanceof import_errors.ModbusInvalidFrameLengthError)
this.logger.error("Modbus invalid frame length error", logContext);
else if (error instanceof import_errors.ModbusInvalidTransactionIdError)
this.logger.error("Modbus invalid transaction ID error", logContext);
else if (error instanceof import_errors.ModbusUnexpectedFunctionCodeError)
this.logger.error("Modbus unexpected function code error", logContext);
else if (error instanceof import_errors.ModbusConnectionRefusedError)
this.logger.error("Modbus connection refused error", logContext);
else if (error instanceof import_errors.ModbusConnectionTimeoutError)
this.logger.error("Modbus connection timeout error", logContext);
else if (error instanceof import_errors.ModbusNotConnectedError)
this.logger.error("Modbus not connected error", logContext);
else if (error instanceof import_errors.ModbusAlreadyConnectedError)
this.logger.error("Modbus already connected error", logContext);
else if (error instanceof import_errors.ModbusInsufficientDataError)
this.logger.error("Modbus insufficient data error", logContext);
else if (error instanceof import_errors.ModbusGatewayPathUnavailableError)
this.logger.error("Modbus gateway path unavailable error", logContext);
else if (error instanceof import_errors.ModbusGatewayTargetDeviceError)
this.logger.error("Modbus gateway target device error", logContext);
else if (error instanceof import_errors.ModbusInvalidStartingAddressError)
this.logger.error("Modbus invalid starting address error", logContext);
else if (error instanceof import_errors.ModbusMemoryParityError)
this.logger.error("Modbus memory parity error", logContext);
else if (error instanceof import_errors.ModbusBroadcastError)
this.logger.error("Modbus broadcast error", logContext);
else if (error instanceof import_errors.ModbusGatewayBusyError)
this.logger.error("Modbus gateway busy error", logContext);
else if (error instanceof import_errors.ModbusDataOverrunError)
this.logger.error("Modbus data overrun error", logContext);
else if (error instanceof import_errors.ModbusTooManyEmptyReadsError)
this.logger.error("Modbus too many empty reads error", logContext);
else if (error instanceof import_errors.ModbusInterFrameTimeoutError)
this.logger.error("Modbus inter-frame timeout error", logContext);
else if (error instanceof import_errors.ModbusSilentIntervalError)
this.logger.error("Modbus silent interval error", logContext);
else this.logger.error("Polling error", logContext);
}
_sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
_withTimeout(promise, timeout) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new import_errors.ModbusTimeoutError("Task timed out")), timeout);
promise.then((result) => {
clearTimeout(timer);
resolve(result);
}).catch((err) => {
clearTimeout(timer);
reject(err);
});
});
}
}
class PollingManager {
config;
tasks;
executionQueue;
mutex;
isProcessing;
paused;
loggerInstance;
logger;
constructor(config = {}, loggerInstance) {
this.config = {
defaultMaxRetries: 3,
defaultBackoffDelay: 1e3,
defaultTaskTimeout: 5e3,
logLevel: "trace",
...config
};
this.tasks = /* @__PURE__ */ new Map();
this.executionQueue = [];
this.mutex = new import_async_mutex.Mutex();
this.isProcessing = false;
this.paused = false;
this.loggerInstance = loggerInstance || new import_logger.default();
if (!loggerInstance) {
this.loggerInstance.setLogFormat(["timestamp", "level", "logger"]);
this.loggerInstance.setCustomFormatter("logger", (value) => {
return value ? `[${value}]` : "";
});
}
this.logger = this.loggerInstance.createLogger("PollingManager");
this.logger.setLevel(this.config.logLevel);
this.logger.info("PollingManager initialized", {
config: JSON.stringify(this.config)
});
}
_validateTaskOptions(options) {
if (!options || typeof options !== "object")
throw new import_errors.PollingTaskValidationError("Task options must be an object");
if (!options.id) throw new import_errors.PollingTaskValidationError('Task must have an "id"');
if (typeof options.interval !== "number" || options.interval <= 0)
throw new import_errors.PollingTaskValidationError("Interval must be a positive number");
if (!options.fn || !Array.isArray(options.fn) && typeof options.fn !== "function")
throw new import_errors.PollingTaskValidationError("fn must be a function or array of functions");
}
addTask(options) {
try {
this._validateTaskOptions(options);
if (this.tasks.has(options.id)) throw new import_errors.PollingTaskAlreadyExistsError(options.id);
const task = new TaskController(
{
...options,
maxRetries: options.maxRetries ?? this.config.defaultMaxRetries,
backoffDelay: options.backoffDelay ?? this.config.defaultBackoffDelay,
taskTimeout: options.taskTimeout ?? this.config.defaultTaskTimeout
},
this
);
this.tasks.set(options.id, task);
if (options.immediate !== false) {
task.start();
}
this.logger.info("Task added successfully", { id: options.id });
} catch (error) {
const err = error instanceof Error ? error : new import_errors.PollingManagerError(String(error));
this.logger.error("Failed to add task", { error: err.message });
throw err;
}
}
updateTask(id, newOptions) {
const oldTask = this.tasks.get(id);
if (!oldTask) throw new import_errors.PollingTaskNotFoundError(id);
const oldOptions = {
id: oldTask.id,
priority: oldTask.priority,
interval: oldTask.interval,
fn: oldTask.fn,
onData: oldTask.onData,
onError: oldTask.onError,
onStart: oldTask.onStart,
onStop: oldTask.onStop,
onFinish: oldTask.onFinish,
onBeforeEach: oldTask.onBeforeEach,
onRetry: oldTask.onRetry,
shouldRun: oldTask.shouldRun,
onSuccess: oldTask.onSuccess,
onFailure: oldTask.onFailure,
name: oldTask.name ?? void 0,
maxRetries: oldTask.maxRetries,
backoffDelay: oldTask.backoffDelay,
taskTimeout: oldTask.taskTimeout
};
const mergedOptions = { ...oldOptions, ...newOptions };
const wasRunning = oldTask.isRunning();
this.removeTask(id);
this.addTask(mergedOptions);
if (wasRunning) this.startTask(id);
}
removeTask(id) {
const task = this.tasks.get(id);
if (task) {
task.stop();
this.tasks.delete(id);
this.removeFromQueue(id);
this.logger.info("Task removed", { id });
} else {
this.logger.warn("Attempt to remove non-existent task", { id });
}
}
restartTask(id) {
const task = this.tasks.get(id);
if (task) {
task.stop();
setTimeout(() => {
const freshTask = this.tasks.get(id);
if (freshTask) freshTask.start();
}, 0);
}
}
startTask(id) {
const task = this.tasks.get(id);
if (task) task.start();
else throw new import_errors.PollingTaskNotFoundError(id);
}
stopTask(id) {
const task = this.tasks.get(id);
if (task) task.stop();
}
pauseTask(id) {
const task = this.tasks.get(id);
if (task) task.pause();
}
resumeTask(id) {
const task = this.tasks.get(id);
if (task) task.resume();
}
setTaskInterval(id, interval) {
const task = this.tasks.get(id);
if (task) task.setInterval(interval);
}
isTaskRunning(id) {
const task = this.tasks.get(id);
return task ? task.isRunning() : false;
}
isTaskPaused(id) {
const task = this.tasks.get(id);
return task ? task.isPaused() : false;
}
getTaskState(id) {
const task = this.tasks.get(id);
return task ? task.getState() : null;
}
getTaskStats(id) {
const task = this.tasks.get(id);
return task ? task.getStats() : null;
}
hasTask(id) {
return this.tasks.has(id);
}
getTaskIds() {
return Array.from(this.tasks.keys());
}
clearAll() {
this.logger.info("Clearing all tasks");
this.paused = true;
for (const task of this.tasks.values()) {
task.stop();
}
this.tasks.clear();
this.executionQueue = [];
this.logger.info("All tasks cleared");
}
restartAllTasks() {
for (const id of this.tasks.keys()) {
this.restartTask(id);
}
}
pauseAllTasks() {
this.paused = true;
for (const task of this.tasks.values()) {
task.pause();
}
}
resumeAllTasks() {
this.paused = false;
for (const task of this.tasks.values()) {
task.resume();
}
this._processQueue();
}
startAllTasks() {
this.paused = false;
for (const task of this.tasks.values()) {
task.start();
}
}
stopAllTasks() {
this.paused = true;
for (const task of this.tasks.values()) {
task.stop();
}
this.executionQueue = [];
}
getAllTaskStats() {
const stats = {};
for (const [id, task] of this.tasks.entries()) {
stats[id] = task.getStats();
}
return stats;
}
getQueueInfo() {
return {
queueLength: this.executionQueue.length,
tasks: this.executionQueue.map((task) => ({
id: task.id,
state: task.getState()
}))
};
}
getSystemStats() {
return {
totalTasks: this.tasks.size,
totalQueues: 1,
queuedTasks: this.executionQueue.length,
tasks: this.getAllTaskStats()
};
}
enqueueTask(task) {
if (!this.executionQueue.includes(task)) {
this.executionQueue.push(task);
this.executionQueue.sort((a, b) => b.priority - a.priority);
this.logger.debug("Task enqueued", {
id: task.id,
queueLen: this.executionQueue.length
});
this._processQueue();
}
}
removeFromQueue(taskId) {
this.executionQueue = this.executionQueue.filter((t) => t.id !== taskId);
}
_sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Основной цикл обработки очереди.
*/
async _processQueue() {
if (this.isProcessing || this.paused || this.executionQueue.length === 0) {
return;
}
this.isProcessing = true;
try {
while (this.executionQueue.length > 0 && !this.paused) {
const task = this.executionQueue[0];
if (task) {
this.executionQueue.shift();
this.logger.debug("Processing task from queue", { id: task.id });
await this._sleep(30);
await task.execute();
}
await this._sleep(10);
}
} catch (error) {
this.logger.error("Critical error in processQueue loop", {
error: error instanceof Error ? error.message : String(error)
});
} finally {
this.isProcessing = false;
if (this.executionQueue.length > 0 && !this.paused) {
setTimeout(() => this._processQueue(), 0);
}
}
}
/**
* Выполняет функцию с захватом мьютекса.
* Используется ModbusClient для обеспечения атомарности операций чтения/записи.
*/
async executeImmediate(fn) {
const release = await this.mutex.acquire();
try {
return await fn();
} finally {
release();
}
}
// === Логгеры ===
enablePollingManagerLogger(level = "info") {
this.logger.setLevel(level);
}
disablePollingManagerLogger() {
this.logger.setLevel("error");
}
enableTaskControllerLoggers(level = "info") {
for (const task of this.tasks.values()) {
task.logger.setLevel(level);
}
}
disableTaskControllerLoggers() {
for (const task of this.tasks.values()) {
task.logger.setLevel("error");
}
}
enableTaskControllerLogger(taskId, level = "info") {
this.tasks.get(taskId)?.logger.setLevel(level);
}
disableTaskControllerLogger(taskId) {
this.tasks.get(taskId)?.logger.setLevel("error");
}
enableAllLoggers(level = "info") {
this.enablePollingManagerLogger(level);
this.enableTaskControllerLoggers(level);
}
disableAllLoggers() {
this.disablePollingManagerLogger();
this.disableTaskControllerLoggers();
}
setLogLevelForAll(level) {
this.logger.setLevel(level);
for (const task of this.tasks.values()) {
task.logger.setLevel(level);
}
}
}
module.exports = PollingManager;