modbus-connect
Version:
Modbus RTU over Web Serial and Node.js SerialPort
315 lines (269 loc) • 10.7 kB
JavaScript
// 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 }