ethernet-ip
Version:
A feature-complete EtherNet/IP client for Rockwell ControlLogix/CompactLogix PLCs
199 lines • 7.7 kB
JavaScript
"use strict";
/**
* Session Manager — orchestrates the connection lifecycle.
* Composes: register-session, forward-open, reconnect, state-machine
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.SessionManager = void 0;
const typed_event_emitter_1 = require("../util/typed-event-emitter");
const state_machine_1 = require("../util/state-machine");
const request_pipeline_1 = require("../pipeline/request-pipeline");
const errors_1 = require("../errors");
const types_1 = require("./types");
const register_session_1 = require("./register-session");
const forward_open_1 = require("./forward-open");
const reconnect_1 = require("./reconnect");
const logger_1 = require("../util/logger");
const EIP_PORT = 44818;
class SessionManager extends typed_event_emitter_1.TypedEventEmitter {
constructor(transport, log = logger_1.noopLogger) {
super();
this.transport = transport;
this.log = log;
this.sm = new state_machine_1.StateMachine('disconnected', {
exits: {
connecting: ['registering'],
registering: ['forward-opening', 'connected'],
'forward-opening': ['connected'],
connected: ['disconnecting', 'reconnecting'],
},
entries: {
connecting: '*',
disconnected: '*',
},
});
this._sessionId = 0;
this._connectionId = 0;
this._connectionSize = 0;
this._connectionSerial = 0;
this._sequenceCount = 0;
this._pipeline = null;
this._options = types_1.DEFAULT_CONNECT_OPTIONS;
this._reconnector = null;
this._ip = '';
this.sm.onStateChange((prev, current) => {
this.log.debug('State transition', { from: prev, to: current });
if (current !== 'reconnecting') {
this.emit(current);
}
});
}
get state() {
return this.sm.state;
}
get sessionId() {
return this._sessionId;
}
get connectionId() {
return this._connectionId;
}
get connectionSize() {
return this._connectionSize;
}
get pipeline() {
return this._pipeline;
}
/** Get and increment the sequence counter for connected messaging. */
nextSequence() {
return this._sequenceCount++ & 0xffff;
}
async connect(ip, options = {}) {
// Clean up any previous session
this._reconnector?.cancel();
if (!this.sm.is('disconnected')) {
this.cleanup();
this.sm.setState('disconnected');
}
this._ip = ip;
this._options = { ...types_1.DEFAULT_CONNECT_OPTIONS, ...options };
// TCP connect
this.sm.setState('connecting');
this.log.info('Connecting', { ip, slot: this._options.slot });
try {
await this.transport.connect(ip, EIP_PORT, this._options.timeoutMs);
}
catch (err) {
this.log.error('TCP connect failed', { ip, error: err.message });
this.sm.setState('disconnected');
throw new errors_1.ConnectionError(`TCP connect failed: ${err.message}`);
}
this._pipeline = new request_pipeline_1.RequestPipeline(this.transport);
this.transport.onClose(() => this.handleClose());
this.transport.onError((err) => {
this.log.error('Transport error', { error: err.message });
this.emit('error', err);
if (!this.transport.connected)
this.handleClose();
});
try {
// Register Session
this.sm.setState('registering');
this._sessionId = await (0, register_session_1.doRegisterSession)(this._pipeline, this._options.timeoutMs);
this.log.info('Session registered', { sessionId: this._sessionId });
// Forward Open (connected messaging)
if (this._options.connected) {
this.sm.setState('forward-opening');
try {
const result = await (0, forward_open_1.doForwardOpen)(this._pipeline, this._sessionId, this._options.slot, this._options.timeoutMs);
this._connectionId = result.connectionId;
this._connectionSize = result.connectionSize;
this._connectionSerial = result.connectionSerial;
this.log.info('Forward Open established', {
connectionId: this._connectionId,
connectionSize: this._connectionSize,
});
}
catch (err) {
this.log.error('Forward Open failed', { error: err.message });
throw new errors_1.ConnectionError(`Forward Open failed — the PLC rejected both Large and Small connection requests. ` +
`Try connecting with { connected: false } to use unconnected messaging. ` +
`Original error: ${err.message}`);
}
}
}
catch (err) {
this.cleanup();
this.sm.setState('disconnected');
throw err;
}
this.sm.setState('connected');
this.log.info('Connected', {
ip,
connected: this._options.connected,
connectionSize: this._connectionSize,
});
}
async disconnect() {
this._reconnector?.cancel();
if (this.sm.is('disconnected') || this.sm.is('reconnecting')) {
this.cleanup();
if (!this.sm.is('disconnected'))
this.sm.setState('disconnected');
return;
}
this.sm.setState('disconnecting');
try {
if (this._pipeline && this._connectionSerial) {
await (0, forward_open_1.doForwardClose)(this._pipeline, this._sessionId, this._connectionSerial, this._options.timeoutMs);
}
if (this._sessionId) {
(0, register_session_1.doUnregisterSession)(this.transport, this._sessionId);
}
}
finally {
this.cleanup();
this.sm.setState('disconnected');
this.log.info('Disconnected');
}
}
cleanup() {
this.transport.close();
this._pipeline = null;
this._sessionId = 0;
this._connectionId = 0;
this._connectionSize = 0;
this._connectionSerial = 0;
this._sequenceCount = 0;
}
handleClose() {
if (this.sm.is('disconnecting'))
return;
if (this.sm.is('disconnected'))
return;
if (this.sm.is('reconnecting'))
return;
this.log.warn('Connection lost');
this._pipeline?.flush(new errors_1.ConnectionError('Transport closed'));
this._pipeline = null;
this._sessionId = 0;
this._connectionId = 0;
this._connectionSize = 0;
this._sequenceCount = 0;
if (this._options.reconnect.enabled) {
this.sm.setState('reconnecting');
this._reconnector = new reconnect_1.Reconnector(this._options.reconnect, async (attempt) => {
this.log.warn('Reconnect attempt', { attempt });
this.emit('reconnecting', attempt);
await this.connect(this._ip, this._options);
});
if (!this._reconnector.schedule()) {
this.sm.setState('disconnected');
}
}
else {
this.sm.setState('disconnected');
}
}
}
exports.SessionManager = SessionManager;
//# sourceMappingURL=session-manager.js.map