使用EV3主机上的按键控制小车还是比较麻烦,能不能通过遥控来控制小车呢?
当然可以!
乐高提供了一个红外线传感器和遥控器:
但是红外遥控器需要很强的方向性,而且那个遥控器太简陋了,连个摇杆都没有。
还有一种方式是使用蓝牙手柄,这样我们就可以找一个游戏手柄直接通过蓝牙控制小车,太方便了有木有!
我找了一个任天堂的游戏手柄:
理论上支持蓝牙的游戏手柄都是可用的,标准游戏手柄按键如下:
┌──────┐ ┌──────┐ ┌┴─────┐│ ┌┴─────┐│ ┌─┴──────┴┴──────────────────┴──────┴┴─┐ │ ┌─┐ ┌─┐ ┌─┐ │ │ ┌─┐ └─┘ └─┘ ┌─┐│X│ │ │ ┌─┘ └─┐ - + │Y│└─┘┌─┐ │ │ └─┐ ┌─┘ ┌─┐ ┌─┐ └─┘┌─┐│A│ │ │ └─┘ └─┘ └─┘ │B│└─┘ │ │ ┌─┐ S H ┌─┐ └─┘ │ │ ┌─┘ └─┐ ┌─┘ └─┐ │ │ └─┐ ┌─┘ └─┐ ┌─┘ │ │ └─┘ └─┘ │ │ ──────────────────────── │ │ / \ │ │ / \ │ └────/ \────┘
把游戏手柄和EV3主机用蓝牙连起来很简单,但是怎么用Python程序读取手柄的输入?
一般来说,从外部设备读取输入时,应用程序并不直接与外设打交道,而是由操作系统通过驱动程序连接外设,然后,通过操作系统提供的API读取输入。Windows程序可以通过DirectX访问外设,运行在网页的JavaScript程序可以通过浏览器提供的Gamepad API。
EV3主机运行的是Debian Linux,那么Python程序如何在Linux下读取手柄的输入?
在Linux中,系统把每个外设都映射为文件,每个设备的输入也被映射为文件。 /proc 目录挂载的就是Linux的虚拟文件系统,映射Linux的进程信息和设备信息。我们登录到EV3,查看 /proc/bus/input/devices 文件:
$ cat /proc/bus/input/devices I: Bus=0000 Vendor=0000 Product=0000 Version=0000 N: Name="LEGO MINDSTORMS EV3 Speaker" P: Phys= S: Sysfs=/devices/platform/sound/input/input0 U: Uniq= H: Handlers=kbd event0 B: PROP=0 B: EV=40001 B: SND=6 I: Bus=0019 Vendor=0001 Product=0001 Version=0100 N: Name="EV3 Brick Buttons" P: Phys=gpio-keys/input0 S: Sysfs=/devices/platform/gpio_keys/input/input1 U: Uniq= H: Handlers=kbd event1 B: PROP=0 B: EV=3 B: KEY=1680 0 0 10004000
这个文件列出了目前系统可用的输入设备,上述两段分别代表扬声器和按钮设备。如果我们把蓝牙手柄连接到EV3,再查看文件,发现多了一段内容:
I: Bus=0005 Vendor=057e Product=2009 Version=0001 N: Name="Pro Controller" P: Phys=00:17:ec:13:d8:1d S: Sysfs=/devices/platform/soc@1c00000/serial8250.2/tty/ttyS2/hci0/hci0:2/0005:057E:2009.0003/input/input4 U: Uniq=00:90:e3:9b:ec:e9 H: Handlers=event2 B: PROP=0 B: EV=10001b B: KEY=ffff0000 0 0 0 0 0 0 0 0 0 B: ABS=3001b B: MSC=10
我们通过 N: Name="xxx" 来搜索手柄设备,然后,通过 H: Handlers=xxx 获取设备的输入文件。例如,上述蓝牙手柄的名称是 Pro Controller ,输入是 event2 ,对应到系统文件就是 /dev/input/event2 。Python代码实现如下:
定义 InputDevice 类表示输入设备:
class InputDevice(): def __init__(self): self.name = '' self.handler = '' def __str__(self): return '<Input Device: name=%s, handler=%s>' % (self.name, self.handler) def setName(self, name): if len(name) >= 2 and name.startswith('"') and name.endswith('"'): name = name[1:len(name)-1] self.name = name def setHandler(self, handlers): for handler in handlers.split(' '): if handler.startswith('event'): self.handler = handler
定义函数 listDevices() 通过读取 /proc/bus/input/devices 文件获取所有设备:
def listDevices(): devices = [] with open('/proc/bus/input/devices', 'r') as f: device = None while True: s = f.readline() if s == '': break s = s.strip() if s == '': devices.append(device) device = None else: if device is None: device = InputDevice() if s.startswith('N: Name='): device.setName(s[8:]) elif s.startswith('H: Handlers='): device.setHandler(s[12:]) return devices
定义函数 detectJoystick() 通过名字模糊查找手柄设备:
def detectJoystick(joystickNames): for device in listDevices(): for joystickName in joystickNames: if joystickName in device.name: # 返回输入文件: return '/dev/input/%s' % device.handler # 未找到返回None: return None
搜索到手柄设备后打开文件读取输入:
eventFile = detectJoystick(['Controller']) if eventFile: with open(eventFile, 'rb') as infile: while True: # 读取输入
现在最关键的问题来了: eventX 文件应该以何种格式读取?
让我们搜索一下Linux文档,在 input.txt 中详细说明了 eventX 文件的输入格式。Linux系统把每一个输入事件都封装为一个C结构体让我们直接读取:
struct input_event { struct timeval time; // 16字节时间戳 unsigned short type; // 2字节事件类型 unsigned short code; // 2字节事件代码 unsigned int value; // 4字节事件值 };
每次读取24字节并按照C的 struct 类型解码,即可得到手柄输入的全部信息。在Python程序中,可以用 struct 读取:
FORMAT = 'llHHI' EVENT_SIZE = struct.calcsize(FORMAT) with open(eventFile, 'rb') as infile: while True: event = infile.read(EVENT_SIZE) _, _, t, c, v = struct.unpack(FORMAT, event) print('t = %s, c = %s, v = %s' % (t, c, v))
注意到 unpack() 方法的返回值,我们丢弃了前两个8字节整数,保留了 t 、 c 、 v ,分别是2字节无符号整数、2字节无符号整数和4字节无符号整数。
根据Linux文档,再打印出每个事件的详细数据,把手柄的按键和摇杆都按一遍,就可以得到按键编码如下:
t==1 时表示按键,此时 v==1 表示按下, v==0 表示释放, v==2 表示持续按下,对应的 c 表示按键编码。要检测 A 、 B 按钮按下,可以这么写:
if t == 1 and v == 1: if c == 305: # Button A pressed pass if c == 304: # Button B pressed pass
摇杆数据则比较复杂,类型 t==3 表示摇杆,如果 c==0 ,表示左摇杆的左右移动,如果 c==1 ,表示左摇杆的上下移动, v 的值介于 0 ~ 65535 , 32768 表示中心值,越往两侧偏移越多则越接近最大和最小值:
0 ▲ │ 0 <───┼───> 65535 │ ▼ 65535
现在我们就可以通过一个读取手柄的无限循环来控制小车:
def joystickLoop(robot, eventFile): FORMAT = 'llHHI' EVENT_SIZE = struct.calcsize(FORMAT) with open(eventFile, 'rb') as infile: while True: event = infile.read(EVENT_SIZE) _, _, t, c, v = struct.unpack(FORMAT, event) if t == 1 and v == 1: if c == 305: # A键加速: robot.setSpeed(1) elif c == 304: # B键减速: robot.setSpeed(-1) elif c == 307: # X键退出: return robot.inactive() elif t == 3: if c == 1: # 左摇杆上下移动: speed = 0 if v < 32768: # 加速: speed = 1 elif v > 32768: # 减速: speed = -1 robot.setSpeed(speed)
但是另一个问题来了:主线程通过死循环读取手柄输入,那么怎么读取超声波传感器的数据?
可以利用Python的 threading 启动多线程,在另一个线程中不断检测超声波传感器,并在条件达到的时候自动停车:
def autoStopLoop(robot): while robot.active: if robot.speed > 0 and robot.ultrasonic.distance() < 200: robot.setSpeed(0) wait(100) joystickEvent = detectJoystick(['Controller']) robot = Robot() t = threading.Thread(target=autoStopLoop, args=(robot,)) t.start() joystickLoop(robot, joystickEvent)
到此为止,一个蓝牙手柄控制的机器人程序就宣告完成。试试效果:
参考源码
rccar