codesys-client
Version:
Node.js Codesys client for reading and writing PLC data using network variable lists (NVL)
376 lines (375 loc) • 15.9 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Receiver = void 0;
const events_1 = require("events");
const dgram_1 = __importDefault(require("dgram"));
const debug_1 = __importDefault(require("debug"));
class Receiver extends events_1.EventEmitter {
/**
* Constructor
*
* @param settings Settings object
*/
constructor(settings) {
super();
this.debug = debug_1.default(`codesys-client-receiver`);
this.debugD = debug_1.default(`codesys-client-receiver:details`);
this.debugIO = debug_1.default(`codesys-client-receiver:raw-data`);
/**
* Active debug level
* - 0 = no debugging
* - 1 = basic debugging (same as $env:DEBUG='codesys-client-receiver')
* - 2 = detailed debugging (same as $env:DEBUG='codesys-client-receiver,codesys-client-receiver:details')
* - 3 = full debugging (same as $env:DEBUG='codesys-client-receiver,codesys-client-receiver:details,codesys-client-receiver:raw-data')
*/
this.debugLevel = 0;
/**
* Active settings
*/
this.settings = {
ListeningPort: 1202
};
/**
* Socket instance
*/
this.socket = undefined;
/**
* Buffer for each list ID of incomplete packets
* (data is still coming up)
*/
this.packetBuffer = {};
/**
* Array handlers added by user
*/
this.handlers = [];
//Updating all provided settings, other left as default
this.settings = {
...this.settings,
...settings
};
this.debug(`Receiver(): initialized with settings %o`, this.settings);
}
/**
* Sets debugging using debug package on/off.
* Another way for environment variable DEBUG:
* - 0 = no debugging
* - 1 = basic debugging (same as $env:DEBUG='codesys-client-receiver')
* - 2 = detailed debugging (same as $env:DEBUG='codesys-client-receiver,codesys-client-receiver:details')
* - 3 = full debugging (same as $env:DEBUG='codesys-client-receiver,codesys-client-receiver:details,codesys-client-receiver:raw-data')
*
* @param level 0 = none, 1 = basic, 2 = detailed, 3 = detailed + raw data
*/
setDebugging(level) {
this.debugLevel = level;
this.debug.enabled = level >= 1;
this.debugD.enabled = level >= 2;
this.debugIO.enabled = level >= 3;
this.debug(`setDebugging(): Debug level set to ${level}`);
}
/**
* Starts listening for incoming data from
* the UDP port (and interface) provided in settings
*/
listen() {
return new Promise(async (resolve, reject) => {
this.debug(`listen(): Starting listening UDP port ${this.settings.LocalAddress ? this.settings.LocalAddress : ''}:${this.settings.ListeningPort}`);
if (this.socket) {
this.debug(`listen(): Failed to start listening - there is already a connection`);
return reject(`Failed to start listening - there is already a connection`);
}
const socket = dgram_1.default.createSocket({
type: 'udp4',
reuseAddr: true
});
this.socket = socket;
this.debug(`listen(): UDP socket created`);
const bindingErrorEvent = (err) => {
//Error during binding
this.debug(`listen(): Binding to UDP port ${this.settings.LocalAddress ? this.settings.LocalAddress : ''}:${this.settings.ListeningPort} failed - ${err}`);
return reject(`Binding to UDP port ${this.settings.LocalAddress ? this.settings.LocalAddress : ''}:${this.settings.ListeningPort} failed - ${err}`);
};
socket.on('error', bindingErrorEvent);
try {
socket.bind(this.settings.ListeningPort, this.settings.LocalAddress, () => {
//Binding successful
this.debug(`listen(): UDP socket successfully binded, listening started`);
//Removing listeners used during connection
socket.off('error', bindingErrorEvent);
//Adding data listener + error listener
socket.on('error', (err) => this.handleSocketError(err));
socket.on('message', (data, info) => this.handleReceivedData(data, info));
resolve(socket.address());
});
}
catch (err) {
//Binding to port failed ("In rare case (e.g. attempting to bind with a closed socket), an Error may be thrown")
this.debug(`listen(): Binding to UDP port ${this.settings.LocalAddress ? this.settings.LocalAddress : ''}:${this.settings.ListeningPort} failed - ${err}`);
reject(`Binding to UDP port ${this.settings.LocalAddress ? this.settings.LocalAddress : ''}:${this.settings.ListeningPort} failed - ${err}`);
}
});
}
/**
* Stops listening for incoming data
*/
close() {
return new Promise(async (resolve) => {
this.debug(`close(): Closing socket end stopping listening`);
//Removes socket and cleans everything up
const cleanup = () => {
var _a;
(_a = this.socket) === null || _a === void 0 ? void 0 : _a.removeAllListeners();
delete this.socket;
this.handlers = [];
this.packetBuffer = {};
};
if (!this.socket) {
this.debug(`close(): No open socket available`);
cleanup();
return resolve();
}
try {
this.socket.close(() => {
this.debug(`close(): Socket closed successfully`);
cleanup();
resolve();
});
}
catch (err) {
this.debug(`close(): Socket closing failed, error: ${err}`);
cleanup();
resolve();
}
});
}
/**
* Adds a new data handler callback for incoming data.
* Callback is called with received data if listID matches
*
* @param listID List ID ("Listidentifier") of the network variable list (NVL)
* @param dataType IEC-61131-3 data type schema of the provided data (like iec.INT, iec.STRUCT, etc.)
* @param callback Callback to be called when data is received
* @returns
*/
addHandler(listID, dataType, callback) {
const handler = {
listID,
dataType,
callback
};
this.handlers.push(handler);
return handler;
}
/**
* Removes the given handler from handler list
* and stops calling its callback
* @param handler Handler to be removed - created previously with addHandler()
*/
removeHandler(handler) {
this.handlers = this.handlers.filter(x => x !== handler);
}
/**
* Removes all registered handlers
*/
removeAllHandlers() {
this.handlers = [];
}
/**
* Callback for socket errors
* @param err
*/
handleSocketError(err) {
this.debug(`handleSocketError(): Socket error occured: ${err}`);
this.emit('socket-error', err);
}
/**
* Handles received data from socket
*
* @param data
* @param info
*/
handleReceivedData(data, info) {
this.debugD(`handleReceivedData(): Data received (${data.byteLength} bytes) from ${info.address}:${info.port}`);
//Todo: Adding to data buffer if < 20 bytes?
this.parseReceivedPacket(data);
}
/**
* Parses received from byte buffer to object
* @param data
*/
parseReceivedPacket(data) {
const packet = {};
//Header
packet.header = this.parseReceivedPacketHeader(data);
//Payload
packet.payload = {
rawData: data.slice(packet.header.rawData.byteLength),
parsed: {}
};
//Raw data
packet.rawData = data;
this.debugD(`parseReceivedPacket(): Received packet for list index ${packet.header.index} (subindex ${packet.header.subIndex})`);
this.handleReceivedPacket(packet);
}
/**
* Parses header from received data
* @param data
* @returns
*/
parseReceivedPacketHeader(data) {
const header = {};
if (data.byteLength < 20) {
//TODO
}
header.rawData = data.slice(0, 20);
let pos = 0;
//0..3 - Identity
header.identity = data.slice(pos, 4);
pos += 4;
//4..7 - Type (0 = network variable)
header.type = data.readUInt32LE(pos);
pos += 4;
//8..9 - Index (list ID)
header.index = data.readUInt16LE(pos);
pos += 2;
//10..11 - SubIndex
header.subIndex = data.readUInt16LE(pos);
pos += 2;
//12..13 - Variable count
header.variableCount = data.readUInt16LE(pos);
pos += 2;
//14..15 - Packet total length
header.packetLength = data.readUInt16LE(pos);
pos += 2;
//16..17 - Increasing counter
header.counter = data.readUInt16LE(pos);
pos += 2;
//19 - Flags
header.flags = data.readUInt8(pos);
header.flagsStr = this.parseHeaderFlags(header.flags);
pos += 1;
//20 - Checksum
header.checksum = data.readUInt8(pos);
pos += 1;
//Payload length (not part of codesys header)
header.payloadLength = header.packetLength - 20;
return header;
}
/**
* Parses header flags as string from bits
* @param flags
* @returns
*/
parseHeaderFlags(flags) {
const flagsStr = [];
//Bit 0 - Send-acknowledgement desired
if ((flags & 0x01) === 0x01)
flagsStr.push('SendAckRequested');
//Bit 1 - Check of checksum desired
if ((flags & 0x02) === 0x02)
flagsStr.push('ChecksumIncluded');
//Bit 2 - Invalid checksum
if ((flags & 0x04) === 0x04)
flagsStr.push('InvalidChecksum');
return flagsStr;
}
/**
* Handles received packet and adds to packet buffer
* @param packet
*/
handleReceivedPacket(packet) {
this.debugD(`handleReceivedPacket(): Received a packet for list ID ${packet.header.index}`);
let buffer = this.packetBuffer[packet.header.index];
if (buffer && buffer.counter === packet.header.counter) {
//There is already data for this list (counter is the same)
//If subindex has increased by one, this is the next one -> OK
if (packet.header.subIndex === buffer.packets[buffer.packets.length - 1].header.subIndex + 1) {
buffer.packets.push(packet);
buffer.totalDataBytes += packet.payload.rawData.byteLength;
this.debugD(`handleReceivedPacket(): There was already data for this list, now total data of ${buffer.totalDataBytes} bytes buffered`);
}
else {
//Something is missing in between. Just delete what we have (= packet is lost)
this.debugD(`handleReceivedPacket(): One or more packets are missing from list ID ${packet.header.index} - Packets lost.`);
delete this.packetBuffer[packet.header.index];
buffer = undefined;
}
}
else {
//Nothing in buffer for this list OR some packets have been lost in between
if (buffer && !buffer.handled) {
this.debug(`handleReceivedPacket(): No listener for list ID ${packet.header.index} or some packets were lost. Counter change from ${buffer.counter} to ${packet.header.counter}. We had ${buffer.totalDataBytes} bytes.`);
delete this.packetBuffer[packet.header.index];
}
//If this packet is the first, start again
if (packet.header.subIndex === 0) {
this.debugD(`handleReceivedPacket(): First packet received for list ID ${packet.header.index}`);
//Creating a new buffer for this counter value
this.packetBuffer[packet.header.index] = {
handled: false,
counter: packet.header.counter,
packets: [packet],
totalDataBytes: packet.payload.rawData.byteLength
};
buffer = this.packetBuffer[packet.header.index];
}
else {
//This is not the first packet, do nothing
this.debugD(`handleReceivedPacket(): Packet number ${packet.header.subIndex} received for list ID ${packet.header.index} but no previous packets -> discarding`);
}
}
if (buffer) {
this.checkReceivedPacket(packet.header.index, buffer);
}
}
/**
* Checks if received packets are fully received
* @param index
* @param buffer
*/
checkReceivedPacket(index, buffer) {
const listeners = this.handlers.filter(listener => listener.listID === index);
if (listeners.length > 0) {
//we have a listener(s) for this listID - do we have all data?
//Note: Checking only the first listener at the moment (todo?)
if (listeners[0].dataType.byteLength === buffer.totalDataBytes) {
this.debug(`checkReceivedPacket(): Full packet received for list ID ${index} (${buffer.totalDataBytes} bytes)`);
const data = this.convertBufferEntriesToPacket(buffer);
for (const listener of listeners) {
listener.callback(listener.dataType.convertFromBuffer(data), listener);
}
buffer.handled = true;
}
else if (listeners[0].dataType.byteLength > buffer.totalDataBytes) {
//Full packet is not yet received
this.debugD(`checkReceivedPacket(): Not enough data for full packet yet. We have ${buffer.totalDataBytes}/${listeners[0].dataType.byteLength} bytes for list ID ${index}`);
}
else {
//We have more data than expected
this.debug(`checkReceivedPacket(): Data sizes do not match for list ID ${index}. Received ${buffer.totalDataBytes} bytes but expecting ${listeners[0].dataType.byteLength} bytes`);
}
}
else {
//No listener for this list ID
this.debug(`checkReceivedPacket(): Received data for list ID ${index} but no listeners registered -> discarding data and following packets`);
delete this.packetBuffer[index];
}
}
/**
* Combines all received packets to one single packet
* @param buffer
* @returns
*/
convertBufferEntriesToPacket(buffer) {
const data = Buffer.alloc(buffer.totalDataBytes);
let pos = 0;
buffer.packets.forEach(packet => {
packet.payload.rawData.copy(data, pos);
pos += packet.payload.rawData.byteLength;
});
return data;
}
}
exports.Receiver = Receiver;