简介:pyPS4Controller 1.2.1 是面向开发者的手柄控制库,用于连接 PS4 游戏手柄。它封装底层蓝牙通信细节,提供简洁接口,适合游戏开发、交互设计、自动化测试等场景,支持按钮监听、摇杆坐标读取、电量查询、振动反馈以及多手柄并发管理,帮助开发者快速将手柄输入集成到项目中。资源压缩包共包含十三份文件,以五份源码文件、四份文本说明文件、两份包信息文件为主,另搭配配置文件与项目文档,整体约 12KB,轻量简洁,目录结构清晰,便于部署和阅读。已有超过一百五十人学习这一资源,适合正在评估或准备集成手柄输入功能的开发者。通过这份资源,读者能够掌握库的安装配置、依赖声明与基础用法,学习创建控制器对象、绑定按键事件、读取摇杆坐标、开启振动反馈等核心操作。同时,包内文档与源码为二次开发、问题排查和功能扩展提供了直接参考和实现基础。
1. pyPS4Controller 不是模拟器:把 DS4 手柄变成 Python 回调的库
把 PS4 手柄接到跑着 Linux 的树莓派或工控机上,最原始的做法是打开 /dev/input/eventX 读原始事件流,自己拆 type、code、value,再对照一张按键码表判断是谁被按下。pyPS4Controller 把这层收走:它监听手柄节点,把按键按下/抬起、摇杆位移翻译成 on_x_press、on_L3_up(value) 这类回调。安装包就是标题里的 pyPS4Controller-1.2.1.tar.gz,一个标准 Python sdist 源码包。
库的边界很清晰:只做读取和回调分发,不识别手柄型号、不产生 UI、也不控制电机。底层依赖 evdev,所以它天生是 Linux 方案,Windows 上跑不起来——这点要先想清楚再决定要不要用。所有业务逻辑,比如把摇杆值换算成电机速度,都得在回调里自己写。
适合遥控小车、巡检机器人、机械臂示教和游戏自动化测试的人,也适合想搞懂 evdev 又不想从零解析字节流的 Python 开发。读完这篇文章,你能完成从 tar.gz 安装、设备节点确认、蓝牙配对,到在回调里输出速度指令的整条链路。
2. 从 input_event 到回调方法:pyPS4Controller 的事件来源与包结构
2.1 先分清 /dev/input/js0 和 /dev/input/eventX
Linux 输入子系统里有两条常见链路。手柄通过 USB 或蓝牙把 HID 报文交给内核驱动,DS4 在现役内核里对应的是 hid-sony 或 hid-playstation 驱动;驱动把数据交给 input core,再由两个 handler 分发到不同节点。evdev handler 生成 /dev/input/eventX,保存的是接近原始 HID 的事件流;joydev handler 生成 /dev/input/jsX,是「经典摇杆」视图,把按键和轴重新统一编号。同一个按键动作,你会在这两类节点上各看到一份事件,只是码表和结构不同。
pyPS4Controller 的默认 interface 参数是 /dev/input/js0,这个默认值来自早期机器人教程的惯用写法。它底层用 evdev 库打开这个节点并循环 read。需要留个心眼:js0 并不是每个发行版、每台机器上都存在,插过多个手柄或者换了内核的机器,节点编号会漂移。所以接好手柄之后第一件事是确认节点,而不是直接跑脚本。
ls -l /dev/input/ cat /proc/bus/input/devices | grep -A 4 -i "wireless controller"第一条命令列出所有节点,第二条从 /proc 总线信息里搜手柄。注意看输出里的 Handlers 一行,内核会同时给出 js 和 event 两种编号,例如 Handlers=event4 js0。把这两个编号记下来:interface 参数和后面 evtest 排查都会用到。
提示:如果 lsusb 能看到 Sony 的设备,/dev/input/ 里却一个节点都没有,通常是内核没加载 DS4 驱动模块。Ubuntu 系可以安装 linux-modules-extra 后重启,树莓派上则先确认蓝牙和 USB 本身工作正常。
2.2 EV_KEY 与 EV_ABS:库怎么把内核事件分类成回调
无论从哪个节点读,内核交出来的都是 input_event 结构,核心字段是 type、code、value。type 决定大类:EV_KEY(1) 是按键,EV_ABS(3) 是绝对轴,EV_SYN(0) 是同步标记,表示一组事件收尾。EV_KEY 的 value 为 1 表示按下、0 表示抬起、2 表示按住重复;EV_ABS 的 value 是位移量,DS4 左右摇杆的范围是 -32768 到 32767,十字键在驱动里走 ABS_HAT0X / ABS_HAT0Y 两个轴。
pyPS4Controller 的 listen() 循环拿到 input_event 后会做一次分类,分类结果直接决定调用哪个回调族:
| 内核事件 | 分类结果 | 回调族 | 是否带参数 |
|---|---|---|---|
| EV_KEY value=1 | 按下 | on_*_press | 不带 |
| EV_KEY value=0 | 抬起 | on_*_release | 不带 |
| EV_ABS 摇杆轴 | 摇杆位移 | on_L3_* / on_R3_* | 带 value |
| EV_ABS HAT 轴 | 十字键 | on_arrow | 不带 |
这张表的含义是:你在回调里拿到的是「语义」而不是「轴号」。推动左摇杆,库已经替你判断出方向,再决定调用 on_L3_up 还是 on_L3_down。十字键同理,库把 HAT 轴的 -1/0/1 三个状态折算成方向键的按下与抬起,所以业务代码里不需要跟 ABS_HAT0X 打交道。
2.3 tar.gz 里有什么:controller.py、event.py 与 evdev 依赖
sdist 源码包的好处是可以直接看实现。下载回来的 pyPS4Controller-1.2.1.tar.gz 是标准 tar 包,先列目录再决定怎么用:
tar -tzf pyPS4Controller-1.2.1.tar.gz | head -30输出里会看到 pyPS4Controller-1.2.1/ 目录下包含 pyPS4Controller/init.py、pyPS4Controller/controller.py、pyPS4Controller/event.py,以及 setup.py、README 和 LICENSE。controller.py 是主类 Controller 的实现,所有 on_* 空方法都在这里;event.py 放事件类型定义和「事件码对应哪个回调」的对照表。setup.py 的 install_requires 声明了 evdev,也就是说运行时依赖只有它一个。
安装之前可以先确认 evdev 是否就位:
python3 -c "import evdev; print(evdev.__version__)"能打印出版本号说明系统里已经有 evdev,否则先补装。注意这里用的是 python3,很多 Linux 发行版里 python 默认指向 2.x 或不存在,后面所有安装命令统一用 python3 -m pip 的写法。
3. 安装 pyPS4Controller-1.2.1.tar.gz 并在 Linux 上跑通最小监听
3.1 两条安装路径:pip 直装 tar.gz,还是先解压再装
第一种方式是让 pip 直接消费 tar.gz。pip 认识 sdist,它会自己解压、解析依赖并安装,你不需要手动解包:
python3 --version python3 -m venv .venv source .venv/bin/activate python -m pip install pyPS4Controller-1.2.1.tar.gz python -c "from pyPS4Controller.controller import Controller; print(Controller)"venv 这步是常规做法。Ubuntu 22.04 这类系统自带 Python 3.10,直接用系统 Python 装包容易污染全局环境;VSCode 的 Python 扩展打开项目时也会引导你创建虚拟环境,殊途同归。最后一条 import 语句验证安装结果,能打印出类对象说明装成功了。
第二种方式是先解压再装,适合你想改库的源码:
tar -xzf pyPS4Controller-1.2.1.tar.gz cd pyPS4Controller-1.2.1/ python -m pip install .老教程里常写的 python setup.py install 在新 Python 版本已被标记为弃用,统一改用 pip install .。两种方式的取舍:
| 安装方式 | 命令核心 | 适用场景 |
|---|---|---|
| pip 直装 | pip install pyPS4Controller-1.2.1.tar.gz | 只想用库,不改源码 |
| 先解压再装 | tar -xzf 后 pip install . | 要改默认映射、加日志 |
3.2 免 root 读手柄:用户组与 udev 规则
装完库直接跑脚本,最常见的报错是 PermissionError: [Errno 13],因为 /dev/input/ 下的节点默认只允许 root 和 input 组成员访问。两个解法建议都做。先把当前用户加进 input 组:
sudo usermod -aG input $USER # 重新登录一次会话后组权限才生效再写一条 udev 规则,让手柄节点对所有用户开放读写:
sudo tee /etc/udev/rules.d/99-ds4.rules > /dev/null <<'EOF' SUBSYSTEM=="input", ATTRS{name}=="*Wireless Controller*", MODE="0666", ENV{ID_INPUT_JOYSTICK}="1" EOF sudo udevadm control --reload-rules sudo udevadm trigger逐段解释规则:SUBSYSTEM 限定在 input 子系统里找设备;ATTRS{name} 匹配设备名,DS4 在蓝牙模式下叫 Wireless Controller,不同固件可能带 Sony Computer Entertainment 或 Sony Interactive Entertainment 前缀,用通配符一次覆盖;MODE=0666 放开读写权限;最后一句把设备标记为摇杆,避免桌面环境把它当鼠标处理。写完后重新插拔一次手柄,或执行 trigger 让内核重读规则。
3.3 DS4 蓝牙配对:Share + PS 进配对,再走 bluetoothctl
USB 连接最简单,插上线 /dev/input/ 里立刻多出节点,本节可以跳过。蓝牙连接多几步,但流程固定。先让手柄进入配对模式:关机状态下同时按住 Share 和 PS 键大约三秒,灯条开始快速白闪。然后用 bluetoothctl 完成配对:
bluetoothctl power on agent on default-agent scan on pair 78:0B:D9:xx:xx:xx trust 78:0B:D9:xx:xx:xx connect 78:0B:D9:xx:xx:xxMAC 地址在 scan on 的输出里找,设备名一般是 Wireless Controller,蓝牙手柄不会出现在 lsusb 里,从 scan 输出里复制地址最可靠。connect 成功后回到 shell,执行 ls /dev/input/ 确认 js0 或 eventN 出现。trust 这步别省,否则每次开机都要重新输入配对码。
3.4 最小监听脚本:继承 Controller,在回调里打印
from pyPS4Controller.controller import Controller class MyController(Controller): def __init__(self, **kwargs): Controller.__init__(self, **kwargs) def on_x_press(self): print("x pressed") def on_x_release(self): print("x released") def on_up_arrow_press(self): print("up arrow pressed") if __name__ == "__main__": controller = MyController(interface="/dev/input/js0", connecting_using_ds4drv=False) controller.listen(timeout=60, debug=True)这段代码说明库的核心用法:不要直接实例化 Controller,要继承它。基类里所有 on_* 方法都是空的,你的业务就是重写其中一部分。listen(timeout=60) 阻塞当前线程,连续 60 秒没有任何事件就返回;debug=True 会在终端打印每个识别出的事件名,是验证映射最快的方式。运行时按一下 X、推一下十字键,终端应当出现对应输出。
4. 把回调接进控制循环:初始化参数、摇杆缩放与组合键
4.1 Controller 的两个初始化参数和 listen 的扩展参数
Controller 的构造参数有两个高频项:interface 和 connecting_using_ds4drv。interface 就是第 2 章确认的节点路径,写成字符串;connecting_using_ds4drv 是布尔值。
controller = MyController( interface="/dev/input/js0", # 用第 2 章查到的 jsN 或 eventN connecting_using_ds4drv=False, # 内置驱动保持 False ) controller.listen(timeout=30)interface 取 js0 还是 event4,取决于实测哪个节点有数据。connecting_using_ds4drv 涉及一个历史背景:ds4drv 是早期的用户态手柄守护进程,老教程普遍靠它做蓝牙映射,它会生成自己的 uinput 节点,轴顺序和内置驱动不一样。现代内核自带 DS4 驱动后,我一般不再装 ds4drv,参数保持 False。如果机器上确实跑着 ds4drv,用 ps aux | grep ds4drv 就能确认,这时才需要置 True。
listen 还有 on_connect 和 on_disconnect 两个回调参数,分别在手柄连接建立和断开时被调用。它们适合做状态通知,比如打印连接状态,或者在断线时让机器人急停。
4.2 常用回调映射表
库的回调命名规律很强,按「物理键名 + press/release」和「摇杆方向」两组记忆:
| 物理操作 | 回调方法 | 参数 |
|---|---|---|
| X 按下 / 抬起 | on_x_press / on_x_release | 无 |
| ○ 按下 / 抬起 | on_circle_press / on_circle_release | 无 |
| □、△ 同理 | on_square_* / on_triangle_* | 无 |
| L1、R1、L2、R2 | on_L1_press、on_R2_release 等 | 无 |
| Share / Options | on_share_* / on_options_* | 无 |
| PS 键 | on_playstation_button_press | 无 |
| 左摇杆上/下/左/右 | on_L3_up / on_L3_down / on_L3_left / on_L3_right | value |
| 左摇杆回中 | on_L3_at_rest | 无 |
| 右摇杆各方向 | on_R3_up 等 | value |
按钮回调一律不带参,摇杆回调带一个 int 型 value。命名走的是 PlayStation 键位名,正面四个键叫 x、circle、square、triangle,不要按 Xbox 的 A/B/X/Y 去猜。有个小差异要留意:不同小版本对「×」键的命名可能是 x 也可能是 cross,以你装的 1.2.1 里 debug=True 打印出来的名字为准。
4.3 摇杆值缩放:把 -32768..32767 变成速度指令
摇杆回调拿到的 value 是 int,范围 -32768 到 32767。直接拿去当 PWM 占空比肯定不行,先归一化到 -1.0..1.0,再乘电机最大转速:
class DriveController(Controller): def __init__(self, **kwargs): Controller.__init__(self, **kwargs) self.speed = 0.0 self.steering = 0.0 def on_L3_up(self, value): # value 是 int,先做类型转换再参与除法,得到 -1.0..1.0 self.speed = float(value) / 32767.0 def on_L3_down(self, value): self.speed = -float(value) / 32767.0 def on_L3_left(self, value): self.steering = -float(value) / 32767.0 def on_L3_right(self, value): self.steering = float(value) / 32767.0 def on_L3_at_rest(self): self.speed = 0.0 self.steering = 0.0四个方向回调各存一个分量,on_L3_at_rest 负责回中清零。这个清零必须写:否则摇杆松手后最后一条速度指令会一直生效,表现就是「松手继续跑」。方向符号不一定符合你的电机接线,第一次上电前先标定:推前拉后各观察一次正负,方向反了就把对应回调里的符号换掉。
4.4 组合键与线程安全:回调里只改状态,别做耗时操作
listen() 是阻塞的,它占住当前线程之后,控制循环得放到另一个线程里读状态。回调里不要做耗时操作,不要发 HTTP、不要 sleep,只更新共享状态:
import threading class BoostController(DriveController): def __init__(self, **kwargs): super().__init__(**kwargs) self._lock = threading.Lock() self._boost = 1.0 def on_L1_press(self): with self._lock: self._boost = 1.8 def on_L1_release(self): with self._lock: self._boost = 1.0 def current_speed(self): with self._lock: return self.speed * self._boost组合键的本质就是「多个回调各自改状态位」:按住 L1 时推左摇杆,速度自然被放大 1.8 倍。加锁是因为 listen 线程和你的控制循环线程会同时读写 speed、_boost 两个属性,Python 的 GIL 管得住单个赋值,管不住「读一个、算一个、写回」这类复合操作,所以养成在回调里加锁的习惯,能省掉后面排查竞态的半天时间。
5. 验证与排错:pyPS4Controller 的节点核对与高频坑处理
5.1 用 evtest 核对事件码,确认节点没选错
sudo apt install evtest sudo evtestevtest 会列出所有 input 设备,选择名字带 Wireless Controller 的 eventX 节点。按下按键时对照这组码表:X 是 BTN_SOUTH(304),○ 是 BTN_EAST(305),□ 是 BTN_WEST(307),△ 是 BTN_NORTH(308),L1/R1 是 BTN_TL(310)/BTN_TR(311),L3/R3 按下去是 BTN_THUMBL(317)/BTN_THUMBR(318),十字键走 ABS_HAT0X(16)/ABS_HAT0Y(17)。
| 物理键 | evtest 显示名 | code |
|---|---|---|
| X | BTN_SOUTH | 304 |
| ○ | BTN_EAST | 305 |
| □ | BTN_WEST | 307 |
| △ | BTN_NORTH | 308 |
| L1 / R1 | BTN_TL / BTN_TR | 310 / 311 |
| L3 / R3 按压 | BTN_THUMBL / BTN_THUMBR | 317 / 318 |
如果在 eventX 节点看到的码跟表对不上,说明机器上有别的映射层干预,别硬凑,回去改 interface 或对照 event.py 调整映射。
5.2 高频报错核对表
| 报错 | 场景 | 处理方式 |
|---|---|---|
| tar: Cannot open ...: No such file or directory | 解压时路径不对 | pwd 看当前目录,ls 核对完整文件名 |
| gzip: stdin: not in gzip format | tar 包没下载完整 | file pyPS4Controller-1.2.1.tar.gz 看文件类型 |
| PermissionError: [Errno 13] | 节点权限不足 | 按 3.2 节配置 udev 规则 |
| Input/output error on /dev/input/js0 | js0 不存在或不可读 | ls /dev/input/,换成 eventN |
| 蓝牙已连接但回调不触发 | 节点漂移或接口配错 | evtest 找当前节点,改 interface |
| listen() 静默返回 | 超时参数生效 | while True: controller.listen(timeout=60) 包一层 |
5.3 一个能一直用的验证技巧:让 debug=True 常驻
把 debug=True 当作常驻参数而不是临时参数。它能打印出库内部识别到的事件名,调试时永远先看它,再决定改代码还是改配置。如果按下 X 打印出来的名字和你预期不符,基本是 interface 选错了节点,换节点比改映射省事。只有当换节点无效时,才打开 site-packages 里的 pyPS4Controller/controller.py(部分版本这份对照在 event.py),找到事件码到回调的映射表,把真实码替换进去。配合 evtest 的码表做一次对齐,之后换内核、换发行版、换手柄固件,都不用再猜映射对不对。我给机器人项目的监听调用永远写成 controller.listen(timeout=60, debug=True),交接调试时不用解释节点在哪,看终端就知道是哪一层出了问题。
本文还有配套的精品资源,点击获取