Using NW.js to communicate with a DS4 controller

DS4 green light

NW.js still provides the Chrome Apps API which has been removed from Chrome, but not ChromeOS. This will allow us to access in a platform-independant manner devices which are connected with the PC per USB.

Without this API, a 3rd party Node.js module like node-hid could be used. This will however come with platform-dependant libraries and will have to be updated or rebuild each time the Node.js version changes.

This article concentrates on sending data to the controller. However it is also possible to retrieve data like pressed buttons using the established connection. Aside from using chrome.hid there is also the Gamepad API for read-only access.

Identifying the controller

First we need a way to identify the DS4. Devices come with a vendor Id and product Id. According to the Gentoo Wiki they are as follows:

Device Vendor Id Product Id
DS4 (1st gen) hex 054C / dec 1356 hex 05C4 / dec 1476
DS4 (2nd gen) hex 054C / dec 1356 hex 09CC / dec 2508

Having tested with both devices, I can also confirm the Ids.

Get the device

For all communication with the device, we will use the chrome.hid API. First we define a filter using the vendor and product Id, and then query the available devices:

var filter = {
    filter: [
        { vendorId: 1356, productId: 1476 },
        { vendorId: 1356, productId: 2508 }
    ]
};

chrome.hid.getDevices( filter, ( devices ) => {
    // Error handling.
    if( chrome.runtime.lastError ) {
        console.error( chrome.runtime.lastError );
        return;
    }
    if( !devices ) {
        return;
    }

    var device = devices[0];
    // Next: Connect to the device.
};

Linux

On Linux you have to add some udev rules to be able to access the devices as non-root. Create a file /etc/udev/rules.d/61-dualshock.rules with the following content:

SUBSYSTEM=="input", GROUP="input", MODE="0666"
SUBSYSTEM=="usb", ATTRS{idVendor}=="054c", ATTRS{idProduct}=="05c4", MODE:="666", GROUP="plugdev"
KERNEL=="hidraw*", SUBSYSTEM=="hidraw", MODE="0664", GROUP="plugdev"

SUBSYSTEM=="input", GROUP="input", MODE="0666"
SUBSYSTEM=="usb", ATTRS{idVendor}=="054c", ATTRS{idProduct}=="09cc", MODE:="666", GROUP="plugdev"
KERNEL=="hidraw*", SUBSYSTEM=="hidraw", MODE="0664", GROUP="plugdev"

Then reload the udev rules with the following command. If your DS4 was plugged in, you will have to disconnect it and then connect it again afterwards.

sudo udevadm control --reload-rules

Source: https://npmjs.com/package/dualshock-controller

Connect

In order to connect with the device the device Id is necessary. On success the callback will be passed a connection object containing a connection Id we will need.

chrome.hid.connect( device.deviceId, ( connection ) => {
    // Error handling.
    if( chrome.runtime.lastError ) {
        console.error( chrome.runtime.lastError );
        return;
    }
    if( !connection ) {
        return;
    }

    var connectionId = connection.connectionId;
    // Next: Send a command.
} );

Send the command

The command will be passed as a buffer of length 10 of type uint8 so the values are between 0 and 255. The report Id has to be 5.

const reportId = 5;

var data = new Uint8Array( 10 );
var i = 0;
// Unchanging beginning.
data[i++] = 0xFF;
data[i++] = 0x04;
data[i++] = 0x00;
// Options we can use.
data[i++] = 0x00; // rumble right
data[i++] = 0x00; // rumble left
data[i++] = 0x00; // red LED brightness
data[i++] = 0xFF; // green LED brightness
data[i++] = 0x00; // blue LED brightness
data[i++] = 0x00; // flash on duration
data[i++] = 0x00; // flash off duration

chrome.hid.send( connectionId, reportId, data.buffer, () => {
    // Error handling.
    if( chrome.runtime.lastError ) {
        console.error( chrome.runtime.lastError );
    }
} );

Setting all RGB values to 0 will turn off the light. The rumble values let the controller side rumble stronger the higher the value. Sending a 0 while it still rumbles – it stops after a few seconds on its own – will stop it immediately.

Source for the format: https://github.com/ehd/node-ds4

Disconnect

When you don't need the device anymore, disconnect it.

chrome.hid.disconnect( connectionId, () => {
    // Error handling.
    if( chrome.runtime.lastError ) {
        console.error( chrome.runtime.lastError );
    }
} );