'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, batteryByte: 52, isDS4: false }; 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.type = "DS5"; } getInputConfig() { return DS5_INPUT_CONFIG; } async getInfo() { return await this._getInfo(false); } async _getInfo(is_edge) { // Device-only: collect info and return a common structure; do not touch the DOM try { const view = lf("ds5_info", await this.receiveFeatureReport(0x20)); 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: this.l("Serial Number"), value: serial_number, cat: "hw" }, { key: this.l("MCU Unique ID"), value: await this.getSystemInfo(1, 9, 9, false), cat: "hw", isExtra: true }, { key: this.l("PCBA ID"), value: reverse_str(await this.getSystemInfo(1, 17, 14)), cat: "hw", isExtra: true }, { key: this.l("Battery Barcode"), value: await this.getSystemInfo(1, 24, 23), cat: "hw", isExtra: true }, { key: this.l("VCM Left Barcode"), value: await this.getSystemInfo(1, 26, 16), cat: "hw", isExtra: true }, { key: this.l("VCM Right Barcode"), value: await this.getSystemInfo(1, 28, 16), cat: "hw", isExtra: true }, { key: this.l("Color"), value: this.l(color), cat: "hw", addInfoIcon: 'color' }, ...(is_edge ? [] : [{ key: this.l("Board Model"), value: this.hwToBoardModel(hwinfo), cat: "hw", addInfoIcon: 'board' }]), { key: this.l("FW Build Date"), value: build_date + " " + build_time, cat: "fw" }, { key: this.l("FW Type"), value: "0x" + dec2hex(fwtype), cat: "fw", isExtra: true }, { key: this.l("FW Series"), value: "0x" + dec2hex(swseries), cat: "fw", isExtra: true }, { key: this.l("HW Model"), value: "0x" + dec2hex32(hwinfo), cat: "hw", isExtra: true }, { key: this.l("FW Version"), value: "0x" + dec2hex32(fwversion), cat: "fw" }, { key: this.l("FW Update"), value: "0x" + dec2hex(updversion), cat: "fw" }, { key: this.l("FW Update Info"), value: "0x" + dec2hex8(unk), cat: "fw", isExtra: true }, { key: this.l("SBL FW Version"), value: "0x" + dec2hex32(fwversion1), cat: "fw", isExtra: true }, { key: this.l("Venom FW Version"), value: "0x" + dec2hex32(fwversion2), cat: "fw", isExtra: true }, { key: this.l("Spider FW Version"), value: "0x" + dec2hex32(fwversion3), cat: "fw", isExtra: true }, { key: this.l("Touchpad ID"), value: await this.getSystemInfo(5, 2, 8, false), cat: "hw", isExtra: true }, { key: this.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: this.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(e) { la("ds5_info_error", {"r": e}) return { ok: false, error: e, 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: ") + String(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(e) { return { ok: false, error: e }; } } async nvsUnlock() { la("ds5_nvunlock"); try { await this.sendFeatureReport(0x80, [3,2, 101, 50, 64, 12]); const data = await this.receiveFeatureReport(0x81); } catch(e) { await sleep(500); throw new Error(this.l("NVS Unlock failed: ") + e); } } 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"); } else { if(decode) return new TextDecoder().decode(pcba_id.buffer.slice(4, 4+length)); else return buf2hex(pcba_id.buffer.slice(4, 4+length)); } return this.l("Unknown"); } 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}); return { ok: false, code: 1, d1 }; } return { ok: true }; } catch(e) { la("ds5_calibrate_sticks_begin_failed", {"r": e}); return { ok: false, error: String(e) }; } } 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}); return { ok: false, code: 2, d1 }; } return { ok: true }; } catch(e) { la("ds5_calibrate_sticks_sample_failed", {"r": e}); return { ok: false, error: String(e) }; } } async calibrateSticksEnd() { la("ds5_calibrate_sticks_end"); try { // Write await this.sendFeatureReport(0x82, [2,1,1]); let 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}); return { ok: false, code: 3, d1 }; } return { ok: true }; } catch(e) { la("ds5_calibrate_sticks_end_failed", {"r": e}); return { ok: false, error: String(e) }; } } 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}); return { ok: false, code: 1, d1 }; } return { ok: true }; } catch(e) { la("ds5_calibrate_range_begin_failed", {"r": e}); return { ok: false, error: String(e) }; } } async calibrateRangeEnd() { la("ds5_calibrate_range_end"); try { // Write await this.sendFeatureReport(0x82, [2,1,2]); // Assert let 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}); return { ok: false, code: 3, d1 }; } return { ok: true }; } catch(e) { la("ds5_calibrate_range_end_failed", {"r": e}); return { ok: false, error: String(e) }; } } 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 (e) { return { device: 'ds5', status: 'error', locked: null, code: 2, error: e }; } } 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 = data.getUint8(1, true); const p2 = data.getUint8(2, true); const p3 = data.getUint8(3, 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); } } export default DS5Controller;