import { EventEmitter } from 'events'; import { TcpTransport } from './transport/tcp-transport.js'; import { buildCR, buildDR, isCC, isDR, isDT, isDTEot, extractDTPayload, computeRemoteTSAP, computeLocalTSAP } from './protocol/cotp.js'; import { buildSetupComm, parseSetupCommResponse, buildReadArea, parseReadAreaResponse, buildWriteArea, parseWriteAreaResponse, buildReadMultiVars, parseReadMultiVarsResponse, buildWriteMultiVars, parseWriteMultiVarsResponse, buildReadSZL, parseReadSZLResponse, buildReadSZLList, buildGetOrderCode, buildGetCpuInfo, buildGetCpInfo, buildGetProtection, buildPlcHotStart, buildPlcColdStart, buildPlcStop, buildCopyRamToRom, buildCompress, buildSetPassword, buildClearPassword, buildGetPlcDateTime, parseGetPlcDateTimeResponse, buildSetPlcDateTime, buildSetPlcSystemDateTime, buildListBlocks, buildGetAgBlockInfo, buildListBlocksOfType, buildUpload, buildFullUpload, buildDownload, buildDelete, buildDBGet, buildDBFill, buildGetPlcStatus, parseGetPlcStatusResponse, parseResponse, S7DataItem, isS7Error, getS7ErrorCode, } from './protocol/s7-protocol.js'; import { S7AreaPE, S7AreaPA, S7AreaMK, S7AreaDB, S7AreaCT, S7AreaTM, S7WLBit, S7WLByte, S7WLWord, S7WLDWord, S7WLReal, S7WLCounter, S7WLTimer, CONNTYPE_PG, CONNTYPE_OP, CONNTYPE_BASIC, S7CpuStatusRun, S7CpuStatusStop, S7CpuStatusUnknown, errIsoConnect, errIsoDisconnect, errIsoInvalidPDU, errIsoInvalidDataSize, errIsoNullPointer, errIsoShortPacket, errIsoTooManyFragments, errIsoPduOverflow, errIsoSendPacket, errIsoRecvPacket, errIsoInvalidParams, errNegotiatingPDU, errCliInvalidParams, errCliJobPending, errCliTooManyItems, errCliInvalidWordLen, errCliPartialDataWritten, errCliSizeOverPDU, errCliInvalidPlcAnswer, errCliAddressOutOfRange, errCliInvalidTransportSize, errCliWriteDataSizeMismatch, errCliItemNotAvailable, errCliInvalidValue, errCliCannotStartPLC, errCliAlreadyRun, errCliCannotStopPLC, errCliCannotCopyRamToRom, errCliCannotCompress, errCliAlreadyStop, errCliFunNotAvailable, errCliUploadSequenceFailed, errCliInvalidDataSizeRecvd, errCliInvalidBlockType, errCliInvalidBlockNumber, errCliInvalidBlockSize, errCliDownloadSequenceFailed, errCliInsertRefused, errCliDeleteRefused, errCliNeedPassword, errCliInvalidPassword, errCliNoPasswordToSetOrClear, errCliJobTimeout, errCliPartialDataRead, errCliBufferTooSmall, errCliFunctionRefused, errCliDestroying, errCliInvalidParamNumber, errCliCannotChangeParam, p_u16_LocalPort, p_u16_RemotePort, p_i32_PingTimeout, p_i32_SendTimeout, p_i32_RecvTimeout, p_i32_WorkInterval, p_u16_SrcRef, p_u16_DstRef, p_u16_SrcTSap, p_i32_PDURequest, p_i32_MaxClients, p_i32_BSendTimeout, p_i32_BRecvTimeout, p_u32_RecoveryTime, p_u32_KeepAliveTime, MAX_VARS, DEFAULT_PDU_REQUESTED, DEFAULT_PDU_MIN, Block_OB, Block_DB, Block_SDB, Block_FC, Block_SFC, Block_FB, Block_SFB, SubBlk_OB, SubBlk_DB, SubBlk_SDB, SubBlk_FC, SubBlk_SFC, SubBlk_FB, SubBlk_SFB, BlockLangAWL, BlockLangKOP, BlockLangFUP, BlockLangSCL, BlockLangDB, BlockLangGRAPH, } from './protocol/constants.js'; import { getByteCount, getTransportSize, readFixedString } from './utils/buffer-utils.js'; import { cliErrorText } from './utils/error-text.js'; // ---- Type Definitions ---- export interface S7OrderCode { Code: string; V1: number; V2: number; V3: number; } export interface S7CpuInfo { ModuleTypeName: string; SerialNumber: string; ASName: string; Copyright: string; ModuleName: string; } export interface S7CpInfo { MaxPduLength: number; MaxConnections: number; MaxMpiRate: number; MaxBusRate: number; } export interface S7Protection { Level: number; Access: number; Password: string; } export interface S7BlockInfo { BlkType: number; BlkNumber: number; BlkLang: number; BlkFlags: number; MC7Size: number; LoadSize: number; LocalData: number; SBBLength: number; CheckSum: number; Version: number; CodeDate: string; InterfaceDate: string; Author: string; Family: string; Header: string; } export interface S7BlocksList { OBCount: number; FBCount: number; FCCount: number; DBCount: number; SDBCount: number; SFCCount: number; SFBCount: number; } export interface S7SZL { Head: { SZL_ID: number; SZL_Index: number }; Data: Buffer; } export type S7CpuStatus = typeof S7CpuStatusRun | typeof S7CpuStatusStop | typeof S7CpuStatusUnknown; type Callback = ((err: number | null, result?: T) => void) | undefined; // ---- Helper: callback or promise ---- function maybeCallback(cb: Callback, err: number, result?: T): void { if (cb) cb(err === 0 ? null : err, result); } // ---- S7Client ---- export class S7Client { // ---- Static constants (node-snap7 compatible) ---- static readonly S7AreaPE = S7AreaPE; static readonly S7AreaPA = S7AreaPA; static readonly S7AreaMK = S7AreaMK; static readonly S7AreaDB = S7AreaDB; static readonly S7AreaCT = S7AreaCT; static readonly S7AreaTM = S7AreaTM; static readonly S7WLBit = S7WLBit; static readonly S7WLByte = S7WLByte; static readonly S7WLWord = S7WLWord; static readonly S7WLDWord = S7WLDWord; static readonly S7WLReal = S7WLReal; static readonly S7WLCounter = S7WLCounter; static readonly S7WLTimer = S7WLTimer; static readonly CONNTYPE_PG = CONNTYPE_PG; static readonly CONNTYPE_OP = CONNTYPE_OP; static readonly CONNTYPE_BASIC = CONNTYPE_BASIC; static readonly Block_OB = Block_OB; static readonly Block_DB = Block_DB; static readonly Block_SDB = Block_SDB; static readonly Block_FC = Block_FC; static readonly Block_SFC = Block_SFC; static readonly Block_FB = Block_FB; static readonly Block_SFB = Block_SFB; static readonly SubBlk_OB = SubBlk_OB; static readonly SubBlk_DB = SubBlk_DB; static readonly SubBlk_SDB = SubBlk_SDB; static readonly SubBlk_FC = SubBlk_FC; static readonly SubBlk_SFC = SubBlk_SFC; static readonly SubBlk_FB = SubBlk_FB; static readonly SubBlk_SFB = SubBlk_SFB; static readonly BlockLangAWL = BlockLangAWL; static readonly BlockLangKOP = BlockLangKOP; static readonly BlockLangFUP = BlockLangFUP; static readonly BlockLangSCL = BlockLangSCL; static readonly BlockLangDB = BlockLangDB; static readonly BlockLangGRAPH = BlockLangGRAPH; static readonly S7CpuStatusUnknown = S7CpuStatusUnknown; static readonly S7CpuStatusRun = S7CpuStatusRun; static readonly S7CpuStatusStop = S7CpuStatusStop; // Error constants static readonly errIsoConnect = errIsoConnect; static readonly errIsoDisconnect = errIsoDisconnect; static readonly errIsoInvalidPDU = errIsoInvalidPDU; static readonly errIsoInvalidDataSize = errIsoInvalidDataSize; static readonly errIsoNullPointer = errIsoNullPointer; static readonly errIsoShortPacket = errIsoShortPacket; static readonly errIsoTooManyFragments = errIsoTooManyFragments; static readonly errIsoPduOverflow = errIsoPduOverflow; static readonly errIsoSendPacket = errIsoSendPacket; static readonly errIsoRecvPacket = errIsoRecvPacket; static readonly errIsoInvalidParams = errIsoInvalidParams; static readonly errNegotiatingPDU = errNegotiatingPDU; static readonly errCliInvalidParams = errCliInvalidParams; static readonly errCliJobPending = errCliJobPending; static readonly errCliTooManyItems = errCliTooManyItems; static readonly errCliInvalidWordLen = errCliInvalidWordLen; static readonly errCliPartialDataWritten = errCliPartialDataWritten; static readonly errCliSizeOverPDU = errCliSizeOverPDU; static readonly errCliInvalidPlcAnswer = errCliInvalidPlcAnswer; static readonly errCliAddressOutOfRange = errCliAddressOutOfRange; static readonly errCliInvalidTransportSize = errCliInvalidTransportSize; static readonly errCliWriteDataSizeMismatch = errCliWriteDataSizeMismatch; static readonly errCliItemNotAvailable = errCliItemNotAvailable; static readonly errCliInvalidValue = errCliInvalidValue; static readonly errCliCannotStartPLC = errCliCannotStartPLC; static readonly errCliAlreadyRun = errCliAlreadyRun; static readonly errCliCannotStopPLC = errCliCannotStopPLC; static readonly errCliCannotCopyRamToRom = errCliCannotCopyRamToRom; static readonly errCliCannotCompress = errCliCannotCompress; static readonly errCliAlreadyStop = errCliAlreadyStop; static readonly errCliFunNotAvailable = errCliFunNotAvailable; static readonly errCliUploadSequenceFailed = errCliUploadSequenceFailed; static readonly errCliInvalidDataSizeRecvd = errCliInvalidDataSizeRecvd; static readonly errCliInvalidBlockType = errCliInvalidBlockType; static readonly errCliInvalidBlockNumber = errCliInvalidBlockNumber; static readonly errCliInvalidBlockSize = errCliInvalidBlockSize; static readonly errCliDownloadSequenceFailed = errCliDownloadSequenceFailed; static readonly errCliInsertRefused = errCliInsertRefused; static readonly errCliDeleteRefused = errCliDeleteRefused; static readonly errCliNeedPassword = errCliNeedPassword; static readonly errCliInvalidPassword = errCliInvalidPassword; static readonly errCliNoPasswordToSetOrClear = errCliNoPasswordToSetOrClear; static readonly errCliJobTimeout = errCliJobTimeout; static readonly errCliPartialDataRead = errCliPartialDataRead; static readonly errCliBufferTooSmall = errCliBufferTooSmall; static readonly errCliFunctionRefused = errCliFunctionRefused; static readonly errCliDestroying = errCliDestroying; static readonly errCliInvalidParamNumber = errCliInvalidParamNumber; static readonly errCliCannotChangeParam = errCliCannotChangeParam; // Param number constants static readonly p_u16_LocalPort = p_u16_LocalPort; static readonly p_u16_RemotePort = p_u16_RemotePort; static readonly p_i32_PingTimeout = p_i32_PingTimeout; static readonly p_i32_SendTimeout = p_i32_SendTimeout; static readonly p_i32_RecvTimeout = p_i32_RecvTimeout; static readonly p_i32_WorkInterval = p_i32_WorkInterval; static readonly p_u16_SrcRef = p_u16_SrcRef; static readonly p_u16_DstRef = p_u16_DstRef; static readonly p_u16_SrcTSap = p_u16_SrcTSap; static readonly p_i32_PDURequest = p_i32_PDURequest; static readonly p_i32_MaxClients = p_i32_MaxClients; static readonly p_i32_BSendTimeout = p_i32_BSendTimeout; static readonly p_i32_BRecvTimeout = p_i32_BRecvTimeout; static readonly p_u32_RecoveryTime = p_u32_RecoveryTime; static readonly p_u32_KeepAliveTime = p_u32_KeepAliveTime; // ---- Instance constants (node-snap7 compatible via prototype) ---- readonly S7AreaPE = S7AreaPE; readonly S7AreaPA = S7AreaPA; readonly S7AreaMK = S7AreaMK; readonly S7AreaDB = S7AreaDB; readonly S7AreaCT = S7AreaCT; readonly S7AreaTM = S7AreaTM; readonly S7WLBit = S7WLBit; readonly S7WLByte = S7WLByte; readonly S7WLWord = S7WLWord; readonly S7WLDWord = S7WLDWord; readonly S7WLReal = S7WLReal; readonly S7WLCounter = S7WLCounter; readonly S7WLTimer = S7WLTimer; readonly CONNTYPE_PG = CONNTYPE_PG; readonly CONNTYPE_OP = CONNTYPE_OP; readonly CONNTYPE_BASIC = CONNTYPE_BASIC; readonly Block_OB = Block_OB; readonly Block_DB = Block_DB; readonly Block_SDB = Block_SDB; readonly Block_FC = Block_FC; readonly Block_SFC = Block_SFC; readonly Block_FB = Block_FB; readonly Block_SFB = Block_SFB; readonly SubBlk_OB = SubBlk_OB; readonly SubBlk_DB = SubBlk_DB; readonly SubBlk_SDB = SubBlk_SDB; readonly SubBlk_FC = SubBlk_FC; readonly SubBlk_SFC = SubBlk_SFC; readonly SubBlk_FB = SubBlk_FB; readonly SubBlk_SFB = SubBlk_SFB; readonly BlockLangAWL = BlockLangAWL; readonly BlockLangKOP = BlockLangKOP; readonly BlockLangFUP = BlockLangFUP; readonly BlockLangSCL = BlockLangSCL; readonly BlockLangDB = BlockLangDB; readonly BlockLangGRAPH = BlockLangGRAPH; readonly S7CpuStatusUnknown = S7CpuStatusUnknown; readonly S7CpuStatusRun = S7CpuStatusRun; readonly S7CpuStatusStop = S7CpuStatusStop; readonly errIsoConnect = errIsoConnect; readonly errIsoDisconnect = errIsoDisconnect; readonly errIsoInvalidPDU = errIsoInvalidPDU; readonly errIsoInvalidDataSize = errIsoInvalidDataSize; readonly errIsoNullPointer = errIsoNullPointer; readonly errIsoShortPacket = errIsoShortPacket; readonly errIsoTooManyFragments = errIsoTooManyFragments; readonly errIsoPduOverflow = errIsoPduOverflow; readonly errIsoSendPacket = errIsoSendPacket; readonly errIsoRecvPacket = errIsoRecvPacket; readonly errIsoInvalidParams = errIsoInvalidParams; readonly errNegotiatingPDU = errNegotiatingPDU; readonly errCliInvalidParams = errCliInvalidParams; readonly errCliJobPending = errCliJobPending; readonly errCliTooManyItems = errCliTooManyItems; readonly errCliInvalidWordLen = errCliInvalidWordLen; readonly errCliPartialDataWritten = errCliPartialDataWritten; readonly errCliSizeOverPDU = errCliSizeOverPDU; readonly errCliInvalidPlcAnswer = errCliInvalidPlcAnswer; readonly errCliAddressOutOfRange = errCliAddressOutOfRange; readonly errCliInvalidTransportSize = errCliInvalidTransportSize; readonly errCliWriteDataSizeMismatch = errCliWriteDataSizeMismatch; readonly errCliItemNotAvailable = errCliItemNotAvailable; readonly errCliInvalidValue = errCliInvalidValue; readonly errCliCannotStartPLC = errCliCannotStartPLC; readonly errCliAlreadyRun = errCliAlreadyRun; readonly errCliCannotStopPLC = errCliCannotStopPLC; readonly errCliCannotCopyRamToRom = errCliCannotCopyRamToRom; readonly errCliCannotCompress = errCliCannotCompress; readonly errCliAlreadyStop = errCliAlreadyStop; readonly errCliFunNotAvailable = errCliFunNotAvailable; readonly errCliUploadSequenceFailed = errCliUploadSequenceFailed; readonly errCliInvalidDataSizeRecvd = errCliInvalidDataSizeRecvd; readonly errCliInvalidBlockType = errCliInvalidBlockType; readonly errCliInvalidBlockNumber = errCliInvalidBlockNumber; readonly errCliInvalidBlockSize = errCliInvalidBlockSize; readonly errCliDownloadSequenceFailed = errCliDownloadSequenceFailed; readonly errCliInsertRefused = errCliInsertRefused; readonly errCliDeleteRefused = errCliDeleteRefused; readonly errCliNeedPassword = errCliNeedPassword; readonly errCliInvalidPassword = errCliInvalidPassword; readonly errCliNoPasswordToSetOrClear = errCliNoPasswordToSetOrClear; readonly errCliJobTimeout = errCliJobTimeout; readonly errCliPartialDataRead = errCliPartialDataRead; readonly errCliBufferTooSmall = errCliBufferTooSmall; readonly errCliFunctionRefused = errCliFunctionRefused; readonly errCliDestroying = errCliDestroying; readonly errCliInvalidParamNumber = errCliInvalidParamNumber; readonly errCliCannotChangeParam = errCliCannotChangeParam; readonly p_u16_LocalPort = p_u16_LocalPort; readonly p_u16_RemotePort = p_u16_RemotePort; readonly p_i32_PingTimeout = p_i32_PingTimeout; readonly p_i32_SendTimeout = p_i32_SendTimeout; readonly p_i32_RecvTimeout = p_i32_RecvTimeout; readonly p_i32_WorkInterval = p_i32_WorkInterval; readonly p_u16_SrcRef = p_u16_SrcRef; readonly p_u16_DstRef = p_u16_DstRef; readonly p_u16_SrcTSap = p_u16_SrcTSap; readonly p_i32_PDURequest = p_i32_PDURequest; readonly p_i32_MaxClients = p_i32_MaxClients; readonly p_i32_BSendTimeout = p_i32_BSendTimeout; readonly p_i32_BRecvTimeout = p_i32_BRecvTimeout; readonly p_u32_RecoveryTime = p_u32_RecoveryTime; readonly p_u32_KeepAliveTime = p_u32_KeepAliveTime; // ---- Private state ---- private transport = new TcpTransport(); private _connected = false; private _transportClosed = false; private _lastError = 0; private _lastTransportError = ''; private _pduLength = 0; private _pduRequested = DEFAULT_PDU_REQUESTED; private _execTime = 0; private _connType = CONNTYPE_PG; private _connNumber = 1; private _port = 102; private _ip = '127.0.0.1'; private _rack = 0; private _slot = 1; private _localTSAP = 0; private _remoteTSAP = 0; private _recvTimeout = 5000; private _sendTimeout = 5000; private _debug = false; private _opQueue: Promise = Promise.resolve(); private async withLock(fn: () => Promise): Promise { const prev = this._opQueue; let release!: () => void; this._opQueue = new Promise(r => { release = r; }); await prev; try { return await fn(); } finally { release(); } } // ---- 6.1 Connection Management ---- Connect(callback: Callback): void { this.ConnectTo(this._ip, this._rack, this._slot, callback); } ConnectTo(ip: string, rack: number, slot: number, callback: Callback): void { this.ConnectToPort(ip, 102, rack, slot, callback); } ConnectToPort(ip: string, port: number, rack: number, slot: number, callback?: Callback): void | Promise { const p = (async (): Promise => { try { this._ip = ip; this._port = port; this._rack = rack; this._slot = slot; this._remoteTSAP = computeRemoteTSAP(rack, slot); this._localTSAP = computeLocalTSAP(this._connType, this._connNumber); this._transportClosed = false; // Sync recv timeout to transport this.transport.setRecvTimeout(this._recvTimeout); // Listen for transport close event (connection lost) this.transport.removeAllListeners('close'); this.transport.on('close', () => { this._connected = false; this._transportClosed = true; this._lastError = errIsoDisconnect; }); // 1. TCP connect await this.transport.connect(ip, port, this._recvTimeout); // 2. COTP Connection Request const cr = buildCR(this._localTSAP, this._remoteTSAP); await this.transport.send(cr); // 3. Wait for COTP Connection Confirm const ccData = await this.transport.recv(); if (!isCC(ccData)) { this._lastError = errIsoConnect; return errIsoConnect; } // 4. S7 Setup Communication const setupReq = buildSetupComm(this._pduRequested); await this.transport.send(buildDTData(setupReq)); // 5. Wait for Setup Communication Response const setupResp = await this.recvS7Message(); const { pduNegotiated } = parseSetupCommResponse(setupResp); this._pduLength = pduNegotiated || DEFAULT_PDU_MIN; if (this._debug) console.log(`[s7js] Setup comm: pduNegotiated=${pduNegotiated}, pduLength=${this._pduLength}`); this._connected = true; this._lastError = 0; return 0; } catch (e) { this._lastError = errIsoConnect; return errIsoConnect; } })(); if (callback) { p.then(err => callback(err === 0 ? null : err)); } else { return p.then(err => { if (err !== 0) throw err; }); } } SetConnectionParams(ip: string, localTSAP: number, remoteTSAP: number): boolean { if (this._connected) return false; this._ip = ip; this._localTSAP = localTSAP; this._remoteTSAP = remoteTSAP; return true; } SetConnectionType(type: number): boolean { if (this._connected) return false; this._connType = type; return true; } SetDebug(enable: boolean): void { this._debug = enable; } Disconnect(): boolean { try { if (this._connected) { const dr = buildDR(); this.transport.send(dr).catch(() => {}); } } catch { // ignore } this.transport.disconnect(); this._connected = false; this._transportClosed = false; return true; } GetParam(paramNumber: number): number { switch (paramNumber) { case p_u16_LocalPort: return 102; case p_u16_RemotePort: return this._port; case p_i32_PingTimeout: return this._recvTimeout; case p_i32_SendTimeout: return this._sendTimeout; case p_i32_RecvTimeout: return this._recvTimeout; case p_u16_SrcRef: return 0x0001; case p_u16_DstRef: return 0x0001; case p_u16_SrcTSap: return this._localTSAP; case p_i32_PDURequest: return this._pduRequested; default: this._lastError = errCliInvalidParamNumber; return 0; } } SetParam(paramNumber: number, value: number): boolean { switch (paramNumber) { case p_i32_PingTimeout: this._recvTimeout = value; return true; case p_i32_SendTimeout: this._sendTimeout = value; return true; case p_i32_RecvTimeout: this._recvTimeout = value; this.transport.setRecvTimeout(value); return true; case p_i32_PDURequest: this._pduRequested = value; return true; case p_u16_SrcTSap: this._localTSAP = value; return true; default: this._lastError = errCliInvalidParamNumber; return false; } } // ---- 6.2 Data I/O Main Functions ---- ReadArea(area: number, dbNumber: number, start: number, amount: number, wordLen: number, callback?: Callback): void | Promise { if (!this._connected) { this._lastError = errIsoConnect; if (callback) { callback(errIsoConnect); return; } return Promise.reject(errIsoConnect); } const p = this.withLock(() => this.readAreaInternal(area, dbNumber, start, amount, wordLen)); if (callback) { p.then(data => callback(null, data)).catch(err => callback(this.handleError(err))); } else { return p; } } private async readAreaInternal(area: number, dbNumber: number, start: number, amount: number, wordLen: number): Promise { const bytesPerElement = getByteCount(wordLen); const totalBytes = amount * bytesPerElement; const maxBytes = this._pduLength - 18; // S7 Header(12) + Param(2) + DataHeader(4) = 18 const result = Buffer.alloc(totalBytes); let offset = 0; let elementsLeft = amount; let currentStart = start; while (elementsLeft > 0) { const chunkElements = Math.min(elementsLeft, Math.floor(maxBytes / bytesPerElement)); if (chunkElements <= 0) { this._lastError = errCliSizeOverPDU; throw errCliSizeOverPDU; } const req = buildReadArea(area, dbNumber, currentStart, chunkElements, wordLen); await this.transport.send(buildDTData(req)); const resp = await this.recvS7Message(); // Check for S7 error response (MsgType 0x02 with non-zero error class/code) if (isS7Error(resp)) { const s7Err = getS7ErrorCode(resp); this._lastError = s7Err || errCliInvalidPlcAnswer; if (this._debug) console.error(`[s7js] S7 error response: code=0x${s7Err.toString(16)}, area=0x${area.toString(16)}, db=${dbNumber}, start=${currentStart}, amount=${chunkElements}, resp=${resp.toString('hex')}`); throw this._lastError; } const parsed = parseReadAreaResponse(resp); if (parsed.result !== 0 && parsed.result !== 0xFF) { this._lastError = errCliInvalidPlcAnswer; if (this._debug) console.error(`[s7js] ReadArea item error: result=0x${parsed.result.toString(16)}, area=0x${area.toString(16)}, db=${dbNumber}, start=${currentStart}, amount=${chunkElements}`); throw errCliInvalidPlcAnswer; } const chunkBytes = chunkElements * bytesPerElement; if (parsed.data.length < chunkBytes && this._debug) { console.warn(`[s7js] ReadArea partial data: expected ${chunkBytes} bytes, got ${parsed.data.length}, area=0x${area.toString(16)}, db=${dbNumber}, start=${currentStart}`); } parsed.data.copy(result, offset, 0, Math.min(parsed.data.length, chunkBytes)); offset += chunkBytes; currentStart += chunkElements; elementsLeft -= chunkElements; } return result; } WriteArea(area: number, dbNumber: number, start: number, amount: number, wordLen: number, buffer: Buffer, callback?: Callback): void | Promise { if (!this._connected) { this._lastError = errIsoConnect; if (callback) { callback(errIsoConnect); return; } return Promise.reject(errIsoConnect); } const p = this.withLock(() => this.writeAreaInternal(area, dbNumber, start, amount, wordLen, buffer)); if (callback) { p.then(() => callback(null)).catch(err => callback(this.handleError(err))); } else { return p; } } private async writeAreaInternal(area: number, dbNumber: number, start: number, amount: number, wordLen: number, buffer: Buffer): Promise { const bytesPerElement = getByteCount(wordLen); const totalBytes = amount * bytesPerElement; const maxBytes = this._pduLength - 18 - 12 - 4; // PDU header + write param item + write data header let offset = 0; let elementsLeft = amount; let currentStart = start; while (elementsLeft > 0) { const chunkElements = Math.min(elementsLeft, Math.floor(maxBytes / bytesPerElement)); if (chunkElements <= 0) { this._lastError = errCliSizeOverPDU; throw errCliSizeOverPDU; } const chunkBytes = chunkElements * bytesPerElement; const chunkBuffer = buffer.subarray(offset, offset + chunkBytes); const req = buildWriteArea(area, dbNumber, currentStart, chunkElements, wordLen, Buffer.from(chunkBuffer)); await this.transport.send(buildDTData(req)); const resp = await this.recvS7Message(); const returnCode = parseWriteAreaResponse(resp); if (returnCode !== 0xFF && returnCode !== 0x00) { this._lastError = errCliInvalidPlcAnswer; throw errCliInvalidPlcAnswer; } offset += chunkBytes; currentStart += chunkElements; elementsLeft -= chunkElements; } } ReadMultiVars(items: S7DataItem[], callback?: Callback): void | Promise { if (!this._connected) { this._lastError = errIsoConnect; if (callback) { callback(errIsoConnect); return; } return Promise.reject(errIsoConnect); } if (items.length > MAX_VARS) { this._lastError = errCliTooManyItems; if (callback) { callback(errCliTooManyItems); return; } return Promise.reject(errCliTooManyItems); } const p = this.withLock(async (): Promise => { const req = buildReadMultiVars(items); await this.transport.send(buildDTData(req)); const resp = await this.recvS7Message(); return parseReadMultiVarsResponse(resp, items); }); if (callback) { p.then(result => callback(null, result)).catch(err => callback(this.handleError(err))); } else { return p; } } WriteMultiVars(items: S7DataItem[], callback?: Callback): void | Promise { if (!this._connected) { this._lastError = errIsoConnect; if (callback) { callback(errIsoConnect); return; } return Promise.reject(errIsoConnect); } if (items.length > MAX_VARS) { this._lastError = errCliTooManyItems; if (callback) { callback(errCliTooManyItems); return; } return Promise.reject(errCliTooManyItems); } const p = this.withLock(async (): Promise => { const req = buildWriteMultiVars(items); await this.transport.send(buildDTData(req)); const resp = await this.recvS7Message(); return parseWriteMultiVarsResponse(resp, items.length); }); if (callback) { p.then(result => callback(null, result)).catch(err => callback(this.handleError(err))); } else { return p; } } // ---- 6.3 Data I/O Convenience Functions ---- DBRead(dbNumber: number, start: number, size: number, callback?: Callback): void | Promise { return this.ReadArea(S7AreaDB, dbNumber, start, size, S7WLByte, callback); } DBWrite(dbNumber: number, start: number, size: number, buffer: Buffer, callback?: Callback): void | Promise { return this.WriteArea(S7AreaDB, dbNumber, start, size, S7WLByte, buffer, callback); } MBRead(start: number, size: number, callback?: Callback): void | Promise { return this.ReadArea(S7AreaMK, 0, start, size, S7WLByte, callback); } MBWrite(start: number, size: number, buffer: Buffer, callback?: Callback): void | Promise { return this.WriteArea(S7AreaMK, 0, start, size, S7WLByte, buffer, callback); } EBRead(start: number, size: number, callback?: Callback): void | Promise { return this.ReadArea(S7AreaPE, 0, start, size, S7WLByte, callback); } EBWrite(start: number, size: number, buffer: Buffer, callback?: Callback): void | Promise { return this.WriteArea(S7AreaPE, 0, start, size, S7WLByte, buffer, callback); } ABRead(start: number, size: number, callback?: Callback): void | Promise { return this.ReadArea(S7AreaPA, 0, start, size, S7WLByte, callback); } ABWrite(start: number, size: number, buffer: Buffer, callback?: Callback): void | Promise { return this.WriteArea(S7AreaPA, 0, start, size, S7WLByte, buffer, callback); } TMRead(start: number, size: number, callback?: Callback): void | Promise { return this.ReadArea(S7AreaTM, 0, start, size, S7WLTimer, callback); } TMWrite(start: number, size: number, buffer: Buffer, callback?: Callback): void | Promise { return this.WriteArea(S7AreaTM, 0, start, size, S7WLTimer, buffer, callback); } CTRead(start: number, size: number, callback?: Callback): void | Promise { return this.ReadArea(S7AreaCT, 0, start, size, S7WLCounter, callback); } CTWrite(start: number, size: number, buffer: Buffer, callback?: Callback): void | Promise { return this.WriteArea(S7AreaCT, 0, start, size, S7WLCounter, buffer, callback); } // ---- 6.4 Directory Functions ---- ListBlocks(callback?: Callback): void | Promise { if (!this._connected) { this._lastError = errIsoConnect; if (callback) { callback(errIsoConnect); return; } return Promise.reject(errIsoConnect); } const p = this.withLock(async (): Promise => { const req = buildListBlocks(); await this.transport.send(buildDTData(req)); const resp = await this.recvS7Message(); return this.parseBlocksList(resp); }); if (callback) { p.then(result => callback(null, result)).catch(err => callback(this.handleError(err))); } else { return p; } } private parseBlocksList(raw: Buffer): S7BlocksList { const resp = parseResponse(raw); const data = resp.data; // Skip SZL data header (4 bytes), then SZL list header (2 bytes), then 2-byte counts if (data.length < 30) { return { OBCount: 0, FBCount: 0, FCCount: 0, DBCount: 0, SDBCount: 0, SFCCount: 0, SFBCount: 0 }; } let offset = 4; // Skip data header (ReturnCode+TS+Length) // Skip SZL header offset += 2; // SZL header length return { OBCount: data.readUInt16BE(offset), FBCount: data.readUInt16BE(offset + 2), FCCount: data.readUInt16BE(offset + 4), DBCount: data.readUInt16BE(offset + 6), SDBCount: data.readUInt16BE(offset + 8), SFCCount: data.readUInt16BE(offset + 10), SFBCount: data.readUInt16BE(offset + 12), }; } GetAgBlockInfo(blockType: number, blockNum: number, callback?: Callback): void | Promise { if (!this._connected) { this._lastError = errIsoConnect; if (callback) { callback(errIsoConnect); return; } return Promise.reject(errIsoConnect); } const p = this.withLock(async (): Promise => { const req = buildGetAgBlockInfo(blockType, blockNum); await this.transport.send(buildDTData(req)); const resp = await this.recvS7Message(); return this.parseBlockInfo(resp); }); if (callback) { p.then(result => callback(null, result)).catch(err => callback(this.handleError(err))); } else { return p; } } private parseBlockInfo(raw: Buffer): S7BlockInfo { const resp = parseResponse(raw); const data = resp.data; // Skip data header (4 bytes) const d = data.subarray(4); if (d.length < 80) { this._lastError = errCliInvalidDataSizeRecvd; throw errCliInvalidDataSizeRecvd; } return { BlkType: d[0], BlkNumber: d.readUInt16BE(2), BlkLang: d[4], BlkFlags: d[5], MC7Size: d.readUInt32BE(6), LoadSize: d.readUInt32BE(10), LocalData: d.readUInt16BE(14), SBBLength: d.readUInt16BE(16), CheckSum: d.readUInt16BE(18), Version: d[20], CodeDate: readFixedString(d, 22, 8), InterfaceDate: readFixedString(d, 30, 8), Author: readFixedString(d, 38, 8), Family: readFixedString(d, 46, 8), Header: readFixedString(d, 54, 8), }; } GetPgBlockInfo(buffer: Buffer): S7BlockInfo { // Parse block info from a buffer (uploaded block header) return this.parseBlockInfoFromBuffer(buffer); } private parseBlockInfoFromBuffer(d: Buffer): S7BlockInfo { if (d.length < 80) { this._lastError = errCliInvalidDataSizeRecvd; throw errCliInvalidDataSizeRecvd; } return { BlkType: d[0], BlkNumber: d.readUInt16BE(2), BlkLang: d[4], BlkFlags: d[5], MC7Size: d.readUInt32BE(6), LoadSize: d.readUInt32BE(10), LocalData: d.readUInt16BE(14), SBBLength: d.readUInt16BE(16), CheckSum: d.readUInt16BE(18), Version: d[20], CodeDate: readFixedString(d, 22, 8), InterfaceDate: readFixedString(d, 30, 8), Author: readFixedString(d, 38, 8), Family: readFixedString(d, 46, 8), Header: readFixedString(d, 54, 8), }; } ListBlocksOfType(blockType: number, callback?: Callback): void | Promise { if (!this._connected) { this._lastError = errIsoConnect; if (callback) { callback(errIsoConnect); return; } return Promise.reject(errIsoConnect); } const p = this.withLock(async (): Promise => { const req = buildListBlocksOfType(blockType); await this.transport.send(buildDTData(req)); const resp = await this.recvS7Message(); return this.parseBlocksOfType(resp); }); if (callback) { p.then(result => callback(null, result)).catch(err => callback(this.handleError(err))); } else { return p; } } private parseBlocksOfType(raw: Buffer): number[] { const resp = parseResponse(raw); const data = resp.data; if (data.length <= 4) return []; let offset = 4; // Skip data header // Skip SZL header (2 bytes) if (offset + 2 > data.length) return []; const szlHeaderLen = data.readUInt16BE(offset); offset += szlHeaderLen; const blocks: number[] = []; while (offset + 2 <= data.length) { blocks.push(data.readUInt16BE(offset)); offset += 2; } return blocks; } // ---- 6.5 Block Functions ---- Upload(blockType: number, blockNum: number, callback?: Callback): void | Promise { return this.uploadInternal(blockType, blockNum, false, callback); } FullUpload(blockType: number, blockNum: number, callback?: Callback): void | Promise { return this.uploadInternal(blockType, blockNum, true, callback); } private uploadInternal(blockType: number, blockNum: number, full: boolean, callback?: Callback): void | Promise { if (!this._connected) { this._lastError = errIsoConnect; if (callback) { callback(errIsoConnect); return; } return Promise.reject(errIsoConnect); } const p = this.withLock(async (): Promise => { const req = full ? buildFullUpload(blockType, blockNum, 0) : buildUpload(blockType, blockNum); await this.transport.send(buildDTData(req)); const resp = await this.recvS7Message(); // Parse upload response to get block data const respParsed = parseResponse(resp); if (respParsed.errorClass !== 0 || respParsed.errorCode !== 0) { this._lastError = errCliUploadSequenceFailed; throw errCliUploadSequenceFailed; } // The response data contains the block if (respParsed.data.length > 4) { return Buffer.from(respParsed.data.subarray(4)); } return Buffer.alloc(0); }); if (callback) { p.then(result => callback(null, result)).catch(err => callback(this.handleError(err))); } else { return p; } } Download(blockNum: number, buffer: Buffer, callback?: Callback): void | Promise { if (!this._connected) { this._lastError = errIsoConnect; if (callback) { callback(errIsoConnect); return; } return Promise.reject(errIsoConnect); } const p = this.withLock(async (): Promise => { const req = buildDownload(blockNum, buffer); await this.transport.send(buildDTData(req)); const resp = await this.recvS7Message(); const respParsed = parseResponse(resp); if (respParsed.errorClass !== 0 || respParsed.errorCode !== 0) { this._lastError = errCliDownloadSequenceFailed; throw errCliDownloadSequenceFailed; } }); if (callback) { p.then(() => callback(null)).catch(err => callback(this.handleError(err))); } else { return p; } } Delete(blockType: number, blockNum: number, callback?: Callback): void | Promise { if (!this._connected) { this._lastError = errIsoConnect; if (callback) { callback(errIsoConnect); return; } return Promise.reject(errIsoConnect); } const p = this.withLock(async (): Promise => { const req = buildDelete(blockType, blockNum); await this.transport.send(buildDTData(req)); const resp = await this.recvS7Message(); const respParsed = parseResponse(resp); if (respParsed.errorClass !== 0 || respParsed.errorCode !== 0) { this._lastError = errCliDeleteRefused; throw errCliDeleteRefused; } }); if (callback) { p.then(() => callback(null)).catch(err => callback(this.handleError(err))); } else { return p; } } DBGet(dbNumber: number, callback?: Callback): void | Promise { if (!this._connected) { this._lastError = errIsoConnect; if (callback) { callback(errIsoConnect); return; } return Promise.reject(errIsoConnect); } const p = this.withLock(async (): Promise => { const req = buildDBGet(dbNumber); await this.transport.send(buildDTData(req)); const resp = await this.recvS7Message(); const respParsed = parseResponse(resp); if (respParsed.data.length > 4) { return Buffer.from(respParsed.data.subarray(4)); } return Buffer.alloc(0); }); if (callback) { p.then(result => callback(null, result)).catch(err => callback(this.handleError(err))); } else { return p; } } DBFill(dbNumber: number, fillChar: number, callback?: Callback): void | Promise { if (!this._connected) { this._lastError = errIsoConnect; if (callback) { callback(errIsoConnect); return; } return Promise.reject(errIsoConnect); } const p = this.withLock(async (): Promise => { const req = buildDBFill(dbNumber, fillChar); await this.transport.send(buildDTData(req)); const resp = await this.recvS7Message(); const respParsed = parseResponse(resp); if (respParsed.errorClass !== 0 || respParsed.errorCode !== 0) { this._lastError = errCliFunctionRefused; throw errCliFunctionRefused; } }); if (callback) { p.then(() => callback(null)).catch(err => callback(this.handleError(err))); } else { return p; } } // ---- 6.6 Date/Time Functions ---- GetPlcDateTime(callback?: Callback): void | Promise { if (!this._connected) { this._lastError = errIsoConnect; if (callback) { callback(errIsoConnect); return; } return Promise.reject(errIsoConnect); } const p = this.withLock(async (): Promise => { const req = buildGetPlcDateTime(); await this.transport.send(buildDTData(req)); const resp = await this.recvS7Message(); return parseGetPlcDateTimeResponse(resp); }); if (callback) { p.then(result => callback(null, result)).catch(err => callback(this.handleError(err))); } else { return p; } } SetPlcDateTime(dateTime: Date, callback?: Callback): void | Promise { if (!this._connected) { this._lastError = errIsoConnect; if (callback) { callback(errIsoConnect); return; } return Promise.reject(errIsoConnect); } const p = this.withLock(async (): Promise => { const req = buildSetPlcDateTime(dateTime); await this.transport.send(buildDTData(req)); await this.recvS7Message(); }); if (callback) { p.then(() => callback(null)).catch(err => callback(this.handleError(err))); } else { return p; } } SetPlcSystemDateTime(callback?: Callback): void | Promise { if (!this._connected) { this._lastError = errIsoConnect; if (callback) { callback(errIsoConnect); return; } return Promise.reject(errIsoConnect); } const p = this.withLock(async (): Promise => { const req = buildSetPlcSystemDateTime(); await this.transport.send(buildDTData(req)); await this.recvS7Message(); }); if (callback) { p.then(() => callback(null)).catch(err => callback(this.handleError(err))); } else { return p; } } // ---- 6.7 System Info Functions ---- ReadSZL(id: number, index: number, callback?: Callback): void | Promise { if (!this._connected) { this._lastError = errIsoConnect; if (callback) { callback(errIsoConnect); return; } return Promise.reject(errIsoConnect); } const p = this.withLock(async (): Promise => { const req = buildReadSZL(id, index); await this.transport.send(buildDTData(req)); const resp = await this.recvS7Message(); const szlData = parseReadSZLResponse(resp); return { Head: { SZL_ID: id, SZL_Index: index }, Data: szlData, }; }); if (callback) { p.then(result => callback(null, result)).catch(err => callback(this.handleError(err))); } else { return p; } } ReadSZLList(callback?: Callback): void | Promise { if (!this._connected) { this._lastError = errIsoConnect; if (callback) { callback(errIsoConnect); return; } return Promise.reject(errIsoConnect); } const p = this.withLock(async (): Promise => { const req = buildReadSZLList(); await this.transport.send(buildDTData(req)); const resp = await this.recvS7Message(); return parseReadSZLResponse(resp); }); if (callback) { p.then(result => callback(null, result)).catch(err => callback(this.handleError(err))); } else { return p; } } GetOrderCode(callback?: Callback): void | Promise { if (!this._connected) { this._lastError = errIsoConnect; if (callback) { callback(errIsoConnect); return; } return Promise.reject(errIsoConnect); } const p = this.withLock(async (): Promise => { const req = buildGetOrderCode(); await this.transport.send(buildDTData(req)); const resp = await this.recvS7Message(); const szlData = parseReadSZLResponse(resp); // SZL 0x0011 layout: OrderCode(20) + V1(1) + V2(1) + V3(1) if (szlData.length < 8) { return { Code: '', V1: 0, V2: 0, V3: 0 }; } let offset = 2; // Skip SZL header return { Code: readFixedString(szlData, offset, 20), V1: szlData[offset + 20], V2: szlData[offset + 21], V3: szlData[offset + 22], }; }); if (callback) { p.then(result => callback(null, result)).catch(err => callback(this.handleError(err))); } else { return p; } } GetCpuInfo(callback?: Callback): void | Promise { if (!this._connected) { this._lastError = errIsoConnect; if (callback) { callback(errIsoConnect); return; } return Promise.reject(errIsoConnect); } const p = this.withLock(async (): Promise => { const req = buildGetCpuInfo(); await this.transport.send(buildDTData(req)); const resp = await this.recvS7Message(); const szlData = parseReadSZLResponse(resp); // SZL 0x001C layout: ModuleTypeName(32) + SerialNumber(24) + ASName(24) + Copyright(26) + ModuleName(24) if (szlData.length < 2) { return { ModuleTypeName: '', SerialNumber: '', ASName: '', Copyright: '', ModuleName: '' }; } let offset = 2; // Skip SZL header return { ModuleTypeName: readFixedString(szlData, offset, 32), SerialNumber: readFixedString(szlData, offset + 32, 24), ASName: readFixedString(szlData, offset + 56, 24), Copyright: readFixedString(szlData, offset + 80, 26), ModuleName: readFixedString(szlData, offset + 106, 24), }; }); if (callback) { p.then(result => callback(null, result)).catch(err => callback(this.handleError(err))); } else { return p; } } GetCpInfo(callback?: Callback): void | Promise { if (!this._connected) { this._lastError = errIsoConnect; if (callback) { callback(errIsoConnect); return; } return Promise.reject(errIsoConnect); } const p = this.withLock(async (): Promise => { const req = buildGetCpInfo(); await this.transport.send(buildDTData(req)); const resp = await this.recvS7Message(); const szlData = parseReadSZLResponse(resp); if (szlData.length < 10) { return { MaxPduLength: 0, MaxConnections: 0, MaxMpiRate: 0, MaxBusRate: 0 }; } let offset = 2; // Skip SZL header return { MaxPduLength: szlData.readUInt16BE(offset), MaxConnections: szlData.readUInt16BE(offset + 2), MaxMpiRate: szlData.readUInt16BE(offset + 4), MaxBusRate: szlData.readUInt16BE(offset + 6), }; }); if (callback) { p.then(result => callback(null, result)).catch(err => callback(this.handleError(err))); } else { return p; } } // ---- 6.8 PLC Control & Security & Properties ---- PlcHotStart(callback?: Callback): void | Promise { if (!this._connected) { this._lastError = errIsoConnect; if (callback) { callback(errIsoConnect); return; } return Promise.reject(errIsoConnect); } const p = this.withLock(async (): Promise => { const req = buildPlcHotStart(); await this.transport.send(buildDTData(req)); const resp = await this.recvS7Message(); const respParsed = parseResponse(resp); if (respParsed.errorClass !== 0 || respParsed.errorCode !== 0) { this._lastError = errCliCannotStartPLC; throw errCliCannotStartPLC; } }); if (callback) { p.then(() => callback(null)).catch(err => callback(this.handleError(err))); } else { return p; } } PlcColdStart(callback?: Callback): void | Promise { if (!this._connected) { this._lastError = errIsoConnect; if (callback) { callback(errIsoConnect); return; } return Promise.reject(errIsoConnect); } const p = this.withLock(async (): Promise => { const req = buildPlcColdStart(); await this.transport.send(buildDTData(req)); const resp = await this.recvS7Message(); const respParsed = parseResponse(resp); if (respParsed.errorClass !== 0 || respParsed.errorCode !== 0) { this._lastError = errCliCannotStartPLC; throw errCliCannotStartPLC; } }); if (callback) { p.then(() => callback(null)).catch(err => callback(this.handleError(err))); } else { return p; } } PlcStop(callback?: Callback): void | Promise { if (!this._connected) { this._lastError = errIsoConnect; if (callback) { callback(errIsoConnect); return; } return Promise.reject(errIsoConnect); } const p = this.withLock(async (): Promise => { const req = buildPlcStop(); await this.transport.send(buildDTData(req)); const resp = await this.recvS7Message(); const respParsed = parseResponse(resp); if (respParsed.errorClass !== 0 || respParsed.errorCode !== 0) { this._lastError = errCliCannotStopPLC; throw errCliCannotStopPLC; } }); if (callback) { p.then(() => callback(null)).catch(err => callback(this.handleError(err))); } else { return p; } } CopyRamToRom(timeout: number, callback?: Callback): void | Promise { if (!this._connected) { this._lastError = errIsoConnect; if (callback) { callback(errIsoConnect); return; } return Promise.reject(errIsoConnect); } const p = this.withLock(async (): Promise => { const req = buildCopyRamToRom(timeout); await this.transport.send(buildDTData(req)); const resp = await this.recvS7Message(); const respParsed = parseResponse(resp); if (respParsed.errorClass !== 0 || respParsed.errorCode !== 0) { this._lastError = errCliCannotCopyRamToRom; throw errCliCannotCopyRamToRom; } }); if (callback) { p.then(() => callback(null)).catch(err => callback(this.handleError(err))); } else { return p; } } Compress(timeout: number, callback?: Callback): void | Promise { if (!this._connected) { this._lastError = errIsoConnect; if (callback) { callback(errIsoConnect); return; } return Promise.reject(errIsoConnect); } const p = this.withLock(async (): Promise => { const req = buildCompress(timeout); await this.transport.send(buildDTData(req)); const resp = await this.recvS7Message(); const respParsed = parseResponse(resp); if (respParsed.errorClass !== 0 || respParsed.errorCode !== 0) { this._lastError = errCliCannotCompress; throw errCliCannotCompress; } }); if (callback) { p.then(() => callback(null)).catch(err => callback(this.handleError(err))); } else { return p; } } GetProtection(callback?: Callback): void | Promise { if (!this._connected) { this._lastError = errIsoConnect; if (callback) { callback(errIsoConnect); return; } return Promise.reject(errIsoConnect); } const p = this.withLock(async (): Promise => { const req = buildGetProtection(); await this.transport.send(buildDTData(req)); const resp = await this.recvS7Message(); const szlData = parseReadSZLResponse(resp); if (szlData.length < 8) { return { Level: 0, Access: 0, Password: '' }; } let offset = 2; // Skip SZL header return { Level: szlData[offset], Access: szlData[offset + 1], Password: readFixedString(szlData, offset + 2, 8), }; }); if (callback) { p.then(result => callback(null, result)).catch(err => callback(this.handleError(err))); } else { return p; } } SetSessionPassword(password: string, callback?: Callback): void | Promise { if (!this._connected) { this._lastError = errIsoConnect; if (callback) { callback(errIsoConnect); return; } return Promise.reject(errIsoConnect); } const p = this.withLock(async (): Promise => { const req = buildSetPassword(password); await this.transport.send(buildDTData(req)); const resp = await this.recvS7Message(); const respParsed = parseResponse(resp); if (respParsed.errorClass !== 0 || respParsed.errorCode !== 0) { this._lastError = errCliInvalidPassword; throw errCliInvalidPassword; } }); if (callback) { p.then(() => callback(null)).catch(err => callback(this.handleError(err))); } else { return p; } } ClearSessionPassword(callback?: Callback): void | Promise { if (!this._connected) { this._lastError = errIsoConnect; if (callback) { callback(errIsoConnect); return; } return Promise.reject(errIsoConnect); } const p = this.withLock(async (): Promise => { const req = buildClearPassword(); await this.transport.send(buildDTData(req)); const resp = await this.recvS7Message(); const respParsed = parseResponse(resp); if (respParsed.errorClass !== 0 || respParsed.errorCode !== 0) { this._lastError = errCliNoPasswordToSetOrClear; throw errCliNoPasswordToSetOrClear; } }); if (callback) { p.then(() => callback(null)).catch(err => callback(this.handleError(err))); } else { return p; } } // ---- Properties ---- ExecTime(): number { return this._execTime; } LastError(): number { return this._lastError; } PDURequested(): number { return this._pduRequested; } PDULength(): number { return this._pduLength; } PlcStatus(callback?: Callback): void | Promise { if (!this._connected) { this._lastError = errIsoConnect; if (callback) { callback(errIsoConnect); return; } return Promise.reject(errIsoConnect); } const p = this.withLock(async (): Promise => { const req = buildGetPlcStatus(); await this.transport.send(buildDTData(req)); const resp = await this.recvS7Message(); return parseGetPlcStatusResponse(resp); }); if (callback) { p.then(result => callback(null, result)).catch(err => callback(this.handleError(err))); } else { return p; } } Connected(): boolean { return this._connected; } ErrorText(errNum: number): string { const base = cliErrorText(errNum); if (this._lastTransportError && (errNum === errIsoConnect || errNum === errIsoDisconnect || errNum === errIsoRecvPacket)) { return `${base}: ${this._lastTransportError}`; } return base; } // ---- Internal Helpers ---- private async recvS7Message(): Promise { try { const cotpData = await this.transport.recv(); if (!isDT(cotpData)) { if (isDR(cotpData)) { this._connected = false; this._lastError = errIsoDisconnect; throw errIsoDisconnect; } this._lastError = errIsoInvalidPDU; throw errIsoInvalidPDU; } const s7Pdu = extractDTPayload(cotpData); if (this._debug && s7Pdu.length > 0) { const msgType = s7Pdu[1]; console.log(`[s7js] recv S7: msgType=0x${msgType.toString(16)}, len=${s7Pdu.length}, hdr=${s7Pdu.subarray(0, Math.min(16, s7Pdu.length)).toString('hex')}`); } return s7Pdu; } catch (e) { if (e instanceof Error) { this._lastTransportError = e.message; this._connected = false; const errCode = this.mapTransportError(e); this._lastError = errCode; throw errCode; } throw e; } } private mapTransportError(e: Error): number { const msg = e.message.toLowerCase(); if (msg.includes('connection reset') || msg.includes('connection refused') || msg.includes('econnreset')) return errIsoDisconnect; if (msg.includes('timeout') || msg.includes('timed out')) return errIsoRecvPacket; if (msg.includes('not connected') || msg.includes('disconnected')) return errIsoConnect; return errIsoConnect; } private handleError(err: unknown): number { if (typeof err === 'number') return err; if (err instanceof Error) { this._lastTransportError = err.message; this._connected = false; return this.mapTransportError(err); } return errIsoConnect; } } // ---- Helper: Wrap S7 PDU in COTP DT frame ---- function buildDTData(s7Pdu: Buffer): Buffer { const dt = Buffer.alloc(3); dt[0] = 0x02; // LI dt[1] = 0xF0 | 0x08; // COTP DT + EOT (bit 3) dt[2] = 0x80; // TPDU-NR: ROA bit set, sequence = 0 return Buffer.concat([dt, s7Pdu]); }