st-ethernet-ip
Version:
A simple node interface for Ethernet/IP.
292 lines (291 loc) • 9.77 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.sendUnitData = exports.sendRRData = exports.unregisterSession = exports.registerSession = exports.parseStatus = exports.commands = exports.validateCommand = exports.CPF = exports.header = void 0;
const commands = {
NOP: 0x00,
ListServices: 0x04,
ListIdentity: 0x63,
ListInterfaces: 0x64,
RegisterSession: 0x65,
UnregisterSession: 0x66,
SendRRData: 0x6f,
SendUnitData: 0x70,
IndicateStatus: 0x72,
Cancel: 0x73
};
exports.commands = commands;
// region Validation Helper Functions
/**
* Parses Encapulation Status Code to Human Readable Error Message.
*
* @param status - Status Code
* @returns Human Readable Error Message
*/
const parseStatus = (status) => {
if (typeof status !== "number")
throw new Error("parseStatus accepts type <Number> only!");
/* eslint-disable indent */
switch (status) {
case 0x00:
return "SUCCESS";
case 0x01:
return "FAIL: Sender issued an invalid ecapsulation command.";
case 0x02:
return "FAIL: Insufficient memory resources to handle command.";
case 0x03:
return "FAIL: Poorly formed or incorrect data in encapsulation packet.";
case 0x64:
return "FAIL: Originator used an invalid session handle.";
case 0x65:
return "FAIL: Target received a message of invalid length.";
case 0x69:
return "FAIL: Unsupported encapsulation protocol revision.";
default:
return `FAIL: General failure <${status}> occured.`;
}
/* eslint-enable indent */
};
exports.parseStatus = parseStatus;
/**
* Checks if Command is a Valid Encapsulation Command
*
* @param cmd - Encapsulation command
* @returns test result
*/
const validateCommand = (cmd) => {
for (let key of Object.keys(commands)) {
if (cmd === commands[key])
return true;
}
return false;
};
exports.validateCommand = validateCommand;
let CPF = {
ItemIDs: {
Null: 0x00,
ListIdentity: 0x0c,
ConnectionBased: 0xa1,
ConnectedTransportPacket: 0xb1,
UCMM: 0xb2,
ListServices: 0x100,
SockaddrO2T: 0x8000,
SockaddrT2O: 0x8001,
SequencedAddrItem: 0x8002
},
/**
* Checks if Command is a Valid Encapsulation Command
*
* @param cmd - Encapsulation command
* @returns test result
*/
isCmd: (cmd) => {
for (let key of Object.keys(CPF.ItemIDs)) {
if (cmd === CPF.ItemIDs[key])
return true;
}
return false;
},
/**
* Builds a Common Packet Formatted Buffer to be
* Encapsulated.
*
* @param dataItems - Array of CPF Data Items
* @returns CPF Buffer to be Encapsulated
*/
build: (dataItems) => {
// Write Item Count and Initialize Buffer
let buf = Buffer.alloc(2);
buf.writeUInt16LE(dataItems.length, 0);
for (let item of dataItems) {
const { TypeID, data } = item;
if (!CPF.isCmd(TypeID))
throw new Error("Invalid CPF Type ID!");
let buf1 = Buffer.alloc(4);
let buf2 = Buffer.from(data);
buf1.writeUInt16LE(TypeID, 0);
buf1.writeUInt16LE(buf2.length, 2);
buf = buf2.length > 0 ? Buffer.concat([buf, buf1, buf2]) : Buffer.concat([buf, buf1]);
}
return buf;
},
/**
* Parses Incoming Common Packet Formatted Buffer
* and returns an Array of Objects.
*
* @param {Buffer} buf - Common Packet Formatted Data Buffer
* @returns {Array} Array of Common Packet Data Objects
*/
parse: (buf) => {
const itemCount = buf.readUInt16LE(0);
let ptr = 2;
let arr = [];
for (let i = 0; i < itemCount; i++) {
// Get Type ID
const TypeID = buf.readUInt16LE(ptr);
ptr += 2;
// Get Data Length
const length = buf.readUInt16LE(ptr);
ptr += 2;
// Get Data from Data Buffer
const data = Buffer.alloc(length);
buf.copy(data, 0, ptr, ptr + length);
// Append Gathered Data Object to Return Array
arr.push({ TypeID, length, data });
ptr += length;
}
return arr;
}
};
exports.CPF = CPF;
let header = {
/**
* Builds an ENIP Encapsolated Packet
*
* @param cmd - Command to Send
* @param session - Session ID
* @param data - Data to Send
* @returns Generated Buffer to be Sent to Target
*/
build: (cmd, session = 0x00, data = []) => {
// Validate requested command
if (!validateCommand(cmd))
throw new Error("Invalid Encapsulation Command!");
const buf = Buffer.from(data);
// Initialize header buffer to appropriate length
let header = Buffer.alloc(24 + buf.length, 0);
// Build header from encapsulation data
header.writeUInt16LE(cmd, 0);
header.writeUInt16LE(buf.length, 2);
header.writeUInt32LE(session, 4);
buf.copy(header, 24);
return header;
},
/**
* Parses an Encapsulated Packet Received from ENIP Target
*
* @param buf - Incoming Encapsulated Buffer from Target
* @returns Parsed Encapsulation Data Object
*/
parse: (buf) => {
if (!Buffer.isBuffer(buf))
throw new Error("header.parse accepts type <Buffer> only!");
const received = {
commandCode: buf.readUInt16LE(0),
command: null,
length: buf.readUInt16LE(2),
session: buf.readUInt32LE(4),
statusCode: buf.readUInt32LE(8),
status: null,
options: buf.readUInt32LE(20),
data: null
};
// Get Returned Encapsulated Data
let dataBuffer = Buffer.alloc(received.length);
buf.copy(dataBuffer, 0, 24);
received.data = dataBuffer;
received.status = parseStatus(received.statusCode);
for (let key of Object.keys(commands)) {
if (received.commandCode === commands[key]) {
received.command = key;
break;
}
}
return received;
},
};
exports.header = header;
// endregion
// region Common Command Helper Build Funtions
/**
* Returns a Register Session Request String
*
* @returns register session buffer
*/
const registerSession = () => {
const { RegisterSession } = commands;
const { build } = header;
const cmdBuf = Buffer.alloc(4);
cmdBuf.writeUInt16LE(0x01, 0); // Protocol Version (Required to be 1)
cmdBuf.writeUInt16LE(0x00, 2); // Opton Flags (Reserved for Future List)
// Build Register Session Buffer and return it
return build(RegisterSession, 0x00, cmdBuf);
};
exports.registerSession = registerSession;
/**
* Returns an Unregister Session Request String
*
* @param session - Encapsulation Session ID
* @returns unregister session buffer
*/
const unregisterSession = (session) => {
const { UnregisterSession } = commands;
const { build } = header;
// Build Unregister Session Buffer
return build(UnregisterSession, session);
};
exports.unregisterSession = unregisterSession;
/**
* Returns a UCMM Encapsulated Packet String
*
* @param session - Encapsulation Session ID
* @param data - Data to be Sent via UCMM
* @param timeout - Timeout (sec)
* @returns UCMM Encapsulated Message Buffer
*/
const sendRRData = (session, data, timeout = 10) => {
const { SendRRData } = commands;
let timeoutBuf = Buffer.alloc(6);
timeoutBuf.writeUInt32LE(0x00, 0); // Interface Handle ID (Shall be 0 for CIP)
timeoutBuf.writeUInt16LE(timeout, 4); // Timeout (sec)
// Enclose in Common Packet Format
let buf = CPF.build([
{ TypeID: CPF.ItemIDs.Null, data: Buffer.from([]), length: undefined },
{ TypeID: CPF.ItemIDs.UCMM, data: data, length: undefined }
]);
// Join Timeout Data with
buf = Buffer.concat([timeoutBuf, buf]);
// Build SendRRData Buffer
return header.build(SendRRData, session, buf);
};
exports.sendRRData = sendRRData;
/**
* Returns a Connected Message Datagram (Transport Class 3) String
*
* @param {number} session - Encapsulation Session ID
* @param {Buffer} data - Data to be Sent via Connected Message
* @param {number} ConnectionID - Connection ID from FWD_OPEN
* @param {number} SequenceNumber - Sequence Number of Datagram
* @returns Connected Message Datagram Buffer
*/
const sendUnitData = (session, data, ConnectionID, SequnceNumber) => {
const { SendUnitData } = commands;
let timeoutBuf = Buffer.alloc(6);
timeoutBuf.writeUInt32LE(0x00, 0); // Interface Handle ID (Shall be 0 for CIP)
timeoutBuf.writeUInt16LE(0x00, 4); // Timeout (sec) (Shall be 0 for Connected Messages)
// Enclose in Common Packet Format
const seqAddrBuf = Buffer.alloc(4);
seqAddrBuf.writeUInt32LE(ConnectionID, 0);
const seqNumberBuf = Buffer.alloc(2);
seqNumberBuf.writeUInt16LE(SequnceNumber, 0);
const ndata = Buffer.concat([
seqNumberBuf,
data
]);
let buf = CPF.build([
{
TypeID: CPF.ItemIDs.ConnectionBased,
data: seqAddrBuf,
length: undefined
},
{
TypeID: CPF.ItemIDs.ConnectedTransportPacket,
data: ndata,
length: undefined
}
]);
// Join Timeout Data with
buf = Buffer.concat([timeoutBuf, buf]);
// Build SendRRData Buffer
return header.build(SendUnitData, session, buf);
};
exports.sendUnitData = sendUnitData;