import { EventEmitter } from 'events'; import { Server, Socket } from 'net'; import { encodeTPKT, parseTPKTHeader, isValidTPKTHeader } from './protocol/tpkt.js'; import { buildCC, isCR, isDT, isDTEot, extractDTPayload, buildDR, } from './protocol/cotp.js'; import { parseResponse, buildSetupComm, parseSetupCommResponse, buildReadArea, parseReadAreaResponse, buildWriteArea, parseWriteAreaResponse, S7Response, } from './protocol/s7-protocol.js'; import { S7_PROTOCOL_ID, S7_MSG_TYPE_REQUEST, S7_MSG_TYPE_RESPONSE, S7_MSG_TYPE_RESPONSE_DATA, S7_MSG_TYPE_USERDATA, S7_FN_SETUP_COMM, S7_FN_READ_AREA, S7_FN_WRITE_AREA, S7AreaPE, S7AreaPA, S7AreaMK, S7AreaDB, S7AreaCT, S7AreaTM, S7WLBit, S7WLByte, S7WLWord, S7WLDWord, S7WLReal, S7WLCounter, S7WLTimer, S7CpuStatusRun, S7CpuStatusStop, S7CpuStatusUnknown, srvAreaPE, srvAreaPA, srvAreaMK, srvAreaCT, srvAreaTM, srvAreaDB, OperationRead, OperationWrite, evcServerStarted, evcServerStopped, evcListenerCannotStart, evcClientAdded, evcClientRejected, evcClientNoRoom, evcClientException, evcClientDisconnected, evcClientTerminated, evcClientsDropped, evcPDUincoming, evcDataRead, evcDataWrite, evcNegotiatePDU, evcReadSZL, evcClock, evcUpload, evcDownload, evcDirectory, evcSecurity, evcControl, evcAll, evcNone, errSrvCannotStart, errSrvDBNullPointer, errSrvAreaAlreadyExists, errSrvUnknownArea, errSrvInvalidParams, errSrvTooManyDB, errSrvInvalidParamNumber, errSrvCannotChangeParam, 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, 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, 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, SrvStopped, SrvRunning, SrvError, } from './protocol/constants.js'; import { getByteCount } from './utils/buffer-utils.js'; import { srvErrorText, srvEventText } from './utils/error-text.js'; // ---- Types ---- export interface S7ServerEvent { EvtCode: number; EvtRetCode: number; EvtParam1: number; EvtParam2: number; } export interface S7ReadWriteEvent { Sender: number; Area: number; DBNumber: number; Start: number; Size: number; Data: Buffer; Operation: number; // OperationRead or OperationWrite } type ServerCallback = ((err: number | null) => void) | undefined; // ---- Area Storage ---- interface AreaInfo { index: number; buffer: Buffer; } // ---- S7Server ---- export class S7Server extends EventEmitter { // ---- Static constants ---- static readonly srvAreaPE = srvAreaPE; static readonly srvAreaPA = srvAreaPA; static readonly srvAreaMK = srvAreaMK; static readonly srvAreaCT = srvAreaCT; static readonly srvAreaTM = srvAreaTM; static readonly srvAreaDB = srvAreaDB; static readonly OperationRead = OperationRead; static readonly OperationWrite = OperationWrite; static readonly evcServerStarted = evcServerStarted; static readonly evcServerStopped = evcServerStopped; static readonly evcListenerCannotStart = evcListenerCannotStart; static readonly evcClientAdded = evcClientAdded; static readonly evcClientRejected = evcClientRejected; static readonly evcClientNoRoom = evcClientNoRoom; static readonly evcClientException = evcClientException; static readonly evcClientDisconnected = evcClientDisconnected; static readonly evcClientTerminated = evcClientTerminated; static readonly evcClientsDropped = evcClientsDropped; static readonly evcPDUincoming = evcPDUincoming; static readonly evcDataRead = evcDataRead; static readonly evcDataWrite = evcDataWrite; static readonly evcNegotiatePDU = evcNegotiatePDU; static readonly evcReadSZL = evcReadSZL; static readonly evcClock = evcClock; static readonly evcUpload = evcUpload; static readonly evcDownload = evcDownload; static readonly evcDirectory = evcDirectory; static readonly evcSecurity = evcSecurity; static readonly evcControl = evcControl; static readonly evcAll = evcAll; static readonly evcNone = evcNone; static readonly errSrvCannotStart = errSrvCannotStart; static readonly errSrvDBNullPointer = errSrvDBNullPointer; static readonly errSrvAreaAlreadyExists = errSrvAreaAlreadyExists; static readonly errSrvUnknownArea = errSrvUnknownArea; static readonly errSrvInvalidParams = errSrvInvalidParams; static readonly errSrvTooManyDB = errSrvTooManyDB; static readonly errSrvInvalidParamNumber = errSrvInvalidParamNumber; static readonly errSrvCannotChangeParam = errSrvCannotChangeParam; 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 S7CpuStatusRun = S7CpuStatusRun; static readonly S7CpuStatusStop = S7CpuStatusStop; static readonly S7CpuStatusUnknown = S7CpuStatusUnknown; static readonly SrvStopped = SrvStopped; static readonly SrvRunning = SrvRunning; static readonly SrvError = SrvError; 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; // ---- Instance constants ---- readonly srvAreaPE = srvAreaPE; readonly srvAreaPA = srvAreaPA; readonly srvAreaMK = srvAreaMK; readonly srvAreaCT = srvAreaCT; readonly srvAreaTM = srvAreaTM; readonly srvAreaDB = srvAreaDB; readonly OperationRead = OperationRead; readonly OperationWrite = OperationWrite; readonly evcServerStarted = evcServerStarted; readonly evcServerStopped = evcServerStopped; readonly evcListenerCannotStart = evcListenerCannotStart; readonly evcClientAdded = evcClientAdded; readonly evcClientRejected = evcClientRejected; readonly evcClientNoRoom = evcClientNoRoom; readonly evcClientException = evcClientException; readonly evcClientDisconnected = evcClientDisconnected; readonly evcClientTerminated = evcClientTerminated; readonly evcClientsDropped = evcClientsDropped; readonly evcPDUincoming = evcPDUincoming; readonly evcDataRead = evcDataRead; readonly evcDataWrite = evcDataWrite; readonly evcNegotiatePDU = evcNegotiatePDU; readonly evcReadSZL = evcReadSZL; readonly evcClock = evcClock; readonly evcUpload = evcUpload; readonly evcDownload = evcDownload; readonly evcDirectory = evcDirectory; readonly evcSecurity = evcSecurity; readonly evcControl = evcControl; readonly evcAll = evcAll; readonly evcNone = evcNone; // ---- Private state ---- private server: Server | null = null; private clients = new Map(); private nextClientId = 1; private _lastError = 0; private _eventsMask = evcAll; private _cpuStatus = S7CpuStatusRun; private _resourceless = false; private _maxClients = 256; private _pduLength = DEFAULT_PDU_REQUESTED; // Area storage: areaType → index → Buffer private areas = new Map>(); // ---- Server Management ---- Start(callback?: ServerCallback): void { this.StartTo('0.0.0.0', callback); } StartTo(ip: string, callback?: ServerCallback): void { this.StartToPort(ip, 102, callback); } StartToPort(ip: string, port: number, callback?: ServerCallback): void { const p = new Promise((resolve) => { if (this.server) { resolve(0); return; } this.server = new Server((socket) => { this.onClientConnected(socket); }); this.server.on('error', (err) => { this._lastError = errSrvCannotStart; this.emitEvent(evcListenerCannotStart, 0, 0, 0); resolve(errSrvCannotStart); }); this.server.listen(port, ip, () => { this.emitEvent(evcServerStarted, 0, 0, 0); resolve(0); }); }); if (callback) { p.then(err => callback(err === 0 ? null : err)); } } Stop(callback?: ServerCallback): void { const p = new Promise((resolve) => { if (!this.server) { resolve(0); return; } // Disconnect all clients for (const [id, worker] of this.clients) { worker.destroy(); } this.clients.clear(); this.server.close(() => { this.server = null; this.emitEvent(evcServerStopped, 0, 0, 0); resolve(0); }); }); if (callback) { p.then(err => callback(err === 0 ? null : err)); } } // ---- Parameter Management ---- GetParam(paramNumber: number): number { switch (paramNumber) { case p_u16_LocalPort: return 102; case p_i32_PingTimeout: return 5000; case p_i32_SendTimeout: return 5000; case p_i32_RecvTimeout: return 5000; case p_i32_WorkInterval: return 100; case p_u16_SrcRef: return 0x0001; case p_u16_DstRef: return 0x0001; case p_i32_PDURequest: return this._pduLength; case p_i32_MaxClients: return this._maxClients; default: this._lastError = errSrvInvalidParamNumber; return 0; } } SetParam(paramNumber: number, value: number): boolean { switch (paramNumber) { case p_i32_MaxClients: this._maxClients = value; return true; case p_i32_PDURequest: this._pduLength = value; return true; default: this._lastError = errSrvCannotChangeParam; return false; } } // ---- Resourceless Mode ---- SetResourceless(enabled: boolean): void { this._resourceless = enabled; } // ---- Area Management ---- RegisterArea(areaCode: number, index: number, buffer: Buffer): number { if (!buffer || buffer.length === 0) return errSrvDBNullPointer; let areaMap = this.areas.get(areaCode); if (!areaMap) { areaMap = new Map(); this.areas.set(areaCode, areaMap); } if (areaMap.has(index)) return errSrvAreaAlreadyExists; areaMap.set(index, buffer); return 0; } UnregisterArea(areaCode: number, index: number): number { const areaMap = this.areas.get(areaCode); if (!areaMap || !areaMap.has(index)) return errSrvUnknownArea; areaMap.delete(index); if (areaMap.size === 0) { this.areas.delete(areaCode); } return 0; } SetArea(areaCode: number, index: number, buffer: Buffer): number { if (!buffer) return errSrvDBNullPointer; const areaMap = this.areas.get(areaCode); if (!areaMap || !areaMap.has(index)) return errSrvUnknownArea; areaMap.set(index, buffer); return 0; } GetArea(areaCode: number, index: number): Buffer | null { const areaMap = this.areas.get(areaCode); if (!areaMap) return null; return areaMap.get(index) ?? null; } LockArea(_areaCode: number, _index: number): void { // In Node.js single-threaded model, no real locking needed // This is a no-op for API compatibility } UnlockArea(_areaCode: number, _index: number): void { // No-op for API compatibility } // ---- Events Mask ---- GetEventsMask(): number { return this._eventsMask; } SetEventsMask(mask: number): void { this._eventsMask = mask; } // ---- Status ---- ServerStatus(): number { return this.server ? SrvRunning : SrvStopped; } ClientsCount(): number { return this.clients.size; } GetCpuStatus(): number { return this._cpuStatus; } SetCpuStatus(status: number): void { this._cpuStatus = status; } // ---- Utility ---- ErrorText(errNum: number): string { return srvErrorText(errNum); } EventText(event: S7ServerEvent): string { return srvEventText(event); } LastError(): number { return this._lastError; } // ---- Internal ---- emitEvent(evtCode: number, retCode: number, param1: number, param2: number): void { if ((this._eventsMask & evtCode) !== 0) { const evt: S7ServerEvent = { EvtCode: evtCode, EvtRetCode: retCode, EvtParam1: param1, EvtParam2: param2, }; this.emit('event', evt); } } private onClientConnected(socket: Socket): void { if (this.clients.size >= this._maxClients) { this.emitEvent(evcClientNoRoom, 0, 0, 0); socket.destroy(); return; } const clientId = this.nextClientId++; const worker = new ClientWorker(clientId, socket, this); this.clients.set(clientId, worker); this.emitEvent(evcClientAdded, 0, clientId, 0); socket.on('close', () => { this.clients.delete(clientId); this.emitEvent(evcClientDisconnected, 0, clientId, 0); }); socket.on('error', () => { this.clients.delete(clientId); this.emitEvent(evcClientException, 0, clientId, 0); }); } // Used by ClientWorker to find area buffer getAreaBuffer(srvArea: number, dbNumber: number): Buffer | null { const areaMap = this.areas.get(srvArea); if (!areaMap) return null; if (srvArea === srvAreaDB) { return areaMap.get(dbNumber) ?? null; } // Non-DB areas have a single entry at index 0 return areaMap.get(0) ?? null; } // Used by ClientWorker in resourceless mode isResourceless(): boolean { return this._resourceless; } } // ---- Client Worker (handles one connected client) ---- class ClientWorker { private socket: Socket; private server: S7Server; private clientId: number; private recvBuffer = Buffer.alloc(0); private connected = false; private pduLength = DEFAULT_PDU_MIN; constructor(clientId: number, socket: Socket, server: S7Server) { this.clientId = clientId; this.socket = socket; this.server = server; socket.on('data', (data: Buffer) => this.onData(data)); socket.on('close', () => { this.connected = false; }); socket.on('error', () => { this.connected = false; }); } destroy(): void { this.connected = false; this.socket.destroy(); } private onData(data: Buffer): void { this.recvBuffer = Buffer.concat([this.recvBuffer, data]); this.processBuffer(); } private processBuffer(): void { while (this.recvBuffer.length >= 4) { if (!isValidTPKTHeader(this.recvBuffer)) { this.recvBuffer = Buffer.alloc(0); return; } const { length } = parseTPKTHeader(this.recvBuffer); if (this.recvBuffer.length < length) return; const payload = this.recvBuffer.subarray(4, length); this.recvBuffer = this.recvBuffer.subarray(length); this.handleCOTP(Buffer.from(payload)); } } private handleCOTP(data: Buffer): void { if (isCR(data)) { this.handleConnectRequest(data); } else if (isDT(data)) { const s7Data = extractDTPayload(data); this.handleS7Message(s7Data); } } private handleConnectRequest(_crData: Buffer): void { // Send COTP CC (Connection Confirm) - must be TPKT-wrapped const cc = buildCC(); this.sendRaw(encodeTPKT(cc)); this.connected = true; } private handleS7Message(s7Data: Buffer): void { if (s7Data.length < 10 || s7Data[0] !== S7_PROTOCOL_ID) return; const msgType = s7Data[1]; const pduRef = s7Data.readUInt16BE(4); const paramLen = s7Data.readUInt16BE(6); const dataLen = s7Data.readUInt16BE(8); const fnCode = s7Data[10]; // First byte of param if (fnCode === S7_FN_SETUP_COMM) { this.handleSetupComm(s7Data, pduRef); } else if (fnCode === S7_FN_READ_AREA) { this.handleReadArea(s7Data, pduRef, paramLen); } else if (fnCode === S7_FN_WRITE_AREA) { this.handleWriteArea(s7Data, pduRef, paramLen); } else { // Unknown function - send error response this.sendS7Error(pduRef, 0x05, 0x00); // Function not available } } private handleSetupComm(s7Data: Buffer, pduRef: number): void { // Parse the setup comm request (Request message has 10-byte header) // param starts at offset 10, format: FN(1) + Reserved(1) + AMQ_Caller(2) + AMQ_Callee(2) + PDU_Length(2) const pduRequested = s7Data.readUInt16BE(16); // offset 10 + 6 this.pduLength = Math.min(pduRequested || DEFAULT_PDU_REQUESTED, DEFAULT_PDU_REQUESTED); // Build setup comm response const param = Buffer.alloc(8); param[0] = S7_FN_SETUP_COMM; param[1] = 0x00; param.writeUInt16BE(0x0100, 2); param.writeUInt16BE(0x0100, 4); param.writeUInt16BE(this.pduLength, 6); const header = Buffer.alloc(12); header[0] = S7_PROTOCOL_ID; header[1] = S7_MSG_TYPE_RESPONSE; header.writeUInt16BE(0, 2); header.writeUInt16BE(pduRef, 4); header.writeUInt16BE(param.length, 6); header.writeUInt16BE(4, 8); // data length = 4 (contains PDU info) header[10] = 0; // error class header[11] = 0; // error code // Data part: Reserved(2) + PDU Requested(2) + PDU Negotiated(2) - but only 4 bytes used const data = Buffer.alloc(4); data.writeUInt16BE(0, 0); // Reserved data.writeUInt16BE(this.pduLength, 2); // Negotiated PDU length this.sendS7(Buffer.concat([header, param, data])); this.server.emitEvent(evcNegotiatePDU, 0, this.clientId, 0); } private handleReadArea(s7Data: Buffer, pduRef: number, paramLen: number): void { // Parse read items from param const itemCount = s7Data[11]; // Number of items const items: { area: number; dbNumber: number; start: number; amount: number; wordLen: number }[] = []; for (let i = 0; i < itemCount && i < 20; i++) { const itemOffset = 12 + i * 12; // header(10) + FN(1) + Count(1) + i*12 if (itemOffset + 12 > 10 + paramLen) break; items.push({ wordLen: s7Data[itemOffset + 3], amount: s7Data.readUInt16BE(itemOffset + 4), dbNumber: s7Data.readUInt16BE(itemOffset + 6), area: s7Data[itemOffset + 8], start: ((s7Data[itemOffset + 9] << 16) | (s7Data[itemOffset + 10] << 8) | s7Data[itemOffset + 11]) / 8, }); } if (this.server.isResourceless()) { // Resourceless mode: emit readWrite event for (const item of items) { const rwEvt: S7ReadWriteEvent = { Sender: this.clientId, Area: item.area, DBNumber: item.dbNumber, Start: item.start, Size: item.amount * getByteCount(item.wordLen), Data: Buffer.alloc(item.amount * getByteCount(item.wordLen)), Operation: OperationRead, }; this.server.emit('readWrite', rwEvt); // Send response with the (potentially modified) data this.sendReadResponse(pduRef, items, [rwEvt.Data]); } } else { // Normal mode: read from registered areas const dataParts: Buffer[] = []; const results: number[] = []; for (const item of items) { const srvArea = this.toSrvArea(item.area); const dbNum = item.area === S7AreaDB ? item.dbNumber : 0; const areaBuffer = this.server.getAreaBuffer(srvArea, dbNum); const byteCount = item.amount * getByteCount(item.wordLen); if (areaBuffer) { const startByte = item.start; const readData = Buffer.alloc(byteCount); const availLen = Math.min(byteCount, Math.max(0, areaBuffer.length - startByte)); if (availLen > 0) { areaBuffer.copy(readData, 0, startByte, startByte + availLen); } dataParts.push(readData); results.push(0xFF); // Success } else { dataParts.push(Buffer.alloc(byteCount)); results.push(0x0A); // Item not available } } this.sendReadResponse(pduRef, items, dataParts, results); this.server.emitEvent(evcDataRead, 0, this.clientId, 0); } } private handleWriteArea(s7Data: Buffer, pduRef: number, paramLen: number): void { const itemCount = s7Data[11]; const items: { area: number; dbNumber: number; start: number; amount: number; wordLen: number }[] = []; for (let i = 0; i < itemCount && i < 20; i++) { const itemOffset = 12 + i * 12; // header(10) + FN(1) + Count(1) + i*12 if (itemOffset + 12 > 10 + paramLen) break; items.push({ wordLen: s7Data[itemOffset + 3], amount: s7Data.readUInt16BE(itemOffset + 4), dbNumber: s7Data.readUInt16BE(itemOffset + 6), area: s7Data[itemOffset + 8], start: ((s7Data[itemOffset + 9] << 16) | (s7Data[itemOffset + 10] << 8) | s7Data[itemOffset + 11]) / 8, }); } // Parse data section const dataStart = 10 + paramLen; let dataOffset = dataStart; const results: number[] = []; for (const item of items) { if (dataOffset + 4 > s7Data.length) { results.push(0x0A); // Item not available continue; } const returnCode = s7Data[dataOffset]; const transportSize = s7Data[dataOffset + 1]; const len = s7Data.readUInt16BE(dataOffset + 2); dataOffset += 4; const writeData = s7Data.subarray(dataOffset, dataOffset + len); dataOffset += len; if (this.server.isResourceless()) { const rwEvt: S7ReadWriteEvent = { Sender: this.clientId, Area: item.area, DBNumber: item.dbNumber, Start: item.start, Size: item.amount * getByteCount(item.wordLen), Data: Buffer.from(writeData), Operation: OperationWrite, }; this.server.emit('readWrite', rwEvt); results.push(0xFF); // Success } else { const srvArea = this.toSrvArea(item.area); const dbNum = item.area === S7AreaDB ? item.dbNumber : 0; const areaBuffer = this.server.getAreaBuffer(srvArea, dbNum); if (areaBuffer) { const startByte = item.start; writeData.copy(areaBuffer, startByte, 0, Math.min(writeData.length, areaBuffer.length - startByte)); results.push(0xFF); // Success } else { results.push(0x0A); // Item not available } } } this.sendWriteResponse(pduRef, results); this.server.emitEvent(evcDataWrite, 0, this.clientId, 0); } private toSrvArea(area: number): number { switch (area) { case S7AreaPE: return srvAreaPE; case S7AreaPA: return srvAreaPA; case S7AreaMK: return srvAreaMK; case S7AreaDB: return srvAreaDB; case S7AreaCT: return srvAreaCT; case S7AreaTM: return srvAreaTM; default: return -1; } } private sendReadResponse(pduRef: number, items: { amount: number; wordLen: number }[], dataParts: Buffer[], results?: number[]): void { const itemCount = items.length; // Param: FN(1) + ItemCount(1) = 2 const param = Buffer.alloc(2); param[0] = S7_FN_READ_AREA; param[1] = itemCount; let totalDataLen = 0; const dataBufs: Buffer[] = []; for (let i = 0; i < itemCount; i++) { const byteCount = items[i].amount * getByteCount(items[i].wordLen); const dataHeader = Buffer.alloc(4); dataHeader[0] = results?.[i] ?? 0xFF; // Return code dataHeader[1] = getTransportSizeForWordLen(items[i].wordLen); dataHeader.writeUInt16BE(byteCount, 2); dataBufs.push(dataHeader); const dataPart = dataParts[i] ?? Buffer.alloc(byteCount); dataBufs.push(dataPart); totalDataLen += 4 + dataPart.length; } const header = Buffer.alloc(12); header[0] = S7_PROTOCOL_ID; header[1] = S7_MSG_TYPE_RESPONSE_DATA; header.writeUInt16BE(0, 2); header.writeUInt16BE(pduRef, 4); header.writeUInt16BE(param.length, 6); header.writeUInt16BE(totalDataLen, 8); header[10] = 0; header[11] = 0; this.sendS7(Buffer.concat([header, param, ...dataBufs])); } private sendWriteResponse(pduRef: number, results: number[]): void { const itemCount = results.length; const param = Buffer.alloc(2); param[0] = S7_FN_WRITE_AREA; param[1] = itemCount; // Data: just return codes const data = Buffer.alloc(itemCount); for (let i = 0; i < itemCount; i++) { data[i] = results[i]; } const header = Buffer.alloc(12); header[0] = S7_PROTOCOL_ID; header[1] = S7_MSG_TYPE_RESPONSE; header.writeUInt16BE(0, 2); header.writeUInt16BE(pduRef, 4); header.writeUInt16BE(param.length, 6); header.writeUInt16BE(data.length, 8); header[10] = 0; header[11] = 0; this.sendS7(Buffer.concat([header, param, data])); } private sendS7Error(pduRef: number, errorClass: number, errorCode: number): void { const header = Buffer.alloc(12); header[0] = S7_PROTOCOL_ID; header[1] = S7_MSG_TYPE_RESPONSE; header.writeUInt16BE(0, 2); header.writeUInt16BE(pduRef, 4); header.writeUInt16BE(0, 6); // param len header.writeUInt16BE(0, 8); // data len header[10] = errorClass; header[11] = errorCode; this.sendS7(header); } private sendS7(s7Pdu: Buffer): void { // Wrap in COTP DT frame const dt = Buffer.alloc(3); dt[0] = 0x02; // LI dt[1] = 0xF0 | 0x08; // DT + EOT (bit 3) dt[2] = 0x80; this.sendRaw(encodeTPKT(Buffer.concat([dt, s7Pdu]))); } private sendRaw(data: Buffer): void { if (!this.socket.destroyed) { this.socket.write(data); } } } function getTransportSizeForWordLen(wordLen: number): number { switch (wordLen) { case S7WLBit: return 0x01; case S7WLByte: return 0x02; case S7WLWord: return 0x04; case S7WLDWord: return 0x06; case S7WLReal: return 0x08; case S7WLCounter: return 0x1C; case S7WLTimer: return 0x1D; default: return 0x02; } }