├── .npm-version ├── .node-version ├── .gitignore ├── manual-test.js ├── src ├── index.ts ├── list-connected.ts ├── joy-con-right.ts ├── joy-con-left.ts ├── open-device.ts └── joy-con.ts ├── tsconfig.json ├── package.json ├── LICENSE └── README.md /.npm-version: -------------------------------------------------------------------------------- 1 | 10.2.4 2 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | v20.11.1 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | -------------------------------------------------------------------------------- /manual-test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S node -i -r 2 | process.env.DEBUG = "switch-joy-con:*"; 3 | 4 | j = require("."); 5 | ds = j.listConnectedJoyCons(); 6 | d = ds[0].open(); 7 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { JoyConLeft, LeftButtons, LeftDirections } from "./joy-con-left"; 2 | import { JoyConRight, RightButtons, RightDirections } from "./joy-con-right"; 3 | import { listConnectedJoyCons } from "./list-connected"; 4 | import { openNodeHidDeviceAsJoyCon } from "./open-device"; 5 | 6 | type JoyCon = JoyConLeft | JoyConRight; 7 | 8 | export { 9 | listConnectedJoyCons, 10 | openNodeHidDeviceAsJoyCon, 11 | JoyConLeft, 12 | JoyConRight, 13 | LeftDirections, 14 | RightDirections, 15 | type JoyCon, 16 | type LeftButtons, 17 | type RightButtons, 18 | }; 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./src"], 3 | 4 | "compilerOptions": { 5 | "lib": ["es2020"], 6 | "target": "es2020", 7 | "module": "commonjs", 8 | "outDir": "./dist", 9 | "declaration": true, 10 | 11 | "strict": true, 12 | "noImplicitAny": true, 13 | "strictNullChecks": true, 14 | "strictFunctionTypes": true, 15 | "strictPropertyInitialization": true, 16 | "noImplicitThis": true, 17 | "alwaysStrict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noImplicitReturns": true, 21 | "noFallthroughCasesInSwitch": true, 22 | 23 | "moduleResolution": "node", 24 | "esModuleInterop": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "switch-joy-con", 3 | "description": "Use Nintendo Switch Joy-Cons as input devices (Bluetooth)", 4 | "keywords": [ 5 | "nintendo", 6 | "switch", 7 | "joycon", 8 | "joy-con", 9 | "gamepad", 10 | "joypad", 11 | "controller", 12 | "hid" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/suchipi/switch-joy-con.git" 17 | }, 18 | "version": "1.1.3", 19 | "main": "dist/index.js", 20 | "license": "MIT", 21 | "author": "Lily Skye ", 22 | "scripts": { 23 | "build": "tsc" 24 | }, 25 | "dependencies": { 26 | "debug": "^4.3.4", 27 | "node-hid": "^3.0.0" 28 | }, 29 | "devDependencies": { 30 | "@types/debug": "^4.1.12", 31 | "@types/node": "^20.12.12", 32 | "prettier": "^3.2.5", 33 | "typescript": "^5.4.5" 34 | }, 35 | "prettier": {} 36 | } 37 | -------------------------------------------------------------------------------- /src/list-connected.ts: -------------------------------------------------------------------------------- 1 | import HID from "node-hid"; 2 | import { openNodeHidDeviceAsJoyCon } from "./open-device"; 3 | import makeDebug from "debug"; 4 | 5 | const debug = makeDebug("switch-joy-con:list-connected"); 6 | 7 | export function listConnectedJoyCons() { 8 | const devices = HID.devices(); 9 | return devices 10 | .filter((device) => { 11 | const shouldInclude = device.vendorId === 1406; 12 | if (shouldInclude) { 13 | debug("Found device at %s", device.path || "null"); 14 | } else { 15 | debug("Skipping device at %s", device.path || "null"); 16 | } 17 | return shouldInclude; 18 | }) 19 | .map((device) => { 20 | return Object.assign({}, device, { 21 | open(optionalSideOverride: "left" | "right") { 22 | return openNodeHidDeviceAsJoyCon(device, optionalSideOverride); 23 | }, 24 | }); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2024 Lily Skye 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/joy-con-right.ts: -------------------------------------------------------------------------------- 1 | import { JoyCon } from "./joy-con"; 2 | 3 | export const RightDirections = { 4 | LEFT: 0x00, 5 | UP_LEFT: 0x01, 6 | UP: 0x02, 7 | UP_RIGHT: 0x03, 8 | RIGHT: 0x04, 9 | DOWN_RIGHT: 0x05, 10 | DOWN: 0x06, 11 | DOWN_LEFT: 0x07, 12 | NEUTRAL: 0x08, 13 | }; 14 | 15 | export type RightButtons = { 16 | a: boolean; 17 | x: boolean; 18 | b: boolean; 19 | y: boolean; 20 | plus: boolean; 21 | home: boolean; 22 | sl: boolean; 23 | sr: boolean; 24 | r: boolean; 25 | zr: boolean; 26 | analogStickPress: boolean; 27 | analogStick: number; 28 | }; 29 | 30 | export class JoyConRight extends JoyCon { 31 | side: "right" = "right"; 32 | Directions = RightDirections; 33 | 34 | constructor(path: string | undefined | null = null) { 35 | super(path); 36 | 37 | this.buttons = { 38 | a: false, 39 | x: false, 40 | b: false, 41 | y: false, 42 | plus: false, 43 | home: false, 44 | sl: false, 45 | sr: false, 46 | r: false, 47 | zr: false, 48 | analogStickPress: false, 49 | analogStick: RightDirections.NEUTRAL, 50 | }; 51 | } 52 | 53 | _buttonsFromInputReport3F(bytes: Array) { 54 | return { 55 | a: Boolean(bytes[1] & 0x01), 56 | x: Boolean(bytes[1] & 0x02), 57 | b: Boolean(bytes[1] & 0x04), 58 | y: Boolean(bytes[1] & 0x08), 59 | 60 | plus: Boolean(bytes[2] & 0x02), 61 | home: Boolean(bytes[2] & 0x10), 62 | 63 | sl: Boolean(bytes[1] & 0x10), 64 | sr: Boolean(bytes[1] & 0x20), 65 | 66 | r: Boolean(bytes[2] & 0x40), 67 | zr: Boolean(bytes[2] & 0x80), 68 | 69 | analogStickPress: Boolean(bytes[2] & 0x08), 70 | analogStick: bytes[3], 71 | }; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/joy-con-left.ts: -------------------------------------------------------------------------------- 1 | import { JoyCon } from "./joy-con"; 2 | 3 | export const LeftDirections = { 4 | RIGHT: 0x00, 5 | DOWN_RIGHT: 0x01, 6 | DOWN: 0x02, 7 | DOWN_LEFT: 0x03, 8 | LEFT: 0x04, 9 | UP_LEFT: 0x05, 10 | UP: 0x06, 11 | UP_RIGHT: 0x07, 12 | NEUTRAL: 0x08, 13 | }; 14 | 15 | export type LeftButtons = { 16 | dpadUp: boolean; 17 | dpadDown: boolean; 18 | dpadLeft: boolean; 19 | dpadRight: boolean; 20 | minus: boolean; 21 | screenshot: boolean; 22 | sl: boolean; 23 | sr: boolean; 24 | l: boolean; 25 | zl: boolean; 26 | analogStickPress: boolean; 27 | analogStick: number; 28 | }; 29 | 30 | export class JoyConLeft extends JoyCon { 31 | side: "left" = "left"; 32 | Directions = LeftDirections; 33 | 34 | constructor(path: string | undefined | null = null) { 35 | super(path); 36 | 37 | this.buttons = { 38 | dpadUp: false, 39 | dpadDown: false, 40 | dpadLeft: false, 41 | dpadRight: false, 42 | minus: false, 43 | screenshot: false, 44 | sl: false, 45 | sr: false, 46 | l: false, 47 | zl: false, 48 | analogStickPress: false, 49 | analogStick: LeftDirections.NEUTRAL, 50 | }; 51 | } 52 | 53 | _buttonsFromInputReport3F(bytes: Array) { 54 | return { 55 | dpadLeft: Boolean(bytes[1] & 0x01), 56 | dpadDown: Boolean(bytes[1] & 0x02), 57 | dpadUp: Boolean(bytes[1] & 0x04), 58 | dpadRight: Boolean(bytes[1] & 0x08), 59 | 60 | minus: Boolean(bytes[2] & 0x01), 61 | screenshot: Boolean(bytes[2] & 0x20), 62 | 63 | sl: Boolean(bytes[1] & 0x10), 64 | sr: Boolean(bytes[1] & 0x20), 65 | 66 | l: Boolean(bytes[2] & 0x40), 67 | zl: Boolean(bytes[2] & 0x80), 68 | 69 | analogStickPress: Boolean(bytes[2] & 0x04), 70 | analogStick: bytes[3], 71 | }; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/open-device.ts: -------------------------------------------------------------------------------- 1 | import HID from "node-hid"; 2 | import { JoyConLeft } from "./joy-con-left"; 3 | import { JoyConRight } from "./joy-con-right"; 4 | import makeDebug from "debug"; 5 | 6 | const debug = makeDebug("switch-joy-con:open-device"); 7 | 8 | const knownProducts: { 9 | [vendorAndProductId: string]: 10 | | undefined 11 | | { side: "left" | "right" | undefined }; 12 | } = { 13 | // Official Nintendo Switch Joy-Con (L) 14 | "1406:8198": { 15 | side: "left", 16 | }, 17 | // Official Nintendo Switch Joy-Con (R) 18 | "1406:8199": { 19 | side: "right", 20 | }, 21 | // A pair of third-party Joy-Cons I own (no branding). Both sides (left/right) 22 | // report same product id, so we can't make any assumptions about side. 23 | "1406:8201": { 24 | side: undefined, 25 | }, 26 | }; 27 | 28 | export function openNodeHidDeviceAsJoyCon( 29 | device: HID.Device, 30 | optionalSideOverride?: "left" | "right", 31 | ) { 32 | const vendorAndProductId = `${device.vendorId}:${device.productId}`; 33 | const knownProduct = knownProducts[vendorAndProductId]; 34 | 35 | if (knownProduct) { 36 | debug( 37 | "Device at %s matches known vendor/product id %s", 38 | device.path || "null", 39 | vendorAndProductId, 40 | ); 41 | } else { 42 | debug( 43 | "Device at %s with vendor/product id %s is not recognized; trying to use it anyway", 44 | device.path || "null", 45 | vendorAndProductId, 46 | ); 47 | } 48 | 49 | const side = 50 | optionalSideOverride || (knownProduct ? knownProduct.side : null) || null; 51 | 52 | switch (side) { 53 | case "left": { 54 | debug( 55 | "Initializing device %s as left-side JoyCon", 56 | device.path || "null", 57 | ); 58 | return new JoyConLeft(device.path); 59 | } 60 | case "right": { 61 | debug( 62 | "Initializing device %s as right-side JoyCon", 63 | device.path || "null", 64 | ); 65 | return new JoyConRight(device.path); 66 | } 67 | default: { 68 | if (device.product && device.product.endsWith("(L)")) { 69 | debug( 70 | "Initializing device %s as left-side JoyCon (due to name ending with '(L)')", 71 | device.path || "null", 72 | ); 73 | return new JoyConLeft(device.path); 74 | } else if (device.product && device.product.endsWith("(R)")) { 75 | debug( 76 | "Initializing device %s as right-side JoyCon (due to name ending with '(R)')", 77 | device.path || "null", 78 | ); 79 | return new JoyConRight(device.path); 80 | } else { 81 | throw new Error( 82 | `Cannot auto-detect whether Joy-Con with vendor:product id '${device.vendorId}:${device.productId}' is a left-side or right-side Joy-Con. Pass either 'left' or 'right' as an argument to the open() method.`, 83 | ); 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/joy-con.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from "events"; 2 | import HID from "node-hid"; 3 | import makeDebug from "debug"; 4 | 5 | const debug = makeDebug("switch-joy-con:joy-con"); 6 | const ioDebug = makeDebug("switch-joy-con:io"); 7 | 8 | const LED_VALUES = { 9 | ONE: 1, 10 | TWO: 2, 11 | THREE: 4, 12 | FOUR: 8, 13 | ONE_FLASH: 16, 14 | TWO_FLASH: 32, 15 | THREE_FLASH: 64, 16 | FOUR_FLASH: 128, 17 | }; 18 | 19 | function numArrayToHexString(bytes: Array): string { 20 | return "0x" + bytes.map((byte) => byte.toString(16)).join(""); 21 | } 22 | 23 | export class JoyCon extends EventEmitter { 24 | LED_VALUES = LED_VALUES; 25 | 26 | _globalPacketNumber = 0; 27 | _device: HID.HID; 28 | 29 | // Subclass is expected to set this property in its constructor 30 | buttons!: Buttons; 31 | 32 | constructor(path: string | undefined | null) { 33 | super(); 34 | 35 | this._device = new HID.HID(path!); 36 | 37 | this._device.on("data", (bytes) => { 38 | this._handleData(bytes); 39 | }); 40 | 41 | // Simple mode. See https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering/blob/master/bluetooth_hid_subcommands_notes.md#subcommand-0x03-set-input-report-mode 42 | this.setInputReportMode(0x3f); 43 | } 44 | 45 | close() { 46 | if (debug.enabled) { 47 | debug( 48 | "Closing device: %s", 49 | this._device.generateDeviceInfo().product || "unnamed device", 50 | ); 51 | } 52 | this._device.close(); 53 | } 54 | 55 | _send(data: Array) { 56 | // See https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering/blob/master/bluetooth_hid_notes.md 57 | 58 | this._globalPacketNumber = (this._globalPacketNumber + 0x1) % 0x10; 59 | 60 | const bytes = [...data]; 61 | bytes[1] = this._globalPacketNumber; 62 | 63 | if (ioDebug.enabled) { 64 | ioDebug(`OUT: ${numArrayToHexString(bytes)}`); 65 | } 66 | 67 | this._device.write(bytes); 68 | } 69 | 70 | setPlayerLEDs(value: number) { 71 | debug("Setting player LEDs value to: %d", value); 72 | 73 | const bytes = new Array(0x40).fill(0); 74 | bytes[0] = 0x01; 75 | bytes[10] = 0x30; 76 | bytes[11] = value; 77 | 78 | this._send(bytes); 79 | } 80 | 81 | // https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering/blob/master/bluetooth_hid_subcommands_notes.md#subcommand-0x03-set-input-report-mode 82 | setInputReportMode(value: number) { 83 | debug("Setting input report mode to: %x", value); 84 | 85 | const bytes = new Array(0x40).fill(0); 86 | bytes[0] = 0x01; 87 | bytes[10] = 0x03; 88 | bytes[11] = value; 89 | 90 | this._send(bytes); 91 | } 92 | 93 | emit(...args: any) { 94 | debug("emitting event", ...args); 95 | // @ts-ignore spread of non-tuple 96 | return super.emit(...args); 97 | } 98 | 99 | _buttonsFromInputReport3F(_bytes: Array): Buttons { 100 | // Implement in subclass 101 | throw new Error( 102 | "_buttonsFromInputReport3F not implemented in subclass " + 103 | this.constructor.name, 104 | ); 105 | } 106 | 107 | _handleData(bytes: Array) { 108 | if (ioDebug.enabled) { 109 | // extra space after 'IN' here is so it aligns with 'OUT' 110 | ioDebug(`IN : ${numArrayToHexString(bytes)}`); 111 | } 112 | 113 | if (bytes[0] !== 0x3f) return; 114 | debug("Handling input report 3F"); 115 | 116 | const nextButtons = this._buttonsFromInputReport3F(bytes); 117 | 118 | for (const pair of Object.entries(nextButtons)) { 119 | const [name, nextValue] = pair as [keyof Buttons, Buttons[keyof Buttons]]; 120 | 121 | const currentValue = this.buttons[name]; 122 | if (currentValue === false && nextValue === true) { 123 | this.emit(`down:${String(name)}`); 124 | } else if (currentValue === true && nextValue === false) { 125 | this.emit(`up:${String(name)}`); 126 | } 127 | 128 | if (currentValue !== nextValue) { 129 | this.emit(`change:${String(name)}`, nextValue); 130 | } 131 | } 132 | 133 | this.buttons = nextButtons; 134 | this.emit("change"); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `switch-joy-con` 2 | 3 | Use Nintendo Switch Joy-Cons as input devices (Bluetooth). 4 | 5 | ## Features 6 | 7 | - Run callback(s) when buttons are pressed/released/changed 8 | - Toggle player LEDs 9 | 10 | ## Installation 11 | 12 | ``` 13 | npm install --save switch-joy-con 14 | ``` 15 | 16 | ## Usage 17 | 18 | ```js 19 | const { listConnectedJoyCons } = require("switch-joy-con"); 20 | 21 | // First, list all the Joy-Cons connected to the computer. 22 | const devices = listConnectedJoyCons(); 23 | console.log(devices); 24 | // [ 25 | // { 26 | // vendorId: 1406, 27 | // productId: 8198, 28 | // path: 'IOService:/IOResources/IOBluetoothHCIController/AppleBroadcomBluetoothHostController/IOBluetoothDevice/IOBluetoothL2CAPChannel/IOBluetoothHIDDriver', 29 | // serialNumber: '94-58-cb-a6-1b-86', 30 | // manufacturer: 'Unknown', 31 | // product: 'Joy-Con (L)', 32 | // release: 1, 33 | // interface: -1, 34 | // usagePage: 1, 35 | // usage: 5, 36 | // open: [Function: open] 37 | // }, 38 | // { 39 | // vendorId: 1406, 40 | // productId: 8199, 41 | // path: 'IOService:/IOResources/IOBluetoothHCIController/AppleBroadcomBluetoothHostController/IOBluetoothDevice/IOBluetoothL2CAPChannel/IOBluetoothHIDDriver', 42 | // serialNumber: '94-58-cb-a5-cb-9d', 43 | // manufacturer: 'Unknown', 44 | // product: 'Joy-Con (R)', 45 | // release: 1, 46 | // interface: -1, 47 | // usagePage: 1, 48 | // usage: 5, 49 | // open: [Function: open] 50 | // } 51 | // ] 52 | 53 | // Decide which to use, and then call `open` on it. 54 | const left = devices[0].open(); 55 | 56 | // NOTE: for third-party Joy-Cons, you may have to pass a side string: 57 | // const left = devices[0].open("left"); 58 | 59 | // The `buttons` property always contains up-to-date buttons state: 60 | console.log(left.buttons); 61 | // { 62 | // dpadUp: false, 63 | // dpadDown: false, 64 | // dpadLeft: false, 65 | // dpadRight: false, 66 | // minus: false, 67 | // screenshot: false, 68 | // sl: false, 69 | // sr: false, 70 | // l: false, 71 | // zl: false, 72 | // analogStickPress: false, 73 | // analogStick: left.Directions.NEUTRAL 74 | // } 75 | 76 | // A "change" event will be emitted whenever the button state changes 77 | left.on("change", () => { 78 | console.log(left.buttons); 79 | }); 80 | 81 | // Whenever a button is pressed or released, a `down:${buttonName}` or `up:${buttonName}` event is emitted. 82 | left.on("down:minus", () => { 83 | console.log("minus pressed down"); 84 | }); 85 | left.on("up:minus", () => { 86 | console.log("minus depressed"); 87 | }); 88 | 89 | // a `change:${buttonName}` event is also emitted: 90 | left.on("change:minus", (pressed) => { 91 | console.log(`minus is now: ${pressed ? "pressed" : "unpressed"}`); 92 | }); 93 | 94 | // The `analogStick` "button" is a number. Use the `Directions` property on the 95 | // instance to understand its value: 96 | left.on("change:analogStick", (value) => { 97 | switch (value) { 98 | case left.Directions.UP: { 99 | console.log("up"); 100 | break; 101 | } 102 | case left.Directions.UP_RIGHT: { 103 | console.log("up-right"); 104 | break; 105 | } 106 | case left.Directions.RIGHT: { 107 | console.log("right"); 108 | break; 109 | } 110 | case left.Directions.DOWN_RIGHT: { 111 | console.log("down-right"); 112 | break; 113 | } 114 | case left.Directions.DOWN: { 115 | console.log("down"); 116 | break; 117 | } 118 | case left.Directions.DOWN_LEFT: { 119 | console.log("down-left"); 120 | break; 121 | } 122 | case left.Directions.LEFT: { 123 | console.log("left"); 124 | break; 125 | } 126 | case left.Directions.UP_LEFT: { 127 | console.log("up-left"); 128 | break; 129 | } 130 | case left.Directions.UP_RIGHT: { 131 | console.log("up-right"); 132 | break; 133 | } 134 | case left.Directions.NEUTRAL: { 135 | console.log("neutral"); 136 | break; 137 | } 138 | } 139 | }); 140 | 141 | // When you're done with the device, call `close` on it: 142 | left.close(); 143 | 144 | // Usage with a right-side Joy-Con is the same, but it has different button names: 145 | const right = devices[1].open(); 146 | console.log(right.buttons); 147 | // { 148 | // a: false, 149 | // x: false, 150 | // b: false, 151 | // y: false, 152 | // plus: false, 153 | // home: false, 154 | // sl: false, 155 | // sr: false, 156 | // r: false, 157 | // zr: false, 158 | // analogStickPress: false, 159 | // analogStick: right.Directions.NEUTRAL 160 | // } 161 | 162 | // If you need to tell whether a Joy-Con is a left or right Joy-Con, use the `side` property: 163 | console.log(left.side); // "left" 164 | console.log(right.side); // "right" 165 | ``` 166 | 167 | ## API Documentation 168 | 169 | The `"switch-joy-con"` module has one named export, `listConnectedJoyCons`. 170 | 171 | ### `listConnectedJoyCons() => Array` 172 | 173 | Returns an array of objects, each describing a connected Joy-Con. 174 | 175 | The objects have the following properties: 176 | 177 | ```ts 178 | interface JoyConDescription { 179 | vendorId: number; 180 | productId: number; 181 | path: string; 182 | serialNumber: string; 183 | manufacturer: string; 184 | product: string; 185 | release: number; 186 | interface: number; 187 | usagePage: number; 188 | usage: number; 189 | open: () => JoyCon; 190 | } 191 | ``` 192 | 193 | The most important thing here is the `open` method, which returns a `JoyCon` instance. 194 | 195 | ### `JoyCon` 196 | 197 | You can obtain a `JoyCon` instance by calling `open` on a `JoyConDescription`, as returned by `listConnectedJoyCons()`. 198 | 199 | Each `JoyCon` adheres to the following interface: 200 | 201 | 202 | ```ts 203 | interface JoyCon extends EventEmitter { 204 | // Whether the Joy-Con attaches to the left or right side of a Switch. 205 | // The buttons will differ depending on this. 206 | side: "left" | "right"; 207 | 208 | // An object containing the current button state. 209 | buttons: // If `side` is "left", `buttons` will have the following properties: 210 | | { 211 | // If a button is pressed, its value will be `true`. Otherwise, it will be `false`. 212 | dpadUp: boolean; 213 | dpadDown: boolean; 214 | dpadLeft: boolean; 215 | dpadRight: boolean; 216 | minus: boolean; 217 | screenshot: boolean; 218 | sl: boolean; 219 | sr: boolean; 220 | l: boolean; 221 | zl: boolean; 222 | analogStickPress: boolean; 223 | 224 | // Use the `Directions` property on the JoyCon to understand this number. 225 | analogStick: number; 226 | } 227 | // Otherwise (if `side` is "right"), `buttons` will have these properties: 228 | | { 229 | // If a button is pressed, its value will be `true`. Otherwise, it will be `false`. 230 | a: boolean; 231 | x: boolean; 232 | b: boolean; 233 | y: boolean; 234 | plus: boolean; 235 | home: boolean; 236 | sl: boolean; 237 | sr: boolean; 238 | r: boolean; 239 | zr: boolean; 240 | analogStickPress: boolean; 241 | 242 | // Use the `Directions` property on the JoyCon to understand this number. 243 | analogStick: number; 244 | }; 245 | 246 | // The analog stick direction constants for this Joy-Con. Note that these differ 247 | // between left/right Joy-Cons, so always rely on this property. 248 | Directions: { 249 | LEFT: number; 250 | UP_LEFT: number; 251 | UP: number; 252 | UP_RIGHT: number; 253 | RIGHT: number; 254 | DOWN_RIGHT: number; 255 | DOWN: number; 256 | DOWN_LEFT: number; 257 | NEUTRAL: number; 258 | }; 259 | 260 | // Call this method when you are done using the Joy-Con. 261 | // It won't be disconnected from Bluetooth, but the handle 262 | // will be released so it can be used by another application. 263 | close(); 264 | 265 | // LED values that can be provided to the `setPlayerLEDs` method. 266 | LED_VALUES: { 267 | // Indicates that the LED in this slot should be lit up solid. 268 | ONE: number; 269 | TWO: number; 270 | THREE: number; 271 | FOUR: number; 272 | 273 | // Indicates that the LED in this slot should be flashing on and off. 274 | ONE_FLASH: number; 275 | TWO_FLASH: number; 276 | THREE_FLASH: number; 277 | FOUR_FLASH: number; 278 | }; 279 | 280 | // Set which player LEDs are lit. Use value(s) from the `LED_VALUES` property. 281 | // To turn on more than one LED, add them together; eg: 282 | // setPlayerLEDs(LED_VALUES.ONE + LED_VALUES.TWO) 283 | setPlayerLEDs(value: number); 284 | } 285 | ``` 286 | 287 | `JoyCon`s are also `EventEmitter`s, and they emit the following events: 288 | 289 | - `change` - Button state has changed. Inspect the `buttons` property on the device for more info. 290 | - `change:${buttonName}` - A specific button changed state. The event listener will be called with one argument, containing the new button value. 291 | - `down:${buttonName}` - A specific button is now being held down. 292 | - `up:${buttonName}` - A specific button is no longer being held down. 293 | 294 | List of button names: 295 | 296 | - `a` 297 | - `x` 298 | - `b` 299 | - `y` 300 | - `plus` 301 | - `home` 302 | - `l` 303 | - `r` 304 | - `zl` 305 | - `zr` 306 | - `dpadUp` 307 | - `dpadDown` 308 | - `dpadLeft` 309 | - `dpadRight` 310 | - `minus` 311 | - `screenshot` 312 | - `sl` 313 | - `sr` 314 | - `analogStickPress` 315 | - `analogStick` 316 | 317 | ## License 318 | 319 | MIT 320 | --------------------------------------------------------------------------------