SerafinTech / ST-node-ethernet-ip

Connect to PLC controllers with Node and Ethernet/ip
MIT License
36 stars 20 forks source link

Supporting SEW VFD (MDX61B/MOVITRAC b drives) #62

Closed greg9504 closed 4 months ago

greg9504 commented 5 months ago

I'm trying to get ethernet-ip working with SEW VFD's. These do NOT support the 0x6b class object used for tags. The drives support the Assembly (0x04) and Register (0x07) object classes. I have been able to connect and configure the drives using the library and the getAttributeSingle/setAttributeSingle methods. I did have to make one small change to the library to get it to work. Code is below for how I'm using the library.

What I'm wondering is:

I'm pretty green on Ethernet-IP. So any suggestions welcome.

Anyway here is how I'm using it to talk to the SEW drive. The small modification I had to make to the library was to add a Buffer parameter to the getAttributeSingle call. This is for getting drive parameter values, you need to send some data identifying which parameter you want to get.

import { ControllerManager, TagList, IO, Tag, Controller } from "st-ethernet-ip";
async function main() {
const PLC = new Controller();
// connect to the SEW VFD, do not do setup, while the VFD supports 
// Identity Class 0x01 (readControllerProps), it does not support
// Class 0x6b for tags (getControllerTagList)
PLC.connect("192.168.1.159", 0,false).then(async () => {
    // write process output data, there are 3 words, PO1/PO2/PO3
    // example configuration on the VFD:
    // PO1 = Control Word (direction of rotation, control command, param set, etc) (0x0005)
    // PO2 = Setpoint Speed (rpm of motor) (6000 rpm, encoded as 6000/0.2 = 30000 = 0x7530)
    // PO3 = Unassigned (set to 0x0000)
    const buf = Buffer.from("050030750000", 'hex');
    await PLC.setAttributeSingle(0x04,120, 3, buf );// class 0x04 Assembly Object, Instance 120 (decimal) output process data, attribute 3
    console.log('wrote PO data');
    // read the process input data, that is the actual values of the drive
    // example configuration
    // PI1 = Status Word
    // PI2 = Actual Speed
    // PI3 = Output Current
    const value = await PLC.getAttributeSingle(0x04, 130, 3);// class 0x04 Assembly Object, Instance 130 (decimal) input process data, attribute 3
    console.log('Got actual values from VFD: ' + value.toString('hex'));
    //SEW drive alays returns 10 words, we only care about the first 3

    //now read a parameter setting, this is done using the Register Object class 0.07
    //data needs to be passed in the getAttributeSingle call to identify which parameter
    //is to be read
    // read parameter 006 Motor Utilization, parameter index is 0x2083
    const paraBuf = Buffer.from("832000000000000000000000", 'hex');
    // paramBuf is in format of SEW paameter channel:
    // Index:       UINT : SEW parameter index (NOT the parameter number)
    // Data:        UDINT : Data(32 bit) 
    // SubIndex:    BYTE: Sew unit subindex (usually 0)
    // Reserved:    BYTE: 0
    // SubAddress1: BYTE: 0 if Parameter of Drive or com card, 1-63 for sbus
    // SubChannel1: BYTE: 0 if Parameter of Drive or com card, 2 sbus
    // SubAddress2: BYTE: 0
    // SubChannel2: BYTE: 0
    const paramValue = await PLC.getAttributeSingle(0x07, 1, 4, paraBuf); //class 0x07 Register, instance 1 to read (2 to set), attribute 4 param value 
    console.log('Got actual parameter value from VFD: ' + paramValue.toString('hex'));
    //we can set SEW paramters values as well, instance 2 must be used, the parameter and value is encoded same as for read
    //but the Data bytes are filled in with the new parameter value.
    // parameter 130 Speed Ramps1::Ramp t11 up CW, index 0x2116, value 6 seconds (6 seconds is transmitted as 6000)
    const newParaBuf = Buffer.from("1621701700000000000000000", 'hex');
    const newParamValue = await PLC.setAttributeSingle(0x07, 2, 4, newParaBuf); //class 0x07 Register, instance 1 to read (2 to set), attribute 4 param value 
    console.log('Set parameter succeeded');

}).catch((error) => {
    console.log(error);
  });
;

console.log('Hello world!');
}
main();

getAttributeSingle:

