UNPKG

dualsense-ts

Version:

The natural interface for your DualSense Classic and DualSense Access controllers, with Typescript

824 lines (596 loc) 34.9 kB
# dualsense-ts `dualsense-ts` is the natural interface for your DualSense and DualSense Access controllers. Simple to use, fully-typed, fully-featured, and supports wired and wireless connections in both node.js and the browser. Check out the **[interactive docs](https://nsfm.github.io/dualsense-ts/)**! Connect a controller (or a few!) and try every feature with live demos. Or, explore the [playground](https://nsfm.github.io/dualsense-ts/playground) to check all of your controller functionality in one place. ## Features - **[Rich input API](https://nsfm.github.io/dualsense-ts/inputs)** providing synchronous, event, iterator, or promise-based updates - **[Bluetooth and USB](https://nsfm.github.io/dualsense-ts/inputs/connection)** support in the browser or node.js - **Automatic connection and reconnection** even when connection type changes - **[Multiplayer support](https://nsfm.github.io/dualsense-ts/multiplayer)**, allowing up to 31 connected controllers at a time - **Lighting control** covering [RGB light bars](https://nsfm.github.io/dualsense-ts/outputs/lightbar), [player LEDs](https://nsfm.github.io/dualsense-ts/outputs/player-leds), and [mute button](https://nsfm.github.io/dualsense-ts/outputs/mute-led) - **Full haptics control** over independent [left/right rumble](https://nsfm.github.io/dualsense-ts/outputs/rumble) plus complete [trigger haptics](https://nsfm.github.io/dualsense-ts/outputs/trigger-effects) - **[Touchpad support](https://nsfm.github.io/dualsense-ts/inputs/touchpad)** with full multi-touch handling - **[Motion tracking](https://nsfm.github.io/dualsense-ts/inputs/motion)** via factory-calibrated gyroscope and accelerometer, with built-in [orientation tracking](https://nsfm.github.io/dualsense-ts/api/orientation) and [shake detection](https://nsfm.github.io/dualsense-ts/api/shake-detector) - **[Battery status](https://nsfm.github.io/dualsense-ts/inputs/battery)** including level and charging state - **[Audio controls](https://nsfm.github.io/dualsense-ts/outputs/audio)** for speaker, headphone, and microphone volume, routing, and muting - **Peripheral status** for connected headphones and microphone - **[Power save](https://nsfm.github.io/dualsense-ts/outputs/power-save)** flags for per-subsystem muting (haptic mute confirmed; subsystem disable flags advisory) - **[Firmware info](https://nsfm.github.io/dualsense-ts/status)** checks providing controller color, hardware/software versions, and more - **[DualSense Access controller](https://nsfm.github.io/dualsense-ts/access)** support with 8 hardware buttons, analog stick, battery, profile switching, and full LED control ## [Getting started](https://nsfm.github.io/dualsense-ts/getting-started) ### Installation [This package is distributed via npm](https://www.npmjs.com/package/dualsense-ts). Install it the usual way: - `npm add dualsense-ts` #### In the browser `dualsense-ts` has zero dependencies and relies on the [WebHID API](https://developer.mozilla.org/en-US/docs/Web/API/WebHID_API). At this time, only Chrome, Edge, and Opera are compatible. #### In node.js `dualsense-ts` relies on `node-hid` as a peer dependency. You'll need to add it to your project as well: - `npm add node-hid` ### [Connecting](https://nsfm.github.io/dualsense-ts/inputs/connection) When you construct a `new Dualsense()`, it will begin searching for a controller. If it finds one, it will connect automatically. ```typescript import { Dualsense } from "dualsense-ts"; // Grab a controller connected via USB or Bluetooth const controller = new Dualsense(); ``` If the device disconnects, `dualsense-ts` will quietly wait for it to come back. You can monitor the connection status with `controller.connection` using any of the Input APIs listed in the next section. ```typescript controller.connection.on("change", ({ active }) => { console.log(`controller ${active ? "" : "dis"}connected`); }); controller.connection.active; // returns true while the controller is available controller.wireless; // returns true while connected over bluetooth ``` When the user switches from wired to wireless or vice versa, `dualsense-ts` will reconnect to the same device seamlessly. ### [Input APIs](https://nsfm.github.io/dualsense-ts/inputs) `dualsense-ts` provides several interfaces for reading input: - _Synchronous_: It's safe to read the current input state at any time. When the controller disconnects, these all reset to their neutral states. ```typescript // Buttons controller.circle.state; // false controller.left.bumper.state; // true // Triggers controller.right.trigger.active; // true controller.right.trigger.pressure; // 0.72, 0 - 1 // Analog Sticks - represented as a position on a unit circle controller.left.analog.x.active; // true, when away from center controller.left.analog.x.state; // 0.51, -1 to 1 controller.left.analog.direction; // 4.32, radians controller.left.analog.magnitude; // 0.23, 0 to 1 // Touchpad - each touch point works like an analog input controller.touchpad.right.contact.state; // false controller.touchpad.right.x.state; // -0.44, -1 to 1 ``` - _Callbacks_: Each input is an EventEmitter or EventTarget that provides `input`, `press`, `release`, and `change` events: ```typescript // Change events are triggered only when an input's value changes controller.triangle.on("change", (input) => console.log(`${input} changed: ${input.active}`), ); // ▲ changed: true // ▲ changed: false // The callback provides two arguments, so you can monitor nested inputs // You'll get a reference to your original input, and the one that changed controller.dpad.on("press", (dpad, input) => { assert(dpad === controller.dpad); console.log(`${input} pressed`); }); // ↑ pressed // → pressed // ↑ pressed // `input` events are triggered whenever there is new information from the controller // Your Dualsense may provide over 250 `input` events per second, so use this sparingly // These events are not available for nested inputs, like the example above controller.left.analog.x.on("input", console.log); // Remove a specific listener const handler = ({ active }) => console.log(active); controller.cross.on("press", handler); controller.cross.off("press", handler); // Remove all listeners for an event controller.cross.removeAllListeners("press"); ``` - _Promises_: Wait for one-off inputs using `await`: ```typescript // Resolves next time `dpad up` is released const { active } = await controller.dpad.up.promise("release"); // Wait for the next press of any dpad button const { left, up, down, right } = await controller.dpad.promise("press"); // Wait for any input at all await controller.promise(); ``` - _Async Iterators_: Each input is an async iterator that provides state changes: ```typescript for await (const { pressure } of controller.left.trigger) { console.log(`L2: ${Math.round(pressure * 100)}%`); } ``` ### Other Supported Features #### [Touchpad](https://nsfm.github.io/dualsense-ts/inputs/touchpad) The touchpad supports two simultaneous touches, each modeled as an analog input with x/y axes ranging from -1 to 1 (center is 0,0). The physical touchpad button is a separate input: ```typescript // React to the first touch point (or single-finger touch) controller.touchpad.left.on("change", (touch) => { console.log( `Touch: x=${touch.x.state.toFixed(2)}, y=${touch.y.state.toFixed(2)}`, ); }); // Detect touch contact and release controller.touchpad.left.contact.on("press", () => console.log("Finger down")); controller.touchpad.left.contact.on("release", () => console.log("Finger up")); // Second touch point for multi-touch controller.touchpad.right.on("change", (touch) => { if (touch.contact.active) { console.log( `Touch 2: x=${touch.x.state.toFixed(2)}, y=${touch.y.state.toFixed(2)}`, ); } }); // Physical click of the touchpad controller.touchpad.button.on("press", () => console.log("Touchpad clicked")); // And another way to detect a touch controller.touchpad.on("press", () => console.log("Touchpad touched")); ``` Each touch point also exposes a `tracker` ([Increment](src/elements/increment.ts)) that provides a touch ID, which increments each time a new finger is placed on the pad. #### [Motion Control](https://nsfm.github.io/dualsense-ts/inputs/motion) Raw values from the controller's 6-axis IMU are provided: ```typescript controller.gyroscope.on("change", ({ x, y, z }) => { console.log(`Gyroscope: \n\t${x}\n\t${y}\n\t${z}`); }); controller.accelerometer.on("change", ({ x, y, z }) => { console.log(`Accelerometer: \n\t${x}\n\t${y}\n\t${z}`); }); controller.accelerometer.z.on("change", ({ magnitude }) => { if (magnitude > 0.3) console.log("Controller is moving!"); }); ``` Gyroscope and accelerometer readings are automatically calibrated using each controller's factory calibration data, which removes gyro bias drift and accelerometer zero-point offset. Calibration is applied transparently — you can inspect the resolved factors via `controller.calibration`. See [Factory Calibration](https://nsfm.github.io/dualsense-ts/inputs/motion) in the docs for details on bias removal, zero-point correction, and per-axis sensitivity normalization. #### [Orientation Tracking](https://nsfm.github.io/dualsense-ts/api/orientation) Built-in sensor fusion provides a stable 3D orientation from the raw IMU data, powered by a zero-dependency [Madgwick AHRS](https://nsfm.github.io/dualsense-ts/api/orientation) filter: ```typescript // Fused Euler angles (radians), updated every IMU sample const { pitch, yaw, roll } = controller.orientation; // Quaternion for 3D rendering const [w, x, y, z] = controller.orientation.quaternion; // Accelerometer-only tilt — no drift, no yaw const steer = controller.orientation.tiltRoll; // Tune the filter: lower beta = smoother, higher = less drift controller.orientation.beta = 0.02; // Reset to identity (e.g. on button press) controller.orientation.reset(); ``` #### [Shake Detection](https://nsfm.github.io/dualsense-ts/api/shake-detector) The built-in shake detector uses [Goertzel frequency analysis](https://nsfm.github.io/dualsense-ts/api/shake-detector) to detect shake intensity, frequency, and direction-reversal rate: ```typescript // Simple shake detection if (controller.shake.active) { triggerEffect(); } // Proportional response controller.left.rumble = controller.shake.intensity; // Frequency-based mechanics if (controller.shake.frequency > 4) { baby.soothe(controller.shake.intensity); } // Tune sensitivity and analysis window at runtime controller.shake.threshold = 0.05; controller.shake.windowSize = 128; // Access raw spectrum for custom visualization for (const bin of controller.shake.spectrum) { drawBar(bin.freq, bin.power); } ``` #### [Battery](https://nsfm.github.io/dualsense-ts/inputs/battery) The controller provides its current battery level and charging status: ```typescript // Check charging status import { ChargeStatus } from "dualsense-ts"; controller.battery.status.on("change", ({ state }) => { switch (state) { case ChargeStatus.Charging: console.log("Charging"); break; case ChargeStatus.Discharging: console.log("On battery"); break; case ChargeStatus.Full: console.log("Fully charged"); break; } }); // React to battery level changes controller.battery.level.on("change", ({ state }) => { console.log(`Battery: ${Math.round(state * 100)}%`); if (state < 0.2) console.log("Low battery!"); }); ``` After connection it may take a second for these values to populate. Please note that the battery level is not a precise reading - it changes in 10% increments and is prone to flip-flopping. `dualsense-ts` makes an attempt to buffer and normalize these values. #### [Rumble](https://nsfm.github.io/dualsense-ts/outputs/rumble) The controller has two haptic rumbles. The left motor produces a stronger, lower-frequency rumble, while the right actuator produces a lighter, higher-frequency vibration. They are controlled independently: ```typescript controller.rumble(1.0); // 100% rumble intensity controller.left.rumble(0.5); // 50% rumble intensity on the left console.log(controller.left.rumble()); // Prints 0.5 console.log(controller.right.rumble()); // Prints 1 controller.rumble(0); // Stop rumbling controller.rumble(true); // Another way to set 100% intensity controller.rumble(false); // Another way to stop rumbling // Control right rumble intensity with the right trigger controller.right.trigger.on("change", (trigger) => { controller.right.rumble(trigger.pressure); }); ``` #### [Adaptive Triggers](https://nsfm.github.io/dualsense-ts/outputs/trigger-effects) Adaptive trigger feedback is controlled via `controller.left.trigger.feedback` / `controller.right.trigger.feedback`. ```typescript import { Dualsense, TriggerEffect } from "dualsense-ts"; const controller = new Dualsense(); // Continuous resistance starting at 30% travel controller.right.trigger.feedback.set({ effect: TriggerEffect.Feedback, position: 0.3, // 0 - 1 strength: 0.8, }); // Weapon trigger — resistance with snap release controller.right.trigger.feedback.set({ effect: TriggerEffect.Weapon, start: 0.2, end: 0.6, strength: 0.9, }); // Vibration with frequency controller.right.trigger.feedback.set({ effect: TriggerEffect.Vibration, position: 0.1, amplitude: 0.7, frequency: 40, // in Hz, 0 - 255 }); // Reset to default linear feel controller.right.trigger.feedback.reset(); // Reset both triggers controller.resetTriggerFeedback(); // Read current config console.log(controller.right.trigger.feedback.config); console.log(controller.right.trigger.feedback.effect); // TriggerEffect.Off ``` Feedback state is automatically restored if the controller disconnects and reconnects - no handling required on your end. #### Trigger effects | Effect | Description | | ------------------------- | ---------------------------------------------------------- | | `TriggerEffect.Off` | No resistance (default linear feel) | | `TriggerEffect.Feedback` | Zone-based continuous resistance from a start position | | `TriggerEffect.Weapon` | Resistance with a snap release point | | `TriggerEffect.Bow` | Weapon feel with snap-back force | | `TriggerEffect.Galloping` | Rhythmic two-stroke oscillation | | `TriggerEffect.Vibration` | Zone-based oscillation with amplitude and frequency | | `TriggerEffect.Machine` | Dual-amplitude vibration with frequency and period control | Each effect accepts a unique set of configuration options; your editor's type hints will guide you through the available parameters for each effect. The [interactive docs](https://nsfm.github.io/dualsense-ts/outputs/trigger-effects) include full slider controls for every effect and parameter, making it a great tool for finding the right values. Effect names are based on [Nielk1's DualSense trigger effect documentation](https://gist.github.com/Nielk1/6d54cc2c00d2201ccb8c2720ad7538db). #### [Lights](https://nsfm.github.io/dualsense-ts/outputs/lightbar) You can control the controller's lightbar as well as the [player indicator](https://nsfm.github.io/dualsense-ts/outputs/player-leds) and [mute](https://nsfm.github.io/dualsense-ts/outputs/mute-led) LEDs: ```typescript import { PlayerID, Brightness } from "dualsense-ts"; // Light bar — set color with {r, g, b} (0–255 per channel) controller.lightbar.set({ r: 255, g: 0, b: 128 }); controller.lightbar.color; // { r: 255, g: 0, b: 128 } // Light bar pulse effects — firmware-driven one-shot animations // This overrides your custom color controller.lightbar.fadeBlue(); // Fades to blue and holds // You must call `fadeOut()` to restore custom lightbar colors controller.lightbar.fadeOut(); // Fades to black, then returns to set color // Player indicator LEDs — 5 white LEDs, individually addressable controller.playerLeds.set(PlayerID.Player1); // Use a preset pattern controller.playerLeds.setLed(0, true); // Toggle individual LEDs (0–4) controller.playerLeds.setLed(4, true); controller.playerLeds.clear(); // All off controller.playerLeds.setBrightness(Brightness.Medium); // High, Medium, or Low // Override the mute LED with a software-controlled mode import { MuteLedMode } from "dualsense-ts"; controller.mute.setLed(MuteLedMode.On); // Solid on controller.mute.setLed(MuteLedMode.Pulse); // Slow pulse controller.mute.setLed(MuteLedMode.Off); // Force off controller.mute.resetLed(); // Return control to firmware ``` The `{r, g, b}` format is compatible with popular color libraries. Pass the output of `colord().toRgb()`, `tinycolor().toRgb()`, or `Color().object()` straight to `lightbar.set()`. By default the mute LED is managed by the controller firmware (toggled by the physical button). Use `setLed()` to override with a specific mode, and `resetLed()` to hand control back. A physical button press will also return the LED to firmware control. #### [Audio Peripherals](https://nsfm.github.io/dualsense-ts/outputs/audio) The controller reports whether a microphone and/or headphones are connected via the 3.5mm jack: ```typescript controller.headphone.on("change", ({ state }) => { console.log(`Headphones ${state ? "" : "dis"}connected`); }); controller.microphone.on("change", ({ state }) => { console.log(`Microphone ${state ? "" : "dis"}connected`); }); controller.headphone.state; // true when headphones are plugged in controller.microphone.state; // true when a microphone is available // `mute.status` is true when the user has muted the microphone // Usually it's tied to the LED state, unless you've overridden the LED controller.mute.status.on("change", ({ state }) => { console.log(`Mute: ${state ? "muted" : "unmuted"}`); }); ``` #### [Audio Control](https://nsfm.github.io/dualsense-ts/outputs/audio) The DualSense has a built-in speaker and microphone. Over USB, it registers as a standard audio device at the OS level. `dualsense-ts` provides volume, routing, and mute controls: ```typescript import { AudioOutput, MicSelect, MicMode } from "dualsense-ts"; // Volume control (0.0-1.0) controller.audio.setSpeakerVolume(0.8); controller.audio.setHeadphoneVolume(0.5); controller.audio.setMicrophoneVolume(1.0); // Audio output routing controller.audio.setOutput(AudioOutput.Speaker); // Internal speaker only controller.audio.setOutput(AudioOutput.Headphone); // Headphone only (default) controller.audio.setOutput(AudioOutput.Split); // Left: headphone, right: speaker // Per-output muting controller.audio.muteSpeaker(true); controller.audio.muteHeadphone(true); controller.audio.muteMicrophone(true); controller.audio.muteSpeaker(false); // Unmute // Microphone source and processing controller.audio.setMicSelect(MicSelect.Internal); // Built-in mic controller.audio.setMicSelect(MicSelect.Headset); // Headset mic controller.audio.setMicMode(MicMode.Chat); // Chat mode // Speaker preamp gain (0–7) and beam forming controller.audio.setPreamp(4); controller.audio.setPreamp(2, true); // Enable beam forming ``` These HID commands control volume and routing but do not stream audio data. To send audio to the controller's speaker or capture from its microphone, use the OS audio device (Web Audio API or a native audio library like PortAudio). The helper function `findDualsenseAudioDevices()` locates matching audio devices in the browser: ```typescript import { findDualsenseAudioDevices } from "dualsense-ts"; const { outputs, inputs } = await findDualsenseAudioDevices(); // Route audio to the controller's speaker if (outputs.length > 0) { const ctx = new AudioContext({ sinkId: outputs[0].deviceId }); } // Capture from the controller's microphone if (inputs.length > 0) { const stream = await navigator.mediaDevices.getUserMedia({ audio: { deviceId: { exact: inputs[0].deviceId } }, }); } ``` For Node.js, enumerate audio devices by USB vendor ID `0x054C` and product ID `0x0CE6` using your audio library of choice. These constants are exported as `DUALSENSE_AUDIO_VENDOR_ID` and `DUALSENSE_AUDIO_PRODUCT_ID`. Unfortunately, multi-controller support is limited. We don't have a dependable way to link individual controllers to their device IDs across all connection types at this time - if you have any ideas, please submit a PR! #### [Color and Serial Number](https://nsfm.github.io/dualsense-ts/status) `dualsense-ts` reads the controller's body color and serial number from factory info after connection: ```typescript import { DualsenseColor } from "dualsense-ts"; controller.color; // DualsenseColor.CosmicRed controller.serialNumber; // Factory-stamped serial number ``` `color` returns a `DualsenseColor` enum value (`DualsenseColor.Unknown` when factory info is unavailable). #### [Firmware and Factory Info](https://nsfm.github.io/dualsense-ts/status) `dualsense-ts` automatically reads firmware details and factory information from the device after connection. These values may take a moment to populate. ```typescript const fw = controller.firmwareInfo; const v = fw.mainFirmwareVersion; console.log(`Firmware: ${v.major}.${v.minor}.${v.patch}`); console.log(`Built: ${fw.buildDate} ${fw.buildTime}`); console.log(`Hardware: ${fw.hardwareInfo}`); const fi = controller.factoryInfo; console.log(`Color: ${fi.colorName}`); // e.g. "Cosmic Red" console.log(`Board: ${fi.boardRevision}`); // e.g. "BDM-030" console.log(`Serial: ${fi.serialNumber}`); ``` The `firmwareInfo` property includes build date/time, firmware versions, hardware info, and device info. The `factoryInfo` property includes the controller's body color, board revision, and serial number. #### [Power Save](https://nsfm.github.io/dualsense-ts/outputs/power-save) The output report exposes per-subsystem power save flags. The **mute flags** (haptic mute, and per-channel audio mutes on `Audio`) have confirmed observable effects. The **disable flags** (touch, motion, haptics, audio) are valid protocol bits but have no confirmed observable impact in our testing - they may affect internal power draw without changing host-visible behavior. ```typescript // Mute haptic output (confirmed working) controller.powerSave.hapticsMuted = true; // Send disable flags (advisory — no confirmed observable effect) controller.powerSave.set({ motion: false, touch: false }); // Re-enable everything controller.powerSave.reset(); ``` See the [Power Save docs](https://nsfm.github.io/dualsense-ts/outputs/power-save) for the full API and hardware notes. ## [Using `dualsense-ts` with React](https://nsfm.github.io/dualsense-ts/react) The library's event-driven API maps naturally to React. Create a singleton instance at module scope, expose it through a context, and use a lightweight hook to subscribe components to input changes: ```typescript import { createContext, useContext, useState, useEffect } from "react"; import { Dualsense, type Input } from "dualsense-ts"; // Singleton + context const controller = new Dualsense(); export const ControllerContext = createContext(controller); // Hook — subscribes to any input, re-renders on change function useControllerInput<T extends Input<T>>( selector: (c: Dualsense) => T, ): T { const c = useContext(ControllerContext); const input = selector(c); const [, tick] = useState(0); useEffect(() => { const h = () => tick((t) => t + 1); input.on("change", h); return () => { input.removeListener("change", h) }; }, [input]); return input; } // Usage function TriggerPressure() { const trigger = useControllerInput((c) => c.right.trigger); return <span>{Math.round(trigger.pressure * 100)}%</span>; } ``` For multi-controller setups, context switching patterns, WebHID permission flows, and performance tips for high-frequency inputs, see the [React Apps guide](https://nsfm.github.io/dualsense-ts/react). The [documentation app source](./documentation_app/) is a full reference implementation. ## [Multiplayer](https://nsfm.github.io/dualsense-ts/multiplayer) `dualsense-ts` supports multiple controllers through the `DualsenseManager` class. The manager automatically discovers controllers, assigns player LEDs, and preserves player slots across disconnects and USB/Bluetooth switches. ### Quick start ```typescript import { DualsenseManager } from "dualsense-ts"; const manager = new DualsenseManager(); // React to controller count changes manager.on("change", ({ active, players }) => { console.log(`${active} controller(s) connected`); for (const [index, controller] of players) { console.log( `Player ${index + 1}: ${controller.connection.active ? "ready" : "away"}`, ); } }); ``` In Node.js, the manager polls for new devices automatically. In the browser, you'll still need to request permission via a user gesture: ```typescript // React / plain JS <button onClick={manager.getRequest()}>Add Controllers</button> ``` This only applies to the first connection for each controller. ### Accessing controllers ```typescript manager.controllers; // readonly Dualsense[] — all managed controllers manager.get(0); // Dualsense | undefined — by slot index manager.count; // number of managed slots (including disconnected) manager.state.active; // number of currently connected controllers // Iterate for (const controller of manager) { console.log(controller.triangle.state); } ``` ### Player LEDs The manager auto-assigns player LED patterns as controllers connect. The first four match the PS5 console convention; slots 5-31 use the remaining 5-bit LED combinations. ```typescript import { DualsenseManager, PlayerID, Brightness } from "dualsense-ts"; const manager = new DualsenseManager(); // Override a specific slot's LED pattern (5-bit bitmask, 0x00–0x1f) manager.setPlayerPattern(4, 0b10001); // Player 5: outer two LEDs manager.getPlayerPattern(0); // 0x04 (PlayerID.Player1) // Disable auto-assignment and manage LEDs yourself manager.autoAssignPlayerLeds = false; manager.get(0)?.playerLeds.set(PlayerID.All); manager.get(0)?.playerLeds.setBrightness(Brightness.Low); ``` ### Reconnection When a controller disconnects, its slot is preserved. If the same controller reconnects, even through a different connection type (USB to Bluetooth or vice versa), it returns to its original slot with the same player number. Reconnection matching uses hardware identity provided by the controller's firmware. ### Slot management Disconnected controllers hold their slot open for reconnection. To free slots: ```typescript // Release a specific slot manager.release(2); // Release all disconnected slots at once manager.releaseDisconnected(); // Stop discovery and disconnect everything manager.dispose(); ``` ### Single player Using `new Dualsense()` directly continues to work exactly as before, allowing you to manage a single controller. `DualsenseManager` is entirely opt-in - you only need it when managing multiple controllers. Do not use standalone `Dualsense` instances and a `DualsenseManager` at the same time. ## Other DualSense Variants The **DualSense Access** controller is fully supported - see the [DualSense Access](#dualsense-access) section below. The DualSense FlexStrike and DualSense Edge controllers are not yet supported. This functionality is on the roadmap. The PS4 DualShock controller is not supported. ## [DualSense Access](https://nsfm.github.io/dualsense-ts/access) The DualSense Access controller is supported via a separate `DualsenseAccess` class. It exposes the controller's raw hardware inputs - independent of the active profile mapping - plus battery, profile state, and all 4 LED systems. ### Quick start ```typescript import { DualsenseAccess } from "dualsense-ts"; const access = new DualsenseAccess(); // 8 hardware buttons (profile-independent) access.b1.on("press", () => console.log("B1 pressed")); access.b3.on("change", ({ active }) => console.log(`B3: ${active}`)); // Center, PS, and profile cycle buttons access.center.on("press", () => console.log("Center pressed")); access.profile.on("press", () => console.log("Profile cycle")); // Analog stick access.stick.on("change", () => { console.log( `Stick: ${access.stick.x.state.toFixed(2)}, ${access.stick.y.state.toFixed(2)}`, ); }); access.stick.button.on("press", () => console.log("Stick clicked")); // Battery and profile access.battery.level.on("change", ({ state }) => { console.log(`Battery: ${Math.round(state * 100)}%`); }); access.profileId.on("change", ({ state }) => { console.log(`Active profile: ${state}`); // 1, 2, or 3 }); ``` ### LED control The Access controller has 4 independent LED systems: ```typescript import { AccessProfileLedMode, AccessPlayerIndicator } from "dualsense-ts"; // RGB lightbar (same API as DualSense) access.lightbar.set({ r: 255, g: 0, b: 128 }); // Profile LEDs — 3-segment arc access.profileLeds.set(AccessProfileLedMode.Sweep); access.profileLeds.set(AccessProfileLedMode.Fade); access.profileLeds.set(AccessProfileLedMode.Off); // Player indicator — 6-segment ring access.playerIndicator.set(AccessPlayerIndicator.Player1); access.playerIndicator.set(AccessPlayerIndicator.Off); // White status LED access.statusLed.set(true); // on access.statusLed.set(false); // off access.statusLed.toggle(); ``` ### Identity Firmware version, factory info (serial number, board revision, body color), and MAC address are loaded automatically after connection: ```typescript const fw = access.firmwareInfo; console.log( `FW: ${fw.mainFirmwareVersion.major}.${fw.mainFirmwareVersion.minor}.${fw.mainFirmwareVersion.patch}`, ); console.log(`Serial: ${access.factoryInfo.serialNumber}`); console.log(`Board: ${access.factoryInfo.boardRevision}`); ``` ### Browser usage In Node.js, `new DualsenseAccess()` auto-connects — no extra setup needed. In the browser, WebHID requires a one-time user gesture to grant device permission: ```typescript import { DualsenseAccess } from "dualsense-ts"; const access = new DualsenseAccess(); // Trigger WebHID permission dialog from a user gesture connectButton.onclick = () => access.hid.provider.getRequest?.(); ``` Once permission is granted, the controller connects automatically and reconnects on disconnect - same as the standard DualSense. ### Important differences from DualSense | | DualSense | DualSense Access | | -------- | -------------------------------------- | ---------------------------------------------------- | | Class | `Dualsense` | `DualsenseAccess` | | Buttons | Face buttons, D-pad, bumpers, triggers | B1B8, center, PS, profile | | Analog | 2 sticks, 2 triggers | 1 stick | | Motion | Gyroscope + accelerometer | None | | Touchpad | Yes | No | | Audio | Speaker, mic, headphone jack | No | | Haptics | Rumble + adaptive triggers | No | | LEDs | Lightbar, player LEDs, mute LED | Lightbar, profile LEDs, player indicator, status LED | | Profiles | N/A | 3 hardware profiles (read-only) | ## Known Issues ### Audio streaming requires USB The DualSense registers as a USB Audio Class device only over USB. Over Bluetooth, there is no audio transport. Audio controls (volume, routing, muting) work over both USB and Bluetooth, but they only affect audio playback over USB. ### Linux - headphone audio plays in one ear only PulseAudio defaults to the mono "Speaker" profile when the DualSense is connected. This sends a single audio channel, which the controller routes to the right side only. To get stereo headphone output, switch to the headphones profile: ```bash # Switch to stereo headphone profile pactl set-card-profile alsa_card.usb-Sony_Interactive_Entertainment_Wireless_Controller-00 "HiFi (Headphones, Mic)" # Set it as the default output and adjust volume pactl set-default-sink alsa_output.usb-Sony_Interactive_Entertainment_Wireless_Controller-00.HiFi__Headphones__sink pactl set-sink-volume alsa_output.usb-Sony_Interactive_Entertainment_Wireless_Controller-00.HiFi__Headphones__sink 100% ``` ### Linux - can't use multiple controllers over Bluetooth Identical Bluetooth devices are not given separate HID interfaces under some circumstances. You may still use multiple USB-connected controllers plus one Bluetooth controller. ### Linux - DualSense Access Bluetooth requires BlueZ configuration The Access controller pairs over Bluetooth but does not bond in the way BlueZ expects by default. BlueZ's input plugin rejects the HID connection with `hidp_add_connection() Rejected connection from !bonded device`. To fix this: 1. Edit `/etc/bluetooth/input.conf` and set `ClassicBondedOnly=false` under `[General]`: ```ini [General] ClassicBondedOnly=false ``` 2. Load the `hidp` kernel module if it isn't already loaded: ```bash sudo modprobe hidp ``` 3. Restart the Bluetooth service: ```bash sudo systemctl restart bluetooth ``` This is a BlueZ-specific issue — Windows and macOS handle pairing seamlessly. The standard DualSense is not affected. ## Migration Guide `dualsense-ts` uses semantic versioning. For more info on breaking changes, [check out the migration guide](MIGRATION_GUIDE.md). ## Credits - [CamTosh](https://github.com/CamTosh)'s [node-dualsense](https://github.com/CamTosh/node-dualsense) - DualSense HID protocol reference - [flok](https://github.com/flok)'s [pydualsense](https://github.com/flok/pydualsense) - DualSense HID protocol reference - [nondebug](https://github.com/nondebug)'s [dualsense reference](https://github.com/nondebug/dualsense) - DualSense WebHID reference - [daidr](https://github.com/daidr)'s [dualsense-tester](https://github.com/daidr/dualsense-tester) - DualSense firmware/factory info reference - [nowrep](https://github.com/nowrep)'s [dualsensectl](https://github.com/nowrep/dualsensectl) - DualSense firmware info reference - [chronovore](https://sr.ht/~chronovore)'s [titania](https://sr.ht/~chronovore/titania/) - DualSense Access HID protocol reference - [jfedor](https://github.com/jfedor2)'s [PS Access Profiles](https://www.jfedor.org/ps-access/) - DualSense Access profile format reference - [Contributors to `dualsense-ts` on Github](https://github.com/nsfm/dualsense-ts/graphs/contributors)