s7-server
Version:
TypeScript S7 PLC Server Simulator
472 lines • 19.1 kB
JavaScript
"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