引言
在iot項目中,Android 端總會有和硬件通信。
通信這里:串口通信,藍牙通信或者局域網通信。
這里講一下串口通信。
什么是串口?
“串口”(Serial Port)通常是指一種用于與外部設備進行串行通信的接口。如下是其中一種DB9的形式:
更加簡單的,還有這樣的形式:
只要有三條線,TX、RX和GND,或者A、B和GND,就可以去實現通訊。
...................................................... 奇怪??????我們手機上看不到這些玩意。
標準的Android智能手機和平板電腦通常并不直接暴露硬件級別的串口接口(如RS-232)給用戶或開發者。
這是因為這些設備為了便攜性和成本考慮,往往采用了不同的通信方式(如USB、藍牙、Wi-Fi等)來與外部設備交互。
然而,在一些特定的Android設備(如自動售賣機的大尺寸屏幕、嵌入式Android設備等)或者通過特定的硬件擴展(如USB轉串口適配器)上,開發者可能能夠訪問到串口接口。
為了在Android應用中使用串口通信,開發者可能會使用到一些第三方庫或框架,這些庫或框架通過JNI(Java Native Interface)技術調用底層Linux系統的串口通信接口。這些庫通常提供了更高級別的API,使得開發者能夠在不直接處理底層細節的情況下實現串口通信。
應用場景
不知道大家有沒有接觸過自動售賣機,在自動售賣機里面裝有Android屏。在海外,大部分國家都沒有普及掃碼支付,都是使用一些紙幣、硬幣以及刷卡進行支付。而這些支付大部分都是使用串口的形式去對接,比如投入多少錢,通過串口發送給數據Android屏,Android屏收到后,觸發指令出商品,今天呢,我們就來聊聊串口開發。
?
正文
串口常用于連接計算機與外部設備,如打印機、調制解調器、傳感器等。串口的主要特點是通信速度比較慢,但傳輸距離可以很長。常見的串口標準有RS-232、RS-485、TTL等。
通訊參數
一般這些數據,都是下位機提供給上位機的【上位機指的就是我們的Android屏幕,下位機指的就是上面我們提到的外部設備】,我們按照參數打開串口就可以收發數據了。
通訊接口是什么?
就是?RS232和RS485 。
RS232和RS485在收發數據上的區別主要體現在傳輸方式、傳輸距離、通信模式以及電平標準等方面。
RS232支持全雙工和半雙工兩種傳輸方式,全雙工可以實現數據的雙向同時傳輸,而半雙工則只能實現數據的單向傳輸,簡單來說,就是只能一邊來發送數據,另外一邊不能主動發數據,只能響應數據,類似客戶端和服務器的通訊一樣。
RS485屬于半雙工總線,即在同一時刻,總線上只能有一個設備在發送數據,而其他設備則處于接收狀態。
?波特率是什么?
34800,9600
波特率表示的是單位時間內傳輸的碼元符號的個數,波特率越高,單位時間內傳輸的數據量就越大。但過大也會存在丟包的情況,視情況設定。
停止位、數據位、校驗位的解釋:
1.停止位:用于表示單個數據包的結束。常見的停止位有1位、1.5位和2位。停止位的主要作用是提供一個時間間隔,以確保數據包的完整性和正確性。例如,如果設置為1位停止位,則每個數據包后面都會跟隨一個邏輯高電平(或邏輯低電平)的時間間隔,用于標識數據包的結束。
2.數據位:用于傳輸實際的數據信息。數據位的長度可以根據需要進行設置,常見的有5位、6位、7位和8位等。數據位越長,每個數據包所能攜帶的信息量就越大。在串口通信中,數據位通常是固定的,例如常用的ASCII碼就是基于7位或8位數據位進行傳輸的。
3.校驗位:用于檢測數據傳輸過程中的錯誤。校驗位可以通過多種方式生成,如奇校驗、偶校驗或無校驗等。如果設置了校驗位,則接收方會根據校驗位的值來判斷接收到的數據是否存在錯誤。如果存在錯誤,則可以根據具體的協議進行錯誤處理或重傳。
下位機的數據發送案例
?廠家自定義協議
我們簡單來看看他的協議,以及我們應該如何發送數據和接收數據。
(1)需要廠家提供通訊參數
(2)通訊文檔,比如,查詢下位機狀態,還有很多協議內容,這里就講一個:
有了這些信息,先不著急寫代碼,先使用串口工具測試一下收發數據是否正常。打開串口通訊工具,設置通訊參數,然后發送數據就可以了。
例如:? 發送? AA 01 02 DD? ? ?接收? AA 02 02 01 DD
粘包、如何知道返回的數據對應誰的,數據通知…等等
在真實項目中,并不會如上面怎么簡單,發送數據和接收數據就可以了,需要考慮:
- 數據丟包情況,需要重發,只到收到數據為止。
- 數據粘包的情況,需要和下位機約定好規則。
- 數據發送過來是二進制,我們需要轉換,具體也是和下位機約定好規則
如何知道返回的數據對應誰的?
簡單來說,就是你發送一個數據的時候,記錄到一個變量里面。等讀到數據后,你把數據和變量里面記錄的內容發送上來,然后再繼續發送下一個數據。以此類推。這樣你就會知道數據是誰的了。
注意,這樣的話,數據的發送,你就需要存儲到一個集合里面,不斷的往里面取,而不是異步隨便調用send方法發送數據了。
?
?如何處理粘包的情況?
粘包是指在串口通信過程中,由于多種原因導致的多個獨立的數據包在傳輸過程中被接收端視為一個連續的數據流,從而使得數據包之間的邊界變得不明確,進而使得數據的解析變得困難。
比如:本來下位機返回的是AA 03 03 07 00 DD變成了AA 03 03 07 00 DD AA 03 03 07 00 DD,或者AA 03 03 07 00 DDAA 03 03兩條數據連在一起情況。
怎么會出現這樣的問題呢?
1、發送方發送數據的速度較快:當發送方連續發送多個數據包,且發送速度較快時,如果接收方的處理速度跟不上,就可能導致多個數據包在接收端被合并成一個大的數據流,即發生粘包現象。【降低上位機數據發送的頻率】
2、接收方處理數據的速度較慢:接收方的處理速度是影響是否發生粘包的重要因素。如果接收方的處理速度較慢,無法及時將接收到的數據按照數據包進行分割和處理,就會發生粘包。【優化下位機代碼】
3、傳輸數據量太大:有時候傳輸數據量太大,導致數據截斷,或者緩存區不夠。
處理辦法:
**添加固定長度頭部和尾部:**發送方在每個數據包前添加固定長度的頭部,頭部中包含數據包的長度信息,接收方根據頭部中的長度信息來解析數據。如下:
左邊藍色是上位機發送給下位機的
右邊橙色是下位機返回給上位機的。
消息頭,數據內容長度,結束,這樣我們就可以很好的處理數據了,如果數據發回來的不完整,或者連在一起,我們可以視情況,對數據進行解析分段,或者丟棄。
使用
Android SerialPort庫通過JNI調用底層Linux的串口設備驅動,使得開發者可以通過簡單的API來進行串口通信操作。
android-serialport-api下有兩個主要的類
谷歌給的庫:
https://code.google.com/archive/p/android-serialport-api/?僅支持串口名稱及波特率 。
那么我找到了兩個做了擴展的兩個庫,根據情況用:
1、下面示例就講這個
GitHub - licheedev/Android-SerialPort-API: Fork自Google開源的Android串口通信Demo,修改成Android Studio項目
2、這個庫用著比較簡單?
GitHub - xmaihh/Android-Serialport: 移植谷歌官方串口庫,僅支持串口名稱及波特率,該項目添加支持校驗位、數據位、停止位、流控配置項
使用見:?快速使用Android串口_io.github.xmaihh:serialport-CSDN博客
示例:
?添加依賴
在 module 中的 build.gradle?中的 dependencies 中添加以下依賴:
dependencies {//串口implementation 'com.github.licheedev:Android-SerialPort-API:2.0.0'
}
低版本的 gradle?在Project 中的 build.gradle 中的 allprojects 中添加以下?maven倉庫 (不添加任然無法加載SerialPort);
allprojects {repositories {maven { url "https://jitpack.io" }//maven倉庫}
}
高版本的 gradle 已經廢棄了 allprojects 在 settings.gradle 中 repositories 添加以下maven倉庫(不添加任然無法加載SerialPort);
dependencyResolutionManagement {repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)repositories {google()mavenCentral()jcenter() // Warning: this repository is going to shut down soonmaven { url "https://jitpack.io" }//maven倉庫}
}
編寫串口處理類
1、串口處理類:SerialHandle ;簡單概括這個類,就是通過串口對象去獲取兩個流(輸入流、輸出流),通過者兩個流來監聽數據或者寫入指令,硬件收到后執行。同時注意配置參數(只要支持串口通訊的硬件,一般說明書上都會有寫)
package com.chj233.serialmode.serialUtil;import android.serialport.SerialPort;
import android.util.Log;import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;/*** 串口實處理類*/
public class SerialHandle implements Runnable {private static final String TAG = "串口處理類";private String path = "";//串口地址private SerialPort mSerialPort;//串口對象private InputStream mInputStream;//串口的輸入流對象private BufferedInputStream mBuffInputStream;//用于監聽硬件返回的信息private OutputStream mOutputStream;//串口的輸出流對象 用于發送指令private SerialInter serialInter;//串口回調接口private ScheduledFuture readTask;//串口讀取任務/*** 添加串口回調** @param serialInter*/public void addSerialInter(SerialInter serialInter) {this.serialInter = serialInter;}/*** 打開串口** @param devicePath 串口地址(根據平板的說明說填寫)* @param baudrate 波特率(根據對接的硬件填寫 - 硬件說明書上"通訊"中會有標注)* @param isRead 是否持續監聽串口返回的數據* @return 是否打開成功*/public boolean open(String devicePath, int baudrate, boolean isRead) {return open(devicePath, baudrate, 7, 1, 2, isRead);}/*** 打開串口** @param devicePath 串口地址(根據平板的說明說填寫)* @param baudrate 波特率(根據對接的硬件填寫 - 硬件說明書上"通訊"中會有標注)* @param dataBits 數據位(根據對接的硬件填寫 - 硬件說明書上"通訊"中會有標注)* @param stopBits 停止位(根據對接的硬件填寫 - 硬件說明書上"通訊"中會有標注)* @param parity 校驗位(根據對接的硬件填寫 - 硬件說明書上"通訊"中會有標注)* @param isRead 是否持續監聽串口返回的數據* @return 是否打開成功*/public boolean open(String devicePath, int baudrate, int dataBits, int stopBits, int parity, boolean isRead) {boolean isSucc = false;try {if (mSerialPort != null) close();File device = new File(devicePath);mSerialPort = SerialPort // 串口對象.newBuilder(device, baudrate) // 串口地址地址,波特率.dataBits(dataBits) // 數據位,默認8;可選值為5~8.stopBits(stopBits) // 停止位,默認1;1:1位停止位;2:2位停止位.parity(parity) // 校驗位;0:無校驗位(NONE,默認);1:奇校驗位(ODD);2:偶校驗位(EVEN).build(); // 打開串口并返回mInputStream = mSerialPort.getInputStream();mBuffInputStream = new BufferedInputStream(mInputStream);mOutputStream = mSerialPort.getOutputStream();isSucc = true;path = devicePath;if (isRead) readData();//開啟識別} catch (Throwable tr) {close();isSucc = false;} finally {return isSucc;}}// 讀取數據private void readData() {if (readTask != null) {readTask.cancel(true);try {Thread.sleep(160);} catch (InterruptedException e) {e.printStackTrace();}//此處睡眠:當取消任務時 線程池已經執行任務,無法取消,所以等待線程池的任務執行完畢readTask = null;}readTask = SerialManage.getInstance().getScheduledExecutor()//獲取線程池.scheduleAtFixedRate(this, 0, 150, TimeUnit.MILLISECONDS);//執行一個循環任務}@Override//每隔 150 毫秒會觸發一次runpublic void run() {if (Thread.currentThread().isInterrupted()) return;try {int available = mBuffInputStream.available();if (available == 0) return;byte[] received = new byte[1024];int size = mBuffInputStream.read(received);//讀取以下串口是否有新的數據if (size > 0 && serialInter != null) serialInter.readData(path, received, size);} catch (IOException e) {Log.e(TAG, "串口讀取數據異常:" + e.toString());}}/*** 關閉串口*/public void close(){try{if (mInputStream != null) mInputStream.close();}catch (Exception e){Log.e(TAG,"串口輸入流對象關閉異常:" +e.toString());}try{if (mOutputStream != null) mOutputStream.close();}catch (Exception e){Log.e(TAG,"串口輸出流對象關閉異常:" +e.toString());}try{if (mSerialPort != null) mSerialPort.close();mSerialPort = null;}catch (Exception e){Log.e(TAG,"串口對象關閉異常:" +e.toString());}}/*** 向串口發送指令*/public void send(final String msg) {byte[] bytes = hexStr2bytes(msg);//字符轉成byte數組try {mOutputStream.write(bytes);//通過輸出流寫入數據} catch (Exception e) {e.printStackTrace();}}/*** 把十六進制表示的字節數組字符串,轉換成十六進制字節數組** @param* @return byte[]*/private byte[] hexStr2bytes(String hex) {int len = (hex.length() / 2);byte[] result = new byte[len];char[] achar = hex.toUpperCase().toCharArray();for (int i = 0; i < len; i++) {int pos = i * 2;result[i] = (byte) (hexChar2byte(achar[pos]) << 4 | hexChar2byte(achar[pos + 1]));}return result;}/*** 把16進制字符[0123456789abcde](含大小寫)轉成字節* @param c* @return*/private static int hexChar2byte(char c) {switch (c) {case '0':return 0;case '1':return 1;case '2':return 2;case '3':return 3;case '4':return 4;case '5':return 5;case '6':return 6;case '7':return 7;case '8':return 8;case '9':return 9;case 'a':case 'A':return 10;case 'b':case 'B':return 11;case 'c':case 'C':return 12;case 'd':case 'D':return 13;case 'e':case 'E':return 14;case 'f':case 'F':return 15;default:return -1;}}}
2、串口回調SerialInter;簡單概括一下這個類,就是將SerialHandle類中產生的結果,返回給上一層的業務代碼,解偶合。
package com.chj233.serialmode.serialUtil;/*** 串口回調*/
public interface SerialInter {/*** 連接結果回調* @param path 串口地址(當有多個串口需要統一處理時,可以用地址來區分)* @param isSucc 連接是否成功*/void connectMsg(String path,boolean isSucc);/*** 讀取到的數據回調* @param path 串口地址(當有多個串口需要統一處理時,可以用地址來區分)* @param bytes 讀取到的數據* @param size 數據長度*/void readData(String path,byte[] bytes,int size);}
?3、串口統一管理SerialManage;簡單概括一下這個類,用于管理串口的連接以及發送等功能,尤其是發送指令,極短時間內發送多個指令(例如:1毫秒內發送10個指令),多個指令之間會相互干擾。可能執行了第一個指令,可能一個都沒執行。這個類不是必須的,如果有更好的方法可以自己定義。
package com.chj233.serialmode.serialUtil;import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;/*** 串口管理類*/
public class SerialManage {private static SerialManage instance;private ScheduledExecutorService scheduledExecutor;//線程池 同一管理保證只有一個private SerialHandle serialHandle;//串口連接 發送 讀取處理對象private Queue<String> queueMsg = new ConcurrentLinkedQueue<String>();//線程安全到隊列private ScheduledFuture sendStrTask;//循環發送任務private boolean isConnect = false;//串口是否連接private SerialManage() {scheduledExecutor = Executors.newScheduledThreadPool(8);//初始化8個線程}public static SerialManage getInstance() {if (instance == null) {synchronized (SerialManage.class) {if (instance == null) {instance = new SerialManage();}}}return instance;}/*** 獲取線程池** @return*/public ScheduledExecutorService getScheduledExecutor() {return scheduledExecutor;}/*** 串口初始化** @param serialInter*/public void init(SerialInter serialInter) {if (serialHandle == null) {serialHandle = new SerialHandle();startSendTask();}serialHandle.addSerialInter(serialInter);}/*** 打開串口*/public void open() {isConnect = serialHandle.open("/dev/ttyS1", 9600, true);//設置地址,波特率,開啟讀取串口數據}/*** 發送指令** @param msg*/public void send(String msg) {/*此處沒有直接使用 serialHandle.send(msg); 方法去發送指令因為 某些硬件在極短時間內只能響應一個指令,232通訊一次發送多個指令會有物理干擾,讓硬件接收到指令不準確;所以 此處將指令添加到隊列中,排隊執行,確保每個指令一定執行.若不相信可以試試用serialHandle.send(msg)方法循環發送10個不同的指令,看看10個指令的執行結果。*/queueMsg.offer(msg);//向隊列添加指令}/*** 關閉串口*/public void colse() {serialHandle.close();//關閉串口}//啟動發送發送任務private void startSendTask() {cancelSendTask();//先檢查是否已經啟動了任務 ? 若有則取消//每隔100毫秒檢查一次 隊列中是否有新的指令需要執行sendStrTask = scheduledExecutor.scheduleAtFixedRate(new Runnable() {@Overridepublic void run() {if (!isConnect) return;//串口未連接 退出if (serialHandle == null) return;//串口未初始化 退出String msg = queueMsg.poll();//取出指令if (msg == null || "".equals(msg)) return;//無效指令 退出serialHandle.send(msg);//發送指令}}, 0, 100, TimeUnit.MILLISECONDS);}//取消發送任務private void cancelSendTask() {if (sendStrTask == null) return;sendStrTask.cancel(true);try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}sendStrTask = null;}}
demo調用
package com.chj233.serialmode;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;
import android.util.Log;
import android.view.View;import com.chj233.serialmode.serialUtil.SerialInter;
import com.chj233.serialmode.serialUtil.SerialManage;public class MainActivity extends AppCompatActivity implements SerialInter {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);SerialManage.getInstance().init(this);//串口初始化SerialManage.getInstance().open();//打開串口findViewById(R.id.send_but).setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {SerialManage.getInstance().send("Z");//發送指令 Z }});}@Overridepublic void connectMsg(String path, boolean isSucc) {String msg = isSucc ? "成功" : "失敗";Log.e("串口連接回調", "串口 "+ path + " -連接" + msg);}@Override//若在串口開啟的方法中 傳入false 此處不會返回數據public void readData(String path, byte[] bytes, int size) {
// Log.e("串口數據回調","串口 "+ path + " -獲取數據" + bytes);}
}
多串口的使用
使用思想:一個單例對象控制一個串口,多串口時,寫多個“SerialManage”就可以了。這里僅僅做舉例不去考慮代碼是否優雅,可以自行優化這段代碼。(此案例中的SerialManage1、SerialManage2、SerialManage3、SerialManage4需要自己去復制,參照上面的SerialManage)
public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);//初始化串口1SerialManage1.getInstance().init(new SerialInter(){@Overridepublic void connectMsg(String path, boolean isSucc) {String msg = isSucc ? "成功" : "失敗";Log.e("串口連接回調", "串口 "+ path + " -連接" + msg);}@Override//若在串口開啟的方法中 傳入false 此處不會返回數據public void readData(String path, byte[] bytes, int size) {
// Log.e("串口數據回調","串口 "+ path + " -獲取數據" + bytes);}});//開啟串口1SerialManage1.getInstance().open();//初始化串口2SerialManage2.getInstance().init(new SerialInter(){@Overridepublic void connectMsg(String path, boolean isSucc) {String msg = isSucc ? "成功" : "失敗";Log.e("串口連接回調", "串口 "+ path + " -連接" + msg);}@Override//若在串口開啟的方法中 傳入false 此處不會返回數據public void readData(String path, byte[] bytes, int size) {
// Log.e("串口數據回調","串口 "+ path + " -獲取數據" + bytes);}});//打開串口2SerialManage2.getInstance().open();//初始化串口3SerialManage3.getInstance().init(new SerialInter(){@Overridepublic void connectMsg(String path, boolean isSucc) {String msg = isSucc ? "成功" : "失敗";Log.e("串口連接回調", "串口 "+ path + " -連接" + msg);}@Override//若在串口開啟的方法中 傳入false 此處不會返回數據public void readData(String path, byte[] bytes, int size) {
// Log.e("串口數據回調","串口 "+ path + " -獲取數據" + bytes);}});//打開串口3SerialManage3.getInstance().open();//初始化串口4SerialManage4.getInstance().init(new SerialInter(){@Overridepublic void connectMsg(String path, boolean isSucc) {String msg = isSucc ? "成功" : "失敗";Log.e("串口連接回調", "串口 "+ path + " -連接" + msg);}@Override//若在串口開啟的方法中 傳入false 此處不會返回數據public void readData(String path, byte[] bytes, int size) {
// Log.e("串口數據回調","串口 "+ path + " -獲取數據" + bytes);}});//打開串口4SerialManage4.getInstance().open();findViewById(R.id.send_but1).setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {SerialManage1.getInstance().send("Z");//給串口1發送指令 Z}});findViewById(R.id.send_but2).setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {SerialManage2.getInstance().send("Z");//給串口2發送指令 Z}});findViewById(R.id.send_but3).setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {SerialManage3.getInstance().send("Z");//給串口3發送指令 Z}});findViewById(R.id.send_but4).setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {SerialManage4.getInstance().send("Z");//給串口4發送指令 Z}});}}
總結
串口通訊對于Android開發者來說,僅需關注如何連接、操作(發送指令)、讀取數據;無論是232、485還是422,對于開發者來說連接、操作、讀取代碼都是一樣的。
參考文章 :?
Android串口開發:Serialport(如何進行串口開發,數據發送,TX和RX,A和B,粘包)_android 串口-CSDN博客
?