modbus-connect
Version:
Modbus RTU over Web Serial and Node.js SerialPort
474 lines (473 loc) • 18.7 kB
JavaScript
"use strict";
var import_constants = require("./constants/constants.js");
class Logger {
LEVELS = ["trace", "debug", "info", "warn", "error"];
currentLevel = "info";
enabled = true;
useColors = true;
buffering = false;
flushInterval = 300;
MAX_BUFFER_SIZE = 1e3;
COLORS = {
trace: "\x1B[1;35m",
debug: "\x1B[1;36m",
info: "\x1B[1;32m",
warn: "\x1B[1;33m",
error: "\x1B[1;31m",
exception: "\x1B[1;41m",
reset: "\x1B[0m"
};
groupLevel = 0;
globalContext = {};
buffer = [];
// private flushTimeout: NodeJS.Timeout | null = null;
categoryLevels = {};
logCounts = { trace: 0, debug: 0, info: 0, warn: 0, error: 0 };
logStats = { bySlaveId: {}, byFuncCode: {}, byExceptionCode: {} };
logFormat = [
"timestamp",
"level",
"logger",
"slaveId",
"funcCode",
"exceptionCode",
"address",
"quantity",
"responseTime"
];
customFormatters = {};
filters = { slaveId: /* @__PURE__ */ new Set(), funcCode: /* @__PURE__ */ new Set(), exceptionCode: /* @__PURE__ */ new Set() };
highlightRules = [];
watchCallback = null;
logRateLimit = 100;
lastLogTime = 0;
// Кэшированные значения для производительности
static FUNCTION_CODE_NAMES = /* @__PURE__ */ new Map([
[import_constants.ModbusFunctionCode.READ_COILS, "READ_COILS"],
[import_constants.ModbusFunctionCode.READ_DISCRETE_INPUTS, "READ_DISCRETE_INPUTS"],
[import_constants.ModbusFunctionCode.READ_HOLDING_REGISTERS, "READ_HOLDING_REGISTERS"],
[import_constants.ModbusFunctionCode.READ_INPUT_REGISTERS, "READ_INPUT_REGISTERS"],
[import_constants.ModbusFunctionCode.WRITE_SINGLE_COIL, "WRITE_SINGLE_COIL"],
[import_constants.ModbusFunctionCode.WRITE_SINGLE_REGISTER, "WRITE_SINGLE_REGISTER"],
[import_constants.ModbusFunctionCode.WRITE_MULTIPLE_COILS, "WRITE_MULTIPLE_COILS"],
[import_constants.ModbusFunctionCode.WRITE_MULTIPLE_REGISTERS, "WRITE_MULTIPLE_REGISTERS"],
[import_constants.ModbusFunctionCode.REPORT_SLAVE_ID, "REPORT_SLAVE_ID"],
[import_constants.ModbusFunctionCode.READ_DEVICE_COMMENT, "READ_DEVICE_COMMENT"],
[import_constants.ModbusFunctionCode.WRITE_DEVICE_COMMENT, "WRITE_DEVICE_COMMENT"],
[import_constants.ModbusFunctionCode.READ_DEVICE_IDENTIFICATION, "READ_DEVICE_IDENTIFICATION"],
[import_constants.ModbusFunctionCode.READ_FILE_LENGTH, "READ_FILE_LENGTH"],
[import_constants.ModbusFunctionCode.READ_FILE_CHUNK, "READ_FILE_CHUNK"],
[import_constants.ModbusFunctionCode.OPEN_FILE, "OPEN_FILE"],
[import_constants.ModbusFunctionCode.CLOSE_FILE, "CLOSE_FILE"],
[import_constants.ModbusFunctionCode.RESTART_CONTROLLER, "RESTART_CONTROLLER"],
[import_constants.ModbusFunctionCode.GET_CONTROLLER_TIME, "GET_CONTROLLER_TIME"],
[import_constants.ModbusFunctionCode.SET_CONTROLLER_TIME, "SET_CONTROLLER_TIME"]
]);
getIndent() {
return " ".repeat(this.groupLevel);
}
getTimestamp() {
return (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
}
/**
* Formats a log message according to the specified level and context.
* @param level - Log level (trace, debug, info, warn, error)
* @param args - Arguments to be logged
* @param context - Context object with additional information
* @returns Formatted log message
*/
format(level, args, context = {}) {
const color = this.useColors ? this.COLORS[level] : "";
const reset = this.useColors ? this.COLORS.reset : "";
const indent = this.getIndent();
const isHighlighted = this.highlightRules.some(
(rule) => (!rule.slaveId || rule.slaveId === context.slaveId) && (!rule.funcCode || rule.funcCode === context.funcCode) && (!rule.exceptionCode || rule.exceptionCode === context.exceptionCode)
);
const headerParts = [];
if (this.logFormat.includes("timestamp")) headerParts.push(`[${this.getTimestamp()}]`);
if (this.logFormat.includes("level")) headerParts.push(`[${level.toUpperCase()}]`);
if (this.logFormat.includes("logger") && context["logger"]) {
const formatter = this.customFormatters["logger"] || ((v) => `[${v}]`);
headerParts.push(formatter(context["logger"]));
}
if (this.logFormat.includes("slaveId")) {
const slaveId = context["slaveId"] ?? this.globalContext["slaveId"] ?? "N/A";
const formatter = this.customFormatters["slaveId"] || ((v) => `[S:${v}]`);
headerParts.push(formatter(String(slaveId)));
}
if (this.logFormat.includes("funcCode")) {
const funcCode = context["funcCode"] != null ? `0x${context["funcCode"].toString(16).padStart(2, "0")}` : "N/A";
const funcName = context["funcCode"] != null ? Logger.FUNCTION_CODE_NAMES.get(context["funcCode"]) || "Unknown" : "N/A";
const formatter = this.customFormatters["funcCode"] || ((v) => `[F:${v}/${funcName}]`);
headerParts.push(formatter(`${funcCode}`));
}
if (this.logFormat.includes("exceptionCode") && context["exceptionCode"] != null) {
const exceptionName = import_constants.MODBUS_EXCEPTION_MESSAGES[context["exceptionCode"]] || "Unknown";
const formatter = this.customFormatters["exceptionCode"] || ((v) => `[E:${v}/${exceptionName}]`);
headerParts.push(
`${this.useColors && isHighlighted ? this.COLORS.exception : ""}${formatter(context["exceptionCode"])}${reset}`
);
}
if (this.logFormat.includes("address") && context["address"] != null) {
const formatter = this.customFormatters["address"] || ((v) => `[A:${v}]`);
headerParts.push(formatter(String(context["address"])));
}
if (this.logFormat.includes("quantity") && context["quantity"] != null) {
const formatter = this.customFormatters["quantity"] || ((v) => `[Q:${v}]`);
headerParts.push(formatter(String(context["quantity"])));
}
if (this.logFormat.includes("responseTime") && context["responseTime"] != null) {
const formatter = this.customFormatters["responseTime"] || ((v) => `[RT:${v}ms]`);
headerParts.push(formatter(String(context["responseTime"])));
}
const formattedArgs = args.map((arg) => {
if (arg instanceof Error) {
return `${arg.message}
${arg.stack || ""}`.trim();
}
return String(arg);
});
const contextToPrint = { ...context };
delete contextToPrint["logger"];
if (Object.keys(contextToPrint).length > 0) {
formattedArgs.push(JSON.stringify(contextToPrint));
}
return [
`${color}${headerParts.join("")}${isHighlighted ? this.COLORS.exception : ""}`,
indent,
...formattedArgs,
reset
];
}
/**
* Determines whether a log message should be logged based on level, context, and filters.
* @param level - Log level (trace, debug, info, warn, error)
* @param context - Context object with additional information
* @returns Whether the log message should be logged
*/
shouldLog(level, context = {}) {
if (!this.enabled) return false;
if (this.filters.slaveId.size > 0 && context["slaveId"] != null && this.filters.slaveId.has(context["slaveId"]))
return false;
if (this.filters.funcCode.size > 0 && context["funcCode"] != null && this.filters.funcCode.has(context["funcCode"]))
return false;
if (this.filters.exceptionCode.size > 0 && context["exceptionCode"] != null && this.filters.exceptionCode.has(context["exceptionCode"]))
return false;
if (context["logger"] && this.categoryLevels[context["logger"]] === "none") return false;
if (context["logger"] && this.categoryLevels[context["logger"]] !== "none") {
const categoryLevel = this.categoryLevels[context["logger"]];
return this.LEVELS.indexOf(level) >= this.LEVELS.indexOf(categoryLevel);
}
return this.LEVELS.indexOf(level) >= this.LEVELS.indexOf(this.currentLevel);
}
/**
* Outputs a log message to the console immediately (asynchronous).
* @param level - Log level (trace, debug, info, warn, error)
* @param args - Arguments to be logged
* @param context - Context object with additional information
* @param immediate - Whether to output immediately
*/
async output(level, args, context, immediate = false) {
if (!this.shouldLog(level, context)) return;
this.logCounts[level] = (this.logCounts[level] || 0) + 1;
if (context["slaveId"] != null)
this.logStats.bySlaveId[context["slaveId"]] = (this.logStats.bySlaveId[context["slaveId"]] || 0) + 1;
if (context["funcCode"] != null)
this.logStats.byFuncCode[context["funcCode"]] = (this.logStats.byFuncCode[context["funcCode"]] || 0) + 1;
if (context["exceptionCode"] != null)
this.logStats.byExceptionCode[context["exceptionCode"]] = (this.logStats.byExceptionCode[context["exceptionCode"]] || 0) + 1;
if (this.watchCallback) {
this.watchCallback({ level, args, context });
}
const now = Date.now();
if (!immediate && now - this.lastLogTime < this.logRateLimit && level !== "error" && level !== "warn")
return;
this.lastLogTime = now;
const formatted = this.format(level, args, context);
if (this.useColors) {
const head = formatted[0] || "";
const indent = formatted[1] || "";
const rest = formatted.slice(2);
console[level](head + indent, ...rest);
} else {
console[level](...formatted);
}
}
/**
* Splits the arguments into the main arguments and the context object.
* @param args - Arguments to be logged
* @returns { args, context }
*/
splitArgsAndContext(args) {
if (args.length > 1) {
const lastArg = args[args.length - 1];
if (typeof lastArg === "object" && lastArg !== null) {
const context = args.pop();
return { args, context };
}
}
return { args, context: {} };
}
async trace(...args) {
const { args: newArgs, context } = this.splitArgsAndContext([...args]);
await this.output("trace", newArgs, context);
}
async debug(...args) {
const { args: newArgs, context } = this.splitArgsAndContext([...args]);
await this.output("debug", newArgs, context);
}
async info(...args) {
const { args: newArgs, context } = this.splitArgsAndContext([...args]);
await this.output("info", newArgs, context);
}
async warn(...args) {
const { args: newArgs, context } = this.splitArgsAndContext([...args]);
await this.output("warn", newArgs, context, true);
}
async error(...args) {
const { args: newArgs, context } = this.splitArgsAndContext([...args]);
await this.output("error", newArgs, context, true);
}
group() {
this.groupLevel++;
}
groupCollapsed() {
this.groupLevel++;
}
groupEnd() {
if (this.groupLevel > 0) this.groupLevel--;
}
setLevel(level) {
if (this.LEVELS.includes(level)) {
this.currentLevel = level;
} else {
throw new Error(`Unknown log level: ${level}`);
}
}
setLevelFor(category, level) {
if (level !== "none" && !this.LEVELS.includes(level))
throw new Error(`Unknown log level: ${level}`);
this.categoryLevels[category] = level;
}
pauseCategory(category) {
this.categoryLevels[category] = "none";
}
resumeCategory(category) {
delete this.categoryLevels[category];
}
enable() {
this.enabled = true;
}
disable() {
this.enabled = false;
}
getLevel() {
return this.currentLevel;
}
isEnabled() {
return this.enabled;
}
disableColors() {
this.useColors = false;
}
setGlobalContext(ctx) {
this.globalContext = { ...ctx };
}
addGlobalContext(ctx) {
this.globalContext = { ...this.globalContext, ...ctx };
}
setTransportType(type) {
this.globalContext["transport"] = type;
}
setBuffering(value) {
this.buffering = !!value;
}
setFlushInterval(ms) {
if (typeof ms !== "number" || ms < 0)
throw new Error("Flush interval must be a non-negative number");
this.flushInterval = ms;
}
setRateLimit(ms) {
if (typeof ms !== "number" || ms < 0)
throw new Error("Rate limit must be a non-negative number");
this.logRateLimit = ms;
}
setLogFormat(fields) {
const validFields = [
"timestamp",
"level",
"logger",
"slaveId",
"funcCode",
"exceptionCode",
"address",
"quantity",
"responseTime"
];
if (!Array.isArray(fields) || !fields.every((f) => validFields.includes(f))) {
throw new Error(`Invalid log format. Valid fields: ${validFields.join(", ")}`);
}
this.logFormat = fields;
}
setCustomFormatter(field, formatter) {
const fieldStr = field;
if (![
"logger",
"slaveId",
"funcCode",
"exceptionCode",
"address",
"quantity",
"responseTime"
].includes(fieldStr)) {
throw new Error(`Invalid formatter field: ${fieldStr}`);
}
if (typeof formatter !== "function") {
throw new Error("Formatter must be a function");
}
this.customFormatters[field] = formatter;
}
mute({ slaveId, funcCode, exceptionCode } = {}) {
if (slaveId != null) this.filters.slaveId.add(slaveId);
if (funcCode != null) this.filters.funcCode.add(funcCode);
if (exceptionCode != null) this.filters.exceptionCode.add(exceptionCode);
}
unmute({ slaveId, funcCode, exceptionCode } = {}) {
if (slaveId != null) this.filters.slaveId.delete(slaveId);
if (funcCode != null) this.filters.funcCode.delete(funcCode);
if (exceptionCode != null) this.filters.exceptionCode.delete(exceptionCode);
}
highlight({ slaveId, funcCode, exceptionCode } = {}) {
this.highlightRules.push({ slaveId, funcCode, exceptionCode });
}
clearHighlights() {
this.highlightRules = [];
}
watch(callback) {
if (typeof callback !== "function") throw new Error("Watch callback must be a function");
this.watchCallback = callback;
}
clearWatch() {
this.watchCallback = null;
}
flush() {
}
inspectBuffer() {
console.log("\x1B[1;36m=== Log Buffer Contents ===\x1B[0m");
if (this.buffer.length === 0) {
console.log("Buffer is empty");
} else {
this.buffer.forEach((item, index) => {
if (typeof item === "object" && item !== null && "level" in item && "args" in item && "context" in item) {
const typedItem = item;
if (typeof typedItem.level === "string" && Array.isArray(typedItem.args) && typeof typedItem.context === "object" && typedItem.context !== null) {
const formatted = this.format(
typedItem.level,
typedItem.args,
typedItem.context
);
console.log(`[${index}] ${formatted.join(" ")}`);
} else {
console.log(`[${index}] ${String(item)}`);
}
} else {
console.log(`[${index}] ${String(item)}`);
}
});
}
console.log(`Buffer Size: ${this.buffer.length}/${this.MAX_BUFFER_SIZE}`);
console.log("\x1B[1;36m==========================\x1B[0m");
}
summary() {
console.log("\x1B[1;36m=== Logger Summary ===\x1B[0m");
console.log(`Trace Messages: ${this.logCounts.trace || 0}`);
console.log(`Debug Messages: ${this.logCounts.debug || 0}`);
console.log(`Info Messages: ${this.logCounts.info || 0}`);
console.log(`Warn Messages: ${this.logCounts.warn || 0}`);
console.log(`Error Messages: ${this.logCounts.error || 0}`);
console.log(
`Total Messages: ${Object.values(this.logCounts).reduce((sum, count) => sum + count, 0)}`
);
console.log(`By Slave ID: ${JSON.stringify(this.logStats.bySlaveId, null, 2)}`);
console.log(
`By Function Code: ${JSON.stringify(
Object.entries(this.logStats.byFuncCode).reduce(
(acc, [code, count]) => {
const name = Logger.FUNCTION_CODE_NAMES.get(parseInt(code)) || "Unknown";
acc[`${code}/${name}`] = count;
return acc;
},
{}
),
null,
2
)}`
);
console.log(
`By Exception Code: ${JSON.stringify(
Object.entries(this.logStats.byExceptionCode).reduce(
(acc, [code, count]) => {
acc[`${code}/${import_constants.MODBUS_EXCEPTION_MESSAGES[parseInt(code)] || "Unknown"}`] = count;
return acc;
},
{}
),
null,
2
)}`
);
console.log(
`Buffering: ${this.buffering ? "Enabled" : "Disabled"} (Interval: ${this.flushInterval}ms)`
);
console.log(`Rate Limit: ${this.logRateLimit}ms`);
console.log(`Buffer Size: ${this.buffer.length}/${this.MAX_BUFFER_SIZE}`);
console.log(`Current Level: ${this.currentLevel}`);
console.log(
`Categories: ${Object.keys(this.categoryLevels).length ? JSON.stringify(this.categoryLevels, null, 2) : "None"}`
);
console.log(
`Filters: slaveId=${JSON.stringify([...this.filters.slaveId])}, funcCode=${JSON.stringify([...this.filters.funcCode])}, exceptionCode=${JSON.stringify([...this.filters.exceptionCode])}`
);
console.log(
`Highlights: ${this.highlightRules.length ? JSON.stringify(this.highlightRules, null, 2) : "None"}`
);
console.log("\x1B[1;36m=====================\x1B[0m");
}
/**
* Creates a logger instance with category.
* @param name - Logger name
* @returns Logger instance
*/
createLogger(name) {
if (!name) throw new Error("Logger name required");
return {
trace: async (...args) => {
const { args: newArgs, context } = this.splitArgsAndContext([...args]);
await this.output("trace", newArgs, { ...context, logger: name });
},
debug: async (...args) => {
const { args: newArgs, context } = this.splitArgsAndContext([...args]);
await this.output("debug", newArgs, { ...context, logger: name });
},
info: async (...args) => {
const { args: newArgs, context } = this.splitArgsAndContext([...args]);
await this.output("info", newArgs, { ...context, logger: name });
},
warn: async (...args) => {
const { args: newArgs, context } = this.splitArgsAndContext([...args]);
await this.output("warn", newArgs, { ...context, logger: name }, true);
},
error: async (...args) => {
const { args: newArgs, context } = this.splitArgsAndContext([...args]);
await this.output("error", newArgs, { ...context, logger: name }, true);
},
group: () => this.group(),
groupCollapsed: () => this.groupCollapsed(),
groupEnd: () => this.groupEnd(),
setLevel: (lvl) => this.setLevelFor(name, lvl),
pause: () => this.pauseCategory(name),
resume: () => this.resumeCategory(name)
};
}
}
module.exports = Logger;