From bda77189f73f3075ef038b89110221832b057a39 Mon Sep 17 00:00:00 2001 From: Michael Wagner Date: Thu, 2 Feb 2023 20:09:44 +0100 Subject: [PATCH 1/5] Add udev rule for reading controller without root privileges --- 70-ps5-controller.rules | 11 +++++++++++ README.md | 10 +++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 70-ps5-controller.rules diff --git a/70-ps5-controller.rules b/70-ps5-controller.rules new file mode 100644 index 0000000..771fd4e --- /dev/null +++ b/70-ps5-controller.rules @@ -0,0 +1,11 @@ +# ref.: https://boilingsteam.com/the-dualsense-is-making-even-more-sense/ +# copy this file to /etc/udev/rules.d +# reload udev rules with: +# udevadm control --reload-rules +# udevadm trigger + +# PS5 DualSense controller over USB hidraw +KERNEL=="hidraw*", ATTRS{idVendor}=="054c", ATTRS{idProduct}=="0ce6", MODE="0660", TAG+="uaccess" + +# PS5 DualSense controller over bluetooth hidraw +KERNEL=="hidraw*", KERNELS=="*054C:0CE6*", MODE="0660", TAG+="uaccess" diff --git a/README.md b/README.md index 5b8f244..7d15618 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,15 @@ pip install --upgrade pydualsense ## Linux -On Linux based system you first need to install the hidapi through your package manager of your system. +On Linux based system you first need to add a udev rule to let the user access the PS5 controller without requiring root privileges. + +```bash +sudo cp 70-ps5-controller.rules /etc/udev/rules.d +sudo udevadm control --reload-rules +sudo udevadm trigger +``` + +Then install the hidapi through your package manager of your system. On an Ubuntu system the package ```libhidapi-dev``` is required. From dc14382b62fd2a38c55a7dc7d53e7fb573f7fb44 Mon Sep 17 00:00:00 2001 From: Michael Wagner Date: Thu, 2 Feb 2023 20:20:37 +0100 Subject: [PATCH 2/5] Add example to continuously read out all states --- examples/read_all_input_channels.py | 54 +++++++++++++++++++++++++++++ pydualsense/pydualsense.py | 3 +- 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 examples/read_all_input_channels.py diff --git a/examples/read_all_input_channels.py b/examples/read_all_input_channels.py new file mode 100644 index 0000000..349adef --- /dev/null +++ b/examples/read_all_input_channels.py @@ -0,0 +1,54 @@ +import curses +import time +from pydualsense import * + + +dualsense = pydualsense() +dualsense.init() + + +while dualsense.states is None: + print("no states found yet") + print(f"epoch: {time.time():.0f}") + time.sleep(0.5) + + +stdscr = curses.initscr() +curses.noecho() +curses.cbreak() + + +while True: + stdscr.addstr(0, 0, f"epoch: {time.time():.2f}") + + pretty_states = [f"{state:03}" for state in dualsense.states] + + stdscr.addstr(1, 0, f"states[0:10]: {pretty_states[0:10]}") + stdscr.addstr(2, 0, f"states[10:20]: {pretty_states[10:20]}") + stdscr.addstr(3, 0, f"states[20:30]: {pretty_states[20:30]}") + stdscr.addstr(4, 0, f"states[30:40]: {pretty_states[30:40]}") + stdscr.addstr(5, 0, f"states[40:50]: {pretty_states[40:50]}") + stdscr.addstr(6, 0, f"states[50:60]: {pretty_states[50:60]}") + stdscr.addstr(7, 0, f"states[60:70]: {pretty_states[60:70]}") + stdscr.addstr(8, 0, f"states[70:78]: {pretty_states[70:78]}") + + stdscr.addstr(11, 0, f"square: {dualsense.state.square} \t triangle: {dualsense.state.triangle} \t circle: {dualsense.state.circle} \t cross: {dualsense.state.cross}") + stdscr.addstr(12, 0, f"DpadUp: {dualsense.state.DpadUp} \t DpadDown: {dualsense.state.DpadDown} \t DpadLeft: {dualsense.state.DpadLeft} \t DpadRight: {dualsense.state.DpadRight}") + stdscr.addstr(13, 0, f"L1: {dualsense.state.L1} \t L2: {dualsense.state.L2} \t L3: {dualsense.state.L3} \t R1: {dualsense.state.R1} \t R2: {dualsense.state.R2} \t R3: {dualsense.state.R3} \t R2Btn: {dualsense.state.R2Btn} \t L2Btn: {dualsense.state.L2Btn}") + stdscr.addstr(14, 0, f"share: {dualsense.state.share} \t options: {dualsense.state.options} \t ps: {dualsense.state.ps} \t touch1: {dualsense.state.touch1} \t touch2: {dualsense.state.touch2} \t touchBtn: {dualsense.state.touchBtn} \t touchRight: {dualsense.state.touchRight} \t touchLeft: {dualsense.state.touchLeft}") + stdscr.addstr(15, 0, f"touchFinger1: {dualsense.state.touchFinger1} \t touchFinger2: {dualsense.state.touchFinger2}") + stdscr.addstr(16, 0, f"micBtn: {dualsense.state.micBtn}") + stdscr.addstr(17, 0, f"RX: {dualsense.state.RX} \t RY: {dualsense.state.RY} \t LX: {dualsense.state.LX} \t LY: {dualsense.state.LY}") + stdscr.addstr(18, 0, f"trackPadTouch0: ID: {dualsense.state.trackPadTouch0.ID} \t isActive: {dualsense.state.trackPadTouch0.isActive} \t X: {dualsense.state.trackPadTouch0.X} \t Y: {dualsense.state.trackPadTouch0.Y}") + stdscr.addstr(19, 0, f"trackPadTouch1: ID: {dualsense.state.trackPadTouch1.ID} \t isActive: {dualsense.state.trackPadTouch1.isActive} \t X: {dualsense.state.trackPadTouch1.X} \t Y: {dualsense.state.trackPadTouch1.Y}") + stdscr.addstr(20, 0, f"gyro: roll: {dualsense.state.gyro.Roll} \t pitch: {dualsense.state.gyro.Pitch} \t yaw: {dualsense.state.gyro.Yaw}") + stdscr.addstr(21, 0, f"acc: X: {dualsense.state.accelerometer.X} \t Y: {dualsense.state.accelerometer.Y} \t Z: {dualsense.state.accelerometer.Z}") + + stdscr.refresh() + # time.sleep(0.1) + +dualsense.close() + +curses.echo() +curses.nobreak() +cursed.endwin() diff --git a/pydualsense/pydualsense.py b/pydualsense/pydualsense.py index 42e1f7e..0344f00 100644 --- a/pydualsense/pydualsense.py +++ b/pydualsense/pydualsense.py @@ -109,6 +109,7 @@ class pydualsense: self.ds_thread = True self.report_thread = threading.Thread(target=self.sendReport) self.report_thread.start() + self.states = None def determineConnectionType(self) -> ConnectionType: """ @@ -228,7 +229,7 @@ class pydualsense: Args: inReport (bytearray): read bytearray containing the state of the whole controller """ - states = list(inReport) # convert bytes to list + self.states = states # states 0 is always 1 self.state.LX = states[1] - 127 self.state.LY = states[2] - 127 From 3b3ec445ada83aa4417347fe5308c5a474b58912 Mon Sep 17 00:00:00 2001 From: Michael Wagner Date: Thu, 2 Feb 2023 20:21:03 +0100 Subject: [PATCH 3/5] Hard code bluetooth mode for now --- pydualsense/pydualsense.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pydualsense/pydualsense.py b/pydualsense/pydualsense.py index 0344f00..607a4f5 100644 --- a/pydualsense/pydualsense.py +++ b/pydualsense/pydualsense.py @@ -102,9 +102,9 @@ class pydualsense: if platform.startswith('Windows'): self.conType = self.determineConnectionType() # determine USB or BT connection else: - # set for usb manually - self.input_report_length = 64 - self.output_report_length = 64 + # set for BT manually + self.input_report_length = 78 + self.output_report_length = 78 self.ds_thread = True self.report_thread = threading.Thread(target=self.sendReport) @@ -121,6 +121,11 @@ class pydualsense: ConnectionType: Detected connection type of the controller. """ + # hardcode to bluetooth for now + self.input_report_length = 78 + self.output_report_length = 78 + return ConnectionType.BT + if self.device._device.input_report_length == 64: self.input_report_length = 64 self.output_report_length = 64 @@ -229,6 +234,7 @@ class pydualsense: Args: inReport (bytearray): read bytearray containing the state of the whole controller """ + states = list(inReport)[1:] # convert bytes to list self.states = states # states 0 is always 1 self.state.LX = states[1] - 127 From e2c16cd29eded1e3d48a8890c81d47f0b39d1bfc Mon Sep 17 00:00:00 2001 From: Michael Wagner Date: Thu, 2 Feb 2023 22:27:59 +0100 Subject: [PATCH 4/5] Detect connection type and support bluetooth receiving --- pydualsense/pydualsense.py | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/pydualsense/pydualsense.py b/pydualsense/pydualsense.py index 607a4f5..a66ae28 100644 --- a/pydualsense/pydualsense.py +++ b/pydualsense/pydualsense.py @@ -98,14 +98,7 @@ class pydualsense: self.triggerL = DSTrigger() # left trigger self.triggerR = DSTrigger() # right trigger self.state = DSState() # controller states - - if platform.startswith('Windows'): - self.conType = self.determineConnectionType() # determine USB or BT connection - else: - # set for BT manually - self.input_report_length = 78 - self.output_report_length = 78 - + self.conType = self.determineConnectionType() # determine USB or BT connection self.ds_thread = True self.report_thread = threading.Thread(target=self.sendReport) self.report_thread.start() @@ -115,22 +108,24 @@ class pydualsense: """ Determine the connection type of the controller. eg USB or BT. - Currently only USB is supported. + We ask the controller for an input report with a length up to 100 bytes + and afterwords check the lenght of the received input report. + The connection type determines the length of the report. + + This way of determining is not pretty but it works.. Returns: ConnectionType: Detected connection type of the controller. """ - # hardcode to bluetooth for now - self.input_report_length = 78 - self.output_report_length = 78 - return ConnectionType.BT + dummy_report = self.device.read(100) + input_report_length = len(dummy_report) - if self.device._device.input_report_length == 64: + if input_report_length == 64: self.input_report_length = 64 self.output_report_length = 64 return ConnectionType.USB - elif self.device._device.input_report_length == 78: + elif input_report_length == 78: self.input_report_length = 78 self.output_report_length = 78 return ConnectionType.BT @@ -234,7 +229,14 @@ class pydualsense: Args: inReport (bytearray): read bytearray containing the state of the whole controller """ - states = list(inReport)[1:] # convert bytes to list + if self.conType == ConnectionType.BT: + # the reports for BT and USB are structured the same, + # but there is one more byte at the start of the bluetooth report. + # We drop that byte, so that the format matches up again. + states = list(inReport)[1:] # convert bytes to list + else: # USB + states = list(inReport) # convert bytes to list + self.states = states # states 0 is always 1 self.state.LX = states[1] - 127 From 4105048784acf21c1cbd9df70140daef59e07070 Mon Sep 17 00:00:00 2001 From: Michael Wagner Date: Sat, 4 Feb 2023 14:43:39 +0100 Subject: [PATCH 5/5] Add option to leave channel-readout example with 'q' --- examples/read_all_input_channels.py | 79 ++++++++++++++--------------- 1 file changed, 39 insertions(+), 40 deletions(-) diff --git a/examples/read_all_input_channels.py b/examples/read_all_input_channels.py index 349adef..2510496 100644 --- a/examples/read_all_input_channels.py +++ b/examples/read_all_input_channels.py @@ -3,52 +3,51 @@ import time from pydualsense import * +def print_states(stdscr): + curses.curs_set(0) + curses.use_default_colors() + stdscr.nodelay(1) + while True: + stdscr.erase() + pretty_states = [f"{state:03}" for state in dualsense.states] + + stdscr.addstr(f"epoch: {time.time():.2f}\n") + stdscr.addstr(f"states[0:10]: {pretty_states[0:10]}\n") + stdscr.addstr(f"states[10:20]: {pretty_states[10:20]}\n") + stdscr.addstr(f"states[20:30]: {pretty_states[20:30]}\n") + stdscr.addstr(f"states[30:40]: {pretty_states[30:40]}\n") + stdscr.addstr(f"states[40:50]: {pretty_states[40:50]}\n") + stdscr.addstr(f"states[50:60]: {pretty_states[50:60]}\n") + stdscr.addstr(f"states[60:70]: {pretty_states[60:70]}\n") + stdscr.addstr(f"states[70:78]: {pretty_states[70:78]}\n") + stdscr.addstr("\n") + stdscr.addstr(f"square: {dualsense.state.square!s:>5} \t triangle: {dualsense.state.triangle!s:>5} \t circle: {dualsense.state.circle!s:>5} \t cross: {dualsense.state.cross!s:>5}\n") + stdscr.addstr(f"DpadUp: {dualsense.state.DpadUp!s:>5} \t DpadDown: {dualsense.state.DpadDown!s:>5} \t DpadLeft: {dualsense.state.DpadLeft!s:>5} \t DpadRight: {dualsense.state.DpadRight!s:>5}\n") + stdscr.addstr(f"L1: {dualsense.state.L1!s:>5} \t L2: {dualsense.state.L2:3} \t L2Btn: {dualsense.state.L2Btn!s:>5} \t L3: {dualsense.state.L3!s:>5} \t R1: {dualsense.state.R1!s:>5} \t R2: {dualsense.state.R2:3d} \t R2Btn: {dualsense.state.R2Btn!s:>5} \t R3: {dualsense.state.R3!s:>5}\n") + stdscr.addstr(f"share: {dualsense.state.share!s:>5} \t options: {dualsense.state.options!s:>5} \t ps: {dualsense.state.ps!s:>5} \t touch1: {dualsense.state.touch1!s:>5} \t touch2: {dualsense.state.touch2!s:>5} \t touchBtn: {dualsense.state.touchBtn!s:>5} \t touchRight: {dualsense.state.touchRight!s:>5} \t touchLeft: {dualsense.state.touchLeft!s:>5}\n") + stdscr.addstr(f"touchFinger1: {dualsense.state.touchFinger1} \t touchFinger2: {dualsense.state.touchFinger2}\n") + stdscr.addstr(f"micBtn: {dualsense.state.micBtn!s:>5}\n") + stdscr.addstr(f"RX: {dualsense.state.RX:4} \t RY: {dualsense.state.RY:4} \t LX: {dualsense.state.LX:4} \t LY: {dualsense.state.LY:4}\n") + stdscr.addstr(f"trackPadTouch0: ID: {dualsense.state.trackPadTouch0.ID} \t isActive: {dualsense.state.trackPadTouch0.isActive!s:>5} \t X: {dualsense.state.trackPadTouch0.X:4d} \t Y: {dualsense.state.trackPadTouch0.Y:4d}\n") + stdscr.addstr(f"trackPadTouch1: ID: {dualsense.state.trackPadTouch1.ID} \t isActive: {dualsense.state.trackPadTouch1.isActive!s:>5} \t X: {dualsense.state.trackPadTouch1.X:4d} \t Y: {dualsense.state.trackPadTouch1.Y:4d}\n") + stdscr.addstr(f"gyro: roll: {dualsense.state.gyro.Roll:6} \t pitch: {dualsense.state.gyro.Pitch:6} \t yaw: {dualsense.state.gyro.Yaw:6}\n") + stdscr.addstr(f"acc: X: {dualsense.state.accelerometer.X:6} \t Y: {dualsense.state.accelerometer.Y:6} \t Z: {dualsense.state.accelerometer.Z:6}\n") + stdscr.addstr("\n") + stdscr.addstr("Exit script with 'q'\n") + + stdscr.refresh() + if stdscr.getch() == ord('q'): + break + + dualsense = pydualsense() dualsense.init() - while dualsense.states is None: - print("no states found yet") + print("Waiting until connection is established...") print(f"epoch: {time.time():.0f}") time.sleep(0.5) -stdscr = curses.initscr() -curses.noecho() -curses.cbreak() - - -while True: - stdscr.addstr(0, 0, f"epoch: {time.time():.2f}") - - pretty_states = [f"{state:03}" for state in dualsense.states] - - stdscr.addstr(1, 0, f"states[0:10]: {pretty_states[0:10]}") - stdscr.addstr(2, 0, f"states[10:20]: {pretty_states[10:20]}") - stdscr.addstr(3, 0, f"states[20:30]: {pretty_states[20:30]}") - stdscr.addstr(4, 0, f"states[30:40]: {pretty_states[30:40]}") - stdscr.addstr(5, 0, f"states[40:50]: {pretty_states[40:50]}") - stdscr.addstr(6, 0, f"states[50:60]: {pretty_states[50:60]}") - stdscr.addstr(7, 0, f"states[60:70]: {pretty_states[60:70]}") - stdscr.addstr(8, 0, f"states[70:78]: {pretty_states[70:78]}") - - stdscr.addstr(11, 0, f"square: {dualsense.state.square} \t triangle: {dualsense.state.triangle} \t circle: {dualsense.state.circle} \t cross: {dualsense.state.cross}") - stdscr.addstr(12, 0, f"DpadUp: {dualsense.state.DpadUp} \t DpadDown: {dualsense.state.DpadDown} \t DpadLeft: {dualsense.state.DpadLeft} \t DpadRight: {dualsense.state.DpadRight}") - stdscr.addstr(13, 0, f"L1: {dualsense.state.L1} \t L2: {dualsense.state.L2} \t L3: {dualsense.state.L3} \t R1: {dualsense.state.R1} \t R2: {dualsense.state.R2} \t R3: {dualsense.state.R3} \t R2Btn: {dualsense.state.R2Btn} \t L2Btn: {dualsense.state.L2Btn}") - stdscr.addstr(14, 0, f"share: {dualsense.state.share} \t options: {dualsense.state.options} \t ps: {dualsense.state.ps} \t touch1: {dualsense.state.touch1} \t touch2: {dualsense.state.touch2} \t touchBtn: {dualsense.state.touchBtn} \t touchRight: {dualsense.state.touchRight} \t touchLeft: {dualsense.state.touchLeft}") - stdscr.addstr(15, 0, f"touchFinger1: {dualsense.state.touchFinger1} \t touchFinger2: {dualsense.state.touchFinger2}") - stdscr.addstr(16, 0, f"micBtn: {dualsense.state.micBtn}") - stdscr.addstr(17, 0, f"RX: {dualsense.state.RX} \t RY: {dualsense.state.RY} \t LX: {dualsense.state.LX} \t LY: {dualsense.state.LY}") - stdscr.addstr(18, 0, f"trackPadTouch0: ID: {dualsense.state.trackPadTouch0.ID} \t isActive: {dualsense.state.trackPadTouch0.isActive} \t X: {dualsense.state.trackPadTouch0.X} \t Y: {dualsense.state.trackPadTouch0.Y}") - stdscr.addstr(19, 0, f"trackPadTouch1: ID: {dualsense.state.trackPadTouch1.ID} \t isActive: {dualsense.state.trackPadTouch1.isActive} \t X: {dualsense.state.trackPadTouch1.X} \t Y: {dualsense.state.trackPadTouch1.Y}") - stdscr.addstr(20, 0, f"gyro: roll: {dualsense.state.gyro.Roll} \t pitch: {dualsense.state.gyro.Pitch} \t yaw: {dualsense.state.gyro.Yaw}") - stdscr.addstr(21, 0, f"acc: X: {dualsense.state.accelerometer.X} \t Y: {dualsense.state.accelerometer.Y} \t Z: {dualsense.state.accelerometer.Z}") - - stdscr.refresh() - # time.sleep(0.1) - +curses.wrapper(print_states) dualsense.close() - -curses.echo() -curses.nobreak() -cursed.endwin()