UNPKG

dualsense-ts

Version:

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

418 lines โ€ข 19.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Dualsense = void 0; const elements_1 = require("./elements"); const input_1 = require("./input"); const hid_1 = require("./hid"); const motion_1 = require("./motion"); /** Represents a Dualsense controller */ class Dualsense extends input_1.Input { /** * Firmware and hardware information. * Contains sensible defaults until the device reports its actual values. */ get firmwareInfo() { return this.hid.firmwareInfo; } /** * Factory information (serial number, body color, board revision). * Contains sensible defaults until the device reports its actual values. */ get factoryInfo() { return this.hid.factoryInfo; } /** * IMU calibration factors derived from the controller's factory calibration * data (Feature Report 0x05). Applied automatically to gyroscope and * accelerometer readings โ€” exposed here for inspection and diagnostics. */ get calibration() { return this.hid.calibration; } /** True if any input at all is active or changing */ get active() { return Object.values(this).some((input) => input !== this && input instanceof input_1.Input && input.active); } /** Returns `true` if the controller is connected via Bluetooth */ get wireless() { return this.hid.wireless; } /** Body color of the controller */ get color() { const { colorCode } = this.hid.factoryInfo; if (colorCode in hid_1.DualsenseColorMap) return hid_1.DualsenseColorMap[colorCode]; return hid_1.DualsenseColor.Unknown; } /** Factory-stamped serial number of the controller */ get serialNumber() { return this.hid.factoryInfo.serialNumber; } constructor(params = {}) { super(params); this.state = this; /** The RGB light bar at the top of the controller */ this.lightbar = new elements_1.Lightbar(); /** The 5 white player indicator LEDs */ this.playerLeds = new elements_1.PlayerLeds(); /** Audio volume, routing, and microphone controls */ this.audio = new elements_1.Audio(); /** Per-subsystem power save controls (disable touch, motion, haptics, audio) */ this.powerSave = new elements_1.PowerSaveControl(); /** Monotonic sensor timestamp in microseconds from the controller's clock. * Updated with each input report โ€” useful for correlating motion sensor * readings with other inputs across frames. Wraps at 2^32 (~71.6 minutes). */ this.sensorTimestamp = 0; /** Previous sensor timestamp for computing dt (microseconds). */ this.prevSensorTimestamp = 0; /** Active interval timers, cleared on dispose */ this.timers = []; /** * Buffered battery reading, sampled on a slow cadence * Battery readings are prone to flip-flopping, so we buffer them */ this.pendingBattery = { peakLevel: 0, status: hid_1.ChargeStatus.Discharging, }; this.ps = new elements_1.Momentary({ icon: "ใŽฐ", name: "Home", ...(params.ps ?? {}), }); this.mute = new elements_1.Mute({ icon: "๐Ÿ•ฉ", name: "Mute", ...(params.mute ?? {}), }); this.microphone = new elements_1.Momentary({ icon: "๐ŸŽค", name: "Microphone" }); this.headphone = new elements_1.Momentary({ icon: "๐ŸŽง", name: "Headphone" }); this.options = new elements_1.Momentary({ icon: "โ‹ฏ", name: "Options", ...(params.options ?? {}), }); this.create = new elements_1.Momentary({ icon: "๐Ÿ–‰", name: "Create", ...(params.create ?? {}), }); this.triangle = new elements_1.Momentary({ icon: "๐ŸŸ•", name: "Triangle", ...(params.triangle ?? {}), }); this.circle = new elements_1.Momentary({ icon: "โŠš", name: "Circle", ...(params.circle ?? {}), }); this.cross = new elements_1.Momentary({ icon: "โฎฟ", name: "Cross", ...(params.cross ?? {}), }); this.square = new elements_1.Momentary({ icon: "๐ŸŸ—", name: "Square", ...(params.square ?? {}), }); this.dpad = new elements_1.Dpad({ icon: "D", name: "D-pad", ...(params.dpad ?? {}), }); this.left = new elements_1.Unisense({ icon: "L", name: "Left", ...(params.left ?? {}), }); this.right = new elements_1.Unisense({ icon: "R", name: "Right", ...(params.right ?? {}), }); this.touchpad = new elements_1.Touchpad({ icon: "โŽš", name: "Touchpad", ...(params.touchpad ?? {}), }); this.connection = new elements_1.Momentary({ icon: "๐Ÿ”—", name: "Connected", ...(params.connection ?? {}), }); this.gyroscope = new elements_1.Gyroscope({ icon: "โˆž", name: "Gyroscope", threshold: 0.01, ...(params.gyroscope ?? {}), }); this.accelerometer = new elements_1.Accelerometer({ icon: "โคฒ", name: "Accelerometer", threshold: 0.01, ...(params.accelerometer ?? {}), }); this.battery = new elements_1.Battery({ icon: "๐Ÿ”‹", name: "Battery", ...(params.battery ?? {}), }); this.orientation = new motion_1.Orientation(params.orientation); this.shake = new motion_1.ShakeDetector(params.shake); this.connection[input_1.InputSet](false); // If a HID instance was supplied externally (e.g. by DualsenseManager), // the owner is responsible for driving discovery + reconnection. // `hid: null` creates a headless instance with no provider โ€” useful for // placeholder controllers in UIs where WebHID may not be available. // Otherwise, construct a default platform provider and run our own // discovery loop. const externallyManaged = params.hid !== undefined; this.hid = params.hid === null ? new hid_1.DualsenseHID(new hid_1.NullHIDProvider()) : (params.hid ?? new hid_1.DualsenseHID(new hid_1.PlatformHIDProvider())); this.hid.register((state) => { this.processHID(state); }); const rumbleMemo = { left: -1, right: -1 }; const triggerFeedbackMemo = { left: "", right: "" }; const lightbarMemo = { key: "" }; const playerLedsMemo = { key: "" }; const muteLedMemo = { mode: undefined }; // Seed audio memo with the initial state so we don't override the // controller's firmware-managed routing on connect. Audio commands // are only sent once the user explicitly changes a setting. const initialAudioKey = this.audio.toKey(); const audioMemo = { key: initialAudioKey, userChanged: false }; const powerSaveMemo = { key: "" }; // Mirror transport-level connect/disconnect into the connection Momentary, // and invalidate output memos on rising-edge connect so the output loop // re-pushes desired state to the new device. this.hid.onConnectionChange((connected) => { this.connection[input_1.InputSet](connected); if (connected) { triggerFeedbackMemo.left = ""; triggerFeedbackMemo.right = ""; lightbarMemo.key = ""; playerLedsMemo.key = ""; muteLedMemo.mode = undefined; powerSaveMemo.key = ""; if (audioMemo.userChanged) audioMemo.key = ""; } // Reset motion helpers on both connect and disconnect so // orientation doesn't carry stale state across sessions. this.orientation.reset(); this.shake.reset(); this.prevSensorTimestamp = 0; }); // Seed the initial state in case the provider was already attached. this.connection[input_1.InputSet](this.hid.provider.connected); // Standalone mode: poll for devices and reconnect on drop. In managed // mode the manager owns this and we must NOT race with it. if (!externallyManaged) { this.timers.push(setInterval(() => { if (!this.hid.provider.connected) { void Promise.resolve(this.hid.provider.connect()).catch(() => { /* surfaced via onError */ }); } }, 200)); } /** Refresh battery state on a slow cadence to filter transient glitches */ this.timers.push(setInterval(() => { if (!this.connection.active) return; this.battery.level[input_1.InputSet](this.pendingBattery.peakLevel); this.battery.status[input_1.InputSet](this.pendingBattery.status); this.pendingBattery.peakLevel = 0; }, 1000)); /** Refresh output state (rumble + trigger feedback) */ this.timers.push(setInterval(() => { if (!this.connection.active) return; const leftRumble = this.left.rumble(); const rightRumble = this.right.rumble(); // Always resend when rumble is active. The controller has an internal // keepalive timeout (~5s) that stops the motors if not refreshed. if (leftRumble > 0 || rightRumble > 0 || leftRumble !== rumbleMemo.left || rightRumble !== rumbleMemo.right) { this.hid.setRumble(leftRumble * 255, rightRumble * 255); rumbleMemo.left = leftRumble; rumbleMemo.right = rightRumble; } const leftFeedback = this.left.trigger.feedback; const rightFeedback = this.right.trigger.feedback; const leftKey = leftFeedback.toKey(); const rightKey = rightFeedback.toKey(); const feedbackChanged = leftKey !== triggerFeedbackMemo.left || rightKey !== triggerFeedbackMemo.right; if (feedbackChanged) { // Force rumble into the same batch so MotorPower scope bit is always present. this.hid.setRumble(leftRumble * 255, rightRumble * 255); rumbleMemo.left = leftRumble; rumbleMemo.right = rightRumble; } if (leftKey !== triggerFeedbackMemo.left) { this.hid.setLeftTriggerFeedback(leftFeedback.toBytes()); triggerFeedbackMemo.left = leftKey; } if (rightKey !== triggerFeedbackMemo.right) { this.hid.setRightTriggerFeedback(rightFeedback.toBytes()); triggerFeedbackMemo.right = rightKey; } const lightbarKey = this.lightbar.toKey(); if (lightbarKey !== lightbarMemo.key) { const { r, g, b } = this.lightbar.color; this.hid.setLightbar(r, g, b); lightbarMemo.key = lightbarKey; } const pulse = this.lightbar.consumePulse(); if (pulse !== hid_1.PulseOptions.Off) { this.hid.setLightbar(0, 0, 0, pulse); } const playerLedsKey = this.playerLeds.toKey(); if (playerLedsKey !== playerLedsMemo.key) { this.hid.setPlayerLeds(this.playerLeds.bitmask, this.playerLeds.brightness); playerLedsMemo.key = playerLedsKey; } const muteLedMode = this.mute.ledMode; if (muteLedMode !== muteLedMemo.mode) { if (muteLedMode !== undefined) { this.hid.setMicrophoneLED(muteLedMode); } muteLedMemo.mode = muteLedMode; } const audioKey = this.audio.toKey(); if (audioKey !== audioMemo.key) { audioMemo.userChanged = true; this.hid.setHeadphoneVolume(this.audio.headphoneVolumeRaw); this.hid.setSpeakerVolume(this.audio.speakerVolumeRaw); this.hid.setMicrophoneVolume(this.audio.microphoneVolumeRaw); this.hid.setAudioFlags(this.audio.audioFlags); this.hid.setSpeakerPreamp(this.audio.preampGain, this.audio.beamForming); audioMemo.key = audioKey; } // Power save byte 10 combines audio mute flags with subsystem disable flags. // Send when either source changes. const psKey = `${this.audio.powerSaveFlags}|${this.powerSave.toKey()}`; if (psKey !== powerSaveMemo.key) { this.hid.setPowerSave(this.audio.powerSaveFlags | this.powerSave.flags); powerSaveMemo.key = psKey; } }, 1000 / 30)); } /** Stop all internal timers and release resources. */ dispose() { this.timers.forEach((timer) => { clearInterval(timer); }); this.timers.length = 0; this.hid.dispose(); } /** Average rumble strength across both halves of the controller. */ get rumbleIntensity() { return (this.left.rumble() + this.right.rumble()) / 2; } /** Reset adaptive trigger feedback on both sides to the default linear feel */ resetTriggerFeedback() { this.left.trigger.feedback.reset(); this.right.trigger.feedback.reset(); } /** * Play a built-in test tone via the onboard DSP. * Works over both USB and Bluetooth. Call `stopTestTone()` to stop. * @param target Output destination โ€” "speaker" (default) or "headphone" * @param tone Which tone to play โ€” "1khz" (default), "100hz", or "both" */ async startTestTone(target = "speaker", tone = "1khz") { return this.hid.startTestTone(target, tone); } /** Stop the DSP test tone */ async stopTestTone() { return this.hid.stopTestTone(); } /** Check or adjust rumble intensity evenly across both sides of the controller */ rumble(intensity) { this.left.rumble(intensity); this.right.rumble(intensity); return this.rumbleIntensity; } /** Distributes HID event values to the controller's public inputs. */ processHID(state) { this.ps[input_1.InputSet](state[hid_1.InputId.Playstation]); this.options[input_1.InputSet](state[hid_1.InputId.Options]); this.create[input_1.InputSet](state[hid_1.InputId.Create]); this.mute[input_1.InputSet](state[hid_1.InputId.Mute]); this.mute.status[input_1.InputSet](state[hid_1.InputId.MuteLed]); this.microphone[input_1.InputSet](state[hid_1.InputId.Microphone]); this.headphone[input_1.InputSet](state[hid_1.InputId.Headphone]); this.triangle[input_1.InputSet](state[hid_1.InputId.Triangle]); this.circle[input_1.InputSet](state[hid_1.InputId.Circle]); this.cross[input_1.InputSet](state[hid_1.InputId.Cross]); this.square[input_1.InputSet](state[hid_1.InputId.Square]); this.dpad.up[input_1.InputSet](state[hid_1.InputId.Up]); this.dpad.down[input_1.InputSet](state[hid_1.InputId.Down]); this.dpad.right[input_1.InputSet](state[hid_1.InputId.Right]); this.dpad.left[input_1.InputSet](state[hid_1.InputId.Left]); this.touchpad.button[input_1.InputSet](state[hid_1.InputId.TouchButton]); this.touchpad.left.x[input_1.InputSet](state[hid_1.InputId.TouchX0]); this.touchpad.left.y[input_1.InputSet](state[hid_1.InputId.TouchY0]); this.touchpad.left.contact[input_1.InputSet](state[hid_1.InputId.TouchContact0]); this.touchpad.left.tracker[input_1.InputSet](state[hid_1.InputId.TouchId0]); this.touchpad.right.x[input_1.InputSet](state[hid_1.InputId.TouchX1]); this.touchpad.right.y[input_1.InputSet](state[hid_1.InputId.TouchY1]); this.touchpad.right.contact[input_1.InputSet](state[hid_1.InputId.TouchContact1]); this.touchpad.right.tracker[input_1.InputSet](state[hid_1.InputId.TouchId1]); this.left.analog.x[input_1.InputSet](state[hid_1.InputId.LeftAnalogX]); this.left.analog.y[input_1.InputSet](state[hid_1.InputId.LeftAnalogY]); this.left.analog.button[input_1.InputSet](state[hid_1.InputId.LeftAnalogButton]); this.left.bumper[input_1.InputSet](state[hid_1.InputId.LeftBumper]); this.left.trigger[input_1.InputSet](state[hid_1.InputId.LeftTrigger]); this.left.trigger.button[input_1.InputSet](state[hid_1.InputId.LeftTriggerButton]); this.right.analog.x[input_1.InputSet](state[hid_1.InputId.RightAnalogX]); this.right.analog.y[input_1.InputSet](state[hid_1.InputId.RightAnalogY]); this.right.analog.button[input_1.InputSet](state[hid_1.InputId.RightAnalogButton]); this.right.bumper[input_1.InputSet](state[hid_1.InputId.RightBumper]); this.right.trigger[input_1.InputSet](state[hid_1.InputId.RightTrigger]); this.right.trigger.button[input_1.InputSet](state[hid_1.InputId.RightTriggerButton]); this.sensorTimestamp = state[hid_1.InputId.SensorTimestamp]; this.gyroscope.x[input_1.InputSet](state[hid_1.InputId.GyroX]); this.gyroscope.y[input_1.InputSet](state[hid_1.InputId.GyroY]); this.gyroscope.z[input_1.InputSet](state[hid_1.InputId.GyroZ]); this.accelerometer.x[input_1.InputSet](state[hid_1.InputId.AccelX]); this.accelerometer.y[input_1.InputSet](state[hid_1.InputId.AccelY]); this.accelerometer.z[input_1.InputSet](state[hid_1.InputId.AccelZ]); // Compute dt from the controller's hardware clock (microseconds). // The timestamp wraps at 2^32 (~71.6 minutes). const ts = state[hid_1.InputId.SensorTimestamp]; if (this.prevSensorTimestamp !== 0 && ts !== 0) { const dtMicro = ts >= this.prevSensorTimestamp ? ts - this.prevSensorTimestamp : 0xFFFFFFFF - this.prevSensorTimestamp + ts + 1; const dt = dtMicro / 1000000; // Sanity check: skip if dt is unreasonable (>0.5s means we // probably missed reports or just reconnected). if (dt > 0 && dt < 0.5) { const gx = state[hid_1.InputId.GyroX]; const gy = state[hid_1.InputId.GyroY]; const gz = state[hid_1.InputId.GyroZ]; const ax = state[hid_1.InputId.AccelX]; const ay = state[hid_1.InputId.AccelY]; const az = state[hid_1.InputId.AccelZ]; this.orientation.update(gx, gy, gz, ax, ay, az, dt); this.shake.update(ax, ay, az, dt); } } this.prevSensorTimestamp = ts; const level = state[hid_1.InputId.BatteryLevel]; if (level > this.pendingBattery.peakLevel) { this.pendingBattery.peakLevel = level; } this.pendingBattery.status = state[hid_1.InputId.BatteryStatus]; } } exports.Dualsense = Dualsense; //# sourceMappingURL=dualsense.js.map