modbus-connect
Version:
Modbus RTU over Web Serial and Node.js SerialPort
599 lines (526 loc) • 20.1 kB
text/typescript
// src/types/modbus-types.ts
import { RegisterType } from '../constants/constants.js';
// !=============================================================================
// ! Типы для плагинов
// !=============================================================================
/**
* Описывает обработчик для одной кастомной функции Modbus.
*/
export interface CustomFunctionHandler {
/** Функция для сборки PDU (тела) запроса. */
buildRequest: (...args: any[]) => Uint8Array;
/** Функция для разбора PDU ответа от устройства. */
parseResponse: (responsePdu: Uint8Array) => any;
}
/**
* Интерфейс, которому должен соответствовать любой плагин.
*/
export interface IModbusPlugin {
/** Уникальное имя плагина, полезно для отладки. */
name: string;
/** Регистрация обработчиков для нестандартных кодов функций. */
customFunctionCodes?: { [functionName: string]: CustomFunctionHandler };
/** Регистрация обработчиков для кастомных типов данных. */
customRegisterTypes?: { [typeName: string]: (registers: number[]) => any[] };
/** Регистрация кастомных алгоритмов расчета контрольной суммы. */
customCrcAlgorithms?: { [algorithmName: string]: (data: Uint8Array) => Uint8Array };
}
export type PluginConstructor = new (...args: any[]) => IModbusPlugin;
// !=============================================================================
// ! Типы для функций чтения Modbus
// !=============================================================================
export type ReadCoilsResponse = boolean[];
export type ReadDiscreteInputsResponse = boolean[];
export type ReadHoldingRegistersResponse = number[];
export type ReadInputRegistersResponse = number[];
// !=============================================================================
// ! Типы для функций записи Modbus
// !=============================================================================
/** Ответ на запрос записи одного coil */
export interface WriteSingleCoilResponse {
address: number;
value: boolean;
}
/** Ответ на запрос записи нескольких coil */
export interface WriteMultipleCoilsResponse {
startAddress: number;
quantity: number;
}
/** Ответ на запрос записи одного регистра */
export interface WriteSingleRegisterResponse {
address: number;
value: number;
}
/** Ответ на запрос записи нескольких регистров */
export interface WriteMultipleRegistersResponse {
startAddress: number;
quantity: number;
}
// !=============================================================================
// ! Типы для специальных функций Modbus
// !=============================================================================
/** Ответ на запрос идентификации устройства (Report Slave ID) */
export interface ReportSlaveIdResponse {
slaveId: number;
isRunning: boolean;
data: Uint8Array;
}
/** Ответ на запрос идентификатора устройства (Read Device Identification) */
export interface ReadDeviceIdentificationResponse {
functionCode: number;
meiType: number;
category: number;
conformityLevel: number;
moreFollows: number;
nextObjectId: number;
numberOfObjects: number;
objects: Record<number, string>;
}
// !=============================================================================
// ! Интерфейсы для транспорта
// !=============================================================================
export type RSMode = 'RS485' | 'RS232';
export type PortStateHandler = (
connected: boolean,
slaveIds?: number[],
error?: { type: ConnectionErrorType; message: string }
) => void;
/** Состояние подключения устройства */
export interface DeviceConnectionStateObject {
slaveId: number;
hasConnectionDevice: boolean;
errorType?: string;
errorMessage?: string;
}
/** Обработчик изменения состояния подключения устройства */
export type DeviceStateHandler = (
slaveId: number,
connected: boolean,
error?: { type: string; message: string }
) => void;
/** Интерфейс для транспорта Modbus */
export interface Transport {
readonly isOpen: boolean;
connect(): Promise<void>;
disconnect(): Promise<void>;
write(buffer: Uint8Array): Promise<void>;
read(length: number, timeout?: number): Promise<Uint8Array>;
flush?(): Promise<void>;
getRSMode(): RSMode;
/**
* Установить обработчик состояния подключения устройства.
*/
setDeviceStateHandler(handler: DeviceStateHandler): void;
/**
* Установить обработчик состояния физического порта.
*/
setPortStateHandler(handler: PortStateHandler): void;
/**
* Отключить уведомления о состоянии устройств.
* После вызова `setDeviceStateHandler` перестанет работать.
*/
disableDeviceTracking(): Promise<void>;
/**
* Включить трекинг устройств (если был отключён).
* @param handler Опционально: установить новый обработчик
*/
enableDeviceTracking(handler?: DeviceStateHandler): Promise<void>;
notifyDeviceConnected?(slaveId: number): void;
notifyDeviceDisconnected?(
slaveId: number,
errorType: ConnectionErrorType,
errorMessage?: string
): void;
}
export interface DeviceConnectionTracker {
setHandler(handler: DeviceStateHandler): Promise<void>;
removeHandler(): Promise<void>;
clearHandler(): Promise<void>;
}
/**
* Типы ошибок подключения
*/
export enum ConnectionErrorType {
UnknownError = 'UnknownError',
PortClosed = 'PortClosed',
Timeout = 'Timeout',
CRCError = 'CRCError',
ConnectionLost = 'ConnectionLost',
DeviceOffline = 'DeviceOffline',
MaxReconnect = 'MaxReconnect',
ManualDisconnect = 'ManualDisconnect',
Destroyed = 'Destroyed',
}
export interface DeviceConnectionTrackerOptions {
/** Интервал дебонса уведомлений об отключении (мс) */
debounceMs?: number;
/** Валидировать slaveId (0–247) */
validateSlaveId?: boolean;
}
// !=============================================================================
// ! Интерфейс для TransportController
// !=============================================================================
export interface TransportControllerInterface {
// === Управление транспортами ===
/**
* Добавить транспорт.
*/
addTransport(
id: string,
type: 'node' | 'web',
options: NodeSerialTransportOptions | (WebSerialTransportOptions & { port: WebSerialPort }),
reconnectOptions?: {
maxReconnectAttempts?: number;
reconnectInterval?: number;
},
pollingConfig?: PollingManagerConfig // <-- Новый аргумент
): Promise<void>;
/**
* Удалить транспорт по указанному ID.
*/
removeTransport(id: string): Promise<void>;
/**
* Получить транспорт по указанному ID.
*/
getTransport(id: string): Transport | null;
/**
* Получить список всех транспортов.
*/
listTransports(): TransportInfo[];
/**
* Перезагрузить транспорт с новыми опциями.
*/
reloadTransport(
id: string,
options: NodeSerialTransportOptions | (WebSerialTransportOptions & { port: WebSerialPort })
): Promise<void>;
// === Подключение/отключение ===
connectAll(): Promise<void>;
disconnectAll(): Promise<void>;
connectTransport(id: string): Promise<void>;
disconnectTransport(id: string): Promise<void>;
// === Маршрутизация ===
getTransportForSlave(slaveId: number, requiredRSMode: RSMode): Transport | null;
assignSlaveIdToTransport(transportId: string, slaveId: number): void;
// === Статусы и диагностика ===
getStatus(id?: string): TransportStatus | Record<string, TransportStatus>;
getActiveTransportCount(): number;
// === Балансировка ===
setLoadBalancer(strategy: LoadBalancerStrategy): void;
// === Управление обработчиками ===
setDeviceStateHandler(handler: DeviceStateHandler): void;
setPortStateHandler(handler: PortStateHandler): void;
setDeviceStateHandlerForTransport(
transportId: string,
handler: DeviceStateHandler
): Promise<void>;
setPortStateHandlerForTransport(transportId: string, handler: PortStateHandler): Promise<void>;
// === Управление PollingManager (Прокси-методы) ===
addPollingTask(transportId: string, options: PollingTaskOptions): void;
removePollingTask(transportId: string, taskId: string): void;
updatePollingTask(
transportId: string,
taskId: string,
newOptions: Partial<PollingTaskOptions>
): void;
controlTask(
transportId: string,
taskId: string,
action: 'start' | 'stop' | 'pause' | 'resume'
): void;
controlPolling(
transportId: string,
action: 'startAll' | 'stopAll' | 'pauseAll' | 'resumeAll'
): void;
getPollingStats(transportId: string): Record<string, PollingTaskStats>;
getPollingQueueInfo(transportId: string): PollingQueueInfo;
/**
* Выполнить функцию в контексте мьютекса транспорта (для ручных команд)
*/
executeImmediate<T>(transportId: string, fn: () => Promise<T>): Promise<T>;
// === Уничтожение ===
destroy(): Promise<void>;
}
// --- Типы, используемые в TransportControllerInterface ---
export interface TransportInfo {
id: string;
type: 'node' | 'web';
transport: Transport;
// pollingManager не экспортируем в публичные типы, так как он внутри реализации
status: 'disconnected' | 'connecting' | 'connected' | 'error';
slaveIds: number[];
fallbacks: string[];
createdAt: Date;
lastError?: Error;
reconnectAttempts: number;
maxReconnectAttempts: number;
reconnectInterval: number;
}
export interface TransportStatus {
id: string;
connected: boolean;
lastError?: Error;
connectedSlaveIds: number[];
uptime: number;
reconnectAttempts: number;
pollingStats?: {
queueLength: number;
tasksRunning: number;
};
}
export type LoadBalancerStrategy = 'round-robin' | 'sticky' | 'first-available';
// !=============================================================================
// ! Интерфейсы для опций клиента
// !=============================================================================
/** Опции для конфигурации Modbus клиента */
export interface ModbusClientOptions {
RSMode?: RSMode;
timeout?: number;
retryCount?: number;
retryDelay?: number;
echoEnabled?: boolean;
plugins?: PluginConstructor[];
crcAlgorithm?:
| 'crc16Modbus'
| 'crc16CcittFalse'
| 'crc32'
| 'crc8'
| 'crc1'
| 'crc8_1wire'
| 'crc8_dvbs2'
| 'crc16_kermit'
| 'crc16_xmodem'
| 'crc24'
| 'crc32mpeg'
| 'crcjam';
}
// !=============================================================================
// ! Типы для логгера
// !=============================================================================
/** Уровни логирования */
export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error';
/** Контекст для логирования */
export interface LogContext {
slaveId?: number;
funcCode?: number;
exceptionCode?: number;
address?: number;
quantity?: number;
responseTime?: number;
logger?: string;
transport?: string;
[key: string]: string | number | boolean | undefined;
}
/** Интерфейс для экземпляра логгера */
export interface LoggerInstance {
trace(...args: unknown[]): Promise<void>;
debug(...args: unknown[]): Promise<void>;
info(...args: unknown[]): Promise<void>;
warn(...args: unknown[]): Promise<void>;
error(...args: unknown[]): Promise<void>;
group(): void;
groupCollapsed(): void;
groupEnd(): void;
setLevel(lvl: LogLevel): void;
pause(): void;
resume(): void;
}
// !=============================================================================
// ! Интерфейсы для транспорта через последовательный порт
// !=============================================================================
/** Опции для транспорта через Node.js SerialPort */
export interface NodeSerialTransportOptions {
baudRate?: number;
dataBits?: 5 | 6 | 7 | 8;
stopBits?: 1 | 2;
parity?: 'none' | 'even' | 'mark' | 'odd' | 'space';
readTimeout?: number;
writeTimeout?: number;
maxBufferSize?: number;
reconnectInterval?: number;
maxReconnectAttempts?: number;
RSMode?: RSMode;
[key: string]: unknown;
}
/** Интерфейс для Web Serial Port */
export interface WebSerialPort {
open(options: WebSerialPortOptions): Promise<void>;
close(): Promise<void>;
readonly readable: ReadableStream<Uint8Array> | null;
readonly writable: WritableStream<Uint8Array> | null;
readonly opened: boolean;
}
/** Опции для Web Serial Port */
export interface WebSerialPortOptions {
baudRate: number;
dataBits: number;
stopBits: number;
parity: 'none' | 'even' | 'mark' | 'odd' | 'space';
flowControl: 'none';
}
/** Опции для транспорта через Web Serial */
export interface WebSerialTransportOptions {
baudRate?: number;
dataBits?: number;
stopBits?: number;
parity?: 'none' | 'even' | 'mark' | 'odd' | 'space';
readTimeout?: number;
writeTimeout?: number;
reconnectInterval?: number;
maxReconnectAttempts?: number;
maxEmptyReadsBeforeReconnect?: number;
RSMode?: RSMode;
[key: string]: unknown;
}
// !=============================================================================
// ! Типы для системы опроса (Polling)
// !=============================================================================
/** Конфигурация менеджера опроса */
export interface PollingManagerConfig {
defaultMaxRetries?: number;
defaultBackoffDelay?: number;
defaultTaskTimeout?: number;
logLevel?: LogLevel;
[key: string]: unknown;
}
/** Опции для задачи опроса */
export interface PollingTaskOptions {
id: string;
// resourceId?: string; // УДАЛЕНО
priority?: number;
interval: number;
fn: (() => Promise<unknown>) | Array<() => Promise<unknown>>;
onData?: (data: unknown[]) => void;
onError?: (error: Error, fnIndex: number, retryCount: number) => void;
onStart?: () => void;
onStop?: () => void;
onFinish?: (success: boolean, results: unknown[]) => void;
onBeforeEach?: () => void;
onRetry?: (error: Error, fnIndex: number, retryCount: number) => void;
shouldRun?: () => boolean;
onSuccess?: (result: unknown) => void;
onFailure?: (error: Error) => void;
name?: string;
immediate?: boolean;
maxRetries?: number;
backoffDelay?: number;
taskTimeout?: number;
}
/** Состояние задачи опроса */
export interface PollingTaskState {
stopped: boolean;
paused: boolean;
running: boolean;
inProgress: boolean;
}
/** Статистика задачи опроса */
export interface PollingTaskStats {
totalRuns: number;
totalErrors: number;
lastError: Error | null;
lastResult: unknown;
lastRunTime: number | null;
retries: number;
successes: number;
failures: number;
}
/** Информация о очереди опроса */
export interface PollingQueueInfo {
// resourceId: string; // УДАЛЕНО
queueLength: number;
tasks: Array<{
id: string;
state: PollingTaskState;
}>;
}
/** Статистика системы опроса */
export interface PollingSystemStats {
totalTasks: number;
totalQueues: number;
queuedTasks: number;
tasks: Record<string, PollingTaskStats>;
}
// !=============================================================================
// ! Типы для эмулятора и регистров
// !=============================================================================
/** Определение одного регистра */
export interface RegisterDefinition {
start: number;
value: number | boolean;
}
/** Определения наборов регистров */
export interface RegisterDefinitions {
coils?: RegisterDefinition[];
discrete?: RegisterDefinition[];
holding?: RegisterDefinition[];
input?: RegisterDefinition[];
}
/** Параметры для бесконечного изменения регистра */
export interface InfinityChangeParams {
typeRegister: 'Holding' | 'Input' | 'Coil' | 'Discrete';
register: number;
range: [number, number];
interval: number;
}
/** Параметры для остановки бесконечного изменения регистра */
export interface StopInfinityChangeParams {
typeRegister: 'Holding' | 'Input' | 'Coil' | 'Discrete';
register: number;
}
/** Опции для эмулятора slave-устройства */
export interface SlaveEmulatorOptions {
loggerEnabled?: boolean;
}
/** Тип для конвертированных регистров с поддержкой различных типов данных */
export type ConvertedRegisters<T extends RegisterType = RegisterType.UINT16> = T extends
| RegisterType.UINT16
| RegisterType.INT16
| RegisterType.UINT32
| RegisterType.INT32
| RegisterType.FLOAT
? number[]
: T extends RegisterType.UINT64 | RegisterType.INT64
? bigint[]
: T extends RegisterType.DOUBLE
? number[]
: T extends RegisterType.UINT32_LE | RegisterType.INT32_LE | RegisterType.FLOAT_LE
? number[]
: T extends
| RegisterType.UINT32_SW
| RegisterType.INT32_SW
| RegisterType.FLOAT_SW
| RegisterType.UINT32_SB
| RegisterType.INT32_SB
| RegisterType.FLOAT_SB
| RegisterType.UINT32_SBW
| RegisterType.INT32_SBW
| RegisterType.FLOAT_SBW
| RegisterType.UINT32_LE_SW
| RegisterType.INT32_LE_SW
| RegisterType.FLOAT_LE_SW
| RegisterType.UINT32_LE_SB
| RegisterType.INT32_LE_SB
| RegisterType.FLOAT_LE_SB
| RegisterType.UINT32_LE_SBW
| RegisterType.INT32_LE_SBW
| RegisterType.FLOAT_LE_SBW
? number[]
: T extends RegisterType.UINT64_LE | RegisterType.INT64_LE
? bigint[]
: T extends RegisterType.DOUBLE_LE
? number[]
: T extends RegisterType.HEX
? string[]
: T extends RegisterType.STRING
? string[]
: T extends RegisterType.BOOL
? boolean[]
: T extends RegisterType.BINARY
? boolean[][]
: T extends RegisterType.BCD
? number[]
: never;
/** Опции для конвертации регистров */
export interface ConvertRegisterOptions {
type?: RegisterType;
}