Python for PS4 dualshock 4 controller
PS4 dualshock 4 controller 是一個很成熟的遊戲控制器:除了最基本的類比搖桿、按鈕、及方向鍵以外,還多了六軸感應器 (accelerate and gyro) 和觸控面板 (touch pad)。雖然在大部分的遊戲裡面派不上用場,但六軸感應和觸控面板功能在動作控制上有很大的自由度,也提供使用者不一樣的操作體驗,因此也激發了我想藉由 dualshock 4 controller (DS4) 來控制自動化 xy 平台的想法。不過,第一步需要做的,就是想辦法讓 PC 能夠讀取控制器中的數據。我所使用的作業系統是 Windows 10,語言是 Python 3.6,套件管理為 Anaconda,控制器的連接方式則是 USB。
要藉由 Python 來讀取 DS4 的資料,主要有兩種方法:
- 使用 pygame 套件。這是一個遊戲控制器通用的套件,而且使用上非常方便,按鈕、搖桿、方向鍵等都分屬不同的 class,可以直接引用。但實際測試後,發現它無法取得 DS4 中六軸感應器的資料,也就是說,gyroscope 和 accelerometer 這兩個最有趣的動作感應功能,無法藉由 pygame 取得。所以我們必須使用第二種方法…
- 使用 pyUSB 套件,直接讀取 DS4 中的原始數據。這個方法對我這種生手來講就麻煩許多,花了兩天才把 pyUSB 和 DS4 的通訊搞定;再來就是如何解釋讀取到的數據,這些都得靠其他開發者流出的文件來摸索。不過至少目前來說,讀取 gyroscope 和 accelerometer 的數據是沒有問題的。
我所使用的作業系統是 Win10,套件管理為 Anaconda;先說明如何用 pygame 讀取資料:
如果沒有 pygame package 的話,第一步就是先安裝它。
pip install pygame程式的部分,當然就是直接 import,然後 initiate 搖桿功能。
import pygame
import os pygame.init()
pygame.joystick.init() controller = pygame.joystick.Joystick(0)
controller.init()
在 pygame 的框架下,有三種控制模式,axis、button、hat:
# Three types of controls: axis, button, and hat
axis = {}
button = {}
hat = {}接著給他們一些初始值 (通常設為 0 或 False):
# Assign initial data values# Axes are initialized to 0.0
for i in range(controller.get_numaxes()):
axis[i] = 0.0# Buttons are initialized to False
for i in range(controller.get_numbuttons()):
button[i] = False# Hats are initialized to 0
for i in range(controller.get_numhats()):
hat[i] = (0, 0)
再來就是將每個軸和按鈕等的標籤定義出來 (要實際測試過,才比較清楚每個按鍵和讀值的對應關係):
# Labels for DS4 controller axes
AXIS_LEFT_STICK_X = 0
AXIS_LEFT_STICK_Y = 1
AXIS_RIGHT_STICK_X = 2
AXIS_RIGHT_STICK_Y = 3
AXIS_R2 = 4
AXIS_L2 = 5# Labels for DS4 controller buttons
# Note that there are 14 buttons (0 to 13 for pygame, 1 to 14 for Windows setup)
BUTTON_SQUARE = 0
BUTTON_CROSS = 1
BUTTON_CIRCLE = 2
BUTTON_TRIANGLE = 3BUTTON_L1 = 4
BUTTON_R1 = 5
BUTTON_L2 = 6
BUTTON_R2 = 7BUTTON_SHARE = 8
BUTTON_OPTIONS = 9BUTTON_LEFT_STICK = 10
BUTTON_RIGHT_STICK = 11BUTTON_PS = 12
BUTTON_PAD = 13# Labels for DS4 controller hats (Only one hat control)
HAT_1 = 0
主要的迴圈,將持續列印出目前從 DS4 讀到的數據。按下 PS4 鍵可以跳出迴圈:
# Main loop, one can press the PS button to break
quit = False
while quit == False:# Get events
for event in pygame.event.get():if event.type == pygame.JOYAXISMOTION:
axis[event.axis] = round(event.value,3)
elif event.type == pygame.JOYBUTTONDOWN:
button[event.button] = True
elif event.type == pygame.JOYBUTTONUP:
button[event.button] = False
elif event.type == pygame.JOYHATMOTION:
hat[event.hat] = event.valuequit = button[BUTTON_PS]# Print out results
os.system('cls') # Note that 'cls' is the command for Windows
# Axes
print("Left stick X:", axis[AXIS_LEFT_STICK_X])
print("Left stick Y:", axis[AXIS_LEFT_STICK_Y])
print("Right stick X:", axis[AXIS_RIGHT_STICK_X])
print("Right stick Y:", axis[AXIS_RIGHT_STICK_Y])
print("L2 strength:", axis[AXIS_L2])
print("R2 strength:", axis[AXIS_R2],"\n")
# Buttons
print("Square:", button[BUTTON_SQUARE])
print("Cross:", button[BUTTON_CROSS])
print("Circle:", button[BUTTON_CIRCLE])
print("Triangle:", button[BUTTON_TRIANGLE])
print("L1:", button[BUTTON_L1])
print("R1:", button[BUTTON_R1])
print("L2:", button[BUTTON_L2])
print("R2:", button[BUTTON_R2])
print("Share:", button[BUTTON_SHARE])
print("Options:", button[BUTTON_OPTIONS])
print("Left stick press:", button[BUTTON_LEFT_STICK])
print("Right stick press:", button[BUTTON_RIGHT_STICK])
print("PS:", button[BUTTON_PS])
print("Touch Pad:", button[BUTTON_PAD],"\n")
# Hats
print("Hat X:", hat[HAT_1][0])
print("Hat Y:", hat[HAT_1][1],"\n")print("Press PS button to quit:", quit)# Limited to 30 frames per second to make the display not so flashy
clock = pygame.time.Clock()
clock.tick(30)
程式運作正常的情況下,你可以看到:

