Full Code of dualshock-tools/ds4-tools for AI

master 94e8d94d0b7d cached
7 files
29.6 KB
8.4k tokens
43 symbols
1 requests
Download .txt
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 <repo link>
$ 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
Download .txt
gitextract_nr2qt5it/

├── .gitignore
├── LICENSE.txt
├── README.md
├── ds4-calibration-tool.py
├── ds4-tool.py
├── ds5-calibration-tool.py
└── requirements.txt
Download .txt
SYMBOL INDEX (43 symbols across 3 files)

FILE: ds4-calibration-tool.py
  function wait_for_device (line 19) | def wait_for_device():
  class HID_REQ (line 31) | class HID_REQ:
  function hid_get_report (line 39) | def hid_get_report(dev, report_id, size):
  function hid_set_report (line 45) | def hid_set_report(dev, report_id, buf):
  function dump_93_data (line 52) | def dump_93_data():
  function do_trigger_calibration (line 81) | def do_trigger_calibration():
  function do_stick_center_calibration (line 129) | def do_stick_center_calibration():
  function do_stick_minmax_calibration (line 160) | def do_stick_minmax_calibration():
  function menu (line 188) | def menu():

FILE: ds4-tool.py
  class HID_REQ (line 14) | class HID_REQ:
  class DS4 (line 27) | class DS4:
    method __init__ (line 29) | def __init__(self):
    method wait_for_device (line 38) | def wait_for_device(self):
    method hid_get_report (line 48) | def hid_get_report(self, report_id, size):
    method hid_set_report (line 56) | def hid_set_report(self, report_id, buf):
  class Handlers (line 63) | class Handlers:
    method __init__ (line 64) | def __init__(self, dev):
    class VersionInfo (line 67) | class VersionInfo:
      method __init__ (line 79) | def __init__(s, buf):
      method __repr__ (line 82) | def __repr__(s):
    method dump_flash (line 95) | def dump_flash(self, args):
    method info (line 116) | def info(self, args):
    method reset (line 120) | def reset(self, args):
    method get_bt_mac_addr (line 129) | def get_bt_mac_addr(self, args):
    method set_bt_mac_addr (line 134) | def set_bt_mac_addr(self, args):
    method get_bt_link_info (line 139) | def get_bt_link_info(self, args):
    method set_bt_link_info (line 148) | def set_bt_link_info(self, args):
    method get_imu_calibration (line 168) | def get_imu_calibration(self, args):
    method set_imu_calibration (line 172) | def set_imu_calibration(self, args):
    method get_flash_mirror_status (line 179) | def get_flash_mirror_status(self, args):
    method set_flash_mirror_status (line 185) | def set_flash_mirror_status(self, args):
    method get_pcba_id (line 200) | def get_pcba_id(self, args):
    method set_pcba_id (line 205) | def set_pcba_id(self, args):
    method get_bt_enable (line 212) | def get_bt_enable(self, args):
    method set_bt_enable (line 218) | def set_bt_enable(self, args):
    method get_serial_number (line 223) | def get_serial_number(self, args):
    method set_serial_number (line 230) | def set_serial_number(self, args):

FILE: ds5-calibration-tool.py
  function wait_for_device (line 19) | def wait_for_device():
  class HID_REQ (line 31) | class HID_REQ:
  function hid_get_report (line 39) | def hid_get_report(dev, report_id, size):
  function hid_set_report (line 45) | def hid_set_report(dev, report_id, buf):
  function do_stick_center_calibration (line 52) | def do_stick_center_calibration():
  function do_stick_minmax_calibration (line 79) | def do_stick_minmax_calibration():
Condensed preview — 7 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (32K chars).
[
  {
    "path": ".gitignore",
    "chars": 12,
    "preview": ".*.sw?\nvenv\n"
  },
  {
    "path": "LICENSE.txt",
    "chars": 1063,
    "preview": "MIT License\n\nCopyright (c) 2024 the_al\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof "
  },
  {
    "path": "README.md",
    "chars": 4278,
    "preview": "# ds4-tools\n\nThis repo contains some Python scripts I use to play and reverse-engineer the\nDualShock 4 controller.\n\n## C"
  },
  {
    "path": "ds4-calibration-tool.py",
    "chars": 7597,
    "preview": "#!/usr/bin/env python3\n\nimport usb.core\nimport usb.util\nimport array\nimport struct\nimport sys\nimport binascii\nimport tim"
  },
  {
    "path": "ds4-tool.py",
    "chars": 12203,
    "preview": "#!/usr/bin/env python3\n\nimport usb.core\nimport usb.util\n\nimport array\nimport struct\nimport sys\nimport binascii\nimport ti"
  },
  {
    "path": "ds5-calibration-tool.py",
    "chars": 5074,
    "preview": "#!/usr/bin/env python3\n\nimport usb.core\nimport usb.util\nimport array\nimport struct\nimport sys\nimport binascii\nimport tim"
  },
  {
    "path": "requirements.txt",
    "chars": 49,
    "preview": "construct==2.10.68\npyusb==1.2.1\nusb==0.0.83.dev0\n"
  }
]

About this extraction

This page contains the full source code of the dualshock-tools/ds4-tools GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 7 files (29.6 KB), approximately 8.4k tokens, and a symbol index with 43 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!