Repository: dualshock-tools/ds4-tools Branch: master Commit: 94e8d94d0b7d Files: 7 Total size: 29.6 KB Directory structure: gitextract_nr2qt5it/ ├── .gitignore ├── LICENSE.txt ├── README.md ├── ds4-calibration-tool.py ├── ds4-tool.py ├── ds5-calibration-tool.py └── requirements.txt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .*.sw? venv ================================================ FILE: LICENSE.txt ================================================ MIT License Copyright (c) 2024 the_al Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # ds4-tools This repo contains some Python scripts I use to play and reverse-engineer the DualShock 4 controller. ## Controller calibration (center and range) Would you like to calibrate your controller? No need to waste time with these scripts. I've developed a website that allows you to calibrate your controller without installing anything on your computer; you just need Google Chrome. Give it a try! Link: [DualShock Calibration GUI](https://dualshock-tools.github.io). ## Warning Use these files at your own risk and be ready to throw away your controller because it could stop working. They have been tested on **only two DS4** on planet Earth, so any slight change of your DS4 w.r.t mine can lead to bricking it. ## Contents - `ds4-tool.py` can be used to play with undocumented commands of your DualShock 4 - `ds4-calibration-tool.py` can be used to calibrate analog sticks or triggers. It has a nice TUI. ## How to use them 1. Clone the repo and go into the directory ``` $ git clone $ cd ds4-tools ``` 2. Install dependencies ``` $ virtualenv venv $ . venv/bin/activate $ pip install -r requirements.txt ``` 3. Play with the scripts ``` $ python3 script.py ``` ## Example ``` $ python3 ds4-tool.py info [+] Waiting for device VendorId=054c ProductId=09cc Compiled at: Sep 21 2018 04:50:51 hw_ver:0100.b400 sw_ver:00000001.a00a sw_series:2010 code size:0002a000 ``` ## DualShock4 Calibration If you are here, there are good probabilities you want to recalibrate your DS4. In that case, the script for you is `ds4-calibration-tool.py`. The DS4 by default will undo changes after a reset or after it goes in standby. This is good to test calibration and see if the result is good enough for you without messing everything up. At some point you may want to make changes permanent. To do that, you should change the flash-mirror status using ds4-tool. I suggest to switch back to temporary right after the calibration is done. Here it follows an example: ``` # 1. Know if changes are temporary or permanent (0: permanent; 1: temporary) $ ./ds4-tool.py get-flash-mirror-status # 2. Change flash mirror behavior to permanent $ ./ds4-tool.py set-flash-mirror-status 0 # 3. Do calibration here $ ./ds4-calibration-tool.py # 4. Change flash mirror behavior back to temporary $ ./ds4-tool.py set-flash-mirror-status 1 ``` ## DualSense Calibration The script `ds5-calibration-tool.py` is an experimental script to calibrate your DualSense. Experimental means: * Tested only on my DualSense with an old firmware ("Dec 16 2022 02:44:31") * May brick your controller * Be aware that it may behave differently with a future firmware update The command-line interface is different than the DS4 tool, sorry for this. With this script, the action is passed by parameter, so that it can be called by other scripts. The script can be used in two ways: * Calibrate center: `./ds5-calibration-tool.py analog-center` * Calibrate range: `./ds5-calibration-tool.py analog-range` In this way you can try the script, but the changes are gone after a reset. To calibrate and store the changes permanently, use the parameter `-p`: * Calibrate center: `./ds5-calibration-tool.py -p analog-center` * Calibrate range: `./ds5-calibration-tool.py -p analog-range` Let me know if this works. ## Notes for Windows The tools won't detect your DualShock 4 until you change default driver to the libusb one. The easiest way to do this is to use the [Zadig](https://zadig.akeo.ie/ "Zadig's Homepage") software. 1. Download and run Zadig 2. Open `Options` menu and check `List All Devices` item ![zadig_setup.png](img/zadig_setup.png) 3. Select your DualShock 4 from list and change the driver to libusb-win32 one * `Wireless Controller` [054c:05c4] for the 1st revision ![zadig_ds4r1.png](img/zadig_ds4r1.png) * `Wireless Controller (Interface 0)` [054c:09cc:00] for the 2nd revision ![zadig_ds4r2.png](img/zadig_ds4r2.png) 4. Press `Replace Driver` button and agree with every other question (if any) ## Notes for Mac OS X If you get `usb.core.NoBackendError: No backend available` error, you should install `libusb`. Using Homebrew you can install all required tools with this command: ``` $ brew install git python virtualenv libusb ``` ================================================ FILE: ds4-calibration-tool.py ================================================ #!/usr/bin/env python3 import usb.core import usb.util import array import struct import sys import binascii import time from construct import * dev = None VALID_DEVICE_IDS = [ (0x054c, 0x05c4), (0x054c, 0x09cc) ] def wait_for_device(): global dev print("Waiting for a DualShock 4...") while True: for i in VALID_DEVICE_IDS: dev = usb.core.find(idVendor=i[0], idProduct=i[1]) if dev is not None: print("Found a DualShock 4: vendorId=%04x productId=%04x" % (i[0], i[1])) return time.sleep(1) class HID_REQ: DEV_TO_HOST = usb.util.build_request_type( usb.util.CTRL_IN, usb.util.CTRL_TYPE_CLASS, usb.util.CTRL_RECIPIENT_INTERFACE) HOST_TO_DEV = usb.util.build_request_type( usb.util.CTRL_OUT, usb.util.CTRL_TYPE_CLASS, usb.util.CTRL_RECIPIENT_INTERFACE) GET_REPORT = 0x01 SET_REPORT = 0x09 def hid_get_report(dev, report_id, size): assert isinstance(size, int), 'get_report size must be integer' assert report_id <= 0xff, 'only support report_type == 0' return dev.ctrl_transfer(HID_REQ.DEV_TO_HOST, HID_REQ.GET_REPORT, report_id, 0, size + 1)[1:].tobytes() def hid_set_report(dev, report_id, buf): assert isinstance(buf, (bytes, array.array) ), 'set_report buf must be buffer' assert report_id <= 0xff, 'only support report_type == 0' buf = struct.pack('B', report_id) + buf return dev.ctrl_transfer(HID_REQ.HOST_TO_DEV, HID_REQ.SET_REPORT, (3 << 8) | report_id, 0, buf) def dump_93_data(): data = hid_get_report(dev, 0x93, 13) assert len(data) == 13 deviceId, targetId, numChunks, curChunk, dataLen = struct.unpack('BBBBBxxxxxxxx', data) if deviceId == 0xff and targetId == 0xff: print("No data to read") return [] theDeviceId, theTargetId = deviceId, targetId print("Data is split in %d chunks; we are at %d" % (numChunks, curChunk)) if numChunks == 0: return [] assert dataLen >= 0 and dataLen <= 8 out = [data[5:5+dataLen]] while curChunk < numChunks - 1: data = hid_get_report(dev, 0x93, 13) assert len(data) == 13 deviceId, targetId, numChunks, curChunk, dataLen = struct.unpack('BBBBBxxxxxxxx', data) if deviceId == 0xff or targetId == 0xff: print("No more data") return out assert (deviceId, targetId) == (theDeviceId, theTargetId) out += [data[5:5+dataLen]] return out def do_trigger_calibration(): print("Starting trigger calibration...") deviceId = 3 hid_set_report(dev, 0x90, struct.pack('BBBB', 1, deviceId, 0, 3)) for i in range(2): print("L2: release and press enter") input() hid_set_report(dev, 0x90, struct.pack('BBBB', 3, deviceId, 1, 1)) for i in range(2): print("L2: mid and press enter") input() hid_set_report(dev, 0x90, struct.pack('BBBB', 3, deviceId, 2, 1)) for i in range(2): print("L2: full and press enter") input() hid_set_report(dev, 0x90, struct.pack('BBBB', 3, deviceId, 3, 1)) for i in range(2): print("R2: release and press enter") input() hid_set_report(dev, 0x90, struct.pack('BBBB', 3, deviceId, 1, 2)) for i in range(2): print("R2: mid and press enter") input() hid_set_report(dev, 0x90, struct.pack('BBBB', 3, deviceId, 2, 2)) for i in range(2): print("R2: full and press enter") input() hid_set_report(dev, 0x90, struct.pack('BBBB', 3, deviceId, 3, 2)) print("Write.") hid_set_report(dev, 0x90, struct.pack('BBBB', 2, deviceId, 0, 3)) print("Trigger calibration done!!") print() print("Here is some debug data from the DS4 about the calibration") data = dump_93_data() for i in range(len(data)): print("Sample %d, data=%s" % (i, binascii.hexlify(data[i]).decode('utf-8'))) def do_stick_center_calibration(): print("Starting analog center calibration...") deviceId = 1 targetId = 1 hid_set_report(dev, 0x90, struct.pack('BBB', 1, deviceId, targetId)) while True: assert hid_get_report(dev, 0x91, 3) == bytes([deviceId,targetId,1]) assert hid_get_report(dev, 0x92, 3) == bytes([deviceId,targetId,0xff]) print("Press S to sample data or W to store calibration (followed by enter)") X = input("> ").upper() if X == "S": hid_set_report(dev, 0x90, struct.pack('BBB', 3, deviceId, targetId)) elif X == "W": hid_set_report(dev, 0x90, struct.pack('BBB', 2, deviceId, targetId)) break else: print("Invalid command") assert hid_get_report(dev, 0x91, 3) == bytes([deviceId,targetId,2]) assert hid_get_report(dev, 0x92, 3) == bytes([deviceId,targetId,1]) print("Stick calibration done!!") print() print("Here is some debug data from the DS4 about the calibration") data = dump_93_data() for i in range(len(data)): print("Sample %d, data=%s" % (i, binascii.hexlify(data[i]).decode('utf-8'))) def do_stick_minmax_calibration(): print("Starting analog min-max calibration...") deviceId = 1 targetId = 2 hid_set_report(dev, 0x90, struct.pack('BBB', 1, deviceId, targetId)) assert hid_get_report(dev, 0x91, 3) == bytes([deviceId,targetId,1]) assert hid_get_report(dev, 0x92, 3) == bytes([deviceId,targetId,0xff]) print("DualShock 4 is now sampling data. Move the analogs all around their range") print("When done, press any key to store calibration.") input() hid_set_report(dev, 0x90, struct.pack('BBB', 2, deviceId, targetId)) assert hid_get_report(dev, 0x91, 3) == bytes([deviceId,targetId,2]) assert hid_get_report(dev, 0x92, 3) == bytes([deviceId,targetId,1]) print("Stick calibration done!!") print() print("Here is some debug data from the DS4 about the calibration") data = dump_93_data() for i in range(len(data)): print("Sample %d, data=%s" % (i, binascii.hexlify(data[i]).decode('utf-8'))) def menu(): print("") print("Choose what you want to calibrate:") print("1. Analog stick center") print("2. Analog stick range (min-max)") print("3. L2 / R2 (beta, let me know if works)") choice_int = -1 try: choice_int = int(input("> ")) except: print("Invalid choice.") return if choice_int == 1: do_stick_center_calibration() if choice_int == 2: do_stick_minmax_calibration() if choice_int == 3: do_trigger_calibration() if __name__ == "__main__": print("*********************************************************") print("* Welcome to the fantastic DualShock 4 Calibration Tool *") print("* *") print("* This tool may break your controller. *") print("* Use at your own risk. Good luck! <3 *") print("* *") print("* Version 0.01 ~ by the_al ~ *") print("*********************************************************") wait_for_device() # Detach kernel driver if sys.platform != 'win32' and dev.is_kernel_driver_active(0): try: dev.detach_kernel_driver(0) except usb.core.USBError as e: sys.exit('Could not detach kernel driver: %s' % str(e)) if dev != None: print("DualShock 4 online!") menu() ================================================ FILE: ds4-tool.py ================================================ #!/usr/bin/env python3 import usb.core import usb.util import array import struct import sys import binascii import time import argparse from construct import * class HID_REQ: DEV_TO_HOST = usb.util.build_request_type( usb.util.CTRL_IN, usb.util.CTRL_TYPE_CLASS, usb.util.CTRL_RECIPIENT_INTERFACE) HOST_TO_DEV = usb.util.build_request_type( usb.util.CTRL_OUT, usb.util.CTRL_TYPE_CLASS, usb.util.CTRL_RECIPIENT_INTERFACE) GET_REPORT = 0x01 SET_REPORT = 0x09 VALID_DEVICE_IDS = [ (0x054c, 0x05c4), (0x054c, 0x09cc) ] class DS4: def __init__(self): self.wait_for_device() if sys.platform != 'win32' and self.__dev.is_kernel_driver_active(0): try: self.__dev.detach_kernel_driver(0) except usb.core.USBError as e: sys.exit('Could not detach kernel driver: %s' % str(e)) def wait_for_device(self): print("Waiting for a DualShock 4...") while True: for i in VALID_DEVICE_IDS: self.__dev = usb.core.find(idVendor=i[0], idProduct=i[1]) if self.__dev is not None: print("Found a DualShock 4: vendorId=%04x productId=%04x" % (i[0], i[1])) return time.sleep(1) def hid_get_report(self, report_id, size): dev = self.__dev #ctrl_transfer(bmRequestType, bRequest, wValue=0, wIndex=0, data_or_wLength=None, timeout=None) assert isinstance(size, int), 'get_report size must be integer' assert report_id <= 0xff, 'only support report_type == 0' return dev.ctrl_transfer(HID_REQ.DEV_TO_HOST, HID_REQ.GET_REPORT, report_id, 0, size + 1)[1:].tobytes() def hid_set_report(self, report_id, buf): dev = self.__dev assert isinstance(buf, (bytes, array.array)), 'set_report buf must be buffer' assert report_id <= 0xff, 'only support report_type == 0' buf = struct.pack('B', report_id) + buf return dev.ctrl_transfer(HID_REQ.HOST_TO_DEV, HID_REQ.SET_REPORT, (3 << 8) | report_id, 0, buf) class Handlers: def __init__(self, dev): self.__dev = dev class VersionInfo: version_info_t = Struct( 'compile_date' / PaddedString(0x10, encoding='ascii'), 'compile_time' / PaddedString(0x10, encoding='ascii'), 'hw_ver_major' / Int16ul, 'hw_ver_minor' / Int16ul, 'sw_ver_major' / Int32ul, 'sw_ver_minor' / Int16ul, 'sw_series' / Int16ul, 'code_size' / Int32ul, ) def __init__(s, buf): s.info = s.version_info_t.parse(buf) def __repr__(s): l = 'Compiled at: %s %s\n'\ 'hw_ver:%04x.%04x\n'\ 'sw_ver:%08x.%04x sw_series:%04x\n'\ 'code size:%08x' % ( s.info.compile_date, s.info.compile_time, s.info.hw_ver_major, s.info.hw_ver_minor, s.info.sw_ver_major, s.info.sw_ver_minor, s.info.sw_series, s.info.code_size ) return l def dump_flash(self, args): def flash_mirror_read(offset): assert offset < 0x800, 'flash mirror offset out of bounds' self.__dev.hid_set_report(0x08, struct.pack('>BH', 0xff, offset)) return self.__dev.hid_get_report(0x11, 2) def dump_flash_mirror(path): # TODO can't correctly calc checksum for some reason if sys.platform == 'win32': path = path.translate({ord(i): None for i in '*<>?:|'}) print('Dumping flash mirror to %s...' % (path)) with open(path, 'wb') as f: for i in range(0, 0x800, 2): word = flash_mirror_read(i) #print('%03x : %s' % (i, binascii.hexlify(word))) f.write(word) print('done') dump_flash_mirror(args.output_file) def info(self, args): info = self.VersionInfo(self.__dev.hid_get_report(0xa3, 0x30)) print(info) def reset(self, args): try: print("Send reset command...") self.__dev.hid_set_report(0xa0, struct.pack('BBB', 4, 1, 0)) except usb.core.USBError as e: # Reset worked self.wait_for_device() print("Reset completed") def get_bt_mac_addr(self, args): ds4_mac = self.__dev.hid_get_report(0x81, 8) ds4_mac_str = "%02x:%02x:%02x:%02x:%02x:%02x" % struct.unpack("BBBBBB", ds4_mac) print("DS4 MAC: %s" % (ds4_mac_str, )) def set_bt_mac_addr(self, args): new_mac_addr = binascii.unhexlify(args.new_mac_addr) assert(len(new_mac_addr) == 6) self.__dev.hid_set_report(0x80, new_mac_addr) def get_bt_link_info(self, args): buf = self.__dev.hid_get_report(0x12, 6 + 3 + 6) ds4_mac, unk, host_mac = buf[0:6], buf[6:9], buf[9:15] assert unk == b'\x08\x25\x00' ds4_mac_str = "%02x:%02x:%02x:%02x:%02x:%02x" % struct.unpack("BBBBBB", ds4_mac) host_mac_str = "%02x:%02x:%02x:%02x:%02x:%02x" % struct.unpack("BBBBBB", host_mac) print("DS4 MAC: %s" % (ds4_mac_str, )) print("Host MAC: %s" % (host_mac_str, )) def set_bt_link_info(self, args): host_addr = binascii.unhexlify(args.host_addr) link_key = binascii.unhexlify(args.link_key) if len(host_addr) != 6 or len(link_key) != 16: print("Usage: set-bt-link-info <6-bytes host addr> <16-bytes link key>") print("Host addr len: %d" % (len(host_addr), )) print("Link key len: %d" % (len(link_key), )) exit(1) assert len(host_addr) == 6 assert len(link_key) == 16 host_addr_str = "%02x:%02x:%02x:%02x:%02x:%02x" % struct.unpack("BBBBBB", host_addr) link_key_str = binascii.hexlify(link_key).decode('utf-8') print("Setting host_addr=%s link_key=%s" % (host_addr_str, link_key_str)) self.__dev.hid_set_report(0x13, host_addr + link_key) def get_imu_calibration(self, args): data = self.__dev.hid_get_report(0x02, 41) print("Raw data: %s" % (binascii.hexlify(data).decode('utf-8'), )) def set_imu_calibration(self, args): data = binascii.unhexlify(args.data) assert len(data) == 36 print("Update IMU calibration data to: %s" % (binascii.hexlify(data).decode('utf-8'))) data = self.__dev.hid_set_report(0x04, data) def get_flash_mirror_status(self, args): # Read byte 12 self.__dev.hid_set_report(0x08, struct.pack('>BH', 0xff, 12)) status = self.__dev.hid_get_report(0x11, 2) print("Changes in flash mirror are temporary: %d" % (status[0], )) def set_flash_mirror_status(self, args): if args.temporary not in [0,1]: print("Error: argument must be 0 or 1") exit(1) if args.temporary == 1: print("Set to: temporary") self.__dev.hid_set_report(0xa0, struct.pack('BBB', 10, 1, 0)) else: print("Set to: permanent") code = binascii.unhexlify("3e717f89") self.__dev.hid_set_report(0xa0, struct.pack('BB', 10, 2) + code ) print("Re-reading flash mirror status..") self.get_flash_mirror_status([]) def get_pcba_id(self, args): # Read byte 12 pcba_id = self.__dev.hid_get_report(0x86, 6) print("PCBA Id: %s" % (binascii.hexlify(pcba_id).decode('utf-8'), )) def set_pcba_id(self, args): data = binascii.unhexlify(args.data) assert len(data) == 6 print("Set to: %s" % (binascii.hexlify(data).decode('utf-8'))) self.__dev.hid_set_report(0x85, data) def get_bt_enable(self, args): # Read byte 0x700 self.__dev.hid_set_report(0x08, struct.pack('>BH', 0xff, 0x700)) status = self.__dev.hid_get_report(0x11, 2) print("BT Enable: %s" % (status[0], )) def set_bt_enable(self, args): raw = struct.pack('B', 1 if args.enable else 0) print("Set to: %s" % (binascii.hexlify(raw).decode('utf-8'))) self.__dev.hid_set_report(0xa1, raw) def get_serial_number(self, args): print('get_serial_number() isn\'t implemented yet') # Read byte 0x700 #self.__dev.hid_set_report(0x08, struct.pack('>BH', 0xff, 0x700)) #status = self.__dev.hid_get_report(0x11, 2) #print("BT Enable: %s" % (status[0], )) def set_serial_number(self, args): data = binascii.unhexlify(args.data) assert len(data) == 2 self.__dev.hid_set_report(0x08, struct.pack('>B', 0x10) + data) print("Change serial number to: %s" % (binascii.hexlify(data).decode('utf-8'))) ds4 = DS4() handlers = Handlers(ds4) parser = argparse.ArgumentParser(description="Play with the DS4 controller", epilog="By the_al") subparsers = parser.add_subparsers(dest="action") # Dump flash mirror p = subparsers.add_parser('dump-flash', help="Dump the flash mirror") p.add_argument('output_file', help="Output file to write the dump to") p.set_defaults(func=handlers.dump_flash) # Info p = subparsers.add_parser('info', help="Print info about the DS4") p.set_defaults(func=handlers.info) # Reset p = subparsers.add_parser('reset', help="Reset the DS4") p.set_defaults(func=handlers.reset) # GET Mac Addr + SET Mac Addr p = subparsers.add_parser('get-bt-mac-addr', help="Get the Bluetooth MAC Address") p.set_defaults(func=handlers.get_bt_mac_addr) p = subparsers.add_parser('set-bt-mac-addr', help="Set the Bluetooth MAC Address") p.add_argument('new_mac_addr', help="New MAC address to store") p.set_defaults(func=handlers.set_bt_mac_addr) # GET BT Link Info + SET BT Link Info p = subparsers.add_parser('get-bt-link-info', help="Get Bluetooth link information") p.set_defaults(func=handlers.get_bt_link_info) p = subparsers.add_parser('set-bt-link-info', help="Update Bluetooth link information") p.add_argument('host_addr', help="Host MAC Address to connect to") p.add_argument('link_key', help="Bluetooth link key") p.set_defaults(func=handlers.set_bt_link_info) # GET IMU Calibration + SET IMU Calibration p = subparsers.add_parser('get-imu-calibration', help="Retrieve IMU calibration data") p.set_defaults(func=handlers.get_imu_calibration) p = subparsers.add_parser('set-imu-calibration', help="Change IMU calibration data") p.add_argument('data', help="New calibration data to store") p.set_defaults(func=handlers.set_imu_calibration) # GET Flash Mirror Enable + SET Flash Mirror Enable p = subparsers.add_parser('get-flash-mirror-status', help="Get flash-mirror status") p.set_defaults(func=handlers.get_flash_mirror_status) p = subparsers.add_parser('set-flash-mirror-status', help="Change how flash mirror works") p.add_argument('temporary', type=int, help="Set if changes in configuration are temporary(1) or permanent(0)") p.set_defaults(func=handlers.set_flash_mirror_status) # GET PCBA Id + SET PCBA Id p = subparsers.add_parser('get-pcba-id', help="Get the PCBA manufacturer ID") p.set_defaults(func=handlers.get_pcba_id) p = subparsers.add_parser('set-pcba-id', help="Change the PCBA manufacturer ID") p.add_argument('data', help="New manufacturer ID (6 bytes)") p.set_defaults(func=handlers.set_pcba_id) # "BT ENABLE" p = subparsers.add_parser('get-bt-enable', help="Read BT enable bit") p.set_defaults(func=handlers.get_bt_enable) p = subparsers.add_parser('set-bt-enable', help="Change the BT enable bit") p.add_argument('enable', type=int, help="0 to disable and 1 to enable") p.set_defaults(func=handlers.set_bt_enable) # GET Serial Number + SET Serial Number p = subparsers.add_parser('get-serial-number', help="Read the serial number") p.set_defaults(func=handlers.get_serial_number) p = subparsers.add_parser('set-serial-number', help="Set the serial number") p.add_argument('data', help="2 bytes hex") p.set_defaults(func=handlers.set_serial_number) args = parser.parse_args() if not hasattr(args, "func"): parser.print_help() exit(1) args.func(args) ================================================ FILE: ds5-calibration-tool.py ================================================ #!/usr/bin/env python3 import usb.core import usb.util import array import struct import sys import binascii import time from construct import * import argparse dev = None VALID_DEVICE_IDS = [ (0x054c, 0x0ce6) ] def wait_for_device(): global dev print("Waiting for a DualSense...") while True: for i in VALID_DEVICE_IDS: dev = usb.core.find(idVendor=i[0], idProduct=i[1]) if dev is not None: print("Found a DualSense: vendorId=%04x productId=%04x" % (i[0], i[1])) return time.sleep(1) class HID_REQ: DEV_TO_HOST = usb.util.build_request_type( usb.util.CTRL_IN, usb.util.CTRL_TYPE_CLASS, usb.util.CTRL_RECIPIENT_INTERFACE) HOST_TO_DEV = usb.util.build_request_type( usb.util.CTRL_OUT, usb.util.CTRL_TYPE_CLASS, usb.util.CTRL_RECIPIENT_INTERFACE) GET_REPORT = 0x01 SET_REPORT = 0x09 def hid_get_report(dev, report_id, size): assert isinstance(size, int), 'get_report size must be integer' assert report_id <= 0xff, 'only support report_type == 0' return dev.ctrl_transfer(HID_REQ.DEV_TO_HOST, HID_REQ.GET_REPORT, report_id, 0, size + 1)[1:].tobytes() def hid_set_report(dev, report_id, buf): assert isinstance(buf, (bytes, array.array) ), 'set_report buf must be buffer' assert report_id <= 0xff, 'only support report_type == 0' buf = struct.pack('B', report_id) + buf return dev.ctrl_transfer(HID_REQ.HOST_TO_DEV, HID_REQ.SET_REPORT, (3 << 8) | report_id, 0, buf) def do_stick_center_calibration(): print("Starting analog center calibration...") deviceId = 1 targetId = 1 hid_set_report(dev, 0x82, struct.pack('BBB', 1, deviceId, targetId)) k = hid_get_report(dev, 0x83, 4) if k != bytes([deviceId,targetId,1,0xff]): print("ERROR: DualSense is in invalid state: %s. Try to reset it" % (binascii.hexlify(k))) return while True: print("Press S to sample data or W to store calibration (followed by enter)") X = input("> ").upper() if X == "S": hid_set_report(dev, 0x82, struct.pack('BBB', 3, deviceId, targetId)) assert hid_get_report(dev, 0x83, 4) == bytes([deviceId,targetId,1,0xff]) elif X == "W": hid_set_report(dev, 0x82, struct.pack('BBB', 2, deviceId, targetId)) break else: print("Invalid command") print("Stick calibration done!!") def do_stick_minmax_calibration(): print("Starting analog min-max calibration...") deviceId = 1 targetId = 2 hid_set_report(dev, 0x82, struct.pack('BBB', 1, deviceId, targetId)) k = hid_get_report(dev, 0x83, 4) if k != bytes([deviceId,targetId,1,0xff]): print("ERROR: DualSense is in invalid state: %s. Try to reset it" % (binascii.hexlify(k))) return print("DualSense is now sampling data. Move the analogs all around their range") print("When done, press any key to store calibration.") input() hid_set_report(dev, 0x82, struct.pack('BBB', 2, deviceId, targetId)) print("Stick calibration done!!") if __name__ == "__main__": print("*********************************************************") print("* Welcome to the fantastic DualSense Calibration Tool *") print("* *") print("* This tool may break your controller. *") print("* Use at your own risk. Good luck! <3 *") print("* *") print("* Version 0.01 (C) 2024 ~ by the_al ~ *") print("*********************************************************") parser = argparse.ArgumentParser(prog='ds5-calibration-tool') parser.add_argument('-p', '--permanent', help="make changes permanent", action='store_true') subparsers = parser.add_subparsers(dest="action") p = subparsers.add_parser('analog-center', help="calibrate the center of analog sticks") p.set_defaults(func=do_stick_center_calibration) p = subparsers.add_parser('analog-range', help="calibrate the range of analog sticks") p.set_defaults(func=do_stick_minmax_calibration) args = parser.parse_args() if not hasattr(args, "func"): parser.print_help() exit(1) wait_for_device() # Detach kernel driver if sys.platform != 'win32' and dev.is_kernel_driver_active(0): try: dev.detach_kernel_driver(0) except usb.core.USBError as e: sys.exit('Could not detach kernel driver: %s' % str(e)) if dev == None: print("Cannot find a DualSense") exit(-1) print("== DualSense online! ==") if args.permanent: print("Unlocking NVS") hid_set_report(dev, 0x80, struct.pack('BBBBBB', 3, 2, 101, 50, 64, 12)) try: args.func() except Exception as e: print(e) if args.permanent: print("Re-locking NVS") hid_set_report(dev, 0x80, struct.pack('BB', 3, 1)) ================================================ FILE: requirements.txt ================================================ construct==2.10.68 pyusb==1.2.1 usb==0.0.83.dev0