st-ethernet-ip
Version:
A simple node interface for Ethernet/IP.
199 lines (169 loc) • 7.73 kB
text/typescript
import * as EthernetIP from "../enip";
import dgram, { Socket } from "dgram";
import { EventEmitter } from "events";
const listIdentityRequest = EthernetIP.encapsulation.header.build(EthernetIP.encapsulation.commands.ListIdentity);
type browserDevice = {
EncapsulationVersion: number,
socketAddress: {
sin_family: number,
sin_port: number,
sin_addr: string,
sin_zero: Buffer
},
vendorID: number,
deviceType: number,
productCode: number,
revision: string,
status: number,
serialNumber: string,
productName: string,
state: number,
timestamp: number
}
export declare interface Browser extends EventEmitter {
socket: Socket;
originatorIPaddress: string;
autoBrowse: boolean;
updateRate: number;
disconnectMultiplier: number;
deviceList: browserDevice[];
updateInterval: NodeJS.Timer;
on(event: 'New Device', listener: (device: browserDevice) => {}): this;
on(event: 'Broadcast Request', listener: () => {}): this;
on(event: 'Device Disconnected', listener: (device: browserDevice) => {}): this;
on(event: 'Device List Updated', listener: (deviceList: browserDevice[]) => {}): this;
on(event: string, listener: Function): this;
}
export class Browser extends EventEmitter{
constructor(originatorPort: number=51687, originatorIPaddress: string="0.0.0.0", autoBrowse: boolean=true, updateRate: number=3000, disconnectMultiplier: number=4) {
super();
this.socket = dgram.createSocket("udp4");
this.originatorIPaddress = originatorIPaddress;
this.autoBrowse = autoBrowse;
this.updateRate = updateRate;
this.disconnectMultiplier = disconnectMultiplier;
this.deviceList = [];
this.socket.bind(originatorPort, originatorIPaddress, () => {
this.socket.setBroadcast(true);
if(this.autoBrowse) this.start();
});
this._setupSocketEvents();
this.updateInterval = null;
}
start() {
if (this.updateInterval) {
clearInterval(this.updateInterval);
}
this.deviceList.forEach( (dev, i) => {
this.deviceList[i].timestamp = Date.now();
});
this.updateInterval = setInterval(() => {
this.checkStatus();
this.socket.send(listIdentityRequest, 44818, "255.255.255.255", (e) => {
if (e) throw e;
this.emit("Broadcast Request");
});
}, this.updateRate);
}
stop() {
clearInterval(this.updateInterval);
}
checkStatus() {
let deviceDisconnected = false;
this.deviceList.forEach(device => {
if ( (Date.now() - device.timestamp) > (this.updateRate * this.disconnectMultiplier) ) {
this.emit("Device Disconnected", device);
deviceDisconnected = true;
}
});
this.deviceList = this.deviceList.filter(device => (Date.now() - device.timestamp) <= (this.updateRate * this.disconnectMultiplier));
if (deviceDisconnected) this.emit("Device List Updated", this.deviceList);
}
_setupSocketEvents() {
this.socket.on("message", msg => {
const device = this._parseListIdentityResponse(msg);
if(Object.keys(device).length !== 0)
this._addDevice(device); // Device is added only if device is not empty object
});
}
_parseListIdentityResponse(msg) {
let response: browserDevice = {
EncapsulationVersion: undefined,
socketAddress: {
sin_family: undefined,
sin_port: undefined,
sin_addr: undefined,
sin_zero: undefined
},
vendorID: undefined,
deviceType: undefined,
productCode: undefined,
revision: undefined,
status: undefined,
serialNumber: undefined,
productName: undefined,
state: undefined,
timestamp: undefined
};
const messageData = EthernetIP.encapsulation.header.parse(msg);
// Check if messageData is not undefined
if (messageData !== undefined) {
// Check if messageData.data is not undefined
if (messageData.data !== undefined) {
// Check if messageData.data has a congruent length
if (messageData.data.length >= 2) {
const cpf = EthernetIP.encapsulation.CPF.parse(messageData.data);
// Check if cpf is not undefined
if (cpf !== undefined) {
// Check if cpf is an array
if (Array.isArray(cpf)) {
// Check if cpf[0] is not undefined
if (cpf[0] !== undefined) {
const data = cpf[0].data;
let ptr = 0;
response.EncapsulationVersion = data.readUInt16LE(ptr);
ptr += 2;
response.socketAddress.sin_family = data.readUInt16BE(ptr);
ptr += 2;
response.socketAddress.sin_port = data.readUInt16BE(ptr);
ptr += 2;
response.socketAddress.sin_addr = data.readUInt8(ptr).toString() + "." + data.readUInt8(ptr + 1).toString() + "." + data.readUInt8(ptr + 2).toString() + "." + data.readUInt8(ptr + 3).toString();
ptr += 4;
response.socketAddress.sin_zero = data.slice(ptr, ptr + 8);
ptr += 8;
response.vendorID = data.readUInt16LE(ptr);
ptr += 2;
response.deviceType = data.readUInt16LE(ptr);
ptr += 2;
response.productCode = data.readUInt16LE(ptr);
ptr += 2;
response.revision = data.readUInt8(ptr).toString() + "." + data.readUInt8(ptr + 1).toString();
ptr += 2;
response.status = data.readUInt16LE(ptr);
ptr += 2;
response.serialNumber = "0x" + data.readUInt32LE(ptr).toString(16);
ptr += 4;
response.productName = data.slice(ptr + 1, ptr + 1 + data.readUInt8(ptr)).toString();
ptr += (1 + data.readUInt8(ptr));
response.state = data.readUInt8(ptr);
}
}
}
}
}
}
return response;
}
_addDevice(device) {
const index = this.deviceList.findIndex(item => item.socketAddress.sin_addr === device.socketAddress.sin_addr);
device.timestamp = Date.now();
if(index > -1) {
this.deviceList[index] = device;
} else {
this.deviceList.push(device);
this.emit("New Device", device);
this.emit("Device List Updated", this.deviceList);
}
}
}
export default Browser;