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