2025年4月14日 星期一 乙巳(蛇)年 正月十五 设为首页 加入收藏
rss
您当前的位置:首页 > 计算机 > 编程开发 > Python

MicroPython 遥控小车

时间:03-29来源:作者:点击数:52

使用EV3主机上的按键控制小车还是比较麻烦,能不能通过遥控来控制小车呢?

当然可以!

乐高提供了一个红外线传感器和遥控器:

remote-control

但是红外遥控器需要很强的方向性,而且那个遥控器太简陋了,连个摇杆都没有。

还有一种方式是使用蓝牙手柄,这样我们就可以找一个游戏手柄直接通过蓝牙控制小车,太方便了有木有!

我找了一个任天堂的游戏手柄:

joystick

理论上支持蓝牙的游戏手柄都是可用的,标准游戏手柄按键如下:

  • ┌──────┐ ┌──────┐
  • ┌┴─────┐│ ┌┴─────┐│
  • ┌─┴──────┴┴──────────────────┴──────┴┴─┐
  • │ ┌─┐ ┌─┐ ┌─┐ │
  • │ ┌─┐ └─┘ └─┘ ┌─┐│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字节整数,保留了tcv,分别是2字节无符号整数、2字节无符号整数和4字节无符号整数。

根据Linux文档,再打印出每个事件的详细数据,把手柄的按键和摇杆都按一遍,就可以得到按键编码如下:

t==1时表示按键,此时v==1表示按下,v==0表示释放,v==2表示持续按下,对应的c表示按键编码。要检测AB按钮按下,可以这么写:

  • 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~6553532768表示中心值,越往两侧偏移越多则越接近最大和最小值:

  • 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

方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门