android下的串口通訊,為毛我總遇到這樣的變態(tài)需求呢。
前言
隨著智能化硬件的發(fā)展android跟智能硬件打交道的越來越常見。而串口通訊是硬件之間最常見的通訊方式,所以android下的串口通訊也可能在某些項目中運用到。
串口開源項目
目前android下用到的串口通訊都是由Google提供的開源項目
此項目下下來正常情況下是能直接使用的。如果想更深入的了解其更多知識可以查看鏈接中的wikis。
但是:直接能使用時因為他已經(jīng)將so已經(jīng)打包生成好了,如果你將.so放在你的項目中你會發(fā)現(xiàn)是不能使用的,原因是因為so中的方法名是通過開源項目的包名+方法名來的。放在你項目中包名都變了,所以so文件將無法找到對應(yīng)的方法的,這樣我們還是得自己生成so文件。
使用
因為開源項目是eclipse的,而我們現(xiàn)在更多使用的是android studio,如果你還沒有使用android studio那就可以直接使用,但是你確定還不使用android studio么?
在android下可以下載一個“串口調(diào)試助手”App可以對你寫的程序做對比。快速驗證問題。
原理
android下的串口通訊是使用jni來使用的,如果你還不太了解Jni使用可以查看我另外一篇文章,其中詳細介紹了JNI的使用。
上面這篇文章也是基于串口下的jni寫的,所以本篇文章并不介紹JNI的使用。只是說直接使用串口通訊
原理:Google開源項目直接提供了SerialPort、SerialPortFinder兩個主要的類。這兩個類提供了打開/關(guān)閉串口的方法。然后我們需要將java下的打開和關(guān)閉串口方法生成.h文件,然后實現(xiàn).c方法;然后相應(yīng)的生成.so文件。.so文件沒問題之后才能正常使用。
實現(xiàn)
步驟一:設(shè)置串口號與波特率
我們知道串口通訊都是通過串口號與波特率來的,這跟我們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個參數(shù);第一個參數(shù)為串口路徑,第二個參數(shù)為波特率,第三個為狀態(tài)
前面兩個參數(shù)我們都是知道的一般為:
<code>private static int baudrate = 115200; //波特率
private static String path = "/dev/ttyS3"; //路徑</code>
請根據(jù)自己的為準。
步驟二:打開串口
打開和關(guān)閉串口是使用jni來調(diào)用c方法的。我們在上層調(diào)用打開串口的方法
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();
打開串口成功之后會開啟一個線程一直讀取數(shù)據(jù)。
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;
}
}
}
}
如果讀取數(shù)據(jù)會調(diào)用onDataReceived方法。onDataReceived個方法為接收方法,如果你需要那個類接收串口數(shù)據(jù)只要實現(xiàn)該方法就好了,下面我們會在數(shù)據(jù)接收的方法中說到
步驟三:發(fā)送串口數(shù)據(jù)
先看開源項目的發(fā)送源碼:
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輸入的字符串轉(zhuǎn)為byte數(shù)據(jù)直接發(fā)送。而我們項目可能是直接發(fā)送byte數(shù)組,所以我修改發(fā)送源碼為:
private void sendSerialData(){
String content = ""; //無數(shù)據(jù)內(nèi)容
int dateLength = ByteUtil.getContentLength(content);//數(shù)據(jù)長度
byte by[] = new byte[6];
by[0] = TokenCommon.ANDROIDSENDROBOT; //協(xié)議頭
by[1] = TokenCommon.ANDROIDDEVICEID; //設(shè)備id
by[2] = (byte) dateLength; // 數(shù)據(jù)長度
by[3] = (byte) (~dateLength); // 數(shù)據(jù)長度取反
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();
}
}
將自己的指令按照協(xié)議組成一個byte[],然后直接發(fā)送byte數(shù)組
然后我們來驗證發(fā)送是否成功。我們可以將串口連接電腦在電腦上開一個串口調(diào)試助手,然后選擇自己的串口號與波特率
我們可以看到接收區(qū)接收到了我們發(fā)送的數(shù)據(jù),則證明發(fā)送是正常的
步驟四:接收串口數(shù)據(jù)
個人覺得串口最蛋疼的就是接收數(shù)據(jù)了,因為他是一個字符一個字符傳過來的,哪怕你的是一條完整的數(shù)據(jù),他也是一個一個字符接收。他不像我們的tcp或udp一樣發(fā)送一條完整數(shù)據(jù)除非過長或網(wǎng)絡(luò)造成的數(shù)據(jù)丟失,我們是能一次性接收一條數(shù)據(jù)的。
而串口一個一個字符接收的,而且還有可能接收的數(shù)據(jù)是亂的,還有可能接收的數(shù)據(jù)是不完整的,還有可能接收到的數(shù)據(jù)在下一條在給你,多么坑啊。好吧既然這樣我們也是有辦法的,這樣就需要我們就必須做數(shù)據(jù)的拼接。
我們先看開源項目的接收源碼
@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是實現(xiàn)的一個抽象方法,因為源碼已經(jīng)封裝只要一旦有數(shù)據(jù)就會調(diào)用此方法。在步驟二中可以看到。
注意2:此方法接收數(shù)據(jù)是已字符串接收的,并且每做任何的數(shù)據(jù)處理,只是單純的字符串追加之后顯示數(shù)據(jù),這肯定是無法滿足我們的需求的,因為我們接收的數(shù)據(jù)也很有可能是一個byte數(shù)組。
所以我們自己的自己寫數(shù)據(jù)處理播放。
@Override
protected void onDataReceived(final byte[] buffer, final int size) {
//獲取串口傳過來的數(shù)據(jù)
System.arraycopy(buffer, 0, b, unDisposeLen, size);
unDisposeLen += + size;
int temp = 0; //數(shù)組下標
while(temp <= unDisposeLen){
Log.e(TAG, "****************************"+Arrays.toString(b));//字節(jié)數(shù)組打印
//判斷數(shù)據(jù)長度是否足夠:
if(unDisposeLen - temp >= FormatToken.DATA_MIN_LEN){
//分別判斷校驗頭、設(shè)備id、數(shù)據(jù)長度(數(shù)據(jù)長度和數(shù)據(jù)長度校驗判斷是否相等)
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,"數(shù)據(jù)長度="+dataLen);
//判斷校驗和位數(shù)據(jù)是否相等;接收到的校驗和與發(fā)送的校驗和相等
//將完整數(shù)據(jù)減掉最后的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相等證明為一條完整數(shù)據(jù)
byte by[] = new byte[FormatToken.DATA_MIN_LEN + dataLen];
//將完整數(shù)據(jù)從總數(shù)據(jù)中copy出來
System.arraycopy(b, temp, by, 0, FormatToken.DATA_MIN_LEN + dataLen);
Log.e(TAG, "完整數(shù)據(jù)=" + Arrays.toString(by));//字節(jié)數(shù)組打印
temp = temp + by.length; //移動下標
byte[] ret = new byte[unDisposeLen - temp];
//將為處理的數(shù)據(jù)copy到ret數(shù)組中
System.arraycopy(b,temp,ret,0,ret.length);
//將未處理的數(shù)據(jù)ret替換從0開始替換到
System.arraycopy(ret,0,b, 0 ,ret.length);
Log.e(TAG, "&&&&&&&&&&&&&&&&&&&&&&&&&&&&" + Arrays.toString(b));//字節(jié)數(shù)組打印
unDisposeLen = unDisposeLen - temp; //存儲未完成的數(shù)據(jù)長度
temp = 0;
if(temp == unDisposeLen) {
temp = 0 ;
unDisposeLen = 0;
break;
}
} else {
temp++;
}
} else {
temp++;
}
} else {
break;
}
}
if(temp == unDisposeLen) {
unDisposeLen = 0;
}
}
上面的代碼主要是將一個一個字符數(shù)據(jù)根據(jù)你的協(xié)議拼接成一條完整的數(shù)據(jù)。代碼中的注釋已經(jīng)寫的很清楚了。
然后我們驗證一下:用電腦調(diào)式工具發(fā)送內(nèi)容
然后看android下的數(shù)據(jù)接收
到此證明數(shù)據(jù)的接收木有問題。
思路是:獲取一段串口數(shù)組數(shù)據(jù),判斷數(shù)據(jù)中是否包含協(xié)議頭和設(shè)備id,如果沒有則繼續(xù)拼接字符串,如果有則判斷數(shù)組的長度是否大于最小協(xié)議長度,如果小于則繼續(xù)追加,如果大于則根據(jù)數(shù)據(jù)長度獲取數(shù)據(jù)部分,然后將數(shù)據(jù)部分加上協(xié)議頭等數(shù)據(jù)組成一個數(shù)組,然后判斷數(shù)組檢驗碼是否相同,如果相等證明是一條完整數(shù)據(jù),并將拼接的數(shù)據(jù)移除掉完整數(shù)據(jù)部分(避免重復(fù)解析已經(jīng)處理過的數(shù)據(jù)。)如果不是則丟棄數(shù)據(jù)。
demo的github地址 https://github.com/SouvDc/SerialPort_project
完結(jié)
OK,到這里我們已經(jīng)能完整解析出數(shù)據(jù),但是一定要根據(jù)你自己的協(xié)議來調(diào)整。
慢慢努力做好身邊所有的事
求知若饑,虛心若愚