├── .editorconfig ├── .gitignore ├── .travis.yml ├── README.md ├── controllerConfigurations ├── README.md ├── dualShock3.json ├── dualShock4-alternate-driver.json ├── dualShock4-generic-driver.json └── dualShock4.json ├── examples ├── README.md ├── consolePrintControllerEvents.js ├── consolePrintDualShock3.js ├── consolePrintDualShock4.js ├── deviceDiscoveryHelp.js └── deviceDiscoveryHelpLinux.js ├── gruntfile.js ├── package.json ├── src ├── analogs.js ├── buttons.js ├── config.js ├── controller.js ├── dualshock.js ├── gyro.js ├── smoothing.js ├── status.js ├── touchpad.js └── utilities.js └── test ├── analog.tests.js ├── buttons.tests.js ├── config.tests.js ├── dualshock.tests.js ├── gyro.tests.js ├── smoothing.tests.js └── status.tests.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | end_of_line = lf 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | coverage 4 | *.iml 5 | *.idea 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6.9.2" 4 | before_script: 5 | install: echo "node-hid installation will fail in travis. Overiding `npm install`" 6 | compiler: clang 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-dualshock-controller 2 | ========================= 3 | [![Build Status](https://travis-ci.org/rdepena/node-dualshock-controller.png?branch=master)](https://travis-ci.org/rdepena/node-dualshock-controller) [![Code Climate](https://codeclimate.com/github/rdepena/node-dualshock-controller.png)](https://codeclimate.com/github/rdepena/node-dualshock-controller) 4 | 5 | `dualshock-controller` Eventing API layer over HID for the Sony DualShock 3 and DualShock 4 controllers 6 | 7 | ## Installation: 8 | 9 | #### OSX/Windows: 10 | 11 | ```bash 12 | npm install dualshock-controller 13 | ``` 14 | #### Linux: 15 | 16 | Review the [Linux support](#linux-support) section. 17 | 18 | ## Using the DualShock library 19 | 20 | `Important: THE CONTROLLER WILL NOT SEND ANY DATA IF YOU DO NOT PRESS THE PS BUTTON.` 21 | 22 | Also, to use the touchpad, rumble and LED capabilities of the controller you 23 | must connect the controller to your computer using a micro-USB cable. 24 | 25 | ~~~~ javascript 26 | var dualShock = require('dualshock-controller'); 27 | 28 | //pass options to init the controller. 29 | var controller = dualShock( 30 | { 31 | //you can use a ds4 by uncommenting this line. 32 | //config: "dualshock4-generic-driver", 33 | //if the above configuration doesn't work for you, 34 | //try uncommenting the following line instead. 35 | //config: "dualshock4-alternate-driver" 36 | //if using ds4 comment this line. 37 | config: "dualShock3", 38 | //smooths the output from the acelerometers (moving averages) defaults to true 39 | accelerometerSmoothing: true, 40 | //smooths the output from the analog sticks (moving averages) defaults to false 41 | analogStickSmoothing: false 42 | }); 43 | 44 | //make sure you add an error event handler 45 | controller.on('error', err => console.log(err)); 46 | 47 | //DualShock 4 control rumble and light settings for the controller 48 | controller.setExtras({ 49 | rumbleLeft: 0, // 0-255 (Rumble left intensity) 50 | rumbleRight: 0, // 0-255 (Rumble right intensity) 51 | red: 0, // 0-255 (Red intensity) 52 | green: 75, // 0-255 (Blue intensity) 53 | blue: 225, // 0-255 (Green intensity) 54 | flashOn: 40, // 0-255 (Flash on time) 55 | flashOff: 10 // 0-255 (Flash off time) 56 | }); 57 | 58 | //DualShock 3 control rumble and light settings for the controller 59 | controller.setExtras({ 60 | rumbleLeft: 0, // 0-1 (Rumble left on/off) 61 | rumbleRight: 0, // 0-255 (Rumble right intensity) 62 | led: 2 // 2 | 4 | 8 | 16 (Leds 1-4 on/off, bitmasked) 63 | }); 64 | 65 | //add event handlers: 66 | controller.on('left:move', data => console.log('left Moved: ' + data.x + ' | ' + data.y)); 67 | 68 | controller.on('right:move', data => console.log('right Moved: ' + data.x + ' | ' + data.y)); 69 | 70 | controller.on('connected', () => console.log('connected')); 71 | 72 | controller.on('square:press', ()=> console.log('square press')); 73 | 74 | controller.on('square:release', () => console.log('square release')); 75 | 76 | //sixasis motion events: 77 | //the object returned from each of the movement events is as follows: 78 | //{ 79 | // direction : values can be: 1 for right, forward and up. 2 for left, backwards and down. 80 | // value : values will be from 0 to 120 for directions right, forward and up and from 0 to -120 for left, backwards and down. 81 | //} 82 | 83 | //DualShock 4 TouchPad 84 | //finger 1 is x1 finger 2 is x2 85 | controller.on('touchpad:x1:active', () => console.log('touchpad one finger active')); 86 | 87 | controller.on('touchpad:x2:active', () => console.log('touchpad two fingers active')); 88 | 89 | controller.on('touchpad:x2:inactive', () => console.log('touchpad back to single finger')); 90 | 91 | controller.on('touchpad:x1', data => console.log('touchpad x1:', data.x, data.y)); 92 | 93 | controller.on('touchpad:x2', data => console.log('touchpad x2:', data.x, data.y)); 94 | 95 | 96 | //right-left movement 97 | controller.on('rightLeft:motion', data => console.log(data)); 98 | 99 | //forward-back movement 100 | controller.on('forwardBackward:motion', data => console.log(data)); 101 | 102 | //up-down movement 103 | controller.on('upDown:motion', data => console.log(data)); 104 | 105 | //controller status 106 | //as of version 0.6.2 you can get the battery %, if the controller is connected and if the controller is charging 107 | controller.on('battery:change', data => console.log(data)); 108 | 109 | controller.on('connection:change', data => console.log(data)); 110 | 111 | controller.on('charging:change', data => console.log(data)); 112 | 113 | ~~~~ 114 | 115 | ## Linux support: 116 | 117 | In order to provide Rumble/Gyro and LED support for all platforms the linux specific joystick implementation has been removed. This means you will need to: 118 | 119 | * [Install node-hid build requirements](#node-hid-build) 120 | * [Install node-hid with hidraw support](#node-hid-hidraw) 121 | * [create udev rules](#create-udev-rules) 122 | 123 | #### Install node-hid build requirements 124 | 125 | To build node-hid you will need to install: 126 | 127 | * libudev-dev 128 | * libusb-1.0-0 129 | * libusb-1.0-0-dev 130 | * build-essential 131 | * git 132 | * node-gyp 133 | * node-pre-gyp 134 | 135 | Using apt-get: 136 | 137 | ```bash 138 | sudo apt-get install libudev-dev libusb-1.0-0 libusb-1.0-0-dev build-essential git 139 | ``` 140 | 141 | ```bash 142 | npm install -g node-gyp node-pre-gyp 143 | ``` 144 | 145 | #### Install node-hid with hidraw support 146 | 147 | Once you have run the installation scripts above you can install the node-dualshock module, then replace the installed node-hid with hidraw support enabled node-hid: 148 | 149 | ```bash 150 | npm install dualshock-controller 151 | ``` 152 | 153 | ```bash 154 | npm install node-hid --driver=hidraw --build-from-source 155 | ``` 156 | 157 | #### Create udev rules 158 | 159 | You will need to create a udev rule to be able to access the hid stream as a non root user. 160 | 161 | Write the following file in `/etc/udev/rules.d/61-dualshock.rules` 162 | 163 | ``` 164 | SUBSYSTEM=="input", GROUP="input", MODE="0666" 165 | SUBSYSTEM=="usb", ATTRS{idVendor}=="054c", ATTRS{idProduct}=="0268", MODE:="666", GROUP="plugdev" 166 | KERNEL=="hidraw*", SUBSYSTEM=="hidraw", MODE="0664", GROUP="plugdev" 167 | 168 | SUBSYSTEM=="input", GROUP="input", MODE="0666" 169 | SUBSYSTEM=="usb", ATTRS{idVendor}=="054c", ATTRS{idProduct}=="05c4", MODE:="666", GROUP="plugdev" 170 | KERNEL=="hidraw*", SUBSYSTEM=="hidraw", MODE="0664", GROUP="plugdev" 171 | ``` 172 | 173 | Reload the rules `sudo udevadm control --reload-rules`, then disconnect/connect the controller. 174 | 175 | The MIT License (MIT) 176 | 177 | Copyright (c) 2017 Ricardo de Pena 178 | 179 | Permission is hereby granted, free of charge, to any person obtaining a copy of 180 | this software and associated documentation files (the "Software"), to deal in 181 | the Software without restriction, including without limitation the rights to 182 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 183 | the Software, and to permit persons to whom the Software is furnished to do so, 184 | subject to the following conditions: 185 | 186 | The above copyright notice and this permission notice shall be included in all 187 | copies or substantial portions of the Software. 188 | 189 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 190 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 191 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 192 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 193 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 194 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 195 | -------------------------------------------------------------------------------- /controllerConfigurations/README.md: -------------------------------------------------------------------------------- 1 | These json files are used to map the controller: 2 | ========================= 3 | 4 | ## To add a new controller: 5 | Use DeviceDiscoveryHelp.js in the examples folder, using node-hid you can obtain the values you need to create a controller.json file and leverage the library to wire it up. 6 | 7 | ### To connect the right controller 8 | the vendorId and the productId need to be set to the right values, you can use node-hid to determine what these are. 9 | 10 | ### Analogs are mapped as: 11 | ~~~~ js 12 | "analogSticks" : [ 13 | { 14 | "name" : "left", 15 | "x" : 7, 16 | "y" : 6 17 | }, 18 | { 19 | "name" : "right", 20 | "x" : 9, 21 | "y" : 8 22 | } 23 | ] 24 | 25 | ### Buttons are usually grouped by a block but should be added as: 26 | 27 | ~~~~ js 28 | "buttons" : [ 29 | { 30 | "name": name of the button used for events, 31 | "buttonBlock": int representing the button block, 32 | "buttonValue": bit value, 33 | "analogPin" : int representing the pin used for analog. 34 | }, 35 | ] 36 | -------------------------------------------------------------------------------- /controllerConfigurations/dualShock3.json: -------------------------------------------------------------------------------- 1 | { 2 | "vendorId" : 1356, 3 | "productId" : 616, 4 | "analogSticks" : [ 5 | { 6 | "name" : "left", 7 | "x" : 6, 8 | "y" : 7, 9 | "joystickXNumber" : 0, 10 | "joystickYNumber" : 1 11 | }, 12 | { 13 | "name" : "right", 14 | "x" : 8, 15 | "y" : 9, 16 | "joystickXNumber" : 2, 17 | "joystickYNumber" : 3 18 | } 19 | ], 20 | "buttons" : [ 21 | { 22 | "name": "l2", 23 | "buttonBlock": 3, 24 | "buttonValue": "0x01", 25 | "analogPin" : 18, 26 | "joystickNumber": 8 27 | }, 28 | { 29 | "name": "r2", 30 | "buttonBlock": 3, 31 | "buttonValue": "0x02", 32 | "analogPin" : 19, 33 | "joystickNumber" : 9 34 | }, 35 | { 36 | "name": "l1", 37 | "buttonBlock": 3, 38 | "buttonValue": "0x04", 39 | "analogPin" : 20, 40 | "joystickNumber" : 10 41 | }, 42 | { 43 | "name":"r1", 44 | "buttonBlock": 3, 45 | "buttonValue": "0x08", 46 | "analogPin" : 21, 47 | "joystickNumber" : 11 48 | }, 49 | { 50 | "name": "triangle", 51 | "buttonBlock": 3, 52 | "buttonValue": "0x10", 53 | "analogPin" : 22, 54 | "joystickNumber" : 12 55 | }, 56 | { 57 | "name": "circle", 58 | "buttonBlock": 3, 59 | "buttonValue": "0x20", 60 | "analogPin" : 23, 61 | "joystickNumber" : 13 62 | }, 63 | { 64 | "name": "x", 65 | "buttonBlock": 3, 66 | "buttonValue": "0x40", 67 | "analogPin" : 24, 68 | "joystickNumber" : 14 69 | }, 70 | { 71 | "name": "square", 72 | "buttonBlock": 3, 73 | "buttonValue": "0x80", 74 | "analogPin": 25, 75 | "joystickNumber" : 15 76 | }, 77 | { 78 | "name": "select", 79 | "buttonBlock": 2, 80 | "buttonValue": "0x1", 81 | "joystickNumber" : 0 82 | }, 83 | { 84 | "name": "leftAnalogBump", 85 | "buttonBlock": 2, 86 | "buttonValue": "0x2", 87 | "joystickNumber" : 1 88 | }, 89 | { 90 | "name": "rightAnalogBump", 91 | "buttonBlock": 2, 92 | "buttonValue": "0x4", 93 | "joystickNumber" : 2 94 | }, 95 | { 96 | "name": "start", 97 | "buttonBlock": 2, 98 | "buttonValue": "0x08", 99 | "joystickNumber" : 3 100 | }, 101 | { 102 | "name": "dpadUp", 103 | "buttonBlock": 2, 104 | "buttonValue": "0x10", 105 | "analogPin": 14, 106 | "joystickNumber" : 4 107 | }, 108 | { 109 | "name": "dpadRight", 110 | "buttonBlock": 2, 111 | "buttonValue": "0x20", 112 | "analogPin": 15, 113 | "joystickNumber" : 5 114 | }, 115 | { 116 | "name": "dpadDown", 117 | "buttonBlock": 2, 118 | "buttonValue" : "0x40", 119 | "analogPin" : 16, 120 | "joystickNumber" : 6 121 | }, 122 | { 123 | "name": "dpadLeft", 124 | "buttonBlock": 2, 125 | "buttonValue": "0x80", 126 | "analogPin": 17, 127 | "joystickNumber" : 7 128 | }, 129 | { 130 | "name": "psxButton", 131 | "buttonBlock":4, 132 | "buttonValue": "0x01", 133 | "joystickNumber" : 16 134 | } 135 | ], 136 | "motionInputs": [ 137 | { 138 | "name" : "rightLeft", 139 | "directionPin" : 41, 140 | "valuePin" : 42 141 | }, 142 | { 143 | "name" : "forwardBackward", 144 | "directionPin" : 43, 145 | "valuePin" : 44 146 | }, 147 | { 148 | "name" : "upDown", 149 | "directionPin" : 45, 150 | "valuePin" : 46 151 | }, 152 | { 153 | "name" : "yaw", 154 | "directionPin" : 47, 155 | "valuePin" : 48 156 | } 157 | ], 158 | "status": [ 159 | { 160 | "name" : "charging", 161 | "pin" : 29, 162 | "states" : [ 163 | { 164 | "value" : 0, 165 | "state" : "Charging" 166 | }, 167 | { 168 | "value" : 2, 169 | "state" : "Charging" 170 | }, 171 | { 172 | "value" : 3, 173 | "state" : "Not Charging" 174 | } 175 | ] 176 | }, 177 | { 178 | "name" : "battery", 179 | "pin" : 30, 180 | "states" : [ 181 | { 182 | "value" : 238, 183 | "state" : "Charging" 184 | }, 185 | { 186 | "value" : 0, 187 | "state" : "No charge" 188 | }, 189 | { 190 | "value" : 1, 191 | "state" : "20%" 192 | }, 193 | { 194 | "value" : 2, 195 | "state" : "40%" 196 | }, 197 | { 198 | "value" : 3, 199 | "state" : "60%" 200 | }, 201 | { 202 | "value" : 4, 203 | "state" : "80%" 204 | }, 205 | { 206 | "value" : 5, 207 | "state" : "100%" 208 | } 209 | ] 210 | }, 211 | { 212 | "name" : "connection", 213 | "pin" : 31, 214 | "states" : [ 215 | { 216 | "value" : 18, 217 | "state" : "Usb" 218 | }, 219 | { 220 | "value" : 22, 221 | "state" : "Bluetooth" 222 | }, 223 | { 224 | "value" : 20, 225 | "state" : "Rumbling" 226 | } 227 | ] 228 | } 229 | ], 230 | "output": { 231 | "defaultBuffer":[ 232 | 1, 0, 254, 0, 254, 0, 0, 0, 0, 0, 0, 233 | 255, 39, 16, 0, 50, 255, 39, 16, 0, 50, 255, 234 | 39, 16, 0, 50, 255, 39, 16, 0, 50, 0, 0, 235 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 236 | 0, 0, 0, 0, 0 237 | ], 238 | "indexes": { 239 | "rumbleLeft": 3, 240 | "rumbleRight": 5, 241 | "led": 10 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /controllerConfigurations/dualShock4-alternate-driver.json: -------------------------------------------------------------------------------- 1 | { 2 | "vendorId" : 1356, 3 | "productId" : 2508, 4 | "analogSticks" : [ 5 | { 6 | "name" : "left", 7 | "x" : 1, 8 | "y" : 2 9 | }, 10 | { 11 | "name" : "right", 12 | "x" : 3, 13 | "y" : 4 14 | } 15 | ], 16 | "buttons" : [ 17 | { 18 | "name": "l2", 19 | "buttonBlock": 6, 20 | "buttonValue": "0x04", 21 | "analogPin" : 8 22 | }, 23 | { 24 | "name": "r2", 25 | "buttonBlock": 6, 26 | "buttonValue": "0x08", 27 | "analogPin" : 9 28 | }, 29 | { 30 | "name": "l1", 31 | "buttonBlock": 6, 32 | "buttonValue": "0x01" 33 | }, 34 | { 35 | "name": "r1", 36 | "buttonBlock": 6, 37 | "buttonValue": "0x02" 38 | }, 39 | { 40 | "name": "leftAnalogBump", 41 | "buttonBlock": 6, 42 | "buttonValue": "0x04" 43 | }, 44 | { 45 | "name": "rightAnalogBump", 46 | "buttonBlock": 6, 47 | "buttonValue": "0x08" 48 | }, 49 | { 50 | "name": "psxButton", 51 | "buttonBlock": 7, 52 | "buttonValue": "0x01" 53 | }, 54 | { 55 | "name": "touchPad", 56 | "buttonBlock": 7, 57 | "buttonValue": "0x02" 58 | }, 59 | { 60 | "name": "square", 61 | "buttonBlock": 5, 62 | "buttonValue": "0x10" 63 | }, 64 | { 65 | "name": "triangle", 66 | "buttonBlock": 5, 67 | "buttonValue": "0x80" 68 | }, 69 | { 70 | "name": "circle", 71 | "buttonBlock": 5, 72 | "buttonValue": "0x40" 73 | }, 74 | { 75 | "name": "x", 76 | "buttonBlock": 5, 77 | "buttonValue": "0x20" 78 | }, 79 | { 80 | "name": "dpadUp", 81 | "buttonBlock": 5, 82 | "buttonValue": "0x00", 83 | "mask": "0xF" 84 | }, 85 | { 86 | "name": "dpadUpRight", 87 | "buttonBlock": 5, 88 | "buttonValue": "0x01", 89 | "mask": "0xF" 90 | }, 91 | { 92 | "name": "dpadRight", 93 | "buttonBlock": 5, 94 | "buttonValue": "0x02", 95 | "mask": "0xF" 96 | }, 97 | { 98 | "name": "dpadDownRight", 99 | "buttonBlock": 5, 100 | "buttonValue": "0x03", 101 | "mask": "0xF" 102 | }, 103 | { 104 | "name": "dpadDown", 105 | "buttonBlock": 5, 106 | "buttonValue" : "0x04", 107 | "mask": "0xF" 108 | }, 109 | { 110 | "name": "dpadDownLeft", 111 | "buttonBlock": 5, 112 | "buttonValue" : "0x05", 113 | "mask": "0xF" 114 | }, 115 | { 116 | "name": "dpadLeft", 117 | "buttonBlock": 5, 118 | "buttonValue": "0x06", 119 | "mask": "0xF" 120 | }, 121 | { 122 | "name": "dpadUpLeft", 123 | "buttonBlock": 5, 124 | "buttonValue": "0x07", 125 | "mask": "0xF" 126 | }, 127 | { 128 | "name": "share", 129 | "buttonBlock": 6, 130 | "buttonValue": "0x10" 131 | }, 132 | { 133 | "name": "options", 134 | "buttonBlock": 6, 135 | "buttonValue": "0x20" 136 | }, 137 | { 138 | "name": "leftStick", 139 | "buttonBlock": 6, 140 | "buttonValue": "0x40" 141 | }, 142 | { 143 | "name": "rightStick", 144 | "buttonBlock": 6, 145 | "buttonValue": "0x80" 146 | } 147 | ], 148 | "motionInputs" : [], 149 | "status" : [], 150 | "output": { 151 | "defaultBuffer":[ 152 | 5,255,4,0,0,0,0,0,0,0,0 153 | ], 154 | "indexes": { 155 | "rumbleLeft": 4, 156 | "rumbleRight": 5, 157 | "red": 6, 158 | "green": 7, 159 | "blue": 8, 160 | "flashOn": 9, 161 | "flashOff": 10 162 | } 163 | }, 164 | "touchPad": [{ 165 | "name":"x1", 166 | "activePin": 35, 167 | "dataPinA": 37, 168 | "dataPinB": 36, 169 | "dataPinC": 38 170 | },{ 171 | "name": "x2", 172 | "activePin": 39, 173 | "dataPinA": 41, 174 | "dataPinB": 40, 175 | "dataPinC": 42 176 | }] 177 | } 178 | -------------------------------------------------------------------------------- /controllerConfigurations/dualShock4-generic-driver.json: -------------------------------------------------------------------------------- 1 | { 2 | "vendorId" : 1356, 3 | "productId" : 1476, 4 | "analogSticks" : [ 5 | { 6 | "name" : "left", 7 | "x" : 1, 8 | "y" : 2 9 | }, 10 | { 11 | "name" : "right", 12 | "x" : 3, 13 | "y" : 4 14 | } 15 | ], 16 | "buttons" : [ 17 | { 18 | "name": "l2", 19 | "buttonBlock": 6, 20 | "buttonValue": "0x04", 21 | "analogPin" : 8 22 | }, 23 | { 24 | "name": "r2", 25 | "buttonBlock": 6, 26 | "buttonValue": "0x08", 27 | "analogPin" : 9 28 | }, 29 | { 30 | "name": "l1", 31 | "buttonBlock": 6, 32 | "buttonValue": "0x01" 33 | }, 34 | { 35 | "name": "r1", 36 | "buttonBlock": 6, 37 | "buttonValue": "0x02" 38 | }, 39 | { 40 | "name": "leftAnalogBump", 41 | "buttonBlock": 6, 42 | "buttonValue": "0x04" 43 | }, 44 | { 45 | "name": "rightAnalogBump", 46 | "buttonBlock": 6, 47 | "buttonValue": "0x08" 48 | }, 49 | { 50 | "name": "psxButton", 51 | "buttonBlock": 7, 52 | "buttonValue": "0x01" 53 | }, 54 | { 55 | "name": "touchPad", 56 | "buttonBlock": 7, 57 | "buttonValue": "0x02" 58 | }, 59 | { 60 | "name": "square", 61 | "buttonBlock": 5, 62 | "buttonValue": "0x10" 63 | }, 64 | { 65 | "name": "triangle", 66 | "buttonBlock": 5, 67 | "buttonValue": "0x80" 68 | }, 69 | { 70 | "name": "circle", 71 | "buttonBlock": 5, 72 | "buttonValue": "0x40" 73 | }, 74 | { 75 | "name": "x", 76 | "buttonBlock": 5, 77 | "buttonValue": "0x20" 78 | }, 79 | { 80 | "name": "dpadUp", 81 | "buttonBlock": 5, 82 | "buttonValue": "0x00", 83 | "mask": "0xF" 84 | }, 85 | { 86 | "name": "dpadUpRight", 87 | "buttonBlock": 5, 88 | "buttonValue": "0x01", 89 | "mask": "0xF" 90 | }, 91 | { 92 | "name": "dpadRight", 93 | "buttonBlock": 5, 94 | "buttonValue": "0x02", 95 | "mask": "0xF" 96 | }, 97 | { 98 | "name": "dpadDownRight", 99 | "buttonBlock": 5, 100 | "buttonValue": "0x03", 101 | "mask": "0xF" 102 | }, 103 | { 104 | "name": "dpadDown", 105 | "buttonBlock": 5, 106 | "buttonValue" : "0x04", 107 | "mask": "0xF" 108 | }, 109 | { 110 | "name": "dpadDownLeft", 111 | "buttonBlock": 5, 112 | "buttonValue" : "0x05", 113 | "mask": "0xF" 114 | }, 115 | { 116 | "name": "dpadLeft", 117 | "buttonBlock": 5, 118 | "buttonValue": "0x06", 119 | "mask": "0xF" 120 | }, 121 | { 122 | "name": "dpadUpLeft", 123 | "buttonBlock": 5, 124 | "buttonValue": "0x07", 125 | "mask": "0xF" 126 | }, 127 | { 128 | "name": "share", 129 | "buttonBlock": 6, 130 | "buttonValue": "0x10" 131 | }, 132 | { 133 | "name": "options", 134 | "buttonBlock": 6, 135 | "buttonValue": "0x20" 136 | }, 137 | { 138 | "name": "leftStick", 139 | "buttonBlock": 6, 140 | "buttonValue": "0x40" 141 | }, 142 | { 143 | "name": "rightStick", 144 | "buttonBlock": 6, 145 | "buttonValue": "0x80" 146 | } 147 | ], 148 | "motionInputs" : [], 149 | "status" : [], 150 | "output": { 151 | "defaultBuffer":[ 152 | 5,255,4,0,0,0,0,0,0,0,0 153 | ], 154 | "indexes": { 155 | "rumbleLeft": 4, 156 | "rumbleRight": 5, 157 | "red": 6, 158 | "green": 7, 159 | "blue": 8, 160 | "flashOn": 9, 161 | "flashOff": 10 162 | } 163 | }, 164 | "touchPad": [{ 165 | "name":"x1", 166 | "activePin": 35, 167 | "dataPinA": 37, 168 | "dataPinB": 36, 169 | "dataPinC": 38 170 | },{ 171 | "name": "x2", 172 | "activePin": 39, 173 | "dataPinA": 41, 174 | "dataPinB": 40, 175 | "dataPinC": 42 176 | }] 177 | } 178 | -------------------------------------------------------------------------------- /controllerConfigurations/dualShock4.json: -------------------------------------------------------------------------------- 1 | { 2 | "vendorId" : 1356, 3 | "productId" : 1476, 4 | "analogSticks" : [ 5 | { 6 | "name" : "left", 7 | "x" : 1, 8 | "y" : 2, 9 | "joystickXNumber" : 0, 10 | "joystickYNumber" : 1 11 | }, 12 | { 13 | "name" : "right", 14 | "x" : 3, 15 | "y" : 4, 16 | "joystickXNumber" : 2, 17 | "joystickYNumber" : 5 18 | }, 19 | { 20 | "name" : "l2Analog", 21 | "x" : 100, 22 | "joystickXNumber" : 3 23 | }, 24 | { 25 | "name" : "r2Analog", 26 | "x" : 101, 27 | "joystickXNumber" : 4 28 | } 29 | ], 30 | "buttons" : [ 31 | { 32 | "name": "l2", 33 | "buttonBlock": 6, 34 | "buttonValue": "0x04", 35 | "analogPin" : 8, 36 | "joystickNumber": 6 37 | }, 38 | { 39 | "name": "r2", 40 | "buttonBlock": 6, 41 | "buttonValue": "0x08", 42 | "analogPin" : 9, 43 | "joystickNumber": 7 44 | }, 45 | { 46 | "name": "l1", 47 | "buttonBlock": 6, 48 | "buttonValue": "0x01", 49 | "joystickNumber": 4 50 | }, 51 | { 52 | "name": "r1", 53 | "buttonBlock": 6, 54 | "buttonValue": "0x02", 55 | "joystickNumber": 5 56 | }, 57 | { 58 | "name": "psxButton", 59 | "buttonBlock": 7, 60 | "buttonValue": "0x01", 61 | "joystickNumber": 12 62 | }, 63 | { 64 | "name": "touchPad", 65 | "buttonBlock": 7, 66 | "buttonValue": "0x02", 67 | "joystickNumber": 13 68 | }, 69 | { 70 | "name": "square", 71 | "buttonBlock": 5, 72 | "buttonValue": "0x10", 73 | "joystickNumber": 0 74 | }, 75 | { 76 | "name": "triangle", 77 | "buttonBlock": 5, 78 | "buttonValue": "0x80", 79 | "joystickNumber": 3 80 | }, 81 | { 82 | "name": "circle", 83 | "buttonBlock": 5, 84 | "buttonValue": "0x40", 85 | "joystickNumber": 2 86 | }, 87 | { 88 | "name": "x", 89 | "buttonBlock": 5, 90 | "buttonValue": "0x20", 91 | "joystickNumber": 1 92 | }, 93 | { 94 | "name": "dpadUp", 95 | "buttonBlock": 5, 96 | "buttonValue": "0x00", 97 | "mask": "0xF" 98 | }, 99 | { 100 | "name": "dpadUpRight", 101 | "buttonBlock": 5, 102 | "buttonValue": "0x01", 103 | "mask": "0xF" 104 | }, 105 | { 106 | "name": "dpadRight", 107 | "buttonBlock": 5, 108 | "buttonValue": "0x02", 109 | "mask": "0xF" 110 | }, 111 | { 112 | "name": "dpadDownRight", 113 | "buttonBlock": 5, 114 | "buttonValue": "0x03", 115 | "mask": "0xF" 116 | }, 117 | { 118 | "name": "dpadDown", 119 | "buttonBlock": 5, 120 | "buttonValue" : "0x04", 121 | "mask": "0xF" 122 | }, 123 | { 124 | "name": "dpadDownLeft", 125 | "buttonBlock": 5, 126 | "buttonValue" : "0x05", 127 | "mask": "0xF" 128 | }, 129 | { 130 | "name": "dpadLeft", 131 | "buttonBlock": 5, 132 | "buttonValue": "0x06", 133 | "mask": "0xF" 134 | }, 135 | { 136 | "name": "dpadUpLeft", 137 | "buttonBlock": 5, 138 | "buttonValue": "0x07", 139 | "mask": "0xF" 140 | }, 141 | { 142 | "name": "share", 143 | "buttonBlock": 6, 144 | "buttonValue": "0x10", 145 | "joystickNumber": 8 146 | }, 147 | { 148 | "name": "options", 149 | "buttonBlock": 6, 150 | "buttonValue": "0x20", 151 | "joystickNumber": 9 152 | }, 153 | { 154 | "name": "leftStick", 155 | "buttonBlock": 6, 156 | "buttonValue": "0x40", 157 | "joystickNumber": 10 158 | }, 159 | { 160 | "name": "rightStick", 161 | "buttonBlock": 6, 162 | "buttonValue": "0x80", 163 | "joystickNumber": 11 164 | } 165 | ], 166 | "motionInputs" : [], 167 | "status" : [], 168 | "output": { 169 | "defaultBuffer":[ 170 | 5,255,4,0,0,0,0,0,0,0,0 171 | ], 172 | "indexes": { 173 | "rumbleLeft": 4, 174 | "rumbleRight": 5, 175 | "red": 6, 176 | "green": 7, 177 | "blue": 8, 178 | "flashOn": 9, 179 | "flashOff": 10 180 | } 181 | }, 182 | "touchPad": [{ 183 | "name":"x1", 184 | "activePin": 35, 185 | "dataPinA": 37, 186 | "dataPinB": 36, 187 | "dataPinC": 38 188 | },{ 189 | "name": "x2", 190 | "activePin": 39, 191 | "dataPinA": 41, 192 | "dataPinB": 40, 193 | "dataPinC": 42 194 | }] 195 | } 196 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | Examples: 2 | ========================= 3 | 4 | ## Don't use this code as a starting point. 5 | I do things here that are not neccesary, you are better off looking at the readme in root as it has a cleaner implementation, this is here just so you can test it right away. 6 | 7 | If you'd like to see more examples for the DualShock 4, feel free to check out 8 | [ds-examples](https://github.com/itaisteinherz/ds-examples). 9 | -------------------------------------------------------------------------------- /examples/consolePrintControllerEvents.js: -------------------------------------------------------------------------------- 1 | var printcontrollerEvents = function(controller, controllerConfiguration) { 2 | 'use strict'; 3 | controller.on('left:move', function(data) { 4 | console.log('left Moved'); 5 | console.log(data); 6 | }); 7 | controller.on('right:move', function(data) { 8 | console.log('right Moved'); 9 | console.log(data); 10 | }); 11 | controller.on('connected', function() { 12 | console.log('connected'); 13 | }); 14 | 15 | controller.on('error', function(data) { 16 | console.log(data); 17 | }); 18 | 19 | var pressed = function(data) { 20 | console.log(data + ": press"); 21 | }; 22 | var released = function(data) { 23 | console.log(data + ": release"); 24 | }; 25 | var analog = function(data) { 26 | console.log(data + ": analog"); 27 | }; 28 | var hold = function(data) { 29 | console.log(data + ": hold"); 30 | }; 31 | var motion = function(motionInput, data) { 32 | console.log(motionInput); 33 | console.log(data); 34 | }; 35 | //subscribe to all the buttons: 36 | for (var i = 0; i < controllerConfiguration.buttons.length; i++) { 37 | controller.on(controllerConfiguration.buttons[i].name + ":press", pressed); 38 | controller.on(controllerConfiguration.buttons[i].name + ":release", released); 39 | controller.on(controllerConfiguration.buttons[i].name + ":analog", analog); 40 | controller.on(controllerConfiguration.buttons[i].name + ":hold", hold); 41 | 42 | } 43 | //subscribe to all the status events: 44 | if (controllerConfiguration.status && controllerConfiguration.status.length) { 45 | for (i = 0; i < controllerConfiguration.status.length; i++) { 46 | controller.on(controllerConfiguration.status[i].name + ":change", console.log); 47 | } 48 | } 49 | //subscribe to the motion events. 50 | controller.on('rightLeft' + ':motion', function(data) { 51 | motion('rightLeft', data); 52 | }); 53 | controller.on('forwardBackward' + ':motion', function(data) { 54 | motion('forwardBackward', data); 55 | }); 56 | controller.on('upDown' + ':motion', function(data) { 57 | motion('upDown', data); 58 | }); 59 | }; 60 | 61 | module.exports = printcontrollerEvents; 62 | -------------------------------------------------------------------------------- /examples/consolePrintDualShock3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var dualShock = require('./../src/dualshock.js'); 3 | var dualShock3; 4 | 5 | //init the controller 6 | dualShock3 = dualShock({ 7 | analogStickSmoothing: false, 8 | config: "dualShock3", 9 | logging: true 10 | }); 11 | 12 | // //for a client implementation we do not need this, this is only to test the inputs. 13 | var controllerConfiguration = require('./../controllerConfigurations/dualShock3'); 14 | 15 | // //init the print events 16 | var consolePrintEvents = require('./consolePrintControllerEvents')(dualShock3, controllerConfiguration); 17 | 18 | dualShock3.on("dpadup:press", () => { 19 | dualShock3.setExtras({ 20 | led: 2 21 | }); 22 | }); 23 | 24 | dualShock3.on("dpadright:press", () => { 25 | dualShock3.setExtras({ 26 | led: 4 27 | }); 28 | }); 29 | 30 | dualShock3.on("dpaddown:press", () => { 31 | dualShock3.setExtras({ 32 | led: 8 33 | }); 34 | }); 35 | 36 | dualShock3.on("dpadleft:press", () => { 37 | dualShock3.setExtras({ 38 | led: 16 39 | }); 40 | }); 41 | 42 | dualShock3.on("r2:analog", (d) => { 43 | dualShock3.setExtras({ 44 | rumbleRight: d 45 | }); 46 | }); 47 | 48 | dualShock3.on("l2:press", (d) => { 49 | dualShock3.setExtras({ 50 | rumbleLeft: 1 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /examples/consolePrintDualShock4.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var dualShock = require('./../src/dualshock.js'); 3 | 4 | var dualShock4 = dualShock({ 5 | config: "dualShock4", 6 | logging: true 7 | }); 8 | 9 | //for a client implementation we do not need this, this is only to test the inputs. 10 | var controllerConfiguration = require('./../controllerConfigurations/dualShock4'); 11 | 12 | //init the print events 13 | var consolePrintEvents = require('./consolePrintControllerEvents')(dualShock4, controllerConfiguration); 14 | 15 | dualShock4.on("dpadup:press", () => { 16 | dualShock4.setExtras({ 17 | red: 255 18 | }); 19 | }); 20 | 21 | dualShock4.on("dpadright:press", () => { 22 | dualShock4.setExtras({ 23 | green: 255 24 | }); 25 | }); 26 | 27 | dualShock4.on("dpaddown:press", () => { 28 | dualShock4.setExtras({ 29 | blue: 255 30 | }); 31 | }); 32 | 33 | dualShock4.on("dpadleft:press", () => { 34 | dualShock4.setExtras({ 35 | red: 255, 36 | green: 255, 37 | blue: 255 38 | }); 39 | }); 40 | 41 | dualShock4.on("x:press", (d) => { 42 | dualShock4.setExtras({ 43 | red: 255, 44 | flashOn: 50, 45 | flashOff: 10 46 | }); 47 | }); 48 | 49 | dualShock4.on("r2:analog", (d) => { 50 | dualShock4.setExtras({ 51 | rumbleRight: d 52 | }); 53 | }); 54 | 55 | dualShock4.on("l2:analog", (d) => { 56 | dualShock4.setExtras({ 57 | rumbleLeft: d 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /examples/deviceDiscoveryHelp.js: -------------------------------------------------------------------------------- 1 | var HID = require('node-hid'); 2 | console.log(HID.devices()); 3 | 4 | var controller = new HID.HID(1356, 616); 5 | 6 | controller.on('data', function(data) { 7 | for (var i = 0; i < data.length; i++) { 8 | if (i === 30) { 9 | console.log(i + " " + data[i]); 10 | } 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /examples/deviceDiscoveryHelpLinux.js: -------------------------------------------------------------------------------- 1 | var joystick = require('joystick'), 2 | controller = new joystick(0, 256, 500); 3 | 4 | controller.on('button', console.log); 5 | 6 | controller.on('axis', console.log); 7 | -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | const files = ['gruntfile.js', 2 | 'src/*.js', 3 | 'src/inputProcessors/*.js', 4 | 'test/*.js', 5 | 'examples/*.js' 6 | ]; 7 | module.exports = function(grunt) { 8 | grunt.initConfig({ 9 | watch: { 10 | files: files, 11 | tasks: ['default'] 12 | }, 13 | jshint: { 14 | files: files, 15 | options: { 16 | reporterOutput: "", 17 | esnext: true, 18 | node: true, 19 | globals: { 20 | describe: true, 21 | it: true, 22 | beforeEach: true 23 | } 24 | } 25 | }, 26 | mochaTest: { 27 | test: { 28 | options: { 29 | reporter: 'spec' 30 | }, 31 | src: ['test/*.js'] 32 | } 33 | }, 34 | jsbeautifier: { 35 | files: files, 36 | options: { 37 | js: { 38 | braceStyle: "collapse", 39 | breakChainedMethods: false, 40 | e4x: false, 41 | evalCode: false, 42 | indentChar: " ", 43 | indentLevel: 0, 44 | indentSize: 4, 45 | indentWithTabs: false, 46 | jslintHappy: false, 47 | keepArrayIndentation: false, 48 | keepFunctionIndentation: false, 49 | maxPreserveNewlines: 10, 50 | preserveNewlines: true, 51 | spaceBeforeConditional: true, 52 | spaceInParen: false, 53 | unescapeStrings: false, 54 | wrapLineLength: 0 55 | } 56 | } 57 | } 58 | }); 59 | 60 | grunt.loadNpmTasks('grunt-contrib-watch'); 61 | grunt.loadNpmTasks('grunt-contrib-jshint'); 62 | grunt.loadNpmTasks('grunt-mocha-test'); 63 | grunt.loadNpmTasks('grunt-jsbeautifier'); 64 | 65 | grunt.registerTask('test', ['jshint', 'mochaTest']); 66 | grunt.registerTask('default', ['jshint', 'mochaTest', 'jsbeautifier']); 67 | }; 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dualshock-controller", 3 | "version": "1.1.1", 4 | "description": "Eventing API layer over HID for the Sony DualShock 3 and DualShock 4 controllers ", 5 | "main": "./src/dualshock.js", 6 | "dependencies": { 7 | "node-hid": "^0.5.0" 8 | }, 9 | "devDependencies": { 10 | "grunt": "^1.0.1", 11 | "grunt-cli": "^1.2.0", 12 | "grunt-contrib-jshint": "^1.1.0", 13 | "grunt-contrib-watch": "^1.0.0", 14 | "grunt-mocha-test": "^0.13.2", 15 | "mocha": "^3.2.0", 16 | "sinon": "^1.8", 17 | "nyc": "^10.0.0", 18 | "grunt-jsbeautifier": "^0.2.7", 19 | "jshint": "^2.5.0", 20 | "mockery": "^2.0.0" 21 | }, 22 | "scripts": { 23 | "test": "grunt test", 24 | "coverage": "nyc npm test" 25 | }, 26 | "engines": { 27 | "node": ">=6.9.2" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/rdepena/node-dualshock-controller.git" 32 | }, 33 | "author": "ricardo de pena", 34 | "license": "MIT", 35 | "readmeFilename": "README.md", 36 | "keywords": [ 37 | "ps3", 38 | "controller", 39 | "gamepad", 40 | "dualshock", 41 | "dualshock3", 42 | "dualshock4", 43 | "dual shock 3", 44 | "dual shock 4", 45 | "node-hid" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /src/analogs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // Module dependencies. 3 | var dsutilities = require('./utilities'), 4 | Smoothing = require('./smoothing'), 5 | config = require('./config'); 6 | 7 | //Proccess Analog stick events. 8 | var Analogs = function(controller) { 9 | 10 | var varianceThreshhold = 1, 11 | smoothInput = config.getOptions().analogStickSmoothing, 12 | outputSmoothing = new Smoothing(smoothInput), 13 | analogSticks = config.getControllerConfig().analogSticks; 14 | 15 | //Private methods 16 | var processStick = function(analogStick, data) { 17 | var currentValue = { 18 | x: data[analogStick.x], 19 | y: data[analogStick.y] 20 | }, 21 | previousValue = { 22 | x: outputSmoothing.readLastPosition(analogStick.name + 'x'), 23 | y: outputSmoothing.readLastPosition(analogStick.name + 'y') 24 | }; 25 | 26 | //we only raise an event if both 27 | if (dsutilities.isWithinVariance(previousValue.x, currentValue.x, varianceThreshhold) || 28 | dsutilities.isWithinVariance(previousValue.y, currentValue.y, varianceThreshhold)) { 29 | 30 | currentValue.x = outputSmoothing.smooth(analogStick.name + 'x', currentValue.x); 31 | currentValue.y = outputSmoothing.smooth(analogStick.name + 'y', currentValue.y); 32 | 33 | // Update and emit 34 | if (controller[analogStick.name]) { 35 | controller[analogStick.name].x = currentValue.x; 36 | controller[analogStick.name].y = currentValue.y; 37 | } 38 | controller.emit(analogStick.name + ':move', currentValue); 39 | } 40 | }; 41 | 42 | // Public methods 43 | //process all the analog events. 44 | this.process = function(data) { 45 | for (var i = 0; i < analogSticks.length; i++) { 46 | processStick(analogSticks[i], data); 47 | } 48 | }; 49 | }; 50 | 51 | module.exports = Analogs; 52 | -------------------------------------------------------------------------------- /src/buttons.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // Module dependencies. 3 | var config = require('./config'), 4 | dsutilities = require('./utilities'); 5 | 6 | //Proccess button events. 7 | var Buttons = function(controller) { 8 | 9 | var buttons = config.getControllerConfig().buttons; 10 | 11 | // convert strings to numbers, e.g. "0x01" to 0x01 12 | // must be converted because JSON doesn't allow numbers with leading zeros 13 | buttons.forEach(function(button) { 14 | if (typeof button.buttonValue == "string") { 15 | button.buttonValue = parseInt(button.buttonValue); 16 | } 17 | 18 | if (typeof button.mask == "string") { 19 | button.mask = parseInt(button.mask); 20 | } else if (!(button.mask instanceof Number)) { 21 | button.mask = 0xFF; 22 | } 23 | 24 | //generate event name aliases: 25 | button.eventPrefixes = dsutilities.generateEventPrefixAliases(button.name); 26 | }); 27 | 28 | 29 | var buffer = {}; 30 | 31 | //Private methods 32 | var emitEvent = function(button, eventText, data) { 33 | button.eventPrefixes.forEach(function(eventPrefix) { 34 | controller.emit(eventPrefix + eventText, data); 35 | }); 36 | }; 37 | var processButton = function(button, data) { 38 | //make sure the data contains a value for the specified block 39 | //and bitwise operation for the button value 40 | 41 | var block = data[button.buttonBlock] & button.mask; 42 | var hit = (block & button.buttonValue) == button.buttonValue; 43 | var value = 0; 44 | var state = 0; // 0: up, 1: down, 2: hold 45 | 46 | // special case for the dualshock 4's dpadUp button as it causes the 47 | // lower 8 bits of it's block to be zeroed 48 | if (!button.buttonValue) { 49 | hit = !block; 50 | } 51 | 52 | // special case for dualshock 4's dpad - they are not bitmasked values as 53 | // they cannot be pressed together - ie. up, left and upleft are three 54 | // different values - upleft is not equal to up & left 55 | if (button.buttonBlock == 5 && block < 0x08) { 56 | hit = block == button.buttonValue; 57 | } 58 | 59 | if (hit) { 60 | value = 1; 61 | 62 | //if the button is in the released state. 63 | if (!buffer[button.name]) { 64 | state = 1; 65 | buffer[button.name] = true; 66 | emitEvent(button, ':press', button.name); 67 | } else { 68 | state = 2; 69 | emitEvent(button, ':hold', button.name); 70 | } 71 | 72 | //send the analog data 73 | if (button.analogPin && data[button.analogPin]) { 74 | emitEvent(button, ':analog', data[button.analogPin]); 75 | } 76 | 77 | } else if (buffer[button.name]) { 78 | //button was pressed and is not released 79 | buffer[button.name] = false; 80 | 81 | //button is no longer pressed, emit a analog 0 event. 82 | if (button.analogPin) { 83 | emitEvent(button, ':analog', 0); 84 | } 85 | //emit the released event. 86 | emitEvent(button, ':release', button.name); 87 | } 88 | 89 | if (controller[button.name]) { 90 | controller[button.name].value = value; 91 | controller[button.name].state = state; 92 | } 93 | }; 94 | 95 | // Public methods 96 | //process all the analog events. 97 | this.process = function(data) { 98 | for (var i = 0; i < buttons.length; i++) { 99 | processButton(buttons[i], data); 100 | } 101 | }; 102 | }; 103 | 104 | module.exports = Buttons; 105 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // Module dependencies. 3 | // we will expose these objects via the manager. 4 | var options, 5 | controllerConfig; 6 | 7 | //provides access to the current options and configs. 8 | var config = { 9 | setOptions: function(opts) { 10 | //no options were passed 11 | options = opts || {}; 12 | 13 | // Defaults: 14 | var defaultValues = { 15 | config: "dualShock3", 16 | accelerometerSmoothing: true, 17 | analogStickSmoothing: false, 18 | logging: false, 19 | forceNodeHid: false, 20 | linuxJoystickId: 0 21 | }; 22 | 23 | for (var name in defaultValues) { 24 | if (defaultValues.hasOwnProperty(name)) { 25 | var target = options[name]; 26 | var orig = defaultValues[name]; 27 | 28 | if (!target) { 29 | options[name] = orig; 30 | } 31 | } 32 | } 33 | 34 | var controllerConfiguration; 35 | //use passed config or load from built-in configs 36 | if (typeof options.config === "object") { 37 | controllerConfiguration = options.config; 38 | } else { 39 | controllerConfiguration = require('./../controllerConfigurations/' + options.config); 40 | } 41 | 42 | //set the current controllerConfiguration 43 | config.setControllerConfig(controllerConfiguration); 44 | }, 45 | getOptions: function() { 46 | return options; 47 | }, 48 | setControllerConfig: function(config) { 49 | controllerConfig = config; 50 | }, 51 | getControllerConfig: function() { 52 | return controllerConfig; 53 | } 54 | }; 55 | 56 | module.exports = config; 57 | -------------------------------------------------------------------------------- /src/controller.js: -------------------------------------------------------------------------------- 1 | // Module dependencies. 2 | const util = require('util'), 3 | dsutilities = require('./utilities'), 4 | Emitter = require('events').EventEmitter, 5 | Gyro = require('./gyro'), 6 | Analogs = require('./analogs'), 7 | Buttons = require('./buttons'), 8 | Status = require('./status'), 9 | HID = require('node-hid'), 10 | config = require('./config'), 11 | TouchPad = require('./touchpad'); 12 | 13 | //generic controller object, it will need a controller Configuration with a buttons array passed into its connect function. 14 | const Controller = function() { 15 | 'use strict'; 16 | Emitter.call(this); 17 | 18 | const controllerConfig = config.getControllerConfig(), 19 | options = config.getOptions(), 20 | indexes = controllerConfig.output.indexes, 21 | analogs = new Analogs(this), 22 | buttons = new Buttons(this), 23 | gyro = new Gyro(this), 24 | status = new Status(this), 25 | touchPad = new TouchPad(this); 26 | 27 | let device = null; 28 | 29 | [{ 30 | type: 'analogSticks', 31 | properties: [{ 32 | name: 'x', 33 | initialValue: 0 34 | }, { 35 | name: 'y', 36 | initialValue: 0 37 | }] 38 | }, { 39 | type: 'buttons', 40 | properties: [{ 41 | name: 'state', 42 | initialValue: 0 43 | }, { 44 | name: 'value', 45 | initialValue: 0 46 | }] 47 | }, { 48 | type: 'motionInputs', 49 | properties: [{ 50 | name: 'value', 51 | initialValue: 0 52 | }, { 53 | name: 'direction', 54 | initialValue: 0 55 | }] 56 | }, { 57 | type: 'status', 58 | properties: [{ 59 | name: 'state', 60 | initialValue: '' 61 | }] 62 | }].forEach(function(setup) { 63 | const entities = controllerConfig[setup.type], 64 | properties = setup.properties; 65 | 66 | if (entities.length) { 67 | entities.forEach(function(entity) { 68 | this[entity.name] = properties.reduce(function(accum, property) { 69 | return (accum[property.name] = property.initialValue, accum); 70 | }, {}); 71 | }, this); 72 | } 73 | }, this); 74 | 75 | //Private methods 76 | //emit an error event or log it to the console. 77 | const handleException = function(ex) { 78 | //if exception was generated within our stream 79 | if (this && this.emit) { 80 | this.emit('error', ex); 81 | } else { 82 | dsutilities.warn(ex); 83 | throw (ex); 84 | } 85 | }; 86 | 87 | //process data from HID connected device. 88 | const processFrame = function(data) { 89 | if (controllerConfig.motionInputs) { 90 | gyro.process(data); 91 | } 92 | if (controllerConfig.analogSticks) { 93 | analogs.process(data); 94 | } 95 | if (controllerConfig.buttons) { 96 | buttons.process(data); 97 | } 98 | if (controllerConfig.status) { 99 | status.process(data); 100 | } 101 | if (controllerConfig.touchPad) { 102 | touchPad.process(data); 103 | } 104 | }; 105 | 106 | const isController = function(device) { 107 | return device.vendorId == controllerConfig.vendorId && device.productId == controllerConfig.productId; 108 | }; 109 | 110 | // Public methods 111 | this.connect = function() { 112 | dsutilities.warn('connect method is deprecated, controller now connects upon declaration.'); 113 | }; 114 | 115 | this.disconnect = function() { 116 | if (device && device.close) { 117 | device.close(); 118 | } 119 | this.emit('disconnecting'); 120 | dsutilities.warn('node dualshock disconnecting'); 121 | }; 122 | 123 | // Used to set controller rumble and light 124 | this.setExtras = function(data) { 125 | 126 | let buff = controllerConfig.output.defaultBuffer.slice(); 127 | 128 | Object.keys(data).forEach(k => { 129 | buff[indexes[k]] = data[k]; 130 | }); 131 | device.write(buff); 132 | }; 133 | 134 | //connect to the controller. 135 | if (typeof options.device === 'undefined') { 136 | dsutilities.warn('node dualshock connecting'); 137 | 138 | const deviceMeta = HID.devices() 139 | .filter(isController)[0]; 140 | if (deviceMeta) { 141 | device = new HID.HID(deviceMeta.path); 142 | } else { 143 | handleException(new Error(`device with VID:${controllerConfig.vendorId} PID:${controllerConfig.productId} not found`)); 144 | } 145 | 146 | } else { 147 | // Allow user-specified device 148 | device = options.device; 149 | } 150 | 151 | device.on('data', processFrame.bind(this)); 152 | device.on('error', handleException.bind(this)); 153 | 154 | //subscribe to the exit event: 155 | process.on('exit', this.disconnect.bind(this)); 156 | }; 157 | 158 | //need to inherit from event emiter. 159 | util.inherits(Controller, Emitter); 160 | 161 | module.exports = Controller; 162 | -------------------------------------------------------------------------------- /src/dualshock.js: -------------------------------------------------------------------------------- 1 | // Module dependencies. 2 | var Controller = require('./controller'), 3 | config = require('./config'); 4 | 5 | // This is the app entry point. 6 | // options you can pass: 7 | // { 8 | // config : "File from controllerConfigurations" or a JS object containing configuration, 9 | // accelerometerSmoothing : true/false, this will activate motion/acelerometer output smoothing. true by default. 10 | // analogStickSmoothing : true/false, this will activate analog thumb stick smoothing 11 | // } 12 | var dualShock = function(options) { 13 | 'use strict'; 14 | 15 | //set the current options 16 | config.setOptions(options); 17 | 18 | //returns the controller. 19 | return new Controller(); 20 | }; 21 | 22 | module.exports = dualShock; 23 | -------------------------------------------------------------------------------- /src/gyro.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // Module dependencies. 3 | var dsutilities = require('./utilities'), 4 | Smoothing = require('./smoothing'), 5 | config = require('./config'); 6 | 7 | //Proccess button events. 8 | var motionProcessor = function(controller) { 9 | 10 | var varianceThreshhold = 1, 11 | smoothInput = config.getOptions().accelerometerSmoothing, 12 | outputSmoothing = new Smoothing(smoothInput), 13 | motionInputs = config.getControllerConfig().motionInputs; 14 | 15 | //generate event name aliases: 16 | motionInputs.forEach(function(motionAxis) { 17 | motionAxis.eventPrefixes = dsutilities.generateEventPrefixAliases(motionAxis.name); 18 | }); 19 | 20 | //Private methods 21 | //data corrections so that each dirrection has a 0 throug x value 22 | var correctData = function(motionAxis, data) { 23 | var value; 24 | //ensuring that both directions start from 0 and move to -x or x; 25 | if (data[motionAxis.directionPin] === 1) { 26 | //we need the values to be from 0 to x. 27 | value = 255 - data[motionAxis.valuePin]; 28 | } else if (data[motionAxis.directionPin] === 2) { 29 | //going in the oposite direction, we need to values to be from 0 to -x; 30 | value = data[motionAxis.valuePin] * -1; 31 | } 32 | 33 | //return an object with both value and dirrection. 34 | return { 35 | direction: data[motionAxis.directionPin], 36 | value: value 37 | }; 38 | }; 39 | 40 | //process the axis movement. 41 | var processAxis = function(motionAxis, data) { 42 | //every motion will have a dirrection and a value 43 | var motionValue = correctData(motionAxis, data), 44 | lastPosition = outputSmoothing.readLastPosition(motionAxis.name); 45 | 46 | //check if the values are within variance 47 | if (dsutilities.isWithinVariance(lastPosition, motionValue.value, varianceThreshhold)) { 48 | motionValue.value = outputSmoothing.smooth(motionAxis.name, motionValue.value); 49 | 50 | // Don't assign motionValue directly to controller[motionAxis.name], 51 | // this will break the reference. 52 | if (controller[motionAxis.name]) { 53 | controller[motionAxis.name].value = motionValue.value; 54 | controller[motionAxis.name].direction = motionValue.direction; 55 | } 56 | 57 | motionAxis.eventPrefixes.forEach(function(eventPrefix) { 58 | controller.emit(eventPrefix + ':motion', motionValue); 59 | }); 60 | } 61 | }; 62 | 63 | // Public methods 64 | //process all configured motion inputs. 65 | this.process = function(data) { 66 | for (var i = 0; i < motionInputs.length; i++) { 67 | processAxis(motionInputs[i], data); 68 | } 69 | }; 70 | }; 71 | 72 | module.exports = motionProcessor; 73 | -------------------------------------------------------------------------------- /src/smoothing.js: -------------------------------------------------------------------------------- 1 | // Module dependencies. 2 | 3 | //smooths data with moving average 4 | var outputSmoothing = function(smoothInput) { 5 | 'use strict'; 6 | 7 | var buffer = {}, 8 | maxFrames = 5; 9 | 10 | // Public methods 11 | this.readLastPosition = function(motionAxisName) { 12 | var axisBuffer = buffer[motionAxisName]; 13 | return axisBuffer ? axisBuffer[axisBuffer.length - 1] : null; 14 | }; 15 | 16 | this.addToBuffer = function(motionAxisName, value) { 17 | if (buffer[motionAxisName]) { 18 | //add the current value to the buffer 19 | buffer[motionAxisName].push(value); 20 | 21 | //remove the head of the buffer 22 | if (buffer[motionAxisName].length > maxFrames) { 23 | buffer[motionAxisName].shift(); 24 | } 25 | } else { 26 | //create an array with the value. 27 | buffer[motionAxisName] = [value]; 28 | } 29 | }; 30 | 31 | //smooth using a moving average. 32 | this.smooth = function(motionAxisName, value) { 33 | this.addToBuffer(motionAxisName, value); 34 | var axisBuffer = buffer[motionAxisName], 35 | sum = 0, 36 | smoothedVal = value; 37 | 38 | if (smoothInput) { 39 | for (var i = 0; i < axisBuffer.length; i++) { 40 | sum += axisBuffer[i]; 41 | } 42 | smoothedVal = Math.floor(sum / axisBuffer.length); 43 | } 44 | 45 | return smoothedVal; 46 | 47 | }; 48 | }; 49 | 50 | module.exports = outputSmoothing; 51 | -------------------------------------------------------------------------------- /src/status.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // Module dependencies. 3 | 4 | var config = require('./config'); 5 | 6 | //Proccess button events. 7 | var Status = function(controller) { 8 | 9 | var buffer = {}, 10 | status = config.getControllerConfig().status; 11 | 12 | var processControllerStatus = function(category, data) { 13 | var state; 14 | for (var i = 0; i < category.states.length; i++) { 15 | if (data[category.pin] === category.states[i].value) { 16 | state = category.states[i].state; 17 | } 18 | } 19 | 20 | if (buffer[category.name] !== state) { 21 | if (controller[category.name]) { 22 | controller[category.name].state = state; 23 | } 24 | controller.emit(category.name + ':change', state); 25 | } 26 | buffer[category.name] = state; 27 | }; 28 | 29 | this.process = function(data) { 30 | for (var i = 0; i < status.length; i++) { 31 | processControllerStatus(status[i], data); 32 | } 33 | }; 34 | }; 35 | 36 | module.exports = Status; 37 | -------------------------------------------------------------------------------- /src/touchpad.js: -------------------------------------------------------------------------------- 1 | const config = require('./config'); 2 | 3 | function genpBufferFromConf(tpAxis) { 4 | return { 5 | name: tpAxis.name, 6 | active: false, 7 | data: { 8 | x: 0, 9 | y: 0 10 | } 11 | }; 12 | } 13 | 14 | module.exports = function TouchPad(controller) { 15 | const touchPad = config.getControllerConfig().touchPad; 16 | let pBuffer = {}; 17 | 18 | function processIsActive(buffer, tpAxis) { 19 | const active = buffer[tpAxis.activePin] < 128; 20 | const axisBuffer = pBuffer[tpAxis.name]; 21 | const evt = active ? 'active' : 'inactive'; 22 | 23 | if (active !== axisBuffer.active) { 24 | controller.emit(`touchpad:${tpAxis.name}:${evt}`); 25 | } 26 | 27 | axisBuffer.active = active; 28 | } 29 | 30 | function processData(buffer, tpAxis) { 31 | const axisBuffer = pBuffer[tpAxis.name]; 32 | 33 | if (axisBuffer.active) { 34 | axisBuffer.data.x = ((buffer[tpAxis.dataPinA] & 15) << 8 | buffer[tpAxis.dataPinB]); 35 | axisBuffer.data.y = buffer[tpAxis.dataPinC] << 4 | ((buffer[tpAxis.dataPinA] & 240) >> 4); 36 | controller.emit(`touchpad:${tpAxis.name}`, axisBuffer.data); 37 | } 38 | 39 | } 40 | 41 | this.process = function process(buffer) { 42 | for (let i = 0; i < touchPad.length; i++) { 43 | //if we have not built a pBuffer profile for this axis lets build it. 44 | if (!pBuffer[touchPad[i].name]) { 45 | pBuffer[touchPad[i].name] = genpBufferFromConf(touchPad[i]); 46 | } 47 | 48 | processIsActive(buffer, touchPad[i]); 49 | processData(buffer, touchPad[i]); 50 | } 51 | }; 52 | }; 53 | -------------------------------------------------------------------------------- /src/utilities.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // Module dependencies. 3 | 4 | var config = require('./config'); 5 | 6 | var unique = function unique(x) { 7 | var result = []; 8 | for (var i = 0; i < x.length; i++) { 9 | if ((result.indexOf(x[i]) < 0)) { 10 | result.push(x[i]); 11 | } 12 | } 13 | return result; 14 | }; 15 | 16 | //provide a few utility functions. 17 | module.exports = { 18 | 19 | //reduces noise from the controller 20 | isWithinVariance: function(x, y, varianceThreshhold) { 21 | return Math.abs(x - y) > varianceThreshhold; 22 | }, 23 | warn: function(message) { 24 | if (config.getOptions().logging) { 25 | console.log(message); 26 | } 27 | }, 28 | generateEventPrefixAliases: function(eventPrefix) { 29 | return unique([ 30 | eventPrefix, 31 | eventPrefix.toLowerCase() 32 | ]); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /test/analog.tests.js: -------------------------------------------------------------------------------- 1 | // Module dependencies. 2 | var Analogs = require('../src/analogs'), 3 | assert = require('assert'), 4 | sinon = require('sinon'), 5 | EventEmitter = require('events').EventEmitter, 6 | config = require('../src/config'); 7 | 8 | describe('the Analogs component', function() { 9 | 'use strict'; 10 | 11 | var mockConfig = [{ 12 | "name": "analog", 13 | "x": 0, 14 | "y": 1 15 | }], 16 | instance = [{ 17 | name: 'process' 18 | }], 19 | dataA = [50, 65], 20 | dataB = [0, 0], 21 | analogs, 22 | emitter, 23 | spy; 24 | 25 | beforeEach(function() { 26 | emitter = new EventEmitter(); 27 | config.setOptions({ 28 | analogStickSmoothing: false 29 | }); 30 | config.setControllerConfig({ 31 | analogSticks: mockConfig 32 | }); 33 | analogs = new Analogs(emitter); 34 | spy = new sinon.spy(); 35 | }); 36 | 37 | describe('object instance', function() { 38 | it('should have the following shape', function() { 39 | //make sure we find these functions. 40 | instance.forEach(function(method) { 41 | assert.equal(typeof analogs[method.name], 'function'); 42 | }); 43 | }); 44 | }); 45 | 46 | describe('move events', function() { 47 | it('should invoke the move event', function() { 48 | emitter.on('analog:move', spy); 49 | analogs.process(dataA); 50 | 51 | assert.equal(spy.called, true); 52 | }); 53 | it('should not invoke the move event', function() { 54 | emitter.on('analog:move', spy); 55 | analogs.process(dataB); 56 | 57 | assert.equal(spy.called, false); 58 | }); 59 | it('should invoke the move event with zero', function() { 60 | analogs.process(dataA); 61 | emitter.on('analog:move', spy); 62 | analogs.process(dataB); 63 | 64 | assert.equal(spy.called, true); 65 | }); 66 | }); 67 | 68 | describe('return values', function() { 69 | it('should return the analog values', function() { 70 | emitter.on('analog:move', spy); 71 | analogs.process(dataA); 72 | var expectedValue = { 73 | x: 50, 74 | y: 65 75 | }, 76 | spyArgument = spy.args[0][0]; 77 | 78 | assert.equal(expectedValue.x, spyArgument.x); 79 | assert.equal(expectedValue.y, spyArgument.y); 80 | assert.equal(spy.called, true); 81 | }); 82 | }); 83 | 84 | }); 85 | -------------------------------------------------------------------------------- /test/buttons.tests.js: -------------------------------------------------------------------------------- 1 | // Module dependencies. 2 | var Buttons = require('../src/buttons'), 3 | assert = require('assert'), 4 | sinon = require('sinon'), 5 | EventEmitter = require('events').EventEmitter, 6 | config = require('../src/config'); 7 | 8 | describe('the Buttons component', function() { 9 | 'use strict'; 10 | 11 | var mockConfig = [{ 12 | "name": "buttonName", 13 | "buttonBlock": 0, 14 | "buttonValue": "0x08", 15 | "analogPin": 1 16 | }, { 17 | "name": "dpadUp", 18 | "buttonBlock": 5, 19 | "buttonValue": "0x00", 20 | "mask": "0xF" 21 | }, { 22 | "name": "dpadDown", 23 | "buttonBlock": 5, 24 | "buttonValue": "0x01", 25 | "mask": "0xF" 26 | }], 27 | instance = [{ 28 | name: 'process' 29 | }], 30 | dataA = [8, 170], 31 | dataB = [0, 0], 32 | buttons, 33 | emitter, 34 | spy, 35 | spyLowerCaseEvents; 36 | 37 | beforeEach(function() { 38 | emitter = new EventEmitter(); 39 | config.setControllerConfig({ 40 | buttons: mockConfig 41 | }); 42 | buttons = new Buttons(emitter); 43 | spy = new sinon.spy(); 44 | spyLowerCaseEvents = new sinon.spy(); 45 | 46 | }); 47 | 48 | describe('object instance', function() { 49 | it('should have the following shape', function() { 50 | //make sure we find these functions. 51 | instance.forEach(function(method) { 52 | assert.equal(typeof buttons[method.name], 'function'); 53 | }); 54 | }); 55 | }); 56 | 57 | describe('press events', function() { 58 | it('should envoke the buttonName:press', function() { 59 | emitter.on('buttonName:press', spy); 60 | emitter.on('buttonname:press', spyLowerCaseEvents); 61 | buttons.process(dataA); 62 | 63 | assert.equal(spy.called, true); 64 | assert.equal(spyLowerCaseEvents.called, true); 65 | }); 66 | it('should not envoke the buttonName:press', function() { 67 | emitter.on('buttonName:release', spy); 68 | emitter.on('buttonname:release', spyLowerCaseEvents); 69 | buttons.process(dataB); 70 | 71 | assert.equal(spy.called, false); 72 | assert.equal(spyLowerCaseEvents.called, false); 73 | }); 74 | }); 75 | 76 | describe('release events', function() { 77 | it('should envoke the buttonName:release', function() { 78 | emitter.on('buttonName:release', spy); 79 | emitter.on('buttonname:release', spyLowerCaseEvents); 80 | buttons.process(dataA); 81 | buttons.process(dataB); 82 | 83 | assert.equal(spy.called, true); 84 | assert.equal(spyLowerCaseEvents.called, true); 85 | }); 86 | it('should not envoke the buttonName:release', function() { 87 | emitter.on('buttonName:release', spy); 88 | emitter.on('buttonname:release', spyLowerCaseEvents); 89 | buttons.process(dataA); 90 | 91 | assert.equal(spy.called, false); 92 | assert.equal(spyLowerCaseEvents.called, false); 93 | }); 94 | }); 95 | 96 | describe('button hold', function() { 97 | it('should raise the hold event', function() { 98 | emitter.on('buttonName:hold', spy); 99 | emitter.on('buttonname:hold', spyLowerCaseEvents); 100 | buttons.process(dataA); 101 | buttons.process(dataA); 102 | assert.equal(spy.args[0][0], 'buttonName'); 103 | assert.equal(spy.called, true); 104 | assert.equal(spyLowerCaseEvents.called, true); 105 | }); 106 | 107 | it('should not raise the hold event', function() { 108 | emitter.on('buttonName:hold', spy); 109 | emitter.on('buttonname:hold', spyLowerCaseEvents); 110 | buttons.process(dataB); 111 | buttons.process(dataB); 112 | assert.equal(spy.called, false); 113 | assert.equal(spyLowerCaseEvents.called, false); 114 | }); 115 | }); 116 | 117 | describe('button analog', function() { 118 | it('should raise the analog event', function() { 119 | emitter.on('buttonName:analog', spy); 120 | emitter.on('buttonname:analog', spyLowerCaseEvents); 121 | buttons.process(dataA); 122 | 123 | assert.equal(spy.args[0][0], dataA[1]); 124 | assert.equal(spy.called, true); 125 | assert.equal(spyLowerCaseEvents.called, true); 126 | }); 127 | 128 | it('should not raise the analog event', function() { 129 | emitter.on('buttonName:analog', spy); 130 | emitter.on('buttonname:analog', spyLowerCaseEvents); 131 | buttons.process(dataB); 132 | 133 | assert.equal(spy.called, false); 134 | assert.equal(spyLowerCaseEvents.called, false); 135 | }); 136 | }); 137 | 138 | describe('ps4 dpad up button', function() { 139 | it('should emit the dpadUp:press event', function() { 140 | emitter.on('dpadUp:press', spy); 141 | emitter.on('dpadup:press', spyLowerCaseEvents); 142 | buttons.process([0, 0, 0, 0, 0, 0]); 143 | 144 | assert.equal(spy.called, true); 145 | assert.equal(spyLowerCaseEvents.called, true); 146 | }); 147 | 148 | it('should not emit the dpadDown:press event', function() { 149 | emitter.on('dpadDown:press', spy); 150 | emitter.on('dpaddown:press', spyLowerCaseEvents); 151 | buttons.process([0, 0, 0, 0, 0, 0]); 152 | 153 | assert.equal(spy.called, false); 154 | assert.equal(spyLowerCaseEvents.called, false); 155 | }); 156 | }); 157 | 158 | describe('ps4 dpad down button', function() { 159 | it('should emit the dpadDown:press event', function() { 160 | emitter.on('dpadDown:press', spy); 161 | emitter.on('dpaddown:press', spyLowerCaseEvents); 162 | buttons.process([0, 0, 0, 0, 0, parseInt("00001001", 2)]); 163 | 164 | assert.equal(spy.called, true); 165 | assert.equal(spyLowerCaseEvents.called, true); 166 | }); 167 | 168 | it('should not emit the dpadUp:press event', function() { 169 | emitter.on('dpadUp:press', spy); 170 | emitter.on('dpadup:press', spyLowerCaseEvents); 171 | buttons.process([0, 0, 0, 0, 0, parseInt("00001001", 2)]); 172 | 173 | assert.equal(spy.called, false); 174 | assert.equal(spyLowerCaseEvents.called, false); 175 | }); 176 | }); 177 | 178 | }); 179 | -------------------------------------------------------------------------------- /test/config.tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | 5 | describe('The Config component', function() { 6 | var mockConfig = { 7 | vendorId: 1556, 8 | productId: 616, 9 | output: [] 10 | }, 11 | mockOptions = { 12 | config: 'dualShock3', 13 | accelerometerSmoothing: true, 14 | logging: false 15 | }, 16 | instance = [{ 17 | name: 'setOptions' 18 | }, { 19 | name: 'getOptions' 20 | }, { 21 | name: 'setControllerConfig' 22 | }, { 23 | name: 'getControllerConfig' 24 | }], 25 | defaultOptionsInstance = [{ 26 | name: 'config' 27 | }, { 28 | name: 'accelerometerSmoothing' 29 | }, { 30 | name: 'analogStickSmoothing' 31 | }], 32 | configA, 33 | configB; 34 | 35 | beforeEach(function() { 36 | configA = require('../src/config'); 37 | configB = require('../src/config'); 38 | }); 39 | 40 | 41 | describe('object instance', function() { 42 | it('should have the following shape', function() { 43 | instance.forEach(function(method) { 44 | assert.equal(typeof configA[method.name], 'function'); 45 | }); 46 | }); 47 | }); 48 | 49 | describe('option methods', function() { 50 | it('should be able to save options', function() { 51 | configA.setOptions(mockOptions); 52 | assert.equal(configA.getOptions(), mockOptions); 53 | }); 54 | 55 | it('should provide a single object accross instances', function() { 56 | configA.setOptions(mockOptions); 57 | assert.equal(configA.getOptions(), configB.getOptions()); 58 | }); 59 | }); 60 | 61 | describe('controllerConfig methods', function() { 62 | it('should be able to save controllerConfig settings', function() { 63 | configA.setControllerConfig(mockConfig); 64 | //change the object 65 | mockConfig.vendorId = 22; 66 | assert.equal(configA.getControllerConfig(), mockConfig); 67 | }); 68 | 69 | it('should provide a single object accross instances', function() { 70 | configA.setControllerConfig(mockConfig); 71 | assert.equal(configA.getControllerConfig(), configB.getControllerConfig()); 72 | }); 73 | }); 74 | 75 | describe('default values', function() { 76 | beforeEach(function() { 77 | configA.setOptions(); 78 | }); 79 | it('should apply default values', function() { 80 | var ops = configA.getOptions(); 81 | defaultOptionsInstance.forEach(function(property) { 82 | assert.notEqual(ops[property.name], void 0); 83 | }); 84 | }); 85 | it('should load default config', function() { 86 | var controllerConfig = configA.getControllerConfig(); 87 | assert.notEqual(controllerConfig, null); 88 | assert.notEqual(controllerConfig, void 0); 89 | }); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /test/dualshock.tests.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | Emitter = require('events').EventEmitter, 3 | config = require('../src/config'), 4 | mockery = require('mockery'); 5 | 6 | function Device() { 7 | Emitter.call(this); 8 | } 9 | 10 | Device.prototype = Object.create(Emitter.prototype, { 11 | constructor: { 12 | value: Device 13 | }, 14 | close: { 15 | value: function() {} 16 | } 17 | }); 18 | 19 | var config = { 20 | analogSticks: [{ 21 | name: 'foo', 22 | x: 0, 23 | y: 1 24 | }], 25 | buttons: [{ 26 | name: 'bar', 27 | buttonBlock: 0, 28 | buttonValue: 0x08, 29 | analogPin: 1 30 | }], 31 | motionInputs: [{ 32 | name: 'baz', 33 | directionPin: 0, 34 | valuePin: 1 35 | }], 36 | status: [{ 37 | name: 'quz', 38 | pin: 5, 39 | states: [{ 40 | value: 0, 41 | state: 'Charging' 42 | }, { 43 | value: 2, 44 | state: 'Charging' 45 | }, { 46 | value: 3, 47 | state: '40%' 48 | }] 49 | }], 50 | output: [] 51 | }; 52 | 53 | var analogs = config.analogSticks; 54 | var buttons = config.buttons; 55 | var motions = config.motionInputs; 56 | var status = config.status; 57 | 58 | describe('the DualShock component', function() { 59 | //enable mockery and mock node-hid: 60 | mockery.enable(); 61 | var nodeHidMock = { 62 | HID: function(vendor, productId) { 63 | return { 64 | on: function() { 65 | //could use this at some point. nothing atm. 66 | } 67 | }; 68 | } 69 | }; 70 | //register mock node-hid. 71 | mockery.registerMock('node-hid', nodeHidMock); 72 | 73 | //once mockery is up we can require the dualshock module. 74 | var DualShock = require('./../src/dualshock.js'), 75 | controller, device; 76 | 77 | before(function() { 78 | device = new Device(); 79 | controller = DualShock({ 80 | config: config, 81 | device: device 82 | }); 83 | }); 84 | 85 | //disable mockery so it does not interfere with other tests. 86 | after(function() { 87 | mockery.deregisterMock('node-hid'); 88 | mockery.disable(); 89 | }); 90 | 91 | beforeEach(function() { 92 | controller.removeAllListeners(Object.keys(controller._events)); 93 | }); 94 | 95 | describe('analog properties', function() { 96 | analogs.forEach(function(analog) { 97 | it(analog.name, function() { 98 | assert.equal(controller[analog.name].x, 0); 99 | assert.equal(controller[analog.name].y, 0); 100 | 101 | device.emit('data', [100, 100]); 102 | 103 | assert.equal(controller[analog.name].x, 100); 104 | assert.equal(controller[analog.name].y, 100); 105 | }); 106 | }); 107 | }); 108 | 109 | describe('button properties', function() { 110 | buttons.forEach(function(button) { 111 | it(button.name, function() { 112 | assert.equal(controller[button.name].value, 0); 113 | assert.equal(controller[button.name].state, 0); 114 | 115 | device.emit('data', [8, 170]); 116 | 117 | assert.equal(controller[button.name].value, 1); 118 | assert.equal(controller[button.name].state, 1); 119 | 120 | device.emit('data', [8, 170]); 121 | 122 | assert.equal(controller[button.name].value, 1); 123 | assert.equal(controller[button.name].state, 2); 124 | 125 | device.emit('data', [0, 0]); 126 | 127 | assert.equal(controller[button.name].value, 0); 128 | assert.equal(controller[button.name].state, 0); 129 | }); 130 | }); 131 | }); 132 | 133 | describe('motion properties', function() { 134 | motions.forEach(function(motion) { 135 | it(motion.name, function() { 136 | assert.equal(controller[motion.name].value, 0); 137 | assert.equal(controller[motion.name].direction, 0); 138 | 139 | device.emit('data', [1, 233]); 140 | 141 | assert.equal(controller[motion.name].value, 22); 142 | assert.equal(controller[motion.name].direction, 1); 143 | }); 144 | }); 145 | }); 146 | 147 | describe('status properties', function() { 148 | status.forEach(function(stat) { 149 | it(stat.name, function() { 150 | device.emit('data', [0, 0, 0, 0, 1, 3]); 151 | assert.equal(controller[stat.name].state, '40%'); 152 | 153 | device.emit('data', [0, 0, 0, 0, 1, 2]); 154 | assert.equal(controller[stat.name].state, 'Charging'); 155 | }); 156 | }); 157 | }); 158 | }); 159 | -------------------------------------------------------------------------------- /test/gyro.tests.js: -------------------------------------------------------------------------------- 1 | // Module dependencies. 2 | var Gyro = require('../src/gyro'), 3 | assert = require('assert'), 4 | sinon = require('sinon'), 5 | EventEmitter = require('events').EventEmitter, 6 | config = require('../src/config'); 7 | 8 | describe('the Gyro component', function() { 9 | 'use strict'; 10 | 11 | var mockConfig = [{ 12 | name: "dirrection", 13 | directionPin: 0, 14 | valuePin: 1 15 | }], 16 | instance = [{ 17 | name: 'process' 18 | }], 19 | dataA = [1, 130], 20 | dataB = [2, 130], 21 | emitter, 22 | spy, 23 | gyro; 24 | 25 | beforeEach(function() { 26 | emitter = new EventEmitter(); 27 | spy = sinon.spy(); 28 | config.setOptions({ 29 | accelerometerSmoothing: false 30 | }); 31 | config.setControllerConfig({ 32 | motionInputs: mockConfig 33 | }); 34 | gyro = new Gyro(emitter); 35 | }); 36 | 37 | describe('object instance', function() { 38 | 39 | it('should have the following shape', function() { 40 | //make sure we find these functions. 41 | instance.forEach(function(method) { 42 | assert.equal(typeof gyro[method.name], 'function'); 43 | }); 44 | }); 45 | }); 46 | 47 | describe('process()', function() { 48 | 49 | it('should envoke the dirrection:motion event with positive values', function() { 50 | emitter.on('dirrection:motion', spy); 51 | gyro.process(dataA); 52 | assert.equal(spy.called, true); 53 | var expectedValue = { 54 | direction: 1, 55 | value: 125 56 | }, 57 | spyArgument = spy.args[0][0]; 58 | 59 | assert.equal(spyArgument.direction, expectedValue.direction); 60 | assert.equal(spyArgument.value, expectedValue.value); 61 | }); 62 | 63 | it('should envoke the dirrection event with negative values.', function() { 64 | emitter.on('dirrection:motion', spy); 65 | gyro.process(dataB); 66 | assert.equal(spy.called, true); 67 | var expectedValue = { 68 | direction: 2, 69 | value: -130 70 | }, 71 | spyArgument = spy.args[0][0]; 72 | 73 | assert.equal(spyArgument.direction, expectedValue.direction); 74 | assert.equal(spyArgument.value, expectedValue.value); 75 | }); 76 | }); 77 | 78 | }); 79 | -------------------------------------------------------------------------------- /test/smoothing.tests.js: -------------------------------------------------------------------------------- 1 | // Module dependencies. 2 | var Smoothing = require('../src/smoothing'), 3 | assert = require('assert'); 4 | 5 | describe('the smoothing component', function() { 6 | 'use strict'; 7 | 8 | var smoothing, 9 | nonSmoothing, 10 | //an instance should have the following functions. 11 | instance = [{ 12 | name: 'readLastPosition' 13 | }, { 14 | name: 'addToBuffer' 15 | }, { 16 | name: 'smooth' 17 | }]; 18 | 19 | beforeEach(function() { 20 | smoothing = new Smoothing(true); 21 | nonSmoothing = new Smoothing(false); 22 | for (var i = 0; i < 5; i++) { 23 | nonSmoothing.addToBuffer('testNonSmoothing', i); 24 | smoothing.addToBuffer('one', i); 25 | smoothing.addToBuffer('two', i + 1); 26 | smoothing.addToBuffer('testSmoothing', i); 27 | } 28 | }); 29 | 30 | describe('object instance', function() { 31 | it('should have the following shape', function() { 32 | //make sure we find these functions. 33 | instance.forEach(function(method) { 34 | assert.equal(typeof smoothing[method.name], 'function'); 35 | }); 36 | }); 37 | }); 38 | 39 | describe('addToBuffer()', function() { 40 | it('should add values to the buffer', function() { 41 | smoothing.addToBuffer('one', 6); 42 | smoothing.addToBuffer('two', 7); 43 | 44 | assert.equal(smoothing.readLastPosition('one'), 6); 45 | assert.equal(smoothing.readLastPosition('two'), 7); 46 | }); 47 | }); 48 | 49 | describe('readLastPosition()', function() { 50 | it('should handle buffers for different objects', function() { 51 | assert.equal(smoothing.readLastPosition('one'), 4); 52 | assert.equal(smoothing.readLastPosition('two'), 5); 53 | }); 54 | }); 55 | 56 | describe('smooth()', function() { 57 | it('should return expected values when smoothing', function() { 58 | //with the data set smoothing of 6 should be 3. 59 | assert.equal(smoothing.smooth('testSmoothing', 6), 3); 60 | //with the data set smoothing of 8 should be 4. 61 | assert.equal(smoothing.smooth('testSmoothing', 8), 4); 62 | }); 63 | 64 | it('should return expected values when not smoothing', function() { 65 | //with smoothing turned off 9 should return 9 66 | assert.equal(nonSmoothing.smooth('testNonSmoothing', 9), 9); 67 | //with smoothing turned off 6 should return 6 68 | assert.equal(nonSmoothing.smooth('testNonSmoothing', 6), 6); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/status.tests.js: -------------------------------------------------------------------------------- 1 | // Module dependencies. 2 | var Status = require('../src/status'), 3 | assert = require('assert'), 4 | sinon = require('sinon'), 5 | EventEmitter = require('events').EventEmitter, 6 | config = require('../src/config'); 7 | 8 | describe('the status component', function() { 9 | var mockConfig = [{ 10 | "name": "chargingState", 11 | "pin": 0, 12 | "states": [{ 13 | "value": 0, 14 | "state": "Charging" 15 | }, { 16 | "value": 2, 17 | "state": "Charging" 18 | }, { 19 | "value": 3, 20 | "state": "40%" 21 | }] 22 | }], 23 | dataA = [0, 0], 24 | dataB = [0, 3], 25 | instance = [{ 26 | name: 'process' 27 | }], 28 | status, 29 | emitter, 30 | spy; 31 | 32 | beforeEach(function() { 33 | emitter = new EventEmitter(); 34 | spy = sinon.spy(); 35 | config.setControllerConfig({ 36 | status: mockConfig 37 | }); 38 | status = new Status(emitter, mockConfig); 39 | }); 40 | 41 | describe('object instance', function() { 42 | it('should have the following shape', function() { 43 | //make sure we find these functions. 44 | instance.forEach(function(method) { 45 | assert.equal(typeof status[method.name], 'function'); 46 | }); 47 | }); 48 | }); 49 | 50 | describe('process()', function() { 51 | it('process should return an object with the expected values', function() { 52 | emitter.on('chargingState:change', spy); 53 | status.process(dataA); 54 | var spyArgument = spy.args[0][0]; 55 | assert.equal(typeof spyArgument, 'string'); 56 | assert.equal(spyArgument, 'Charging'); 57 | spyArgument = null; 58 | }); 59 | }); 60 | }); 61 | --------------------------------------------------------------------------------