因此,如果需要利用 DS4 的輸出來控制機器的話,只要把數值對應的範圍抓出來就可以了 (例如搖桿的部分、 L2 和 R2 等類比輸出,全押全放的數值範圍是 -1 到 +1)。所以如果不需要用到六軸感應的數據的話,pygame 套件可以很快速地把想要的控制數據從 DS4 取出來。完整的 code 請參考 GitHub (https://github.com/anubisankh/dualshock4-python)。
再來就是六軸感應的部分了。
因為這邊使用的 DS4 是用 USB 連接到 PC,因此我們將使用 pyUSB:
pip install pyusb
pip install libusb這邊須注意的是,只有安裝 pyUSB 還不夠,相關的 library 也是必要的,因此如果之前沒有 libusb 的話,須同時安裝。再來就是困擾我許久,但是最關鍵的部分了。如果我們按照官方的例子來寫:
import usb.coredev = usb.core.find()
接著就會出現 usb.core.NoBackendError: No backend available 的錯誤訊息。為了要讓 backend (*.dll) 能夠被找到,我們必須特別把它的位置找出來。如果 pip install libusb 成功的話,通常可以在 Anaconda3\Lib\site-packages\libusb\_platform\_windows\x64\libusb-1.0.dll 中找到。如果用 pip 安裝了 libusb 還是找不到 libusb-1.0.dll,可以在網路上找到適合該作業系統的 libusb-1.0.dll 下載下來,並把路徑複製到程式碼中:
import usb.core
import usb.util
import usb.backend.libusb1# You have to change the path below to your libusb-1.0.dll
BACKEND = usb.backend.libusb1.get_backend(find_library=lambda x: "C:\\Users\\YourUserName\\Anaconda3\\Lib\\site-packages\\libusb\\_platform\\_windows\\x64\\libusb-1.0.dll")
再來我們就可以列出所有 USB 裝置的相關資訊:
dev = usb.core.find(find_all=True)if dev is None:
print('Device not found')
else:
for d in dev:
print('Decimal Vendor ID: ' + str(d.idVendor) + ' & ProductID: ' + str(d.idProduct)) # Print all connected USB devices in decimal
print('Hexadecimal Vendor ID: ' + hex(d.idVendor) + ' & ProductID: ' + hex(d.idProduct) + '\n') # Print all connected USB devices in hex
這邊會列出所有連接上的 USB 裝置的廠商 ID (vendor ID) 和產品 ID (product ID),範例輸出如下:

列印出的裝置資訊可以用十進位 (decimal) 或標準的十六進位 (hex) 來表示。把 DS4 拔出後跑上述程式碼,再重新裝回,可進一步發現 DS4 的 vendor ID: 0x54c / product id: 0x9cc。把這個訊息記錄下來後,我們開始只針對 DS4 來深入研究:
import usb.core
import usb.backend.libusb1
import os
import time# DS4 controller ids
VENDOR_ID = 0x54c
PRODUCT_ID = 0x9ccBACKEND = usb.backend.libusb1.get_backend(find_library=lambda x: "C:\\Users\\YourUserName\\Anaconda3\\Lib\\site-packages\\libusb\\_platform\\_windows\\x64\\libusb-1.0.dll")dev = usb.core.find(idVendor=VENDOR_ID, idProduct=PRODUCT_ID, backend=BACKEND)
這邊定義的 USB 裝置 (device, dev),就只有 DS4 controller。每一個 USB device 可能會有不同的 configuration(s),在之下又會有幾個不同的 interface(s),而 interface 下面又會有不同的 endpoint(s),如下圖所示:

因此,當我們取得 DS 的 configuration 時:
cfg = dev.get_active_configuration()print('Configuration data:\n\n', cfg,'\n')
會得到一長串的資料:

其中可以看到有些 interface 是給 Audio,再往下拉的話,可以看到其中一個 interface 是 Human Interface Device (HID),這就是我們 DS4 控制器主要輸入的介面。在該介面下,還包含了 endpoint 0x84 和 endpoint 0x3 兩個端點。Endpoint 0x84: Interrupt IN 則是我們想要取得 access 的端點。

設定 endpoint 資訊如下:
INTERFACE_DS4 = 3 # HID interface
SETTING_DS4 = 0
interface = cfg[(INTERFACE_DS4, SETTING_DS4)]ENDPOINT_DS4_OUT = 0 # Input endpoint
endpoint = interface[ENDPOINT_DS4_OUT]print(endpoint.read(0x40)) # wMaxPacketSize 0x40
執行後,列印出 endpoint 讀取資料的結果為一個陣列:array(‘B’, [1, 131, 128, 129, 132, 8, 0, 16, 0, 0, 189, 178, 11, 4, 0, 3, 0, 255, 255, 103, 1, 177, 31, 113, 6, 0, 0, 0, 0, 0, 27, 0, 0, 1, 135, 134, 187, 180, 32, 132, 29, 55, 12, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 128, 0])。[]中的數字便是我們想要的 DS4 各個按鈕、搖桿、還有六軸感應器的讀值了。
接下來,最麻煩的地方就在於如何解讀這些數字。這部分可以參考 PS4 開發者已經試出來的資訊。這些輸出當中,Readout [1] ~ [9] 是和搖桿及按鈕相關的資訊,而我們有興趣的 gyroscope 和 accelerometer 則是在 [14] ~ [25]。
所以搖桿及按鈕的部分 (同時列出 hex 和 binary 的結果):
# Axesprint('Readout L stick X:', format(endpoint.read(0x40)[1],'#04X'), format(endpoint.read(0x40)[1],'08b'), endpoint.read(0x40)[1])print('Readout L stick Y:', format(endpoint.read(0x40)[2],'#04X'), format(endpoint.read(0x40)[2],'08b'), endpoint.read(0x40)[2])print('Readout R stick X:', format(endpoint.read(0x40)[3],'#04X'), format(endpoint.read(0x40)[3],'08b'), endpoint.read(0x40)[3])print('Readout R stick Y:', format(endpoint.read(0x40)[4],'#04X'), format(endpoint.read(0x40)[4],'08b'), endpoint.read(0x40)[4])print('Readout L2 trigger:', format(endpoint.read(0x40)[8],'#04X'), format(endpoint.read(0x40)[8],'08b'), endpoint.read(0x40)[8])print('Readout R2 trigger:', format(endpoint.read(0x40)[9],'#04X'), format(endpoint.read(0x40)[9],'08b'), endpoint.read(0x40)[9],'\n')
Gyroscope 和 accelerometer 的六軸部分,一個軸的輸出需要兩個 byte 的數據,所以:
# Accelerometers (2 bytes to signed integer)
data = endpoint.read(0x40)
print('Accelerometer X:', format(256*data[18]+data[19],'016b'), 256*data[18]+data[19]-(65536 if data[18] > 127 else 0))print('Accelerometer Y:', format(256*data[16]+data[17],'016b'), 256*data[16]+data[17]-(65536 if data[16] > 127 else 0)print('Accelerometer Z:', format(256*data[14]+data[15],'016b'), 256*data[14]+data[15]-(65536 if data[14] > 127 else 0),'\n')# Gyroscopes
data = endpoint.read(0x40)
print('Gyroscope X (Roll) :', format(256*data[20]+data[21],'016b'), 256*data[20]+data[21]-(65536 if data[20] > 127 else 0))print('Gyroscope Y (Yaw) :', format(256*data[22]+data[23],'016b'), 256*data[22]+data[23]-(65536 if data[22] > 127 else 0))print('Gyroscope Z (Pitch):', format(256*data[24]+data[25],'016b'), 256*data[24]+data[25]-(65536 if data[24] > 127 else 0),'\n')
加上其他按鈕和功能的數據,範例輸出如下:

如此一來,便可以藉由 pyUSB 讀取 DS 4 六軸感測器的數據來操控想控制的機器了。另外,DS4 還有 touch pad,甚至支援兩點觸控,若想使用 touch pad 功能還可以用 Readout[34] ~ [51] 的輸出來設計自己想要的操控模式。相關原始碼及參考資料可參考:https://github.com/anubisankh/dualshock4-python
