好久没写blog最近把这一年乱七八糟的工作做个总结吧。这一年工作主要和开发板这块结合的比较多,公司要给别人做一些小游戏,然后为了节约成本,我们采用的Orange Pi PC2这样一块开发板,至于什么是Orange Pi 不知道的话这里有一个官网http://www.orangepi.org/orangepipc2/。其实就相当于一台手机不过跟手机不同的是他支持好多底层的接口。最初选择这个的原因是因为它比较便宜,才145RMB。当然 买回来只是一块没有系统的裸板这个时候我们需要给这块开发板刷系统。这里我们需要一个Android5.0系统,当然官网它哪里也有现成的系统但是好多功能无法提供,例如串口通信,所以需要我们自己定义引脚。至于引脚怎么定义这里就不介绍了,这里我可以提供2个Android5.0的系统一个横版一个竖版,并且对串口做了定义,可以直接用,如果有需要的话可以qq联系我或者我将它放到百度网盘上。既然对串口做了定义接下来就是我们的Unity写的程序如何和我们的开发板进行通信的问题了。说了串口通信PC端会比较简单因为C#封装了对串口的操作,所以就比较简单但是Unity对Android串口通信可没有封装,所以这里我们只有自己写jar包然后通过Unity来调用Jar包来实现通信。串口通信的Android jar包和Unity调用Jar包里面的方法这个网络上好多bolg都有,大家可以依葫芦画瓢就可以自己折腾出来,同样这里我也可以把这个jar提供出来。接下来就是Unity如何调用Jar包里面的方法及如何封装通信这块。首先定义一个抽象类,这个抽象类的作用就是封装四个方法,分别是打开串口,关闭串口,发送数据,接受数据。因为PC端也存在串口通信,所以我们可以将这所有串口操作封装成一个整体,打包成一个Dll。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
public abstract class BaseSerialport
{
public abstract int Open();
public abstract void Close();
public abstract byte[] Read(int len);
public abstract void Write(byte[] buf);
}
然后建一个Android串口通信的类来继承这个基类。定义为AndroidSerialport。
using UnityEngine;
namespace Android.IO
{
public class AndroidSerialport : BaseSerialport
{
private AndroidJavaObject javaObj = null;
private AndroidJavaObject GetJavaObject()
{
if (javaObj == null)
{
javaObj = new
AndroidJavaObject("com.SerialportApi.SerialPortReadWrite");
}
return javaObj;
}
public override void Close()
{
GetJavaObject().Call("Close");
}
public override byte[] Read(int len)
{
return GetJavaObject().Call<byte[]>("Read", len);
}
public override void Write(byte[] buf)
{
GetJavaObject().Call("Write", buf);
}
public override int Open()
{
return GetJavaObject().Call<int>("Open", new object[] { "/dev/ttyS2", 115200, 8, 'N', 1 });
}
}
}
同样给出PC端串口通信的核心代码:
public class PcSerialport : BaseSerialport
{
private SerialPort _serialPort;
private byte[] _readBuf;
public override int Open()
{
_serialPort = new SerialPort("COM1", 9600, Parity.None, 8, StopBits.One);
_serialPort.ReadTimeout = 1000;
_serialPort.WriteTimeout = 1000;
try
{
_serialPort.Open();
}
catch (Exception exception)
{
Debug.LogError("ssssssssss");
}
if (_serialPort.IsOpen)
return 0;
return 1;
}
public override byte[] Read(int len)
{
try
{
_readBuf = new byte[len];
_serialPort.Read(_readBuf, 0, len);
}
catch (TimeoutException)
{
}
return _readBuf;
}
public override void Write(byte[] buf)
{
_serialPort.DiscardInBuffer();
_serialPort.Write(buf, 0, buf.Length);
}
public override void Close()
{
_serialPort.Close();
}
}
PC端这边打开的串口1,Pc端这个打开串口方法应该定义传进去的串口参数和频率,而不应该写死,这里主要是为了图简单。
接下来就是具体具体处理数据这样一个模块,因为从串口读数据的时候他存在粘包的情况,具体的解释就是当前帧你要从串口缓存里面读4个字节的数据,但是你有可能只读了2个字节的数据,而下一帧的时候才读到了4个字节的数据所以这6个字节的数据它的协议头和协议尾需要自己去判断,这里我们将其定义为IOMrg,然后最主要就是这样一个read方法。我们将其定义为OnRead,然后方法的主体就是如何将读到的数据存到一个list里面然后每帧去取数据然并且取到的数据是你想要的数据。
private void OnRead(int head, int tail,int headtailOffset)
{
if (!_bOpen)
return;
int sindex = 0;
int fag = 0;
bool isHead = false;
bool isTail = false;
byte[] buff = _serialport.Read(_readBufSize);
string str = "";
for (int i = 0; i < buff.Length; i++)
{
str += buff[i] + ",";
}
int num = buff.Length;
if (num > 0)
{
for (int i = 0; i < num; i++)
{
_buf.Add(buff[i]);
}
for (int i = 0; i < _buf.Count; i++)
{
if (_buf[i] == head && !isHead)
{
sindex = i;
fag = -1;
isHead = true;
isTail = false;
}
if (_buf[i] == tail && !isTail)
{
fag = 1;
var eindex = i;
bool b1 = _buf.Count > (sindex + _readBufSize - 1);
bool b2 = (eindex - sindex) == headtailOffset;
if (b1 && b2 && isHead)
{
isTail = true;
isHead = false;
int k = 0;
for (int j = sindex; j <= (sindex + _readBufSize - 1); j++)
{
_userReadBuf[k] = _buf[j];
k++;
}
VerifyData();
}
}
}
if (fag == 1)
_buf.Clear();
else if (fag == -1)
{
for (int i = 0; i < sindex; i++)
{
_buf.RemoveAt(0);
}
}
}
}
方法具体的作用是首先从串口缓存中读取数据,然后将读取到的数据放到一个_buf存起来。然后从_buf第0个元素查找判断哪个一个是我们数据的协议头如果是那么再找协议尾,如果找到了判断这个数据大长度是不是我们定义的headtailOffset大小。如果是我们通过VerifyData方法来处理这样一块数据。然后继续读一直到没有找到我们需要的数据,同时那些处理掉的数据我们将它从_buf移除掉。整个过程其实不是很复杂其实就相当于我们大学的里面那种从一串字符串里面找到我们需要的字符串序列。这里VerifyData处理数据根据每个人的需求不同所以具体的处理结果也不相同。
private void VerifyData()
{
if (_isInitied)
_ioReadHandler.VerifyData(_userReadBuf);
if (_coinIoHandler != null)
_coinIoHandler.VerifyData(_userReadBuf);
}
这里的 _ioReadHandler这个变量在定义的时候我们将其定义为BaseIOReadHandler,它是一个基类,所以这里需要我们自己去定义一个自己数据的处理类,因为每个公司他们协议数据不一样。这里给出这个基类的一些属性及抽象方法。
using System.Collections.Generic;
namespace Android.IO
{
public abstract class BaseIOReadHandler
{
public int Head { set; get; }
public int Tail { set; get; }
public int HeadTailOffset { set; get; }
public int ReadSize { set; get; }
public List<BaseIOEvent> IoEvents { set; get; }
public abstract void Initi();
public abstract void VerifyData(byte[] bytes);
public abstract void HandlerData(BaseIOEvent ioEvent);
}
}
首先有属性数据的协议头,协议尾及数据长度,及一个存储数据的list,然后有一个初始化方法(主要定义数据的协议头,数据的协议尾,数据的长度还有list初始化等)VerifyData方法主要是判断取到的数据是否为我们有用数据如果有用将它存放到我们IoEvents缓存中,HandlerData方法就是处理具体的数据定义了,比如取到数据它是左键按下或者右键按下了等。
这里给出一个例子
public class JoyStickIOReadHandler : BaseIOReadHandler
{
private IOEvent _preEvent1;
private IOEvent _preEvent2;
public override void Initi()
{
Head = 0x1F;
Tail = 0x2F;
ReadSize = 5;
HeadTailOffset = 4;
IoEvents = new List<BaseIOEvent>();
_preEvent1 = new IOEvent();
_preEvent2 = new IOEvent();
}
public override void VerifyData(byte[] bytes)
{
bool ishead = bytes[0] == Head;
bool istail = bytes[ReadSize - 1] == Tail;
IOEvent cevent = new IOEvent();
if (ishead && istail)
{
cevent.Initi(bytes);
if (!cevent.IsEqual(_preEvent2))
{
_preEvent2 = cevent;
IoEvents.Add(cevent);
}
}
}
public override void HandlerData(BaseIOEvent ioEvent)
{
var tt = ioEvent as IOEvent;
if (tt != null)
{
if (tt.OneByteClickDown(0, _preEvent1))
{
EventManager.TriggerEvent(new FirstByteClick1DownEvent());
}
if (tt.OneByteClickDown(1, _preEvent1))
{
EventManager.TriggerEvent(new FirstByteClick2DownEvent());
}
if (tt.OneByteClickDown(2, _preEvent1))
{
EventManager.TriggerEvent(new FirstByteClick3DownEvent());
}
if (tt.OneByteClickDown(3, _preEvent1))
{
EventManager.TriggerEvent(new FirstByteClick4DownEvent());
}
if (tt.OneByteClickDown(4, _preEvent1))
{
EventManager.TriggerEvent(new FirstByteClick5DownEvent());
}
if (tt.OneByteClickDown(5, _preEvent1))
{
EventManager.TriggerEvent(new FirstByteClick6DownEvent());
}
if (tt.OneByteClickDown(6, _preEvent1))
{
EventManager.TriggerEvent(new FirstByteClick7DownEvent());
}
if (tt.OneByteClickDown(7, _preEvent1))
{
EventManager.TriggerEvent(new FirstByteClick8DownEvent());
}
if (tt.OneByteClickUp(0, _preEvent1))
{
EventManager.TriggerEvent(new FirstByteClick1UpEvent());
}
if (tt.OneByteClickUp(1, _preEvent1))
{
EventManager.TriggerEvent(new FirstByteClick2UpEvent());
}
if (tt.OneByteClickUp(4, _preEvent1))
{
EventManager.TriggerEvent(new FirstByteClick5UpEvent());
}
_preEvent1 = tt;
}
}
}
这是一个简单的左右摇杆及确定按钮操作。由于我们下位机单片机程序采用的是查询式的工作模式,即我们程序需要发送数据到下位机,然后下位机返回它当前的运行状态。还有这个Android串口通信不能用线程来操作,所以我们需要将发送数据模块放到FixedUpdate里面来固定更新。所以如果是PC端的话我们需要用到线程来接受和发送数据。所以在发布平台的时候我们 _serialport = new 不同的串口通信处理类。具体的类我就不粘贴出来了,我直接放到百度网盘。链接: https://pan.baidu.com/s/1hjb5WOOKuaxFG5Gf0VVrdQ 提取码: wd9e