UNPKG

s7-server

Version:
472 lines 19.1 kB
"use strict"; /** * S7 Protocol Data Unit (PDU) Implementation * Handles S7 protocol message structures and parsing */ Object.defineProperty(exports, "__esModule", { value: true }); exports.S7PDUParser = exports.TS_RES_OCTET = exports.TS_RES_REAL = exports.TS_RES_INT = exports.TS_RES_BYTE = exports.TS_RES_BIT = exports.S7_ERROR_DATA_SIZE_MISMATCH = exports.S7_ERROR_ADDRESS_OUT_OF_RANGE = exports.S7_ERROR_INVALID_PARAMETER = exports.S7_ERROR_RESOURCE_NOT_AVAILABLE = exports.S7_ERROR_NO_ERROR = exports.S7_FUNC_SET_CLOCK = exports.S7_FUNC_GET_CLOCK = exports.S7_FUNC_DOWNLOAD = exports.S7_FUNC_UPLOAD = exports.S7_FUNC_CONTROL = exports.S7_FUNC_NEGOTIATE = exports.S7_FUNC_WRITE = exports.S7_FUNC_READ = exports.S7_PDU_TYPE_USERDATA = exports.S7_PDU_TYPE_RESPONSE = exports.S7_PDU_TYPE_REQUEST = exports.S7_PROTOCOL_ID = void 0; // Node.js built-in modules const buffer_1 = require("buffer"); // Internal modules const s7_types_1 = require("../types/s7-types"); // S7 Protocol Constants exports.S7_PROTOCOL_ID = 0x32; exports.S7_PDU_TYPE_REQUEST = 0x01; exports.S7_PDU_TYPE_RESPONSE = 0x03; exports.S7_PDU_TYPE_USERDATA = 0x07; // S7 Function Codes exports.S7_FUNC_READ = 0x04; exports.S7_FUNC_WRITE = 0x05; exports.S7_FUNC_NEGOTIATE = 0xf0; exports.S7_FUNC_CONTROL = 0x28; exports.S7_FUNC_UPLOAD = 0x1d; exports.S7_FUNC_DOWNLOAD = 0x1e; exports.S7_FUNC_GET_CLOCK = 0x01; exports.S7_FUNC_SET_CLOCK = 0x02; // S7 Error Codes exports.S7_ERROR_NO_ERROR = 0x0000; exports.S7_ERROR_RESOURCE_NOT_AVAILABLE = 0x0a; exports.S7_ERROR_INVALID_PARAMETER = 0x0b; exports.S7_ERROR_ADDRESS_OUT_OF_RANGE = 0x05; exports.S7_ERROR_DATA_SIZE_MISMATCH = 0x06; // Transport Size Codes (matching C++ SNAP7) exports.TS_RES_BIT = 0x03; exports.TS_RES_BYTE = 0x04; exports.TS_RES_INT = 0x05; exports.TS_RES_REAL = 0x07; // Fixed: was 0x06, should be 0x07 exports.TS_RES_OCTET = 0x09; class S7PDUParser { /** * Parse S7 request header */ static parseRequestHeader(data) { if (data.length < 10) { return null; } return { P: data.readUInt8(0), PDUType: data.readUInt8(1), AB_EX: data.readUInt16BE(2), Sequence: data.readUInt16BE(4), ParLen: data.readUInt16BE(6), DataLen: data.readUInt16BE(8) }; } /** * Parse S7 response header */ static parseResponseHeader(data) { if (data.length < 14) { return null; } return { P: data.readUInt8(0), PDUType: data.readUInt8(1), AB_EX: data.readUInt16BE(2), Sequence: data.readUInt16BE(4), ParLen: data.readUInt16BE(6), DataLen: data.readUInt16BE(8), Error: data.readUInt16BE(10) }; } /** * Parse S7 read request */ static parseReadRequest(data) { const header = this.parseRequestHeader(data); if (!header || header.P !== exports.S7_PROTOCOL_ID) { return null; } const paramsOffset = 10; if (data.length < paramsOffset + 2) { return null; } const funRead = data.readUInt8(paramsOffset); const itemCount = data.readUInt8(paramsOffset + 1); if (funRead !== exports.S7_FUNC_READ) { return null; } const items = []; let offset = paramsOffset + 2; for (let i = 0; i < itemCount; i++) { if (data.length < offset + 12) { break; } const item = { TransportSize: data.readUInt8(offset + 3), // Skip ItemHead[3] Length: data.readUInt16BE(offset + 4), // After ItemHead[3] + TransportSize[1] DBNumber: data.readUInt16BE(offset + 6), // After ItemHead[3] + TransportSize[1] + Length[2] Area: data.readUInt8(offset + 8), // After ItemHead[3] + TransportSize[1] + Length[2] + DBNumber[2] Addr: data.slice(offset + 9, offset + 12) // After ItemHead[3] + TransportSize[1] + Length[2] + DBNumber[2] + Area[1] }; items.push(item); offset += 12; } return { FunRead: funRead, ItemCount: itemCount, Items: items }; } /** * Parse S7 write request */ static parseWriteRequest(data) { const header = this.parseRequestHeader(data); if (!header || header.P !== exports.S7_PROTOCOL_ID) { return null; } const paramsOffset = 10; if (data.length < paramsOffset + 2) { return null; } const funWrite = data.readUInt8(paramsOffset); const itemCount = data.readUInt8(paramsOffset + 1); if (funWrite !== exports.S7_FUNC_WRITE) { return null; } const items = []; let offset = paramsOffset + 2; // Parse item specifications for (let i = 0; i < itemCount; i++) { if (data.length < offset + 12) { break; } const transportSize = data.readUInt8(offset + 3); // Skip ItemHead[3] const length = data.readUInt16BE(offset + 4); // After ItemHead[3] + TransportSize[1] const dbNumber = data.readUInt16BE(offset + 6); // After ItemHead[3] + TransportSize[1] + Length[2] const area = data.readUInt8(offset + 8); // After ItemHead[3] + TransportSize[1] + Length[2] + DBNumber[2] const addr = data.slice(offset + 9, offset + 12); // After ItemHead[3] + TransportSize[1] + Length[2] + DBNumber[2] + Area[1] items.push({ TransportSize: transportSize, Length: length, DBNumber: dbNumber, Area: area, Addr: addr, Data: buffer_1.Buffer.alloc(0) // Will be filled below }); offset += 12; } // Parse data section - each item has a 4-byte header + data for (let i = 0; i < items.length; i++) { const item = items[i]; // Read data item header (4 bytes: ReturnCode + TransportSize + DataLength) if (data.length < offset + 4) { break; } const dataLength = data.readUInt16BE(offset + 2); offset += 4; // Calculate actual data length based on transport size (like C++ code) let actualDataLength; if (item.TransportSize === 0x1d || item.TransportSize === 0x1c || item.TransportSize === 0x01) { // S7WLTimer, S7WLCounter, S7WLBit - data length is in bits actualDataLength = dataLength; } else { // Other types - data length is in bits, convert to bytes actualDataLength = dataLength / 8; } // Extract the actual data if (data.length < offset + actualDataLength) { break; } item.Data = data.slice(offset, offset + actualDataLength); offset += actualDataLength; // S7 doesn't transfer odd byte amounts, so round up to even if (actualDataLength % 2 !== 0) { offset += 1; } } return { FunWrite: funWrite, ItemCount: itemCount, Items: items }; } /** * Parse S7 negotiate request * Supports both 6-byte (nodes7 client) and 8-byte (standard SNAP7) formats */ static parseNegotiateRequest(data) { const header = this.parseRequestHeader(data); if (!header || header.P !== exports.S7_PROTOCOL_ID) { return null; } const paramsOffset = 10; const availableParams = data.length - paramsOffset; // Check if we have at least 6 bytes (minimum for negotiate request) if (availableParams < 6) { return null; } // The client is sending standard 8-byte format, so read accordingly const funNegotiate = data.readUInt8(paramsOffset); if (funNegotiate !== exports.S7_FUNC_NEGOTIATE) { return null; } // Standard SNAP7 format (8 bytes): FunNegotiate(1) + Unknown(1) + ParallelJobs_1(2) + ParallelJobs_2(2) + PDULength(2) const result = { FunNegotiate: funNegotiate, Unknown: data.readUInt8(paramsOffset + 1), ParallelJobs_1: data.readUInt16BE(paramsOffset + 2), ParallelJobs_2: data.readUInt16BE(paramsOffset + 4), PDULength: data.readUInt16BE(paramsOffset + 6) }; return result; } /** * Parse S7 control request */ static parseControlRequest(data) { const header = this.parseRequestHeader(data); if (!header || header.P !== exports.S7_PROTOCOL_ID) { return null; } const paramsOffset = 10; if (data.length < paramsOffset + 3) { return null; } const funControl = data.readUInt8(paramsOffset); if (funControl !== exports.S7_FUNC_CONTROL) { return null; } return { FunControl: funControl, Unknown: data.readUInt8(paramsOffset + 1), ControlCode: data.readUInt8(paramsOffset + 2) }; } /** * Create S7 read response */ static createReadResponse(sequence, items) { const headerSize = 12; const paramsSize = 2; let dataSize = 0; // Calculate total data size for (const item of items) { dataSize += 4 + item.Data.length; // ReturnCode + TransportSize + DataLength + Data } const totalSize = headerSize + paramsSize + dataSize; const buffer = buffer_1.Buffer.alloc(totalSize); // S7 Response Header buffer.writeUInt8(exports.S7_PROTOCOL_ID, 0); // P buffer.writeUInt8(exports.S7_PDU_TYPE_RESPONSE, 1); // PDUType buffer.writeUInt16BE(0, 2); // AB_EX buffer.writeUInt16BE(sequence, 4); // Sequence buffer.writeUInt16BE(paramsSize, 6); // ParLen buffer.writeUInt16BE(dataSize, 8); // DataLen buffer.writeUInt16BE(exports.S7_ERROR_NO_ERROR, 10); // Error // Response Parameters buffer.writeUInt8(exports.S7_FUNC_READ, 12); // FunRead buffer.writeUInt8(items.length, 13); // ItemCount // Response Items let offset = 14; for (const item of items) { buffer.writeUInt8(item.ReturnCode, offset++); buffer.writeUInt8(item.TransportSize, offset++); buffer.writeUInt16BE(item.DataLength, offset); offset += 2; item.Data.copy(buffer, offset); offset += item.Data.length; } return buffer; } /** * Create S7 write response */ static createWriteResponse(sequence, returnCodes) { const headerSize = 12; const paramsSize = 2; const dataSize = returnCodes.length; const totalSize = headerSize + paramsSize + dataSize; const buffer = buffer_1.Buffer.alloc(totalSize); // S7 Response Header buffer.writeUInt8(exports.S7_PROTOCOL_ID, 0); // P buffer.writeUInt8(exports.S7_PDU_TYPE_RESPONSE, 1); // PDUType buffer.writeUInt16BE(0, 2); // AB_EX buffer.writeUInt16BE(sequence, 4); // Sequence buffer.writeUInt16BE(paramsSize, 6); // ParLen buffer.writeUInt16BE(dataSize, 8); // DataLen buffer.writeUInt16BE(exports.S7_ERROR_NO_ERROR, 10); // Error // Response Parameters buffer.writeUInt8(exports.S7_FUNC_WRITE, 12); // FunWrite buffer.writeUInt8(returnCodes.length, 13); // ItemCount // Return Codes for (let i = 0; i < returnCodes.length; i++) { buffer.writeUInt8(returnCodes[i], 14 + i); } return buffer; } /** * Create S7 negotiate response * Creates response in standard SNAP7 format (8 bytes) to match C++ implementation */ static createNegotiateResponse(sequence, pduLength) { const headerSize = 12; // TS7ResHeader23 is 12 bytes const paramsSize = 8; // Standard SNAP7 format const totalSize = headerSize + paramsSize; const buffer = buffer_1.Buffer.alloc(totalSize); // S7 Response Header (matches C++ TS7ResHeader23 structure) buffer.writeUInt8(exports.S7_PROTOCOL_ID, 0); // P buffer.writeUInt8(exports.S7_PDU_TYPE_RESPONSE, 1); // PDUType buffer.writeUInt16BE(0, 2); // AB_EX buffer.writeUInt16BE(sequence, 4); // Sequence buffer.writeUInt16BE(paramsSize, 6); // ParLen (8 bytes) buffer.writeUInt16BE(0, 8); // DataLen buffer.writeUInt16BE(exports.S7_ERROR_NO_ERROR, 10); // Error // Response Parameters (matches C++ TResFunNegotiateParams structure) buffer.writeUInt8(exports.S7_FUNC_NEGOTIATE, 12); // FunNegotiate buffer.writeUInt8(0, 13); // Unknown buffer.writeUInt16BE(1, 14); // ParallelJobs_1 (word) buffer.writeUInt16BE(1, 16); // ParallelJobs_2 (word) buffer.writeUInt16BE(pduLength, 18); // PDULength (word) return buffer; } /** * Create S7 control response */ static createControlResponse(sequence, controlCode) { const headerSize = 12; const paramsSize = 1; const totalSize = headerSize + paramsSize; const buffer = buffer_1.Buffer.alloc(totalSize); // S7 Response Header buffer.writeUInt8(exports.S7_PROTOCOL_ID, 0); // P buffer.writeUInt8(exports.S7_PDU_TYPE_RESPONSE, 1); // PDUType buffer.writeUInt16BE(0, 2); // AB_EX buffer.writeUInt16BE(sequence, 4); // Sequence buffer.writeUInt16BE(paramsSize, 6); // ParLen buffer.writeUInt16BE(0, 8); // DataLen buffer.writeUInt16BE(exports.S7_ERROR_NO_ERROR, 10); // Error // Response Parameters buffer.writeUInt8(controlCode, 12); // Control code return buffer; } /** * Create S7 clock response */ static createClockResponse(sequence, clockData) { const headerSize = 12; const paramsSize = 1; const dataSize = clockData.length; const totalSize = headerSize + paramsSize + dataSize; const buffer = buffer_1.Buffer.alloc(totalSize); // S7 Response Header buffer.writeUInt8(exports.S7_PROTOCOL_ID, 0); // P buffer.writeUInt8(exports.S7_PDU_TYPE_RESPONSE, 1); // PDUType buffer.writeUInt16BE(0, 2); // AB_EX buffer.writeUInt16BE(sequence, 4); // Sequence buffer.writeUInt16BE(paramsSize, 6); // ParLen buffer.writeUInt16BE(dataSize, 8); // DataLen buffer.writeUInt16BE(exports.S7_ERROR_NO_ERROR, 10); // Error // Response Parameters buffer.writeUInt8(exports.S7_FUNC_GET_CLOCK, 12); // FunClock // Clock Data clockData.copy(buffer, 13); return buffer; } /** * Get response transport size and data length from request transport size * This matches the C++ SNAP7 implementation exactly */ static getResponseTransportSizeAndLength(requestTransportSize, dataSize) { switch (requestTransportSize) { case 0x01: // S7WLBit return { transportSize: exports.TS_RES_BIT, dataLength: dataSize }; // Size in bytes case 0x02: // S7WLByte case 0x04: // S7WLWord case 0x06: // S7WLDWord return { transportSize: exports.TS_RES_BYTE, dataLength: dataSize * 8 }; // Size * 8 in bits case 0x05: // S7WLInt case 0x07: // S7WLDInt return { transportSize: exports.TS_RES_INT, dataLength: dataSize * 8 }; // Size * 8 in bits case 0x08: // S7WLReal return { transportSize: exports.TS_RES_REAL, dataLength: dataSize }; // Size in bytes case 0x03: // S7WLChar case 0x1c: // S7WLCounter case 0x1d: // S7WLTimer return { transportSize: exports.TS_RES_OCTET, dataLength: dataSize }; // Size in bytes default: return { transportSize: exports.TS_RES_BYTE, dataLength: dataSize * 8 }; // Default: Size * 8 in bits } } /** * Get transport size code from word length (legacy function) */ static getTransportSizeCode(wordLen) { switch (wordLen) { case s7_types_1.S7WordLength.Bit: return exports.TS_RES_BIT; case s7_types_1.S7WordLength.Byte: case s7_types_1.S7WordLength.Word: case s7_types_1.S7WordLength.DWord: return exports.TS_RES_BYTE; case s7_types_1.S7WordLength.Real: return exports.TS_RES_REAL; case s7_types_1.S7WordLength.Counter: case s7_types_1.S7WordLength.Timer: return exports.TS_RES_OCTET; default: return exports.TS_RES_BYTE; } } /** * Get byte count from word length */ static getByteCountFromWordLen(wordLen) { switch (wordLen) { case s7_types_1.S7WordLength.Bit: case s7_types_1.S7WordLength.Byte: return 1; case s7_types_1.S7WordLength.Word: case s7_types_1.S7WordLength.Counter: case s7_types_1.S7WordLength.Timer: return 2; case s7_types_1.S7WordLength.Real: case s7_types_1.S7WordLength.DWord: return 4; default: return 0; } } /** * Validate S7 address */ static validateAddress(area, dbNumber, start, size) { // Basic validation if (start < 0 || size <= 0) { return false; } // Area-specific validation switch (area) { case s7_types_1.S7Area.DB: return dbNumber > 0 && dbNumber <= 65535; case s7_types_1.S7Area.PE: case s7_types_1.S7Area.PA: case s7_types_1.S7Area.MK: return dbNumber === 0; case s7_types_1.S7Area.CT: case s7_types_1.S7Area.TM: return dbNumber === 0 && start + size <= 256; default: return false; } } /** * Parse address from 3-byte buffer (matches C++ SNAP7 implementation) */ static parseAddress(addr) { if (addr.length < 3) { return 0; } // S7 address format: 3-byte address, where bit 0-2 are bit address, bits 3-23 are byte address // C++ implementation: reads as 32-bit value and shifts right by 3 to get byte address const address32 = (addr.readUInt8(0) << 16) | (addr.readUInt8(1) << 8) | addr.readUInt8(2); const byteAddress = address32 >> 3; // start byte (ignore bit index) return byteAddress; } } exports.S7PDUParser = S7PDUParser; //# sourceMappingURL=s7-pdu.js.map