Files
dualshock-tools.github.io/js/controllers/ds5-controller.js
Mathias Malmqvist be8e151212 Update e to error
2025-09-25 00:12:25 +02:00

434 lines
14 KiB
JavaScript

'use strict';
import BaseController from './base-controller.js';
import {
sleep,
buf2hex,
dec2hex,
dec2hex32,
dec2hex8,
format_mac_from_view,
reverse_str,
la,
lf
} from '../utils.js';
// DS5 Button mapping configuration
const DS5_BUTTON_MAP = [
{ name: 'up', byte: 7, mask: 0x0 }, // Dpad handled separately
{ name: 'right', byte: 7, mask: 0x1 },
{ name: 'down', byte: 7, mask: 0x2 },
{ name: 'left', byte: 7, mask: 0x3 },
{ name: 'square', byte: 7, mask: 0x10, svg: 'Square' },
{ name: 'cross', byte: 7, mask: 0x20, svg: 'Cross' },
{ name: 'circle', byte: 7, mask: 0x40, svg: 'Circle' },
{ name: 'triangle', byte: 7, mask: 0x80, svg: 'Triangle' },
{ name: 'l1', byte: 8, mask: 0x01, svg: 'L1' },
{ name: 'l2', byte: 4, mask: 0xff }, // analog handled separately
{ name: 'r1', byte: 8, mask: 0x02, svg: 'R1' },
{ name: 'r2', byte: 5, mask: 0xff }, // analog handled separately
{ name: 'create', byte: 8, mask: 0x10, svg: 'Create' },
{ name: 'options', byte: 8, mask: 0x20, svg: 'Options' },
{ name: 'l3', byte: 8, mask: 0x40, svg: 'L3' },
{ name: 'r3', byte: 8, mask: 0x80, svg: 'R3' },
{ name: 'ps', byte: 9, mask: 0x01, svg: 'PS' },
{ name: 'touchpad', byte: 9, mask: 0x02, svg: 'Trackpad' },
{ name: 'mute', byte: 9, mask: 0x04, svg: 'Mute' },
];
// DS5 Input processing configuration
const DS5_INPUT_CONFIG = {
buttonMap: DS5_BUTTON_MAP,
dpadByte: 7,
l2AnalogByte: 4,
r2AnalogByte: 5,
touchpadOffset: 32,
};
function ds5_color(x) {
const colorMap = {
'00': 'White',
'01': 'Midnight Black',
'02': 'Cosmic Red',
'03': 'Nova Pink',
'04': 'Galactic Purple',
'05': 'Starlight Blue',
'06': 'Grey Camouflage',
'07': 'Volcanic Red',
'08': 'Sterling Silver',
'09': 'Cobalt Blue',
'10': 'Chroma Teal',
'11': 'Chroma Indigo',
'12': 'Chroma Pearl',
'30': '30th Anniversary',
'Z1': 'God of War Ragnarok',
'Z2': 'Spider-Man 2',
'Z3': 'Astro Bot',
'Z4': 'Fortnite',
'Z6': 'The Last of Us',
};
const colorCode = x.slice(4, 6);
const colorName = colorMap[colorCode] || 'Unknown';
return colorName;
}
/**
* DualSense (DS5) Controller implementation
*/
class DS5Controller extends BaseController {
constructor(device, uiDependencies = {}) {
super(device, uiDependencies);
this.model = "DS5";
this.finetuneMaxValue = 65535; // 16-bit max value for DS5
}
getInputConfig() {
return DS5_INPUT_CONFIG;
}
async getInfo() {
return this._getInfo(false);
}
async _getInfo(is_edge) {
const { l } = this;
// Device-only: collect info and return a common structure; do not touch the DOM
try {
console.log("Fetching DS5 info...");
const view = lf("ds5_info", await this.receiveFeatureReport(0x20));
console.log("Got DS5 info report:", buf2hex(view.buffer));
const cmd = view.getUint8(0, true);
if(cmd != 0x20 || view.buffer.byteLength != 64)
return { ok: false, error: new Error("Invalid response for ds5_info") };
const build_date = new TextDecoder().decode(view.buffer.slice(1, 1+11));
const build_time = new TextDecoder().decode(view.buffer.slice(12, 20));
const fwtype = view.getUint16(20, true);
const swseries = view.getUint16(22, true);
const hwinfo = view.getUint32(24, true);
const fwversion = view.getUint32(28, true);
const updversion = view.getUint16(44, true);
const unk = view.getUint8(46, true);
const fwversion1 = view.getUint32(48, true);
const fwversion2 = view.getUint32(52, true);
const fwversion3 = view.getUint32(56, true);
const serial_number = await this.getSystemInfo(1, 19, 17);
const color = ds5_color(serial_number);
const infoItems = [
{ key: l("Serial Number"), value: serial_number, cat: "hw" },
{ key: l("MCU Unique ID"), value: await this.getSystemInfo(1, 9, 9, false), cat: "hw", isExtra: true },
{ key: l("PCBA ID"), value: reverse_str(await this.getSystemInfo(1, 17, 14)), cat: "hw", isExtra: true },
{ key: l("Battery Barcode"), value: await this.getSystemInfo(1, 24, 23), cat: "hw", isExtra: true },
{ key: l("VCM Left Barcode"), value: await this.getSystemInfo(1, 26, 16), cat: "hw", isExtra: true },
{ key: l("VCM Right Barcode"), value: await this.getSystemInfo(1, 28, 16), cat: "hw", isExtra: true },
{ key: l("Color"), value: l(color), cat: "hw", addInfoIcon: 'color' },
...(is_edge ? [] : [{ key: l("Board Model"), value: this.hwToBoardModel(hwinfo), cat: "hw", addInfoIcon: 'board' }]),
{ key: l("FW Build Date"), value: build_date + " " + build_time, cat: "fw" },
{ key: l("FW Type"), value: "0x" + dec2hex(fwtype), cat: "fw", isExtra: true },
{ key: l("FW Series"), value: "0x" + dec2hex(swseries), cat: "fw", isExtra: true },
{ key: l("HW Model"), value: "0x" + dec2hex32(hwinfo), cat: "hw", isExtra: true },
{ key: l("FW Version"), value: "0x" + dec2hex32(fwversion), cat: "fw" },
{ key: l("FW Update"), value: "0x" + dec2hex(updversion), cat: "fw" },
{ key: l("FW Update Info"), value: "0x" + dec2hex8(unk), cat: "fw", isExtra: true },
{ key: l("SBL FW Version"), value: "0x" + dec2hex32(fwversion1), cat: "fw", isExtra: true },
{ key: l("Venom FW Version"), value: "0x" + dec2hex32(fwversion2), cat: "fw", isExtra: true },
{ key: l("Spider FW Version"), value: "0x" + dec2hex32(fwversion3), cat: "fw", isExtra: true },
{ key: l("Touchpad ID"), value: await this.getSystemInfo(5, 2, 8, false), cat: "hw", isExtra: true },
{ key: l("Touchpad FW Version"), value: await this.getSystemInfo(5, 4, 8, false), cat: "fw", isExtra: true },
];
const old_controller = build_date.search(/ 2020| 2021/);
let disable_bits = 0;
if(old_controller != -1) {
la("ds5_info_error", {"r": "old"})
disable_bits |= 2; // 2: outdated firmware
}
const nv = await this.queryNvStatus();
const bd_addr = await this.getBdAddr();
infoItems.push({ key: l("Bluetooth Address"), value: bd_addr, cat: "hw" });
const pending_reboot = (nv?.status === 'pending_reboot');
return { ok: true, infoItems, nv, disable_bits, pending_reboot };
} catch(error) {
la("ds5_info_error", {"r": error})
return { ok: false, error, disable_bits: 1 };
}
}
async flash(progressCallback = null) {
la("ds5_flash");
try {
await this.nvsUnlock();
const lockRes = await this.nvsLock();
if(!lockRes.ok) throw (lockRes.error || new Error("NVS lock failed"));
return { success: true, message: this.l("Changes saved successfully") };
} catch(error) {
throw new Error(this.l("Error while saving changes"), { cause: error });
}
}
async reset() {
la("ds5_reset");
try {
await this.sendFeatureReport(0x80, [1,1]);
} catch(error) {
}
}
async nvsLock() {
la("ds5_nvlock");
try {
await this.sendFeatureReport(0x80, [3,1]);
await this.receiveFeatureReport(0x81);
return { ok: true };
} catch(error) {
return { ok: false, error };
}
}
async nvsUnlock() {
la("ds5_nvunlock");
try {
await this.sendFeatureReport(0x80, [3,2, 101, 50, 64, 12]);
const data = await this.receiveFeatureReport(0x81);
} catch(error) {
await sleep(500);
throw new Error(this.l("NVS Unlock failed"), { cause: error });
}
}
async getBdAddr() {
await this.sendFeatureReport(0x80, [9,2]);
const data = lf("ds5_getbdaddr", await this.receiveFeatureReport(0x81));
return format_mac_from_view(data, 4);
}
async getSystemInfo(base, num, length, decode = true) {
await this.sendFeatureReport(128, [base,num])
const pcba_id = lf("ds5_pcba_id", await this.receiveFeatureReport(129));
if(pcba_id.getUint8(1) != base || pcba_id.getUint8(2) != num || pcba_id.getUint8(3) != 2) {
return this.l("error");
}
if(decode)
return new TextDecoder().decode(pcba_id.buffer.slice(4, 4+length));
return buf2hex(pcba_id.buffer.slice(4, 4+length));
}
async calibrateSticksBegin() {
la("ds5_calibrate_sticks_begin");
try {
// Begin
await this.sendFeatureReport(0x82, [1,1,1]);
// Assert
const data = await this.receiveFeatureReport(0x83);
if(data.getUint32(0, false) != 0x83010101) {
const d1 = dec2hex32(data.getUint32(0, false));
la("ds5_calibrate_sticks_begin_failed", {"d1": d1});
throw new Error(`Stick center calibration begin failed: ${d1}`);
}
return { ok: true };
} catch(error) {
la("ds5_calibrate_sticks_begin_failed", {"r": error});
return { ok: false, error };
}
}
async calibrateSticksSample() {
la("ds5_calibrate_sticks_sample");
try {
// Sample
await this.sendFeatureReport(0x82, [3,1,1]);
// Assert
const data = await this.receiveFeatureReport(0x83);
if(data.getUint32(0, false) != 0x83010101) {
const d1 = dec2hex32(data.getUint32(0, false));
la("ds5_calibrate_sticks_sample_failed", {"d1": d1});
throw new Error(`Stick center calibration sample failed: ${d1}`);
}
return { ok: true };
} catch(error) {
la("ds5_calibrate_sticks_sample_failed", {"r": error});
return { ok: false, error };
}
}
async calibrateSticksEnd() {
la("ds5_calibrate_sticks_end");
try {
// Write
await this.sendFeatureReport(0x82, [2,1,1]);
const data = await this.receiveFeatureReport(0x83);
if(data.getUint32(0, false) != 0x83010102) {
const d1 = dec2hex32(data.getUint32(0, false));
la("ds5_calibrate_sticks_failed", {"s": 3, "d1": d1});
throw new Error(`Stick center calibration end failed: ${d1}`);
}
return { ok: true };
} catch(error) {
la("ds5_calibrate_sticks_end_failed", {"r": error});
return { ok: false, error };
}
}
async calibrateRangeBegin() {
la("ds5_calibrate_range_begin");
try {
// Begin
await this.sendFeatureReport(0x82, [1,1,2]);
// Assert
const data = await this.receiveFeatureReport(0x83);
if(data.getUint32(0, false) != 0x83010201) {
const d1 = dec2hex32(data.getUint32(0, false));
la("ds5_calibrate_range_begin_failed", {"d1": d1});
throw new Error(`Stick range calibration begin failed: ${d1}`);
}
return { ok: true };
} catch(error) {
la("ds5_calibrate_range_begin_failed", {"r": error});
return { ok: false, error };
}
}
async calibrateRangeEnd() {
la("ds5_calibrate_range_end");
try {
// Write
await this.sendFeatureReport(0x82, [2,1,2]);
// Assert
const data = await this.receiveFeatureReport(0x83);
if(data.getUint32(0, false) != 0x83010202) {
const d1 = dec2hex32(data.getUint32(0, false));
la("ds5_calibrate_range_end_failed", {"d1": d1});
throw new Error(`Stick range calibration end failed: ${d1}`);
}
return { ok: true };
} catch(error) {
la("ds5_calibrate_range_end_failed", {"r": error});
return { ok: false, error };
}
}
async queryNvStatus() {
try {
await this.sendFeatureReport(0x80, [3,3]);
const data = lf("ds5_nvstatus", await this.receiveFeatureReport(0x81));
const ret = data.getUint32(1, false);
if (ret === 0x15010100) {
return { device: 'ds5', status: 'pending_reboot', locked: null, code: 4, raw: ret };
}
if (ret === 0x03030201) {
return { device: 'ds5', status: 'locked', locked: true, mode: 'temporary', code: 1, raw: ret };
}
if (ret === 0x03030200) {
return { device: 'ds5', status: 'unlocked', locked: false, mode: 'permanent', code: 0, raw: ret };
}
if (ret === 1 || ret === 2) {
return { device: 'ds5', status: 'unknown', locked: null, code: 2, raw: ret };
}
return { device: 'ds5', status: 'unknown', locked: null, code: ret, raw: ret };
} catch (error) {
return { device: 'ds5', status: 'error', locked: null, code: 2, error };
}
}
hwToBoardModel(hw_ver) {
const a = (hw_ver >> 8) & 0xff;
if(a == 0x03) {
return "BDM-010";
} else if(a == 0x04) {
return "BDM-020";
} else if(a == 0x05) {
return "BDM-030";
} else if(a == 0x06) {
return "BDM-040";
} else if(a == 0x07 || a == 0x08) {
return "BDM-050";
} else {
return this.l("Unknown");
}
}
async getInMemoryModuleData() {
// DualSense
await this.sendFeatureReport(0x80, [12, 2]);
await sleep(100);
const data = await this.receiveFeatureReport(0x81);
const cmd = data.getUint8(0, true);
const [p1, p2, p3] = [1, 2, 3].map(i => data.getUint8(i, true));
if(cmd != 129 || p1 != 12 || (p2 != 2 && p2 != 4) || p3 != 2)
return null;
return Array.from({ length: 12 }, (_, i) => data.getUint16(4 + i * 2, true));
}
async writeFinetuneData(data) {
const pkg = data.reduce((acc, val) => acc.concat([val & 0xff, val >> 8]), [12, 1]);
await this.sendFeatureReport(0x80, pkg);
}
/**
* Parse DS5 battery status from input data
*/
parseBatteryStatus(data) {
const bat = data.getUint8(52); // DS5 battery byte is at position 52
// DS5: bat_charge = low 4 bits, bat_status = high 4 bits
const bat_charge = bat & 0x0f;
const bat_status = bat >> 4;
let bat_capacity = 0;
let cable_connected = false;
let is_charging = false;
let is_error = false;
switch (bat_status) {
case 0:
// On battery power
bat_capacity = Math.min(bat_charge * 10 + 5, 100);
break;
case 1:
// Charging
bat_capacity = Math.min(bat_charge * 10 + 5, 100);
is_charging = true;
cable_connected = true;
break;
case 2:
// Fully charged
bat_capacity = 100;
cable_connected = true;
break;
default:
// Error state
is_error = true;
break;
}
return { bat_capacity, cable_connected, is_charging, is_error };
}
}
export default DS5Controller;