android下的串口通讯,为毛我总遇到这样的变态需求呢。
前言
随着智能化硬件的发展android跟智能硬件打交道的越来越常见。而串口通讯是硬件之间最常见的通讯方式,所以android下的串口通讯也可能在某些项目中运用到。
串口开源项目
目前android下用到的串口通讯都是由Google提供的开源项目
此项目下下来正常情况下是能直接使用的。如果想更深入的了解其更多知识可以查看链接中的wikis。
但是:直接能使用时因为他已经将so已经打包生成好了,如果你将.so放在你的项目中你会发现是不能使用的,原因是因为so中的方法名是通过开源项目的包名+方法名来的。放在你项目中包名都变了,所以so文件将无法找到对应的方法的,这样我们还是得自己生成so文件。
使用
因为开源项目是eclipse的,而我们现在更多使用的是android studio,如果你还没有使用android studio那就可以直接使用,但是你确定还不使用android studio么?
在android下可以下载一个“串口调试助手”App可以对你写的程序做对比。快速验证问题。
原理
android下的串口通讯是使用jni来使用的,如果你还不太了解Jni使用可以查看我另外一篇文章,其中详细介绍了JNI的使用。
上面这篇文章也是基于串口下的jni写的,所以本篇文章并不介绍JNI的使用。只是说直接使用串口通讯
原理:Google开源项目直接提供了SerialPort、SerialPortFinder两个主要的类。这两个类提供了打开/关闭串口的方法。然后我们需要将java下的打开和关闭串口方法生成.h文件,然后实现.c方法;然后相应的生成.so文件。.so文件没问题之后才能正常使用。
实现
步骤一:设置串口号与波特率
我们知道串口通讯都是通过串口号与波特率来的,这跟我们tcp必须知道ip、port一样的原理。
public SerialPort getSerialPort() throws SecurityException, IOException, InvalidParameterException {
if (mSerialPort == null) {
/* Read serial port parameters */
SharedPreferences sp = getSharedPreferences("android_serialport_api.sample_preferences", MODE_PRIVATE);
String path = sp.getString("DEVICE", "");
int baudrate = Integer.decode(sp.getString("BAUDRATE", "-1"));
Log.e("TAG","path="+path+" baudrate="+baudrate);
/* Check parameters */
if ( (path.length() == 0) || (baudrate == -1)) {
throw new InvalidParameterException();
}
/* Open the serial port */
mSerialPort = new SerialPort(new File(path), baudrate, 0);
}
return mSerialPort;
}
<code>mSerialPort = new SerialPort(new File(path), baudrate, 0);</code>
这里面有3个参数;第一个参数为串口路径,第二个参数为波特率,第三个为状态
前面两个参数我们都是知道的一般为:
<code>private static int baudrate = 115200; //波特率
private static String path = "/dev/ttyS3"; //路径</code>
请根据自己的为准。
步骤二:打开串口
打开和关闭串口是使用jni来调用c方法的。我们在上层调用打开串口的方法
Log.e(TAG,"串口打开");
mSerialPort = serialPortUtil.getSerialPort(TokenCommon.ROBOT_SERIALPORT_BAUDRATE,TokenCommon.ROBOT_SERIALPORT_PATH);
mOutputStream = mSerialPort.getOutputStream();
mInputStream = mSerialPort.getInputStream();
/* Create a receiving thread */
mReadThread = new ReadThread();
mReadThread.start();
打开串口成功之后会开启一个线程一直读取数据。
private class ReadThread extends Thread {
@Override
public void run() {
super.run();
while(!isInterrupted()) {
int size;
try {
byte[] buffer = new byte[64];
if (mInputStream == null) return;
size = mInputStream.read(buffer);
if (size > 0) {
onDataReceived(buffer, size);
}
} catch (IOException e) {
e.printStackTrace();
return;
}
}
}
}
如果读取数据会调用onDataReceived方法。onDataReceived个方法为接收方法,如果你需要那个类接收串口数据只要实现该方法就好了,下面我们会在数据接收的方法中说到
步骤三:发送串口数据
先看开源项目的发送源码:
EditText Emission = (EditText) findViewById(R.id.EditTextEmission);
Emission.setOnEditorActionListener(new OnEditorActionListener() {
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
int i;
CharSequence t = v.getText();
char[] text = new char[t.length()];
for (i=0; i<t.length(); i++) {
text[i] = t.charAt(i);
}
try {
mOutputStream.write(new String(text).getBytes());
mOutputStream.write('\n');
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
});
因为项目是直接将edittext输入的字符串转为byte数据直接发送。而我们项目可能是直接发送byte数组,所以我修改发送源码为:
private void sendSerialData(){
String content = ""; //无数据内容
int dateLength = ByteUtil.getContentLength(content);//数据长度
byte by[] = new byte[6];
by[0] = TokenCommon.ANDROIDSENDROBOT; //协议头
by[1] = TokenCommon.ANDROIDDEVICEID; //设备id
by[2] = (byte) dateLength; // 数据长度
by[3] = (byte) (~dateLength); // 数据长度取反
by[4] = TokenCommon.REQUESTGETROBOTDATA; // 命令字:获取主控信息
by[5] = ByteUtil.getCheckSum(by);
try {
if(null != mOutputStream){
mOutputStream.write(by);
} else {
Log.e(TAG,"串口打开失败");
}
} catch (IOException e) {
e.printStackTrace();
}
}
将自己的指令按照协议组成一个byte[],然后直接发送byte数组
然后我们来验证发送是否成功。我们可以将串口连接电脑在电脑上开一个串口调试助手,然后选择自己的串口号与波特率

我们可以看到接收区接收到了我们发送的数据,则证明发送是正常的
步骤四:接收串口数据
个人觉得串口最蛋疼的就是接收数据了,因为他是一个字符一个字符传过来的,哪怕你的是一条完整的数据,他也是一个一个字符接收。他不像我们的tcp或udp一样发送一条完整数据除非过长或网络造成的数据丢失,我们是能一次性接收一条数据的。
而串口一个一个字符接收的,而且还有可能接收的数据是乱的,还有可能接收的数据是不完整的,还有可能接收到的数据在下一条在给你,多么坑啊。好吧既然这样我们也是有办法的,这样就需要我们就必须做数据的拼接。
我们先看开源项目的接收源码
@Override
protected void onDataReceived(final byte[] buffer, final int size) {
runOnUiThread(new Runnable() {
public void run() {
if (mReception != null) {
mReception.append(new String(buffer, 0, size));
}
}
});
}
注意1:onDataReceived是实现的一个抽象方法,因为源码已经封装只要一旦有数据就会调用此方法。在步骤二中可以看到。
注意2:此方法接收数据是已字符串接收的,并且每做任何的数据处理,只是单纯的字符串追加之后显示数据,这肯定是无法满足我们的需求的,因为我们接收的数据也很有可能是一个byte数组。
所以我们自己的自己写数据处理播放。
@Override
protected void onDataReceived(final byte[] buffer, final int size) {
//获取串口传过来的数据
System.arraycopy(buffer, 0, b, unDisposeLen, size);
unDisposeLen += + size;
int temp = 0; //数组下标
while(temp <= unDisposeLen){
Log.e(TAG, "****************************"+Arrays.toString(b));//字节数组打印
//判断数据长度是否足够:
if(unDisposeLen - temp >= FormatToken.DATA_MIN_LEN){
//分别判断校验头、设备id、数据长度(数据长度和数据长度校验判断是否相等)
if(b[temp] == FormatToken.head && (b[temp + 1] == FormatToken.deviceId) && b[temp + 2] == ~b[temp + 3]){
int dataLen = (int)b[temp + 2];
Log.e(TAG,"数据长度="+dataLen);
//判断校验和位数据是否相等;接收到的校验和与发送的校验和相等
//将完整数据减掉最后的chesum之后得到一个byte[]
byte che[] = new byte[FormatToken.DATA_MIN_LEN + dataLen - 1];
System.arraycopy(b, temp, che, 0, FormatToken.DATA_MIN_LEN + dataLen - 1);
//将减掉最后的chesum之后得到一个byte[]校验得到chesum
byte chesum = ByteUtil.getCheckSum(che); //得到chesum
//判断计算出来的chesum与命令中的chesum是否相等
if(b[temp + dataLen + 5] == chesum){
//chesum相等证明为一条完整数据
byte by[] = new byte[FormatToken.DATA_MIN_LEN + dataLen];
//将完整数据从总数据中copy出来
System.arraycopy(b, temp, by, 0, FormatToken.DATA_MIN_LEN + dataLen);
Log.e(TAG, "完整数据=" + Arrays.toString(by));//字节数组打印
temp = temp + by.length; //移动下标
byte[] ret = new byte[unDisposeLen - temp];
//将为处理的数据copy到ret数组中
System.arraycopy(b,temp,ret,0,ret.length);
//将未处理的数据ret替换从0开始替换到
System.arraycopy(ret,0,b, 0 ,ret.length);
Log.e(TAG, "&&&&&&&&&&&&&&&&&&&&&&&&&&&&" + Arrays.toString(b));//字节数组打印
unDisposeLen = unDisposeLen - temp; //存储未完成的数据长度
temp = 0;
if(temp == unDisposeLen) {
temp = 0 ;
unDisposeLen = 0;
break;
}
} else {
temp++;
}
} else {
temp++;
}
} else {
break;
}
}
if(temp == unDisposeLen) {
unDisposeLen = 0;
}
}
上面的代码主要是将一个一个字符数据根据你的协议拼接成一条完整的数据。代码中的注释已经写的很清楚了。
然后我们验证一下:用电脑调式工具发送内容

然后看android下的数据接收

到此证明数据的接收木有问题。
思路是:获取一段串口数组数据,判断数据中是否包含协议头和设备id,如果没有则继续拼接字符串,如果有则判断数组的长度是否大于最小协议长度,如果小于则继续追加,如果大于则根据数据长度获取数据部分,然后将数据部分加上协议头等数据组成一个数组,然后判断数组检验码是否相同,如果相等证明是一条完整数据,并将拼接的数据移除掉完整数据部分(避免重复解析已经处理过的数据。)如果不是则丢弃数据。
demo的github地址 https://github.com/SouvDc/SerialPort_project
完结
OK,到这里我们已经能完整解析出数据,但是一定要根据你自己的协议来调整。
慢慢努力做好身边所有的事
求知若饥,虚心若愚
网友评论
try {
byte[] buffer = new byte[1024];
if (mInputStream == null) return;
size = mInputStream.read(buffer);
if (size > 0) {
onDataReceived(buffer, size);
}
} catch (IOException e) {
e.printStackTrace();
return;
}
我现在做的这个项目是做一个Android app,实现串口调试功能,可以理解为把windows系统上的串口调试工具,在Android上实现出来。第一次接触串口这方面的东西,有点懵,有些关于串口的问题想跟你请教一下。
我的设备是使用的3线串口。我调试的时候是把TXD和RDX接在一起。使用了一个网友基于google串口修改过的一个库。
发送和你的步骤三基本一样,直接通过OutputStream对象write(字符串.getBytes())
但是没有进行by[0],by[1]这些,
1. 这是不是就是没有定义协议,直接就是裸的?
接收是另开个进程,使用InputStream.read(buffer)读取,和你步骤二中类似,这样的做法现在的问题是我发现,发送东西的时候会截断。比如我发送qwerty,会分多次读取,第一次读取werty,第二次循环读到q,乱序的。
2. 我遇到的这种现象是不是就是你在步骤四:接收串口数据中提到的那些坑?
3. 因为有这些问题,所以需要进行粘包操作什么?
4. 如果使用自己的数据处理方法,这是不是就是自己的私有协议?
5. 那是否就意味着,不能跟其他的串口设备进行通讯了呢?因为其他设备无法知道你是怎么解析数据的,也就不能使用正确的方法发送数据。
6. 串口通信难道没有一个通用的通信协议或者标准么?为什么一定要自己实现协议呢?可是反过来想,如果没有统一的协议标准,那网上那么多串口调试工具是如何通信的?
如果你方便的话可否指教一下我的这些疑问?
另外串口传输文件的话是否是需要在java层去实现xmodem,ymodem,zmodem之类的协议?
1、因为串口通讯不是一段一段读取的(类似socket通讯)所以就会出现“截断”的现象。
2、是的
3、对,粘包其实就是将“截断”的数据组合起来,组合成一条完整的数据。
4、是的
5、能跟其他设备进行通信,只是获取到的数据,不是根据你的协议来解析的。所以还是获取的到,只是如果其他设备的通讯协议跟你的不一样就无法知道他的通讯协议是什么意思,也就是说无法解析,如果你知道其他设备的通讯协议,也就可以按照此协议来进行解析。
6、你说的标准应该理解的不一样,比如:我买了一个打印机设备,打印机连接到一个程序,并能进行打印。那是因为这个程序按照了通讯协议来进行打印。但是如果你理解成一个通用的标准协议,那是不是以为着所以的人,只要连接上了串口,就能打印出东西?那岂不是说这个东西很容易就被破解了?或者说协议都是公司的秘密(你如果知道其他公司的通讯协议,就可以获取到这个通讯过程中发送和接收了什么?)在如:我们微信支付等等都是由微信根据他自己的一套协议来实现的,里面实现了加密什么的。安全。这就是为什么要自己实现协议的原因。串口调试助手每次发送接收都会带一个\r\n的结束符,这里面的“\r\n”你也可以理解成协议。所以串口调试工具每次解析是否包含\r\n的字符,如果没有,就继续接收数据。如果有,则证明接收到了一条完整的数据。
说的也比较笼统,因为要下班了。
另外,这种需不需要手机root?
邮箱:xiaodong_lxd93@163.com
谢谢!!!
2、读到的数据还没确认是一条完整的一定要保存起来。直到为一条完整数据在处理。
3、数据发送要有一定的格式来确实是否为一条完整数据。
暂时想到就这么多了。
1、先确定串口是否有问题。
2、确定是否权限是否打开。
因为我那时做的时候同时打开多个串口是很顺利的,所以具体什么问题我可能也分析不出来。
byte[] buffer = new byte[64];
if (mInputStream == null) return;
size = mInputStream.read(buffer);
if (size > 0) {
onDataReceived(buffer, size);
}
} catch (IOException e) {
e.printStackTrace();
return;
}
楼主,你好。我也是使用这个开源项目和模拟器操作串口的,目前实现数据的收发,但是现在遇到的一个问题时,使用上面的代码接收数据每次不能接收大于8字节的数据,如果使用串口助手发送超过8字节的数据,模拟器立即死掉。
请问你是否遇到过这种情况????
上面那段接收协议完整的一包数据的,下位机或则串口调试助手最大包时多少个字节??如一个包30字节,请问你的下位机是分包发送,还是一次性发送。
但是我看你上面的串口助手的截图,却是一次性发送了超过8字节数据,请问这有什么细节吗????
因为我这边下位机,一般都是发送超过8字节的完整一包数据,这个问题困扰很久了,如果可以,请回复一下,先谢谢了。。
是单纯的重启串口吗?
你的方案是怎么样的?
指教下?
至于后面的重启就是你理解的意思。
还有你那边的硬件工程师,建议重启10次的意思是不是:
如果重启10次以内,如果成功了,就不重启了,重启次数清零;
如果失败,继续重启,直到成功,最大次数是10次的意思吗?
你说的攻破目前我以为的话最多就是用第三方包保证数据的完整性,然后返回一条完整的数据的接口,但也是在应用层上处理的。
如果在硬件上处理的话,我感觉目前不太可能实现,毕竟它们都是虚拟连接。
Process su;
su = Runtime.getRuntime().exec("/system/bin/su");
String cmd = "chmod 666 " + device.getAbsolutePath() + "\n"
+ "exit\n";
通过su得到权限。
try {
byte[] buffer = new byte[64];
if (mInputStream == null) return;
size = mInputStream.read(buffer);
if (size > 0) {
onDataReceived(buffer, size);
}
} catch (IOException e) {
e.printStackTrace();
return;
}
你刚才是说数据可能接受不完全,,我感觉只需要把byte[] buffer = new byte[64];一次性接受数据该大一点,就可以了,,毕竟设备传递的消息不会是很大,,