import { describe, it, expect } from 'vitest'; import { buildSetupComm, parseSetupCommResponse, buildReadArea, parseReadAreaResponse, buildWriteArea, parseWriteAreaResponse, buildReadMultiVars, parseReadMultiVarsResponse, buildWriteMultiVars, parseWriteMultiVarsResponse, buildReadSZL, parseReadSZLResponse, buildReadSZLList as buildReadSZLListFn, 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, isS7Error, getS7ErrorCode, S7DataItem, } from '../../src/protocol/s7-protocol.js'; import { S7_PROTOCOL_ID, S7_MSG_TYPE_REQUEST, S7_MSG_TYPE_RESPONSE, S7_FN_SETUP_COMM, S7_FN_READ_AREA, S7_FN_WRITE_AREA, S7AreaDB, S7AreaMK, S7AreaPE, S7AreaPA, S7AreaCT, S7AreaTM, S7WLBit, S7WLByte, S7WLWord, S7WLDWord, S7WLReal, } from '../../src/protocol/constants.js'; // Helper: wrap S7 PDU in a minimal response frame for testing parse functions function buildReadAreaResponse(data: Buffer, result = 0xFF): Buffer { // S7 Response Header (12 bytes) + param(2 bytes) + data_header(4 bytes) + data const paramLen = 2; const dataLen = 4 + data.length; const header = Buffer.alloc(12); header[0] = S7_PROTOCOL_ID; header[1] = 0x03; // Response Data header.writeUInt16BE(0, 2); header.writeUInt16BE(1, 4); // pdu ref header.writeUInt16BE(paramLen, 6); header.writeUInt16BE(dataLen, 8); header[10] = 0; header[11] = 0; const param = Buffer.alloc(paramLen); param[0] = S7_FN_READ_AREA; param[1] = 1; // item count const dataHeader = Buffer.alloc(4); dataHeader[0] = result; dataHeader[1] = 0x02; // transport size byte dataHeader.writeUInt16BE(data.length, 2); return Buffer.concat([header, param, dataHeader, data]); } function buildWriteAreaResponse(returnCode = 0xFF): Buffer { const paramLen = 2; const dataLen = 1; const header = Buffer.alloc(12); header[0] = S7_PROTOCOL_ID; header[1] = 0x02; // Response header.writeUInt16BE(0, 2); header.writeUInt16BE(1, 4); header.writeUInt16BE(paramLen, 6); header.writeUInt16BE(dataLen, 8); header[10] = 0; header[11] = 0; const param = Buffer.alloc(paramLen); param[0] = S7_FN_WRITE_AREA; param[1] = 1; const data = Buffer.alloc(1); data[0] = returnCode; return Buffer.concat([header, param, data]); } describe('S7 Protocol', () => { describe('Setup Communication', () => { it('should build setup comm request', () => { const pdu = buildSetupComm(960); expect(pdu[0]).toBe(S7_PROTOCOL_ID); expect(pdu[1]).toBe(S7_MSG_TYPE_REQUEST); expect(pdu.readUInt16BE(6)).toBe(8); // param length expect(pdu[10]).toBe(S7_FN_SETUP_COMM); expect(pdu.readUInt16BE(16)).toBe(960); // PDU length requested }); it('should build with default PDU', () => { const pdu = buildSetupComm(); expect(pdu.readUInt16BE(16)).toBe(960); }); it('should parse setup comm response', () => { // Build a proper setup comm response // 12-byte header + 8-byte param + 6-byte data (2 extra for negotiated PDU) const paramLen = 8; const dataLen = 6; const totalLen = 12 + paramLen + dataLen; const resp = Buffer.alloc(totalLen); resp[0] = S7_PROTOCOL_ID; resp[1] = 0x02; // Response resp.writeUInt16BE(0, 2); resp.writeUInt16BE(1, 4); // PDU ref resp.writeUInt16BE(paramLen, 6); resp.writeUInt16BE(dataLen, 8); resp[10] = 0; // error class resp[11] = 0; // error code // param (offset 12..19) resp[12] = S7_FN_SETUP_COMM; resp[13] = 0; resp.writeUInt16BE(0x0100, 14); resp.writeUInt16BE(0x0100, 16); resp.writeUInt16BE(960, 18); // data (offset 20..25) resp[20] = 0; resp[21] = 0; resp.writeUInt16BE(960, 22); // PDU requested resp.writeUInt16BE(480, 24); // PDU negotiated const result = parseSetupCommResponse(resp); expect(result.pduRequested).toBe(960); expect(result.pduNegotiated).toBe(480); }); }); describe('Read Area', () => { it('should build read area request', () => { const pdu = buildReadArea(S7AreaDB, 1, 0, 10, S7WLByte); expect(pdu[0]).toBe(S7_PROTOCOL_ID); expect(pdu[1]).toBe(S7_MSG_TYPE_REQUEST); // Param: FN(1) + Count(1) + Item(12) = 14 expect(pdu.readUInt16BE(6)).toBe(14); // param len = 1 + 1 + 12 expect(pdu[10]).toBe(S7_FN_READ_AREA); expect(pdu[11]).toBe(0x01); // Item count // Item: spec=0x12, len=0x0A, syntax=0x10, transportSize, amount, DB, area, address expect(pdu[12]).toBe(0x12); // Item spec expect(pdu[13]).toBe(0x0A); // Item len }); it('should encode DB number for DB area', () => { const pdu = buildReadArea(S7AreaDB, 42, 0, 10, S7WLByte); // Header(10) + FN(1) + Count(1) + Item: spec(1)+len(1)+syntax(1)+transportSize(1)+amount(2)+DB(2) // DB number is at offset 10+2+6 = 18 expect(pdu.readUInt16BE(18)).toBe(42); }); it('should encode zero DB number for non-DB area', () => { const pdu = buildReadArea(S7AreaMK, 0, 0, 10, S7WLByte); expect(pdu.readUInt16BE(18)).toBe(0); }); it('should parse read area response', () => { const testData = Buffer.from([0x01, 0x02, 0x03, 0x04]); const resp = buildReadAreaResponse(testData); const result = parseReadAreaResponse(resp); expect(result.result).toBe(0xFF); expect(result.data).toEqual(testData); }); it('should handle error result in read response', () => { const resp = buildReadAreaResponse(Buffer.alloc(0), 0x0A); const result = parseReadAreaResponse(resp); expect(result.result).toBe(0x0A); }); }); describe('Write Area', () => { it('should build write area request', () => { const data = Buffer.from([0xAA, 0xBB, 0xCC, 0xDD]); const pdu = buildWriteArea(S7AreaDB, 1, 0, 4, S7WLByte, data); expect(pdu[0]).toBe(S7_PROTOCOL_ID); expect(pdu[1]).toBe(S7_MSG_TYPE_REQUEST); expect(pdu[10]).toBe(S7_FN_WRITE_AREA); }); it('should include data in write request', () => { const data = Buffer.from([0x11, 0x22]); const pdu = buildWriteArea(S7AreaMK, 0, 0, 2, S7WLByte, data); // Data should be at the end const dataStart = pdu.length - data.length; expect(pdu.subarray(dataStart)).toEqual(data); }); it('should parse write area response', () => { const resp = buildWriteAreaResponse(0xFF); const result = parseWriteAreaResponse(resp); expect(result).toBe(0xFF); }); it('should parse write area error response', () => { const resp = buildWriteAreaResponse(0x0A); const result = parseWriteAreaResponse(resp); expect(result).toBe(0x0A); }); }); describe('MultiVars', () => { it('should build read multi-vars request', () => { const items: S7DataItem[] = [ { Area: S7AreaDB, DBNumber: 1, Start: 0, Amount: 10, WordLen: S7WLByte }, { Area: S7AreaMK, DBNumber: 0, Start: 0, Amount: 5, WordLen: S7WLByte }, ]; const pdu = buildReadMultiVars(items); expect(pdu[0]).toBe(S7_PROTOCOL_ID); expect(pdu[1]).toBe(S7_MSG_TYPE_REQUEST); expect(pdu[10]).toBe(S7_FN_READ_AREA); // param = 2 + 2*12 = 26 (FN + ItemCount + 2 Items) expect(pdu.readUInt16BE(6)).toBe(26); expect(pdu[11]).toBe(2); // Item count }); it('should build write multi-vars request', () => { const items: S7DataItem[] = [ { Area: S7AreaDB, DBNumber: 1, Start: 0, Amount: 4, WordLen: S7WLByte, Data: Buffer.from([0x01, 0x02, 0x03, 0x04]) }, ]; const pdu = buildWriteMultiVars(items); expect(pdu[0]).toBe(S7_PROTOCOL_ID); expect(pdu[10]).toBe(S7_FN_WRITE_AREA); }); }); describe('SZL', () => { it('should build read SZL request', () => { const pdu = buildReadSZL(0x001C, 0x0000); expect(pdu[0]).toBe(S7_PROTOCOL_ID); expect(pdu[1]).toBe(0x07); // User Data }); it('should build read SZL list request', () => { const pdu = buildReadSZLListFn(); expect(pdu[0]).toBe(S7_PROTOCOL_ID); }); }); describe('System Info builders', () => { it('should build GetOrderCode', () => { const pdu = buildGetOrderCode(); expect(pdu[0]).toBe(S7_PROTOCOL_ID); expect(pdu[1]).toBe(0x07); // User Data }); it('should build GetCpuInfo', () => { const pdu = buildGetCpuInfo(); expect(pdu[0]).toBe(S7_PROTOCOL_ID); }); it('should build GetCpInfo', () => { const pdu = buildGetCpInfo(); expect(pdu[0]).toBe(S7_PROTOCOL_ID); }); it('should build GetProtection', () => { const pdu = buildGetProtection(); expect(pdu[0]).toBe(S7_PROTOCOL_ID); }); }); describe('PLC Control', () => { it('should build PLC Hot Start', () => { const pdu = buildPlcHotStart(); expect(pdu[0]).toBe(S7_PROTOCOL_ID); expect(pdu[1]).toBe(S7_MSG_TYPE_REQUEST); }); it('should build PLC Cold Start', () => { const pdu = buildPlcColdStart(); expect(pdu[0]).toBe(S7_PROTOCOL_ID); }); it('should build PLC Stop', () => { const pdu = buildPlcStop(); expect(pdu[0]).toBe(S7_PROTOCOL_ID); }); it('should build CopyRamToRom with timeout', () => { const pdu = buildCopyRamToRom(5000); expect(pdu[0]).toBe(S7_PROTOCOL_ID); }); it('should build Compress with timeout', () => { const pdu = buildCompress(3000); expect(pdu[0]).toBe(S7_PROTOCOL_ID); }); }); describe('Security', () => { it('should build Set Password', () => { const pdu = buildSetPassword('PASS1234'); expect(pdu[0]).toBe(S7_PROTOCOL_ID); }); it('should build Clear Password', () => { const pdu = buildClearPassword(); expect(pdu[0]).toBe(S7_PROTOCOL_ID); }); }); describe('Date/Time', () => { it('should build Get PLC DateTime', () => { const pdu = buildGetPlcDateTime(); expect(pdu[0]).toBe(S7_PROTOCOL_ID); expect(pdu[1]).toBe(0x07); // User Data }); it('should build Set PLC DateTime', () => { const now = new Date(2025, 0, 15, 10, 30, 0); const pdu = buildSetPlcDateTime(now); expect(pdu[0]).toBe(S7_PROTOCOL_ID); }); it('should build Set PLC System DateTime', () => { const pdu = buildSetPlcSystemDateTime(); expect(pdu[0]).toBe(S7_PROTOCOL_ID); }); }); describe('Block Functions', () => { it('should build ListBlocks', () => { const pdu = buildListBlocks(); expect(pdu[0]).toBe(S7_PROTOCOL_ID); }); it('should build GetAgBlockInfo', () => { const pdu = buildGetAgBlockInfo(0x41, 1); // DB1 expect(pdu[0]).toBe(S7_PROTOCOL_ID); }); it('should build ListBlocksOfType', () => { const pdu = buildListBlocksOfType(0x41); // DB type expect(pdu[0]).toBe(S7_PROTOCOL_ID); }); it('should build Upload', () => { const pdu = buildUpload(0x41, 1); expect(pdu[0]).toBe(S7_PROTOCOL_ID); }); it('should build FullUpload', () => { const pdu = buildFullUpload(0x41, 1, 100); expect(pdu[0]).toBe(S7_PROTOCOL_ID); }); it('should build Download', () => { const pdu = buildDownload(1, Buffer.alloc(100)); expect(pdu[0]).toBe(S7_PROTOCOL_ID); }); it('should build Delete', () => { const pdu = buildDelete(0x41, 1); expect(pdu[0]).toBe(S7_PROTOCOL_ID); }); it('should build DBGet', () => { const pdu = buildDBGet(1); expect(pdu[0]).toBe(S7_PROTOCOL_ID); }); it('should build DBFill', () => { const pdu = buildDBFill(1, 0xFF); expect(pdu[0]).toBe(S7_PROTOCOL_ID); }); }); describe('PLC Status', () => { it('should build GetPlcStatus', () => { const pdu = buildGetPlcStatus(); expect(pdu[0]).toBe(S7_PROTOCOL_ID); }); }); describe('parseResponse', () => { it('should parse basic response', () => { const resp = Buffer.alloc(14); resp[0] = S7_PROTOCOL_ID; resp[1] = 0x02; // Response resp.writeUInt16BE(0, 2); resp.writeUInt16BE(42, 4); // pdu ref resp.writeUInt16BE(2, 6); // param len resp.writeUInt16BE(0, 8); // data len resp[10] = 0; // error class resp[11] = 0; // error code // param resp[12] = S7_FN_READ_AREA; resp[13] = 1; const result = parseResponse(resp); expect(result.msgType).toBe(0x02); expect(result.pduRef).toBe(42); expect(result.paramLen).toBe(2); expect(result.errorClass).toBe(0); expect(result.errorCode).toBe(0); }); }); describe('isS7Error / getS7ErrorCode', () => { it('should detect error response', () => { const resp = Buffer.alloc(12); resp[0] = S7_PROTOCOL_ID; resp[1] = 0x02; resp[10] = 0x05; resp[11] = 0x01; expect(isS7Error(resp)).toBe(true); expect(getS7ErrorCode(resp)).toBe(0x0501); }); it('should detect no error', () => { const resp = Buffer.alloc(12); resp[0] = S7_PROTOCOL_ID; resp[1] = 0x02; resp[10] = 0; resp[11] = 0; expect(isS7Error(resp)).toBe(false); }); it('should handle short buffer', () => { expect(isS7Error(Buffer.alloc(4))).toBe(false); expect(getS7ErrorCode(Buffer.alloc(4))).toBe(0); }); }); });