UNPKG

s7-server

Version:
354 lines 13.1 kB
"use strict"; /** * S7 Protocol Handler * * This module provides the core S7 protocol implementation, integrating: * - ISO-on-TCP transport layer * - S7 PDU parsing and generation * - Connection management * - Event-driven architecture * * The protocol handler manages the complete S7 communication lifecycle, * from initial connection establishment through PDU negotiation to * read/write operations and graceful disconnection. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.S7Protocol = void 0; // Node.js built-in modules const events_1 = require("events"); const net_1 = require("net"); // Internal modules const s7_pdu_1 = require("./s7-pdu"); const connection_manager_1 = require("./connection-manager"); /** * S7 Protocol Handler Class * * Manages the complete S7 communication protocol stack including: * - TCP server lifecycle management * - Client connection handling * - ISO-on-TCP protocol processing * - S7 PDU parsing and response generation * - Event emission for server integration */ class S7Protocol extends events_1.EventEmitter { /** * Constructor * @param config - Protocol configuration parameters */ constructor(config) { super(); this.server = null; this.isRunning = false; this.config = config; this.connectionManager = new connection_manager_1.ConnectionManager(config.maxClients, config.maxPDUSize); this.setupConnectionHandlers(); } /** * Start the S7 protocol server * * Creates a TCP server and begins listening for client connections. * Emits 'serverStarted' event on successful startup or 'listenerCannotStart' on failure. * * @returns Promise<boolean> - true if server started successfully, rejects on error * @throws {Error} - If server cannot start (e.g., port already in use) */ async start() { return new Promise((resolve, reject) => { try { this.server = new net_1.Server((socket) => { this.handleNewConnection(socket); }); this.server.listen(this.config.port, () => { this.isRunning = true; this.emit('serverStarted'); resolve(true); }); this.server.on('error', (err) => { this.emit('listenerCannotStart', err); reject(err); }); } catch (error) { reject(error); } }); } /** * Stop the S7 protocol server * * Gracefully shuts down the TCP server, disconnects all clients, * and cleans up resources. Emits 'serverStopped' event when complete. * * @returns Promise<boolean> - always resolves to true */ async stop() { return new Promise(resolve => { if (this.server) { this.server.close(async () => { await this.connectionManager.disconnectAllClients(); this.isRunning = false; this.server = null; this.emit('serverStopped'); resolve(true); }); } else { resolve(true); } }); } /** * Handle new client connection */ async handleNewConnection(socket) { try { const clientId = await this.connectionManager.handleNewConnection(socket); if (!clientId) { return; // Connection rejected } console.log(`New client connected: ${clientId} from ${socket.remoteAddress}:${socket.remotePort}`); } catch (error) { this.emit('connectionError', { error }); } } /** * Setup connection manager event handlers */ setupConnectionHandlers() { this.connectionManager.on('clientAdded', clientInfo => { this.emit('clientAdded', clientInfo); }); this.connectionManager.on('clientDisconnected', clientInfo => { this.emit('clientDisconnected', clientInfo); }); this.connectionManager.on('clientRejected', info => { this.emit('clientRejected', info); }); this.connectionManager.on('clientException', info => { this.emit('clientException', info); }); this.connectionManager.on('connectionEstablished', clientInfo => { this.emit('connectionEstablished', clientInfo); }); this.connectionManager.on('pduNegotiated', info => { this.emit('negotiatePDU', info); }); this.connectionManager.on('readRequest', async ({ clientId, request }) => { await this.handleReadRequest(clientId, request); }); this.connectionManager.on('writeRequest', async ({ clientId, request }) => { await this.handleWriteRequest(clientId, request); }); this.connectionManager.on('controlRequest', async ({ clientId, request }) => { await this.handleControlRequest(clientId, request); }); this.connectionManager.on('malformedPDU', info => { this.emit('malformedPDU', info); }); this.connectionManager.on('unknownRequest', info => { this.emit('unknownRequest', info); }); this.connectionManager.on('clientsDropped', info => { this.emit('clientsDropped', info); }); } /** * Handle S7 read request */ async handleReadRequest(clientId, request) { try { for (const item of request.Items) { const tag = { Area: item.Area, DBNumber: item.DBNumber, Start: this.parseAddress(item.Addr), Size: item.Length, WordLen: item.TransportSize }; // Emit read event for each item with transport size this.emit('dataRead', { clientId, tag, transportSize: item.TransportSize }); // Emit dataRead event for each item - the server will handle the response // Don't send response here as the server will call sendReadResponse with actual data } } catch (error) { this.emit('readError', { clientId, error }); } } /** * Handle S7 write request */ async handleWriteRequest(clientId, request) { try { const returnCodes = []; for (const item of request.Items) { const tag = { Area: item.Area, DBNumber: item.DBNumber, Start: this.parseAddress(item.Addr), Size: item.Length, WordLen: item.TransportSize }; // Emit write event for each item with data this.emit('dataWrite', { clientId, tag, data: item.Data }); // For now, assume success returnCodes.push(0xff); } // Send response // Get the correct sequence number from the connection const connection = this.connectionManager.getConnection(clientId); const sequenceNumber = connection ? connection.sequenceNumber : 1; const response = s7_pdu_1.S7PDUParser.createWriteResponse(sequenceNumber, returnCodes); await this.connectionManager.sendS7Response(clientId, response); } catch (error) { this.emit('writeError', { clientId, error }); } } /** * Handle S7 control request */ async handleControlRequest(clientId, request) { try { // Emit control event this.emit('controlRequest', { clientId, controlCode: request.ControlCode }); // Send response const response = s7_pdu_1.S7PDUParser.createControlResponse(request.ControlCode, request.ControlCode); await this.connectionManager.sendS7Response(clientId, response); } catch (error) { this.emit('controlError', { clientId, error }); } } /** * Send read response with actual data */ async sendReadResponse(clientId, tag, data, transportSize) { try { // Get the correct sequence number from the connection const connection = this.connectionManager.getConnection(clientId); const sequenceNumber = connection ? connection.sequenceNumber : 1; const { transportSize: responseTransportSize, dataLength } = s7_pdu_1.S7PDUParser.getResponseTransportSizeAndLength(transportSize || 2, data.length); const responseItem = { ReturnCode: 0xff, // Success TransportSize: responseTransportSize, // Use correct response transport size DataLength: dataLength, // Use correct data length (bytes or bits) Data: data }; const response = s7_pdu_1.S7PDUParser.createReadResponse(sequenceNumber, [responseItem]); return await this.connectionManager.sendS7Response(clientId, response); } catch (error) { this.emit('sendError', { clientId, error }); return false; } } /** * Send read error response */ async sendReadErrorResponse(clientId, _tag, _errorMessage) { try { // Get the correct sequence number from the connection const connection = this.connectionManager.getConnection(clientId); const sequenceNumber = connection ? connection.sequenceNumber : 1; const responseItem = { ReturnCode: 0x0a, // Area not available TransportSize: 0x00, DataLength: 0x0004, // 4 bytes for error code Data: Buffer.from([0x0a, 0x00, 0x00, 0x00]) // Error code }; const response = s7_pdu_1.S7PDUParser.createReadResponse(sequenceNumber, [responseItem]); return await this.connectionManager.sendS7Response(clientId, response); } catch (error) { this.emit('sendError', { clientId, error }); return false; } } /** * Send write response */ async sendWriteResponse(clientId, success) { try { // Get the correct sequence number from the connection const connection = this.connectionManager.getConnection(clientId); const sequenceNumber = connection ? connection.sequenceNumber : 1; const returnCode = success ? 0xff : 0x0a; // Success or Resource not available const response = s7_pdu_1.S7PDUParser.createWriteResponse(sequenceNumber, [returnCode]); return await this.connectionManager.sendS7Response(clientId, response); } catch (error) { this.emit('sendError', { clientId, error }); return false; } } /** * Parse address from 3-byte buffer (matches C++ SNAP7 implementation) */ 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; } /** * Get connected clients count */ getClientsCount() { return this.connectionManager.getClientsCount(); } /** * Get all connected clients */ getAllClients() { return this.connectionManager.getAllClients(); } /** * Check if server is running */ isServerRunning() { return this.isRunning; } /** * Disconnect specific client */ async disconnectClient(clientId) { await this.connectionManager.disconnectClient(clientId); } /** * Disconnect all clients */ async disconnectAllClients() { await this.connectionManager.disconnectAllClients(); } /** * Update server configuration */ updateConfig(config) { this.config = { ...this.config, ...config }; this.connectionManager.updateConfig(this.config.maxClients, this.config.maxPDUSize); } /** * Get server statistics */ getStatistics() { return { isRunning: this.isRunning, port: this.config.port, connections: this.connectionManager.getStatistics() }; } /** * Cleanup inactive connections */ cleanupInactiveConnections(timeoutMs) { this.connectionManager.cleanupInactiveConnections(timeoutMs); } } exports.S7Protocol = S7Protocol; //# sourceMappingURL=s7-protocol.js.map