UNPKG

modbus-connect

Version:

Modbus RTU over Web Serial and Node.js SerialPort

315 lines (269 loc) 10.7 kB
// utils/diagnostics.js /** * Class that collects and analyzes statistics about Modbus communication. * @class * @alias Diagnostics */ class Diagnostics { constructor() { this.reset(); } /** * Resets all statistics and counters to their initial state. * This function is called internally upon construction of the Diagnostics object. * @private */ reset() { this.startTime = Date.now(); this.totalRequests = 0; this.successfulResponses = 0; this.errorResponses = 0; this.timeouts = 0; this.crcErrors = 0; this.modbusExceptions = 0; this.totalRetries = 0; this.totalRetrySuccesses = 0; this.lastResponseTime = null; this._totalResponseTime = 0; this._totalResponseTimeAll = 0; this.lastErrorMessage = null; this.lastErrors = []; this.functionCallCounts = {}; this.errorMessageCounts = {}; this.lastSuccessDetails = null; this._lastFuncCode = null; this.lastExceptionCode = null; this.totalDataSent = 0; this.totalDataReceived = 0; this.lastRequestTimestamp = null; this.lastSuccessTimestamp = null; this.lastErrorTimestamp = null; this.totalSessions ??= 0; this.totalSessions++; } destroy() { clearInterval(this.interval); } /** * Records a request event (increments counter and updates timestamp) * @method recordRequest */ recordRequest() { this.totalRequests++; this.lastRequestTimestamp = new Date().toISOString(); } /** * Records a retry event (increments retry counter) * @method recordRetry * @param {number} attempts - Number of retry attempts */ recordRetry(attempts) { this.totalRetries += attempts; } /** * Records a successful retry event (increments the counter for successful retries) */ recordRetrySuccess() { this.totalRetrySuccesses++; } /** * Records a function call event (increments the counter for the specified function code) * @method recordFunctionCall * @param {number} funcCode - Function code of the Modbus function */ recordFunctionCall(funcCode) { if (funcCode == null) return; this._lastFuncCode = funcCode; this.functionCallCounts[funcCode] ??= 0; this.functionCallCounts[funcCode]++; } /** * Records a successful response event (increments counter, updates response time, and stores details) * @method recordSuccess * @param {number} responseTimeMs - Response time in milliseconds */ recordSuccess(responseTimeMs) { this.successfulResponses++; this.lastResponseTime = responseTimeMs; this._totalResponseTime += responseTimeMs; this._totalResponseTimeAll += responseTimeMs; this.lastSuccessTimestamp = new Date().toISOString(); this.lastSuccessDetails = { responseTime: responseTimeMs, timestamp: this.lastSuccessTimestamp, funcCode: this._lastFuncCode ?? null }; } /** * Records an error event (increments error counter, updates error details, and stores error message) * @method recordError * @param {Error} error - Error object * @param {Object} [options] - Optional parameters * @param {string} [options.code] - Error code * @param {number} [options.responseTimeMs] - Response time in milliseconds * @param {number} [options.exceptionCode] - Modbus exception code */ recordError(error, { code = null, responseTimeMs = 0, exceptionCode = null } = {}) { this.errorResponses++; this.lastErrorMessage = error.message || String(error); this._totalResponseTimeAll += responseTimeMs; this.lastErrorTimestamp = new Date().toISOString(); this.lastErrors.push(this.lastErrorMessage); if (this.lastErrors.length > 10) { this.lastErrors.shift(); } const msg = (error.message || '').toLowerCase(); if (code === 'timeout' || msg.includes('timeout')) { this.timeouts++; } else if (code === 'crc' || msg.includes('crc')) { this.crcErrors++; } else if (code === 'modbus-exception' || msg.includes('modbus exception')) { this.modbusExceptions++; if (typeof exceptionCode === 'number') { this.lastExceptionCode = exceptionCode; } } this.errorMessageCounts[this.lastErrorMessage] ??= 0; this.errorMessageCounts[this.lastErrorMessage]++; } /** * Records the amount of outgoing data in bytes * @method recordDataSent * @param {number} byteLength - Number of bytes sent */ recordDataSent(byteLength) { this.totalDataSent += byteLength; } /** * Records the amount of incoming data in bytes * @method recordDataReceived * @param {number} byteLength - Number of bytes received */ recordDataReceived(byteLength) { this.totalDataReceived += byteLength; } /** * Returns the average response time in milliseconds * @method get averageResponseTime * @returns {number|null} Average response time in milliseconds */ get averageResponseTime() { return this.successfulResponses === 0 ? null : this._totalResponseTime / this.successfulResponses; } /** * Returns the average response time in milliseconds * @method get averageResponseTimeAll * @returns {number|null} Average response time in milliseconds */ get averageResponseTimeAll() { const total = this.successfulResponses + this.errorResponses; return total === 0 ? null : this._totalResponseTimeAll / total; } /** * Calculates the error rate as a percentage of total requests. * Returns null if no requests have been made. * @method get errorRate * @returns {number|null} Error rate percentage or null if totalRequests is zero */ get errorRate() { return this.totalRequests === 0 ? null : (this.errorResponses / this.totalRequests) * 100; } /** * Returns the uptime in seconds * @method get uptimeSeconds * @returns {number} Uptime in seconds */ get uptimeSeconds() { return Math.floor((Date.now() - this.startTime) / 1000); } /** * Returns a JSON object containing all statistics and counters * @method getStats * @returns {Object} Object containing all statistics and counters */ getStats() { return { uptimeSeconds: this.uptimeSeconds, totalSessions: this.totalSessions, totalRequests: this.totalRequests, successfulResponses: this.successfulResponses, errorResponses: this.errorResponses, timeouts: this.timeouts, crcErrors: this.crcErrors, modbusExceptions: this.modbusExceptions, lastExceptionCode: this.lastExceptionCode, totalRetries: this.totalRetries, totalRetrySuccesses: this.totalRetrySuccesses, lastResponseTime: this.lastResponseTime, averageResponseTime: this.averageResponseTime, averageResponseTimeAll: this.averageResponseTimeAll, errorRate: this.errorRate, lastErrorMessage: this.lastErrorMessage, lastErrors: [...this.lastErrors], lastSuccessDetails: this.lastSuccessDetails, functionCallCounts: { ...this.functionCallCounts }, commonErrors: Object.entries(this.errorMessageCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 3) .map(([message, count]) => ({ message, count })), dataSent: this.totalDataSent, dataReceived: this.totalDataReceived, lastRequestTimestamp: this.lastRequestTimestamp, lastSuccessTimestamp: this.lastSuccessTimestamp, lastErrorTimestamp: this.lastErrorTimestamp }; } /** * Returns a JSON string containing all statistics and counters * @method serialize * @returns {string} JSON string containing all statistics and counters */ serialize() { return JSON.stringify(this.getStats(), null, 2); } /** * Returns an array of objects containing metric names and their values * @method toTable * @returns {Array<Object>} Array of objects containing metric names and their values */ toTable() { const stats = this.getStats(); return Object.entries(stats).map(([metric, value]) => ({ metric, value })); } /** * Merges another Diagnostics object into this one * @method mergeWith * @param {Diagnostics} other - Diagnostics object to merge with */ mergeWith(other) { if (!(other instanceof Diagnostics)) return; this.totalRequests += other.totalRequests; this.successfulResponses += other.successfulResponses; this.errorResponses += other.errorResponses; this.timeouts += other.timeouts; this.crcErrors += other.crcErrors; this.modbusExceptions += other.modbusExceptions; this.totalRetries += other.totalRetries; this.totalRetrySuccesses += other.totalRetrySuccesses; this._totalResponseTime += other._totalResponseTime; this._totalResponseTimeAll += other._totalResponseTimeAll; this.totalDataSent += other.totalDataSent; this.totalDataReceived += other.totalDataReceived; other.lastErrors.forEach(err => this.lastErrors.push(err)); if (this.lastErrors.length > 10) { this.lastErrors = this.lastErrors.slice(-10); } for (const [code, count] of Object.entries(other.functionCallCounts)) { this.functionCallCounts[code] ??= 0; this.functionCallCounts[code] += count; } for (const [msg, count] of Object.entries(other.errorMessageCounts)) { this.errorMessageCounts[msg] ??= 0; this.errorMessageCounts[msg] += count; } this.totalSessions += other.totalSessions ?? 0; } } module.exports = { Diagnostics }