继续树莓派小车的内容,这次记录手柄控制小车运动的实现。
对于手动控制小车的工具,大概有这么几种:
①用红外遥控器,小车上放一个接收器,读取遥控器信息。实现应该比较简单,红外收发元件也很便宜,不过遥控器得对着小车,恐怕不太方便;
②蓝牙手柄,因为树莓派带蓝牙,可以通过蓝牙接收手柄数据,不过一个蓝牙手柄可不便宜;
③有线手柄,相比无线设备肯定low一点,不过我手头就只有一个有线手柄,50多块钱的小鸡G3,就是个xbox 360手柄;
④手机,如果写个带方向键的手机客户端来控制小车应该是最酷的了,不过我不会Android或IOS开发。
最终选择用有线手柄控制小车,而控制方式不是把手柄连到树莓派上,而是连到我的笔记本上,用C#写个客户端程序,一方面读取手柄数据,一方面与树莓派通信。至于通信,采用TCP通信,树莓派上运行服务器程序,接收控制信息,电脑上运行客户端程序,发送手柄数据。
现在主要的问题是怎么读取手柄数据呢?通过网络搜索,发现可以使用SharpDX库在.NET平台下读取手柄输入数据,SharpDX是一个全新的、开源的、封装了 DirectX API的项目(Wiki文档链接:http://sharpdx.org/ ,目前还不全),其支持的API包括2D和3D渲染、音频、设备输入等方面,貌似游戏开发中使用的挺多的。不过我不需要用到那么多功能,只需要使用XInput库就行,XInput支持XBox360手柄的数据读取。关于SharpDX及XInput的教程或文档有点难找,Wiki上的官方文档只是接口说明,而且现在还不全;微软官方文档上可以找到XInput的相关文档,不过它是针对C++的;我在Stack Overflow找到一个回答,介绍了SharpDX.XInput的简单使用方法(https://stackoverflow.com/questions/39109609/how-to-use-xbox-one-controller-in-c-sharp-application/39109610#39109610)。
首先需要下载SharpDX.XInput库,可以在Visual Studio的Nuget包管理器里直接下,不过如果直接下SharpDX.XInput会提示需要依赖SharpDX库,因此先下SharpDX库。下好SharpDX后再下载SharpDX.XInput竟然还是报错,说缺少SharpDX依赖,这时可以先卸载Nuget管理器再重安装(“工具”->“扩展和更新”),这样就可以顺利下载了。
添加XInput的引用using SharpDX.XInput;
,然后可以写一个手柄控制类:
class XInputController
{
Controller controller;
Gamepad gamepad;
public bool connected = false;
public XInputController()
{
controller = new Controller(UserIndex.One);
connected = controller.IsConnected;
}
///
/// 读取方向键信息
///
///
public string GetDirection()
{
if (!controller.IsConnected)
return null;
gamepad = controller.GetState().Gamepad;
GamepadButtonFlags flag = gamepad.Buttons;
int resultStart = ((int)flag) & 0x10;
int resultBack = ((int)flag) & 0x20;
int resultUp=((int)flag) & 0x01;
int resultDown=((int)flag) & 0x02;
int resultLeft=((int)flag) & 0x04;
int resultRight=((int)flag) & 0x08;
if (resultStart != 0)
return "start";
else if (resultBack != 0)
return "back";
else if (resultUp != 0)
return "up";
else if (resultDown != 0)
return "down";
else if (resultLeft != 0)
return "left";
else if (resultRight != 0)
return "right";
else
return "undefine";
}
}
这个类代码很短,因为我只需要读取方向键、start和back键,如果要读取摇杆和震动数据的话就需要麻烦一些了。Controller对象就代表了手柄设备,初始化的时候指定了设备号为“UserIndex.One”,总共好像可以有四个设备;Gamepad对象存储了手柄当前的状态信息,其中有一个结构体为GamepadButtonFlags属性,它以二进制位的形式存储了按键信息,比如0x0001就表示“上”方向键处于按下状态,0x0002就表示“下”方向键处于按下状态,在上述代码中,我读取Buttons属性,判断每一位的状态来检测哪个键被按下,如果是没有键被按下或者除了方向键、start和back键外的键被按下,就返回“undefine”。
新建一个Winform项目,编写PC客户端程序。因为要做TCP客户端,需要使用socket相关类;另外也需要考虑读取手柄信息的方式,是事件机制还是主动轮询,怎么处理手柄的事件我还不知道怎么写,我采用定时器的方式主动查询手柄状态。下图是PC端程序的流程图。
下面是程序主体代码:
public partial class HandleControlForm : Form
{
private TcpClient client = null;
private NetworkStream streamToServer = null;
private XInputController controller = null;
public HandleControlForm()
{
InitializeComponent();
this.button1.Enabled = false;
this.timer1.Interval = 100;
this.timer1.Stop();
}
private void HandleControlForm_Load(object sender, EventArgs e)
{
//手柄初始化
controller = new XInputController();
if (controller.connected)
{
this.textBox1.Text = "the handle has connected...\r\n";
this.button1.Enabled = true;
}
}
///
/// 启动
///
private void button1_Click(object sender, EventArgs e)
{
//连接小车
client = new TcpClient();
try
{
client.Connect(IPAddress.Parse("192.168.1.17"), 5150);
}
catch
{
this.textBox1.Text += "connect to the car falled...\r\n";
this.button1.Enabled = false;
return;
}
this.textBox1.Text += "connect to the car successfully...\r\n";
streamToServer = client.GetStream();
this.timer1.Start();//开始接收手柄输入
this.button1.Enabled = false;
}
///
/// 定时器处理
///
private void timer1_Tick(object sender, EventArgs e)
{
string input = null;
input = controller.GetDirection();
if (input == null)
{
this.textBox1.Text += "the handle disconnect...\r\n";
return;
}
this.textBox1.Text += input + "\r\n";
//向小车发送控制指令
byte[] buffer = Encoding.UTF8.GetBytes(input);
streamToServer.Write(buffer, 0, buffer.Length);
if (input == "back")
{
//关闭
this.timer1.Stop();
streamToServer.Close();
client.Close();
streamToServer = null;
client = null;
this.button1.Enabled = true;
this.textBox1.Text += "shut down the connection...\r\n";
}
}
private void textBox1_TextChanged(object sender, EventArgs e)
{
this.textBox1.SelectionStart = this.textBox1.Text.Length;
this.textBox1.ScrollToCaret();
}
}
在发送数据到小车的代码中,是把字符串编码为UTF-8格式的,一开始我用的UNICODE编码,结果树莓派上的Python程序接收乱码,因为Python中的网络数据解析默认是按照UTF-8格式的。下图是程序界面,用了一个文本框显示程序信息。
树莓派上的Python程序主要由电机控制模块和TCP服务器代码组成,程序流程如下图所示:
Python代码如下:
#! /usr/bin/python3
import socket
import re
import Motor_Module
print("init motor module...")
try:
motor=Motor_Module.Motor_Module()
motor.setup()
except:
print("init motor module fail...")
exit()
server=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
host='192.168.1.17'
port=5150
try:
server.bind((host,port))
except:
print("socket bind fail...")
exit()
server.listen(5)
print("listening for connect...")
client,addr=server.accept()
print("Accept the connect , now receving commands...")
duration=1
#不断接收指令
while True:
data=client.recv(1024)
info=bytes.decode(data) #解码字符串
if re.match('back',info):
break
elif re.match('up',info):
print("move ahead")
motor.ahead()
elif re.match('down',info):
print("move rear")
motor.rear()
elif re.match('left',info):
print("move left")
motor.left()
elif re.match('right',info):
print("move right")
motor.right()
elif re.match('undefine',info):
print("no control command")
motor.stop()
print("end the connection...")
motor.stop()
client.close()
在接收处理的代码中, 我用match方法匹配相应的关键词而不是直接用等于号,是因为TCP传输中可能会出现拆包、黏包等现象,用match方法稳妥一些;对于“undefine”指令,直接停止小车,而受到“back”指令时终止程序。
首先启动树莓派上的程序监听连接,然后启动PC端程序,手柄需要提前连接上,否则PC程序上的按钮不可用;由于程序用定时器轮询手柄,并且设置了“undefine”指令,小车可实现随时停止的效果,长按方向键可以持续运行,松开按钮小车立即停止;此外,可以在树莓派上插上摄像头,后台运行mjpg-streamer,在PC端网页查看实时视频,这样就可以实现简易的远程控制了。