import { S7Client } from '../client.js'; import { EnhancedClientOptions, S7DataType, ReadPoint, WritePoint, PollPoint, ChangeRecord, ChangeHandler, ErrorHandler, ConnectHandler } from './types.js'; import { parseAddress, typeSize, decode, encode, normalize } from './codec.js'; import { groupByDb, byteRange } from './batch.js'; import { PollingRunner } from './polling.js'; export class S7EnhancedClient { private client = new S7Client(); private _connected = false; private _disconnected = false; // true only after explicit disconnect() private _connecting: Promise | null = null; private _reconnectTimer: ReturnType | null = null; private _polling: PollingRunner; private _pollingRequested = false; private _onError: ErrorHandler | null = null; private _onConnect: ConnectHandler | null = null; private _onDisconnect: ConnectHandler | null = null; constructor(private options: EnhancedClientOptions) { this._polling = new PollingRunner(this.client); this._polling.onError(err => { this._handleError(err); }); } // ---- Connection ---- async connect(): Promise { if (this._connected) return; if (this._connecting) return this._connecting; this._disconnected = false; this._connecting = this._doConnect(); try { await this._connecting; } finally { this._connecting = null; } } disconnect(): void { this._disconnected = true; this._connected = false; this._pollingRequested = false; this._cancelReconnect(); this._polling.stopPolling(); this.client.Disconnect(); } // ---- Single-value read ---- async readBool(address: string): Promise { return this._readSingle(address, 'bool'); } async readByte(address: string): Promise { return this._readSingle(address, 'byte'); } async readInt(address: string): Promise { return this._readSingle(address, 'int'); } async readWord(address: string): Promise { return this._readSingle(address, 'word'); } async readDInt(address: string): Promise { return this._readSingle(address, 'dint'); } async readDWord(address: string): Promise { return this._readSingle(address, 'dword'); } async readLInt(address: string): Promise { return this._readSingle(address, 'lint'); } async readULInt(address: string): Promise { return this._readSingle(address, 'ulint'); } async readReal(address: string): Promise { return this._readSingle(address, 'real'); } async readString(address: string, stringLen?: number): Promise { return this._readSingle(address, 'string', stringLen); } async readWString(address: string, stringLen?: number): Promise { return this._readSingle(address, 'wstring', stringLen); } // ---- Single-value write ---- async writeBool(address: string, value: boolean): Promise { return this._writeSingle(address, 'bool', value); } async writeByte(address: string, value: number): Promise { return this._writeSingle(address, 'byte', value); } async writeInt(address: string, value: number): Promise { return this._writeSingle(address, 'int', value); } async writeWord(address: string, value: number): Promise { return this._writeSingle(address, 'word', value); } async writeDInt(address: string, value: number): Promise { return this._writeSingle(address, 'dint', value); } async writeDWord(address: string, value: number): Promise { return this._writeSingle(address, 'dword', value); } async writeLInt(address: string, value: bigint | number): Promise { return this._writeSingle(address, 'lint', value); } async writeULInt(address: string, value: bigint | number): Promise { return this._writeSingle(address, 'ulint', value); } async writeReal(address: string, value: number): Promise { return this._writeSingle(address, 'real', value); } async writeString(address: string, value: string, stringLen?: number): Promise { return this._writeSingle(address, 'string', value, stringLen); } async writeWString(address: string, value: string, stringLen?: number): Promise { return this._writeSingle(address, 'wstring', value, stringLen); } // ---- Batch read/write ---- async batchRead(points: ReadPoint[]): Promise> { await this._ensureConnected(); const result = new Map(); if (!points.length) return result; const normalized = points.map(normalize); for (const [dbNumber, items] of groupByDb(normalized)) { const { start, size } = byteRange(items, p => typeSize(p.dataType, p.stringLen)); const buf = await this.client.DBRead(dbNumber, start, size) as Buffer; for (const { addr, item } of items) { result.set(item.address, decode(buf, { ...addr, byteOffset: addr.byteOffset - start }, item.dataType)); } } return result; } async batchWrite(points: WritePoint[]): Promise { await this._ensureConnected(); if (!points.length) return; const normalized = points.map(normalize); for (const [dbNumber, items] of groupByDb(normalized)) { const { start, size } = byteRange(items, p => typeSize(p.dataType, p.stringLen)); const buf = await this.client.DBRead(dbNumber, start, size) as Buffer; for (const { addr, item } of items) { encode(buf, { ...addr, byteOffset: addr.byteOffset - start }, item.dataType, item.value, item.stringLen); } await this.client.DBWrite(dbNumber, start, size, buf); } } // ---- Polling ---- setPollPoints(points: PollPoint[]): void { this._polling.setPollPoints(points); } setPollInterval(ms: number): void { this._polling.setPollInterval(ms); } async startPolling(): Promise { this._pollingRequested = true; await this._ensureConnected(); this._polling.startPolling(); } stopPolling(): void { this._pollingRequested = false; this._polling.stopPolling(); } // ---- Event callbacks ---- onChange(handler: ChangeHandler): void { this._polling.onChange(handler); } onError(handler: ErrorHandler): void { this._onError = handler; } onConnect(handler: ConnectHandler): void { this._onConnect = handler; } onDisconnect(handler: ConnectHandler): void { this._onDisconnect = handler; } // ---- Private: connection ---- private async _doConnect(): Promise { await this.client.ConnectToPort(this.options.ip, this.options.port, this.options.rack, this.options.slot); this._connected = true; this._onConnect?.(); if (this._pollingRequested) { this._polling.startPolling(); } } private async _ensureConnected(): Promise { if (this._disconnected) { this._disconnected = false; // allow reconnect on next call } if (this._connected) return; await this.connect(); } private _handleError(err: any): void { if (!this._connected) return; this._connected = false; this.client.Disconnect(); this._polling.handleDisconnect(); this._onDisconnect?.(); this._onError?.(err); this._scheduleReconnect(); } private _scheduleReconnect(): void { if (this._disconnected || this._reconnectTimer) return; const interval = this.options.reconnectInterval ?? 5000; this._reconnectTimer = setTimeout(async () => { this._reconnectTimer = null; if (this._disconnected) return; try { await this._doConnect(); } catch { this._scheduleReconnect(); } }, interval); } private _cancelReconnect(): void { if (this._reconnectTimer) { clearTimeout(this._reconnectTimer); this._reconnectTimer = null; } } // ---- Private: read/write ---- private async _readSingle(address: string, dataType: S7DataType, stringLen?: number): Promise { await this._ensureConnected(); try { const addr = parseAddress(address); const size = typeSize(dataType, stringLen); const buf = await this.client.DBRead(addr.dbNumber, addr.byteOffset, size) as Buffer; return decode(buf, { ...addr, byteOffset: 0 }, dataType) as T; } catch (err) { this._handleError(err); throw err; } } private async _writeSingle(address: string, dataType: S7DataType, value: any, stringLen?: number): Promise { await this._ensureConnected(); try { const addr = parseAddress(address); const size = typeSize(dataType, stringLen); const buf = await this.client.DBRead(addr.dbNumber, addr.byteOffset, size) as Buffer; encode(buf, { ...addr, byteOffset: 0 }, dataType, value, stringLen); await this.client.DBWrite(addr.dbNumber, addr.byteOffset, size, buf); } catch (err) { this._handleError(err); throw err; } } }