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, S7_ITEM_SPEC, S7_ITEM_LEN, S7_SYNTAX_ID_ANY, S7AreaDB, S7WLBit, DEFAULT_PDU_REQUESTED, } from './constants.js'; import { getTransportSize, getByteCount } from '../utils/buffer-utils.js'; let pduRefCounter = 0; function nextPDURef(): number { return ++pduRefCounter & 0xFFFF; } // ---- S7 Header ---- function buildHeader(msgType: number, paramLen: number, dataLen: number, pduRef?: number): Buffer { const buf = Buffer.alloc(10); buf[0] = S7_PROTOCOL_ID; buf[1] = msgType; buf.writeUInt16BE(0x0000, 2); // Reserved buf.writeUInt16BE(pduRef ?? nextPDURef(), 4); buf.writeUInt16BE(paramLen, 6); buf.writeUInt16BE(dataLen, 8); return buf; } function buildHeaderUserData(paramLen: number, dataLen: number, pduRef?: number): Buffer { const buf = Buffer.alloc(12); buf[0] = S7_PROTOCOL_ID; buf[1] = S7_MSG_TYPE_USERDATA; buf.writeUInt16BE(0x0000, 2); buf.writeUInt16BE(pduRef ?? nextPDURef(), 4); buf.writeUInt16BE(paramLen, 6); buf.writeUInt16BE(dataLen, 8); buf.writeUInt16BE(0x0000, 10); // Error class / error code return buf; } // ---- Parse Response ---- export interface S7Response { msgType: number; pduRef: number; paramLen: number; dataLen: number; errorClass: number; errorCode: number; result: number; param: Buffer; data: Buffer; } export function parseResponse(raw: Buffer): S7Response { const msgType = raw[1]; const pduRef = raw.readUInt16BE(4); const paramLen = raw.readUInt16BE(6); const dataLen = raw.readUInt16BE(8); let errorClass = 0; let errorCode = 0; let headerLen = 10; if (msgType === S7_MSG_TYPE_RESPONSE_DATA || msgType === S7_MSG_TYPE_USERDATA) { headerLen = 12; if (raw.length >= 12) { errorClass = raw[10]; errorCode = raw[11]; } } else if (msgType === S7_MSG_TYPE_RESPONSE) { headerLen = 12; if (raw.length >= 12) { errorClass = raw[10]; errorCode = raw[11]; } } const param = raw.subarray(headerLen, headerLen + paramLen); const data = raw.subarray(headerLen + paramLen, headerLen + paramLen + dataLen); // Read first item result from data let result = 0; if (data.length >= 1 && msgType !== S7_MSG_TYPE_REQUEST) { // For Read/Write response, result is in data[0] if (dataLen > 0) { result = data[0]; } } return { msgType, pduRef, paramLen, dataLen, errorClass, errorCode, result, param: Buffer.from(param), data: Buffer.from(data) }; } export function isS7Error(raw: Buffer): boolean { if (raw.length < 12) return false; return raw[1] === S7_MSG_TYPE_RESPONSE && (raw[10] !== 0 || raw[11] !== 0); } export function getS7ErrorCode(raw: Buffer): number { if (raw.length < 12) return 0; return (raw[10] << 8) | raw[11]; } // ---- Setup Communication ---- export function buildSetupComm(pduLength = DEFAULT_PDU_REQUESTED): Buffer { const param = Buffer.alloc(8); param[0] = S7_FN_SETUP_COMM; param[1] = 0x00; // Reserved param.writeUInt16BE(0x0100, 2); // Max AMQ Caller param.writeUInt16BE(0x0100, 4); // Max AMQ Callee param.writeUInt16BE(pduLength, 6); const header = buildHeader(S7_MSG_TYPE_REQUEST, param.length, 0); return Buffer.concat([header, param]); } export function parseSetupCommResponse(raw: Buffer): { pduRequested: number; pduNegotiated: number } { const resp = parseResponse(raw); // Real PLC puts PDU length in PARAM section (offset 6-7) // Some implementations also include it in DATA section let pduFromParam = 0; if (resp.param.length >= 8) { pduFromParam = resp.param.readUInt16BE(6); } const data = resp.data; if (data.length >= 6) { const pduRequested = data.readUInt16BE(2); const pduNegotiated = data.readUInt16BE(4); if (pduNegotiated > 0) return { pduRequested, pduNegotiated }; } // Fallback: read from parameter section if (pduFromParam > 0) { return { pduRequested: pduFromParam, pduNegotiated: pduFromParam }; } return { pduRequested: 0, pduNegotiated: 0 }; } // ---- Read Area ---- function buildReadItem(area: number, dbNumber: number, start: number, amount: number, wordLen: number): Buffer { const transportSize = getTransportSize(wordLen); const buf = Buffer.alloc(12); buf[0] = S7_ITEM_SPEC; buf[1] = S7_ITEM_LEN; buf[2] = S7_SYNTAX_ID_ANY; buf[3] = transportSize; buf.writeUInt16BE(amount, 4); buf.writeUInt16BE(area === S7AreaDB ? dbNumber : 0, 6); buf[8] = area; const addr = wordLen === S7WLBit ? start * 8 : start * 8; buf[9] = (addr >> 16) & 0xFF; buf[10] = (addr >> 8) & 0xFF; buf[11] = addr & 0xFF; return buf; } export function buildReadArea(area: number, dbNumber: number, start: number, amount: number, wordLen: number): Buffer { const item = buildReadItem(area, dbNumber, start, amount, wordLen); const param = Buffer.alloc(2 + item.length); param[0] = S7_FN_READ_AREA; param[1] = 0x01; // Item count item.copy(param, 2); const header = buildHeader(S7_MSG_TYPE_REQUEST, param.length, 0); return Buffer.concat([header, param]); } export function parseReadAreaResponse(raw: Buffer): { data: Buffer; result: number } { const resp = parseResponse(raw); // Data starts with: ReturnCode(1) + TransportSize(1) + Length(2) + Data if (resp.data.length < 4) { // If data section is too short, treat as error // resp.data[0] could be an error return code (e.g. 0x05=address out of range) // or undefined if data is completely empty (error response with DataLen=0) const result = resp.data.length > 0 ? resp.data[0] : 0x01; // 0x01 = hardware error return { data: Buffer.alloc(0), result }; } const result = resp.data[0]; const len = resp.data.readUInt16BE(2); const data = resp.data.subarray(4, 4 + len); return { data: Buffer.from(data), result }; } // ---- Write Area ---- export function buildWriteArea(area: number, dbNumber: number, start: number, amount: number, wordLen: number, buffer: Buffer): Buffer { const item = buildReadItem(area, dbNumber, start, amount, wordLen); const transportSize = getTransportSize(wordLen); const byteCount = getByteCount(wordLen) * amount; // Data part: ReturnCode(1) + TransportSize(1) + Length(2) + Data const dataHeader = Buffer.alloc(4); dataHeader[0] = 0x00; // Return code = success dataHeader[1] = transportSize; dataHeader.writeUInt16BE(byteCount, 2); const param = Buffer.alloc(2 + item.length); param[0] = S7_FN_WRITE_AREA; param[1] = 0x01; // Item count item.copy(param, 2); const header = buildHeader(S7_MSG_TYPE_REQUEST, param.length, dataHeader.length + buffer.length); return Buffer.concat([header, param, dataHeader, buffer]); } export function parseWriteAreaResponse(raw: Buffer): number { const resp = parseResponse(raw); if (resp.data.length < 1) return 0xFF; return resp.data[0]; // Return code: 0xFF = success } // ---- Read MultiVars ---- export interface S7DataItem { Area: number; WordLen: number; DBNumber: number; Start: number; Amount: number; Data?: Buffer; Result?: number; } export function buildReadMultiVars(items: S7DataItem[]): Buffer { const paramItems = Buffer.alloc(2 + items.length * 12); paramItems[0] = S7_FN_READ_AREA; paramItems[1] = items.length; // Item count for (let i = 0; i < items.length; i++) { const item = buildReadItem(items[i].Area, items[i].DBNumber, items[i].Start, items[i].Amount, items[i].WordLen); item.copy(paramItems, 2 + i * 12); } const header = buildHeader(S7_MSG_TYPE_REQUEST, paramItems.length, 0); return Buffer.concat([header, paramItems]); } export function parseReadMultiVarsResponse(raw: Buffer, items: S7DataItem[]): S7DataItem[] { const resp = parseResponse(raw); const data = resp.data; let offset = 0; const results: S7DataItem[] = []; for (const item of items) { if (offset + 4 > data.length) { results.push({ ...item, Result: 0xFF, Data: Buffer.alloc(0) }); continue; } const result = data[offset]; const transportSize = data[offset + 1]; const len = data.readUInt16BE(offset + 2); offset += 4; const byteCount = getByteCount(item.WordLen) * item.Amount; const readLen = Math.min(len, byteCount); const itemData = data.subarray(offset, offset + readLen); offset += readLen; // Pad if needed const finalData = Buffer.alloc(byteCount); itemData.copy(finalData); results.push({ ...item, Result: result, Data: finalData }); } return results; } // ---- Write MultiVars ---- export function buildWriteMultiVars(items: S7DataItem[]): Buffer { let totalParamLen = 2 + items.length * 12; let totalDataLen = 0; const dataParts: Buffer[] = []; for (const item of items) { const byteCount = getByteCount(item.WordLen) * item.Amount; const dataHeader = Buffer.alloc(4); dataHeader[0] = 0x00; dataHeader[1] = getTransportSize(item.WordLen); dataHeader.writeUInt16BE(byteCount, 2); dataParts.push(dataHeader); dataParts.push(item.Data ?? Buffer.alloc(byteCount)); totalDataLen += 4 + byteCount; } const param = Buffer.alloc(totalParamLen); param[0] = S7_FN_WRITE_AREA; param[1] = items.length; // Item count for (let i = 0; i < items.length; i++) { const itemBuf = buildReadItem(items[i].Area, items[i].DBNumber, items[i].Start, items[i].Amount, items[i].WordLen); itemBuf.copy(param, 2 + i * 12); } const header = buildHeader(S7_MSG_TYPE_REQUEST, param.length, totalDataLen); return Buffer.concat([header, param, ...dataParts]); } export function parseWriteMultiVarsResponse(raw: Buffer, count: number): number[] { const resp = parseResponse(raw); const results: number[] = []; for (let i = 0; i < count; i++) { if (i < resp.data.length) { results.push(resp.data[i]); } else { results.push(0xFF); } } return results; } // ---- SZL (System Status List) ---- export function buildReadSZL(id: number, index: number): Buffer { const param = Buffer.alloc(8); param[0] = 0x00; // Read SZL param[1] = 0x01; // Method: request param.writeUInt16BE(0x000C, 2); // Type/Function param.writeUInt16BE(id, 4); param.writeUInt16BE(index, 6); const data = Buffer.alloc(4); data[0] = 0x0A; // Return code data[1] = 0x00; data.writeUInt16BE(0x0000, 2); const header = buildHeaderUserData(param.length, data.length); return Buffer.concat([header, param, data]); } export function parseReadSZLResponse(raw: Buffer): Buffer { const resp = parseResponse(raw); // Skip data header (ReturnCode + TransportSize + Length) if (resp.data.length <= 4) return resp.data; return resp.data.subarray(4); } export function buildReadSZLList(): Buffer { return buildReadSZL(0x0000, 0x0000); } // ---- Get Order Code / CPU Info / CP Info ---- export function buildGetOrderCode(): Buffer { return buildReadSZL(0x0011, 0x0000); } export function buildGetCpuInfo(): Buffer { return buildReadSZL(0x001C, 0x0000); } export function buildGetCpInfo(): Buffer { return buildReadSZL(0x0013, 0x0000); } // ---- Get Protection ---- export function buildGetProtection(): Buffer { return buildReadSZL(0x0023, 0x0000); } // ---- PLC Control ---- export function buildPlcControl(subfunc: number, data: Buffer = Buffer.alloc(0)): Buffer { const param = Buffer.alloc(8); param[0] = 0x28; // PLC Control param[1] = subfunc; param.writeUInt16BE(0, 2); param.writeUInt16BE(0, 4); param.writeUInt16BE(0, 6); const header = buildHeader(S7_MSG_TYPE_REQUEST, param.length, data.length); return Buffer.concat([header, param, data]); } export function buildPlcHotStart(): Buffer { return buildPlcControl(0x02); } export function buildPlcColdStart(): Buffer { return buildPlcControl(0x01); } export function buildPlcStop(): Buffer { return buildPlcControl(0x03); } export function buildCopyRamToRom(timeout: number): Buffer { const data = Buffer.alloc(4); data.writeUInt32BE(timeout, 0); return buildPlcControl(0x05, data); } export function buildCompress(timeout: number): Buffer { const data = Buffer.alloc(4); data.writeUInt32BE(timeout, 0); return buildPlcControl(0x04, data); } // ---- Get PLC Status ---- export function buildGetPlcStatus(): Buffer { const param = Buffer.alloc(8); param[0] = 0x00; // Read SZL param[1] = 0x01; param.writeUInt16BE(0x0004, 2); param.writeUInt16BE(0x0024, 4); param.writeUInt16BE(0x0000, 6); const header = buildHeaderUserData(param.length, 0); return Buffer.concat([header, param]); } export function parseGetPlcStatusResponse(raw: Buffer): number { const resp = parseResponse(raw); if (resp.data.length >= 8) { return resp.data[7]; } return 0x00; // Unknown } // ---- Security ---- export function buildSetPassword(password: string): Buffer { const pwdBuf = Buffer.alloc(8, 0x20); // 8 chars, space-padded Buffer.from(password, 'ascii').copy(pwdBuf, 0, 0, Math.min(password.length, 8)); const param = Buffer.alloc(8); param[0] = 0x2C; // Security param[1] = 0x01; // Set password param.writeUInt16BE(0, 2); param.writeUInt16BE(0, 4); param.writeUInt16BE(0, 6); const header = buildHeader(S7_MSG_TYPE_REQUEST, param.length, pwdBuf.length); return Buffer.concat([header, param, pwdBuf]); } export function buildClearPassword(): Buffer { const param = Buffer.alloc(8); param[0] = 0x2C; param[1] = 0x02; // Clear password param.writeUInt16BE(0, 2); param.writeUInt16BE(0, 4); param.writeUInt16BE(0, 6); const header = buildHeader(S7_MSG_TYPE_REQUEST, param.length, 0); return Buffer.concat([header, param]); } // ---- Date/Time ---- export function buildGetPlcDateTime(): Buffer { const param = Buffer.alloc(8); param[0] = 0x00; param[1] = 0x01; param.writeUInt16BE(0x0003, 2); param.writeUInt16BE(0x0007, 4); param.writeUInt16BE(0x0000, 6); const header = buildHeaderUserData(param.length, 0); return Buffer.concat([header, param]); } export function parseGetPlcDateTimeResponse(raw: Buffer): Date { const resp = parseResponse(raw); // SZL data contains: year, month, day, weekday, hour, minute, second, msHi, msLo if (resp.data.length >= 14) { const offset = 4; // Skip SZL data header const year = resp.data[offset] << 8 | resp.data[offset + 1]; const month = resp.data[offset + 2]; const day = resp.data[offset + 3]; const hour = resp.data[offset + 5]; const minute = resp.data[offset + 6]; const second = resp.data[offset + 7]; return new Date(year, month - 1, day, hour, minute, second); } return new Date(0); } export function buildSetPlcDateTime(date: Date): Buffer { const data = Buffer.alloc(14); data[0] = 0xFF; // Return code data[1] = 0x09; // Transport size: OCTET STRING data.writeUInt16BE(10, 2); // Length data[4] = (date.getFullYear() >> 8) & 0xFF; data[5] = date.getFullYear() & 0xFF; data[6] = date.getMonth() + 1; data[7] = date.getDate(); data[8] = date.getDay() + 1; // S7 weekday: 1=Sun..7=Sat data[9] = date.getHours(); data[10] = date.getMinutes(); data[11] = date.getSeconds(); data.writeUInt16BE(date.getMilliseconds(), 12); const param = Buffer.alloc(8); param[0] = 0x00; param[1] = 0x02; // Write param.writeUInt16BE(0x0003, 2); param.writeUInt16BE(0x0007, 4); param.writeUInt16BE(0x0000, 6); const header = buildHeaderUserData(param.length, data.length); return Buffer.concat([header, param, data]); } export function buildSetPlcSystemDateTime(): Buffer { const now = new Date(); return buildSetPlcDateTime(now); } // ---- Block Functions ---- export function buildListBlocks(): Buffer { const param = Buffer.alloc(8); param[0] = 0x00; param[1] = 0x01; param.writeUInt16BE(0x0003, 2); param.writeUInt16BE(0x0017, 4); // Block list SZL ID param.writeUInt16BE(0x0000, 6); const header = buildHeaderUserData(param.length, 0); return Buffer.concat([header, param]); } export function buildGetAgBlockInfo(blockType: number, blockNum: number): Buffer { const param = Buffer.alloc(12); param[0] = 0x00; param[1] = 0x01; param.writeUInt16BE(0x0003, 2); param.writeUInt16BE(0x0017, 4); param.writeUInt16BE(0x0001, 6); param[8] = blockType; param.writeUInt16BE(blockNum, 10); const header = buildHeaderUserData(param.length, 0); return Buffer.concat([header, param]); } export function buildListBlocksOfType(blockType: number): Buffer { const param = Buffer.alloc(8); param[0] = 0x00; param[1] = 0x01; param.writeUInt16BE(0x0003, 2); param.writeUInt16BE(blockType, 4); param.writeUInt16BE(0x0000, 6); const header = buildHeaderUserData(param.length, 0); return Buffer.concat([header, param]); } export function buildUpload(blockType: number, blockNum: number): Buffer { const param = Buffer.alloc(8); param[0] = 0x1D; // Start Upload param[1] = 0x00; param[2] = 0x00; param[3] = blockType; param.writeUInt32BE(blockNum, 4); const header = buildHeader(S7_MSG_TYPE_REQUEST, param.length, 0); return Buffer.concat([header, param]); } export function buildFullUpload(blockType: number, blockNum: number, size: number): Buffer { const param = Buffer.alloc(12); param[0] = 0x1D; param[1] = 0x00; param[2] = 0x00; param[3] = blockType; param.writeUInt32BE(blockNum, 4); param.writeUInt32BE(size, 8); const header = buildHeader(S7_MSG_TYPE_REQUEST, param.length, 0); return Buffer.concat([header, param]); } export function buildDownload(blockNum: number, buffer: Buffer): Buffer { const param = Buffer.alloc(8); param[0] = 0x1A; // Request Download param[1] = 0x00; param.writeUInt16BE(0, 2); param.writeUInt16BE(blockNum >= 0 ? blockNum : 0, 4); param.writeUInt16BE(0, 6); const header = buildHeader(S7_MSG_TYPE_REQUEST, param.length, buffer.length); return Buffer.concat([header, param, buffer]); } export function buildDelete(blockType: number, blockNum: number): Buffer { const param = Buffer.alloc(12); param[0] = 0x28; // PLC Control param[1] = 0x06; // Delete param.writeUInt16BE(0, 2); param[4] = blockType; param.writeUInt16BE(blockNum, 6); param.writeUInt16BE(0, 8); param.writeUInt16BE(0, 10); const header = buildHeader(S7_MSG_TYPE_REQUEST, param.length, 0); return Buffer.concat([header, param]); } export function buildDBGet(dbNumber: number): Buffer { return buildGetAgBlockInfo(0x41, dbNumber); } export function buildDBFill(dbNumber: number, fillChar: number): Buffer { const data = Buffer.alloc(4); data.writeUInt8(fillChar, 0); const param = Buffer.alloc(8); param[0] = 0x28; param[1] = 0x06; param.writeUInt16BE(0, 2); param[4] = 0x41; // Block_DB param.writeUInt16BE(dbNumber, 6); const header = buildHeader(S7_MSG_TYPE_REQUEST, param.length, data.length); return Buffer.concat([header, param, data]); }