#!/usr/bin/env python3 """ DS5Dongle (OLED Edition) configuration tool. Reads and writes the firmware's persistent config over USB HID feature reports — no browser, no WebHID, works in any terminal. Ported from loteran/DS5Dongle's scripts/set_ds5.py and extended for this fork's Config_body layout (adds `config_version` at the start and `current_slot` between `controller_mode` and `auto_haptics_*`). Requires EITHER `cython-hidapi` (preferred — `pip install hidapi`) OR the `hid` package (`pip install hid`). The script auto-detects which is installed; both expose `hid.device` somewhere with slightly different constructor APIs, and we handle both. Quick usage: scripts/set_ds5.py # print current config scripts/set_ds5.py --auto-haptics fallback --auto-haptics-gain 120 scripts/set_ds5.py --haptics-gain 1.5 --speaker-volume -10 scripts/set_ds5.py --slot 2 # switch active pairing slot scripts/set_ds5.py --version # print firmware version scripts/set_ds5.py --rssi # print live BT RSSI in dBm Credit: protocol + script structure from loteran/DS5Dongle commit 5d6bc2f. """ import argparse import struct import sys try: import hid except ImportError: print("[ERROR] Missing dependency: install with pip install hidapi", file=sys.stderr) sys.exit(1) def _open_hid(vid, pid): """Open a HID device, abstracting over cython-hidapi vs apmorton/pyhidapi.""" if hasattr(hid, 'Device'): # apmorton/pyhidapi: hid.Device(vid, pid) return hid.Device(vid, pid) if hasattr(hid, 'device'): # cython-hidapi: hid.device() + .open(vid, pid) d = hid.device() d.open(vid, pid) return d raise RuntimeError("installed `hid` module has neither Device nor device — unknown variant") SONY_VID = 0x054C DS5_PID = 0x0CE6 DSE_PID = 0x0DF2 # Our Config_body wire layout — 19 bytes, little-endian, packed struct. # uint8 config_version [0] (firmware-set, ignored on write) # float haptics_gain [1:5] # float speaker_volume [5:9] # uint8 inactive_time [9] # uint8 disable_inactive_disc [10] # uint8 disable_pico_led [11] # uint8 polling_rate_mode [12] # uint8 audio_buffer_length [13] # uint8 controller_mode [14] # uint8 current_slot [15] (fork-specific: multi-slot pairing) # uint8 auto_haptics_enable [16] 0=off 1=fallback 2=mix 3=replace # uint8 auto_haptics_gain [17] [0..200] percent # uint8 auto_haptics_lowpass [18] 0=80Hz 1=160Hz 2=250Hz 3=400Hz CONFIG_FMT = '= 128 else val # int8 def fmt_cfg(c): return ( f" config_version {c['config_version']}\n" f" haptics_gain {c['haptics_gain']:.3f}\n" f" speaker_volume {c['speaker_volume']:.1f} dB\n" f" inactive_time {c['inactive_time']} min\n" f" inactive_disc {'disabled' if c['disable_inactive_disconnect'] else 'enabled'}\n" f" pico_led {'off' if c['disable_pico_led'] else 'on'}\n" f" polling_rate {c['polling_rate_mode']} ({POLLING_MODES.get(c['polling_rate_mode'], '?')})\n" f" audio_buffer {c['audio_buffer_length']}\n" f" controller_mode {c['controller_mode']} ({CONTROLLER_MODES.get(c['controller_mode'], '?')})\n" f" current_slot {c['current_slot']}\n" f" auto_haptics {c['auto_haptics_enable']} ({AUTO_HAP_MODES.get(c['auto_haptics_enable'], '?')})\n" f" auto_haptics_gain {c['auto_haptics_gain']}%\n" f" auto_haptics_lp {c['auto_haptics_lowpass']} ({LOWPASS_MODES.get(c['auto_haptics_lowpass'], '?')})" ) AUTO_HAP_ARG = {'off': 0, 'fallback': 1, 'mix': 2, 'replace': 3} LOWPASS_ARG = {'80': 0, '160': 1, '250': 2, '400': 3} CTRL_MODE_ARG = {'ds5': 0, 'dse': 1, 'auto': 2} POLL_ARG = {'250': 0, '500': 1, 'realtime': 2, 'rt': 2} def build_parser(): p = argparse.ArgumentParser(description="DS5Dongle (OLED Edition) config tool.") p.add_argument('--version', action='store_true', help="print firmware version and exit") p.add_argument('--rssi', action='store_true', help="print live BT RSSI (dBm) and exit") p.add_argument('--haptics-gain', type=float, help="float [1.0, 2.0]") p.add_argument('--speaker-volume', type=float, help="dB [-100, 0]") p.add_argument('--inactive-time', type=int, help="minutes [10, 60]") p.add_argument('--inactive-disc', choices=['on', 'off'], help="silent disconnect on idle") p.add_argument('--pico-led', choices=['on', 'off']) p.add_argument('--polling', choices=POLL_ARG.keys(), help="USB HID polling rate") p.add_argument('--audio-buffer', type=int, help="haptic buffer length [16, 128]") p.add_argument('--controller-mode', choices=CTRL_MODE_ARG.keys()) p.add_argument('--slot', type=int, choices=[0, 1, 2, 3], help="active multi-pairing slot") p.add_argument('--auto-haptics', choices=AUTO_HAP_ARG.keys(), help="Auto Haptics mode") p.add_argument('--auto-haptics-gain', type=int, help="percent [0, 200]") p.add_argument('--auto-haptics-lp', choices=LOWPASS_ARG.keys(), help="LP cutoff Hz") return p def main(): args = build_parser().parse_args() device = open_device() if args.version: print(f"Firmware: {get_version(device)}") return if args.rssi: rssi = get_rssi(device) print(f"BT RSSI: {rssi} dBm" if rssi is not None else "RSSI unavailable") return cfg = get_config(device) changes = [] def set_kv(key, val, label=None): if cfg[key] != val: changes.append(f" {label or key}: {cfg[key]} → {val}") cfg[key] = val if args.haptics_gain is not None: set_kv('haptics_gain', args.haptics_gain) if args.speaker_volume is not None: set_kv('speaker_volume', args.speaker_volume) if args.inactive_time is not None: set_kv('inactive_time', args.inactive_time) if args.inactive_disc is not None: set_kv('disable_inactive_disconnect', 1 if args.inactive_disc == 'off' else 0, label='inactive_disc') if args.pico_led is not None: set_kv('disable_pico_led', 1 if args.pico_led == 'off' else 0, label='pico_led') if args.polling is not None: set_kv('polling_rate_mode', POLL_ARG[args.polling]) if args.audio_buffer is not None: set_kv('audio_buffer_length', args.audio_buffer) if args.controller_mode is not None: set_kv('controller_mode', CTRL_MODE_ARG[args.controller_mode]) if args.slot is not None: set_kv('current_slot', args.slot) if args.auto_haptics is not None: set_kv('auto_haptics_enable', AUTO_HAP_ARG[args.auto_haptics]) if args.auto_haptics_gain is not None: set_kv('auto_haptics_gain', args.auto_haptics_gain) if args.auto_haptics_lp is not None: set_kv('auto_haptics_lowpass', LOWPASS_ARG[args.auto_haptics_lp]) if changes: print("Updating config:") print("\n".join(changes)) write_config(device, cfg) print("Saved to flash.") # Re-read to show what stuck after firmware validation cfg = get_config(device) print("\nNew config:") else: print("Current config:") print(fmt_cfg(cfg)) if __name__ == '__main__': main()