async getAttributeSingle(classID: number, instance: number, attribute: number, attData?: Buffer): Promise<Buffer> {
        const { GET_ATTRIBUTE_SINGLE } = CIP.MessageRouter.services;
        const { LOGICAL } = CIP.EPATH.segments;

        const identityPath = Buffer.concat([
            LOGICAL.build(LOGICAL.types.ClassID, classID), 
            LOGICAL.build(LOGICAL.types.InstanceID, instance), 
            LOGICAL.build(LOGICAL.types.AttributeID, attribute) 
        ]);

        const MR = CIP.MessageRouter.build(GET_ATTRIBUTE_SINGLE, identityPath, attData ? attData :  Buffer.from([]));

        super.write_cip(MR, super.established_conn);
...
greg9504 commented 5 months ago

Looked at it some more and got the PO/IO VFD data being read using implicit messaging via the IO class. There was one change to make in the src/io/ tcp/index.ts Controller class. The order of the input and output had to be swamped, otherwise the VFD reported back invalid path. I'm trying to find if he order is actually specified anywhere.

Line 375 (and probably line 373) From

 cipPath = Buffer.concat([assemblyObjectClass, configInstance, pointOT, pointTO]);

To

cipPath = Buffer.concat([assemblyObjectClass, configInstance, pointTO, pointOT]); //swap order of pointTO and pointOT

Full working code

import { ControllerManager, TagList, IO, Tag, Controller } from "st-ethernet-ip";

// connect to the SEW VFD IO data (PO1/PO2/PO2 and PI1/PI2/PI3) using the IO scanner
async function main() {
// If you have any running instances of Ethernet tools (say EnIPExplorer) shut it down or 
// opening the port 2222 will fail
    const scanner = new IO.Scanner(2222, '0.0.0.0'); // Iinitalize new scanner on default port 2222

    // device configuration from manufacturer.
    const config = {
        // this defines the connection path, see SEW eds file example: "20 04 24 01 2C 78 2C 82"
        // see Fieldbus Interface DFE33B manual page 57, 6.2 Data Exchange Assembly Object 0x04
        configInstance: {
            assembly: 1,
            size: 0
        },
        outputInstance: {
            assembly: 130,//0x82
            size: 20
        },
        inputInstance: {
            assembly: 120, //0x78
            size: 20
        }
    }

    // Add a connection with (device config, rpi, ip_address)
    const conn = scanner.addConnection(config, 500, '192.168.1.159');//VFD at '192.168.1.159'
    // Above does forwardOpen async, returns before connection is open
    // connection is not marked open until first UDP packet (acutal data) is received

    // Called when UDP packets are not receiving. (Timeout is based on rpi setting)
    conn.on('disconnected', () => {
        console.log('Disconnected');
    });
    // After first UDP packet is received
    conn.on('connected', () => {
        console.log('Connected')
        console.log('input data => ', conn.inputData); // Display Input Data Buffer.

        console.log('output data => ', conn.outputData); // Display Output Data Buffer.
        // Create alias for bits and integers (can be named what ever you want)
        conn.addOutputBit(0, 8, 'outMotorDir'); // Using outputData. bit 8 is motor direction in control word
        conn.addOutputInt(2, 'outMotorSpeed'); // second outputData word is configured as motor speed
        conn.addInputBit(0, 5, 'inFaultorWarn'); // first inputData word is Status Word, bit 8 is fault or warn indicator
        conn.addInputInt(2, 'inActualSpeed'); // 
        // Set values to be written to devices
        conn.setValue('outMotorDir', true);
        conn.setValue('outMotorSpeed', 23500);
        // Read values from device connection
        console.log(conn.getValue('inFaultorWarn'));
        console.log(conn.getValue('inActualSpeed'));
        setInterval(() => {
            if (conn.connected) {
                console.log('input data => ', conn.inputData.toString('hex')); // Display Input Data Buffer.
                console.log('Fault: ' + conn.getValue('inFaultorWarn'));
                console.log('Acutal Speed: ' + conn.getValue('inActualSpeed'));
            }

        }, 4000);
    });

    console.log('Hello world!');
}
main();

Does seem like the scanner.addConnection method needs a way to indicate an error opening the connection. If I understand it, the code will just try over and over to establish the connection without being able to signal to caller that an error happened.

greg9504 commented 4 months ago

Going to close this. You can ignore the problem I had with the Input/Output of the IO. I'll open a pull request for the getAttributeSingle change.