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