前言
在上一篇文章中我們講解了關(guān)于串口的基礎(chǔ)知識(shí)扁藕,沒有看過的同學(xué)推薦先看一下,否則你可能會(huì)不太理解這篇文章所述的某些內(nèi)容。
這篇文章我們將講解安卓端的串口通信實(shí)踐揖盘,即如何使用串口通信實(shí)現(xiàn)安卓設(shè)備與其他設(shè)備例如PLC主板之間數(shù)據(jù)交互楷力。
需要注意的是正如上一篇文章所說的喊式,我目前的條件只允許我使用 ESP32 開發(fā)版燒錄 Arduino 程序與安卓真機(jī)(小米10U)進(jìn)行串口通信演示。
準(zhǔn)備工作
由于我們需要使用 ESP32 燒錄 Arduino 程序演示安卓端的串口通信萧朝,所以在開始之前我們應(yīng)該先把程序燒錄好岔留。
那么燒錄一個(gè)怎樣的程序呢?
很簡單检柬,我這里直接燒了一個(gè) ESP32 使用 9600 的波特率進(jìn)行串口通信献联,程序內(nèi)容就是 ESP32 不斷的向串口發(fā)送數(shù)據(jù) “e” ,并且監(jiān)聽串口數(shù)據(jù)何址,如果接收到數(shù)據(jù) “o” 則打開開發(fā)版上自帶的 LED 燈里逆,如果接收到數(shù)據(jù) “c” 則關(guān)閉這個(gè) LED 燈。
代碼如下:
#define LED 12
void setup() {
Serial.begin(9600);
pinMode(LED, OUTPUT);
}
void loop() {
if (Serial.available()) {
char c = Serial.read();
if (c == 'o') {
digitalWrite(LED, HIGH);
}
if (c == 'c') {
digitalWrite(LED, LOW);
}
}
Serial.write('e');
delay(100);
}
上面的 12 號(hào) Pin 是這塊開發(fā)版的 LED用爪。
使用 Arduino自帶串口監(jiān)視器測試結(jié)果:
可以看到原押,確實(shí)如我們?cè)O(shè)想的通過串口不斷的發(fā)送字符 “e”,并且在接收到字符 “o” 后點(diǎn)亮了 LED偎血。
安卓實(shí)現(xiàn)串口通信
原理概述
眾所周知诸衔,安卓其實(shí)是基于 Linux 的操作系統(tǒng),所以在安卓中對(duì)于串口的處理與 Linux 一致颇玷。
在 Linux 中串口會(huì)被視為一個(gè)“設(shè)備”署隘,并體現(xiàn)為 /dev/ttys
文件。
/dev/ttys
又被稱為字符終端亚隙,例如 ttys0
對(duì)應(yīng)的是 DOS/Windows 系統(tǒng)中的 COM1 串口文件磁餐。
通常,我們可以簡單理解,如果我們插入了某個(gè)串口設(shè)備诊霹,則這個(gè)設(shè)備與 Linux 的通信會(huì)由 /dev/ttys
文件進(jìn)行 “中轉(zhuǎn)”羞延。
即,如果 Linux 想要發(fā)送數(shù)據(jù)給串口設(shè)備脾还,則可以通過往 /dev/ttys
文件中直接寫入要發(fā)送的數(shù)據(jù)來實(shí)現(xiàn)伴箩,如:
echo test > /dev/ttyS1
這個(gè)命令會(huì)將 “test” 這串字符發(fā)送給串口設(shè)備。
如果想讀取串口發(fā)送的數(shù)據(jù)也是一樣的鄙漏,可以通過讀取 /dev/ttys
文件內(nèi)容實(shí)現(xiàn)嗤谚。
所以,如果我們?cè)诎沧恐邢胍獙?shí)現(xiàn)串口通信怔蚌,大概率也會(huì)想到直接讀取/寫入這個(gè)特殊文件巩步。
android-serialport-api
在上文中我們說到,在安卓中也可以通過與 Linux 一樣的方式--直接讀寫 /dev/ttys
實(shí)現(xiàn)串口通信桦踊。
但是其實(shí)并不需要我們自己去處理讀寫和數(shù)據(jù)的解析椅野,因?yàn)楣雀韫俜浇o出了一個(gè)解決方案:android-serialport-api
為了便于理解,我們會(huì)大致說一下這個(gè)解決方案的源碼籍胯,但是就不上示例了竟闪,至于為什么,同學(xué)們往下看就知道了杖狼。另外炼蛤,雖然這個(gè)方案歷史比較悠久,也很長時(shí)間沒有人維護(hù)了蝶涩,但是并不意味著不能使用了理朋,只是使用條件比較苛刻,當(dāng)然子寓,我司目前使用的還是這套方案(哈哈哈哈)暗挑。
不過這里我們不直接看 android-serialport-api 的源碼笋除,而是通過其他大佬二次封裝的庫來看: Android-SerialPort-API
在這個(gè)庫中斜友,通過
// 默認(rèn)直接初始化,使用8N1(8數(shù)據(jù)位垃它、無校驗(yàn)位鲜屏、1停止位),path為串口路徑(如 /dev/ttys1)国拇,baudrate 為波特率
SerialPort serialPort = new SerialPort(path, baudrate);
// 使用可選參數(shù)配置初始化洛史,可配置數(shù)據(jù)位、校驗(yàn)位酱吝、停止位 - 7E2(7數(shù)據(jù)位也殖、偶校驗(yàn)、2停止位)
SerialPort serialPort = SerialPort
.newBuilder(path, baudrate)
// 校驗(yàn)位;0:無校驗(yàn)位(NONE忆嗜,默認(rèn))己儒;1:奇校驗(yàn)位(ODD);2:偶校驗(yàn)位(EVEN)
// .parity(2)
// 數(shù)據(jù)位,默認(rèn)8;可選值為5~8
// .dataBits(7)
// 停止位捆毫,默認(rèn)1闪湾;1:1位停止位;2:2位停止位
// .stopBits(2)
.build();
初始化串口绩卤,然后通過:
InputStream in = serialPort.getInputStream();
OutputStream out = serialPort.getOutputStream();
獲取到輸入/輸出流刷晋,通過讀取/寫入這兩個(gè)流來實(shí)現(xiàn)與串口設(shè)備的數(shù)據(jù)通信埠通。
我們首先來看看初始化串口是怎么做的。
首先檢查了當(dāng)前是否具有串口文件的讀寫權(quán)限,如果沒有則通過 shell 命令更改權(quán)限為 666
甥厦,更改后再次檢查是否有權(quán)限,如果還是沒有就拋出異常久橙。
注意這里的執(zhí)行 shell 時(shí)使用的 runtime 是 Runtime.getRuntime().exec(sSuPath);
也就是說茵肃,它是通過 root 權(quán)限來執(zhí)行這段命令的!
換句話說辐烂,如果想要通過這種方式實(shí)現(xiàn)串口通信遏插,必須要有 ROOT 權(quán)限!這就是我說我不會(huì)給出示例的原因纠修,因?yàn)槲沂诸^的設(shè)備無法 ROOT 啊胳嘲。至于為啥我司還能繼續(xù)使用這種方案的原因也很簡單,因?yàn)槲覀児た貦C(jī)的安卓設(shè)備都是定制版的啊扣草,擁有 ROOT 權(quán)限不是基本操作了牛?
確定權(quán)限可用后通過 open
方法拿到一個(gè)類型為 FileDescriptor
的變量 mFd
,最后通過這個(gè) mFd
拿到輸入輸出流辰妙。
所以核心在于 open
方法鹰祸,而 open 方法是一個(gè) native 方法,即 C 代碼:
private native FileDescriptor open(String absolutePath, int baudrate, int dataBits, int parity,
int stopBits, int flags);
C 的源碼這里就不放了密浑,只需要知道它做的工作就是打開了 /dev/ttys
文件(準(zhǔn)確的說是“終端”)蛙婴,然后通過傳遞進(jìn)去的這些參數(shù)去按串口規(guī)則解析數(shù)據(jù),最后返回一個(gè) java 的 FileDescriptor
對(duì)象尔破。
在 java 中我們?cè)偻ㄟ^這個(gè) FileDescriptor
對(duì)象可以拿到輸入/輸出流街图。
原理說起來是十分的簡單。
看完通信部分的原理后懒构,我們?cè)賮砜纯次覀內(nèi)绾尾檎铱捎玫拇谀兀?/p>
其實(shí)和 Linux 上也一樣:
public Vector<File> getDevices() {
if (mDevices == null) {
mDevices = new Vector<File>();
File dev = new File("/dev");
File[] files = dev.listFiles();
if (files != null) {
int i;
for (i = 0; i < files.length; i++) {
if (files[i].getAbsolutePath().startsWith(mDeviceRoot)) {
Log.d(TAG, "Found new device: " + files[i]);
mDevices.add(files[i]);
}
}
}
}
return mDevices;
}
也是通過直接遍歷 /dev
下的文件餐济,只不過這里做了一些額外的過濾。
或者也可以通過讀取 /proc/tty/drivers
配置文件后過濾:
Vector<Driver> getDrivers() throws IOException {
if (mDrivers == null) {
mDrivers = new Vector<Driver>();
LineNumberReader r = new LineNumberReader(new FileReader("/proc/tty/drivers"));
String l;
while ((l = r.readLine()) != null) {
// Issue 3:
// Since driver name may contain spaces, we do not extract driver name with split()
String drivername = l.substring(0, 0x15).trim();
String[] w = l.split(" +");
if ((w.length >= 5) && (w[w.length - 1].equals("serial"))) {
Log.d(TAG, "Found new driver " + drivername + " on " + w[w.length - 4]);
mDrivers.add(new Driver(drivername, w[w.length - 4]));
}
}
r.close();
}
return mDrivers;
}
關(guān)于讀取可用串口設(shè)備胆剧,其實(shí)從這里的路徑也可以看出絮姆,都是系統(tǒng)路徑,也就是說,如果沒有權(quán)限篙悯,大概率也是讀取不到東西的冤灾。
這就是使用與 Linux 一樣的方式去讀取串口數(shù)據(jù)的基本原理,那么問題來了辕近,既然我說這個(gè)方法使用條件比較苛刻韵吨,那么更易用的替代方案是什么呢?
我們下面就會(huì)介紹移宅,那就是使用安卓的 USB host
(USB主機(jī))的功能归粉。
USB host
Android 3.1(API 級(jí)別 12)或更高版本的平臺(tái)直接支持 USB 配件和主機(jī)模式。USB 配件模式還作為插件庫向后移植到 Android 2.3.4(API 級(jí)別 10)中漏峰,以支持更廣泛的設(shè)備糠悼。設(shè)備制造商可以選擇是否在設(shè)備的系統(tǒng)映像中添加該插件庫。
在安卓 3.1 版本開始浅乔,支持將USB作為主機(jī)模式(USB host)使用倔喂,而我們?nèi)绻胍ㄟ^ USB 讀取串口數(shù)據(jù)則需要依賴于這個(gè)主機(jī)模式。
在正式開始介紹USB主機(jī)模式前靖苇,我們先簡要介紹一下安卓上支持的USB模式席噩。
安卓上的USB支持三種模式:設(shè)備模式、主機(jī)模式贤壁、配件模式悼枢。
設(shè)備模式即我們常用的直接將安卓設(shè)備連接至電腦上,此時(shí)電腦上顯示為 USB 外設(shè)脾拆,即可以當(dāng)成 “U盤” 使用拷貝數(shù)據(jù)馒索,不過現(xiàn)在安卓普遍還支持 MTP模式(作為攝像頭)、文件傳輸模式(即當(dāng)U盤用)名船、網(wǎng)卡模式等绰上。
主機(jī)模式即將我們的安卓設(shè)備作為主機(jī),連接其他外設(shè)渠驼,此時(shí)安卓設(shè)備就相當(dāng)于上面設(shè)備模式中的電腦蜈块。此時(shí)安卓設(shè)備可以連接鍵盤、鼠標(biāo)渴邦、U盤以及嵌入式應(yīng)用USB轉(zhuǎn)串口疯趟、轉(zhuǎn)I2C等設(shè)備拘哨。但是如果想要將安卓設(shè)備作為主機(jī)模式可能需要一條支持 OTG 的數(shù)據(jù)線或轉(zhuǎn)接頭谋梭。(Micro-USB 或 USB type-c 轉(zhuǎn) USB-A 口)
而在 USB 配件模式下,外部 USB 硬件充當(dāng) USB 主機(jī)倦青。配件示例可能包括機(jī)器人控制器瓮床、擴(kuò)展塢、診斷和音樂設(shè)備、自助服務(wù)終端隘庄、讀卡器等等踢步。這樣,不具備主機(jī)功能的 Android 設(shè)備就能夠與 USB 硬件互動(dòng)丑掺。Android USB 配件必須設(shè)計(jì)為與 Android 設(shè)備兼容获印,并且必須遵守 Android 配件通信協(xié)議。
設(shè)備模式與配件模式的區(qū)別在于在配件模式下街州,除了 adb 之外兼丰,主機(jī)還可以看到其他 USB 功能。
使用USB主機(jī)模式與外設(shè)交互數(shù)據(jù)
在介紹完安卓中的三種USB模式后唆缴,下面我們開始介紹如何使用USB主機(jī)模式鳍征。當(dāng)然,這里只是大概介紹原生APi的使用方法面徽,我們?cè)趯?shí)際使用中一般都都是直接使用大佬編寫的第三方庫艳丛。
準(zhǔn)備工作
在開始正式使用USB主機(jī)模式時(shí)我們需要先做一些準(zhǔn)備工作。
首先我們需要在清單文件(AndroidManifest.xml)中添加:
<!-- 聲明需要USB主機(jī)模式支持趟紊,避免不支持的設(shè)備安裝了該應(yīng)用 -->
<uses-feature android:name="android.hardware.usb.host" />
<!-- …… -->
<!-- 聲明需要接收USB連接事件 -->
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
一個(gè)完整的清單文件示例如下:
<manifest ...>
<uses-feature android:name="android.hardware.usb.host" />
<uses-sdk android:minSdkVersion="12" />
...
<application>
<activity ...>
...
<intent-filter>
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
</intent-filter>
</activity>
</application>
</manifest>
聲明好清單文件后氮双,我們就可以查找當(dāng)前可用的設(shè)備信息了:
private fun scanDevice(context: Context) {
val manager = context.getSystemService(Context.USB_SERVICE) as UsbManager
val deviceList: HashMap<String, UsbDevice> = manager.deviceList
Log.i(TAG, "scanDevice: $deviceList")
}
將 ESP32 開發(fā)版插上手機(jī),運(yùn)行程序霎匈,輸出如下:
可以看到眶蕉,正確的查找到了我們的 ESP32 開發(fā)版。
這里提一下唧躲,因?yàn)槲覀兊氖謾C(jī)只有一個(gè) USB 口造挽,此時(shí)已經(jīng)插上了 ESP32 開發(fā)版,所以無法再通過數(shù)據(jù)線直接連接電腦的 ADB 了弄痹,此時(shí)我們需要使用無線 ADB饭入,具體怎么使用無線 ADB,請(qǐng)自行搜索肛真。
另外谐丢,如果我們想要通過查找到設(shè)備后請(qǐng)求連接的方式連接到串口設(shè)備的話,還需要額外申請(qǐng)權(quán)限蚓让。(同理乾忱,如果我們直接在清單文件中提前聲明需要連接的設(shè)備則不需要額外申請(qǐng)權(quán)限,具體可以看看參考資料5历极,這里不再贅述)
首先聲明一個(gè)廣播接收器窄瘟,用于接收授權(quán)結(jié)果:
private lateinit var permissionIntent: PendingIntent
private const val ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION"
private val usbReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (ACTION_USB_PERMISSION == intent.action) {
synchronized(this) {
val device: UsbDevice? = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
device?.apply {
// 已授權(quán),可以在這里開始請(qǐng)求連接
connectDevice(context, device)
}
} else {
Log.d(TAG, "permission denied for device $device")
}
}
}
}
}
聲明好之后在 Acticity 的 OnCreate 中注冊(cè)這個(gè)廣播接收器:
permissionIntent = PendingIntent.getBroadcast(this, 0, Intent(ACTION_USB_PERMISSION), FLAG_MUTABLE)
val filter = IntentFilter(ACTION_USB_PERMISSION)
registerReceiver(usbReceiver, filter)
最后趟卸,在查找到設(shè)備后蹄葱,調(diào)用 manager.requestPermission(deviceList.values.first(), permissionIntent)
彈出對(duì)話框申請(qǐng)權(quán)限氏义。
連接到設(shè)備并收發(fā)數(shù)據(jù)
完成上述的準(zhǔn)備工作后,我們終于可以連接搜索到的設(shè)備并進(jìn)行數(shù)據(jù)交互了:
private fun connectDevice(context: Context, device: UsbDevice) {
val usbManager = context.getSystemService(Context.USB_SERVICE) as UsbManager
CoroutineScope(Dispatchers.IO).launch {
device.getInterface(0).also { intf ->
intf.getEndpoint(0).also { endpoint ->
usbManager.openDevice(device)?.apply {
claimInterface(intf, forceClaim)
while (true) {
val validLength = bulkTransfer(endpoint, bytes, bytes.size, TIMEOUT)
if (validLength > 0) {
val result = bytes.copyOfRange(0, validLength)
Log.i(TAG, "connectDevice: length = $validLength")
Log.i(TAG, "connectDevice: byte = ${result.contentToString()}")
}
else {
Log.i(TAG, "connectDevice: Not recv data!")
}
}
}
}
}
}
}
在上面的代碼中图云,我們使用 usbManager.openDevice
打開了指定的設(shè)備惯悠,即連接到設(shè)備。
然后通過 bulkTransfer
接收數(shù)據(jù)竣况,它會(huì)將接收到的數(shù)據(jù)寫入緩沖數(shù)組 bytes
中克婶,并返回成功接收到的數(shù)據(jù)長度。
運(yùn)行程序丹泉,連接設(shè)備鸠补,日志打印如下:
[圖片上傳失敗...(image-7d29a2-1669728238118)]
可以看到,輸出的數(shù)據(jù)并不是我們預(yù)料中的數(shù)據(jù)嘀掸。
這是因?yàn)檫@是非常原始的數(shù)據(jù)紫岩,如果我們想要讀取數(shù)據(jù),還需要針對(duì)不同的串口轉(zhuǎn)USB芯片或協(xié)議編寫驅(qū)動(dòng)程序才能獲取到正確的數(shù)據(jù)睬塌。
順道一提泉蝌,如果想要將數(shù)據(jù)寫入串口數(shù)據(jù)的話可以使用 controlTransfer()
。
所以揩晴,我們?cè)趯?shí)際生產(chǎn)環(huán)境中使用的都是基于此封裝好的第三方庫勋陪。
這里推薦使用 usb-serial-for-android
usb-serial-for-android
使用這個(gè)庫的第一步當(dāng)然是導(dǎo)入依賴:
// 添加倉庫
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
// 添加依賴
dependencies {
implementation 'com.github.mik3y:usb-serial-for-android:3.4.6'
}
添加完依賴同樣需要在清單文件中添加相應(yīng)字段以及處理權(quán)限,因?yàn)楹蜕鲜鍪褂迷鶤PI一致硫兰,所以這里不再贅述诅愚。
和原生 API 不同的是,因?yàn)槲覀兇藭r(shí)已經(jīng)知道了我們的 ESP32 主板的設(shè)備信息劫映,以及使用的驅(qū)動(dòng)(CDC)违孝,所以我們就不使用原生的查找可用設(shè)備的方法了,我們這里直接指定我們已知的這個(gè)設(shè)備(當(dāng)然泳赋,你也可以繼續(xù)使用原生API的查找和連接方法):
private fun scanDevice(context: Context) {
val manager = context.getSystemService(Context.USB_SERVICE) as UsbManager
val customTable = ProbeTable()
// 添加我們的設(shè)備信息雌桑,三個(gè)參數(shù)分別為 vendroId、productId祖今、驅(qū)動(dòng)程序
customTable.addProduct(0x1a86, 0x55d3, CdcAcmSerialDriver::class.java)
val prober = UsbSerialProber(customTable)
// 查找指定的設(shè)備是否存在
val drivers: List<UsbSerialDriver> = prober.findAllDrivers(manager)
if (drivers.isNotEmpty()) {
val driver = drivers[0]
// 這個(gè)設(shè)備存在校坑,連接到這個(gè)設(shè)備
val connection = manager.openDevice(driver.device)
}
else {
Log.i(TAG, "scanDevice: 無設(shè)備!")
}
}
連接到設(shè)備后千诬,下一步就是和數(shù)據(jù)交互耍目,這里封裝的十分方便,只需要獲取到 UsbSerialPort
后徐绑,直接調(diào)用它的 read()
和 write()
即可讀寫數(shù)據(jù):
port = driver.ports[0] // 大多數(shù)設(shè)備都只有一個(gè) port邪驮,所以大多數(shù)情況下直接取第一個(gè)就行
port.open(connection)
// 設(shè)置連接參數(shù),波特率9600泵三,以及 “8N1”
port.setParameters(9600, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE)
// 讀取數(shù)據(jù)
val responseBuffer = ByteArray(1024)
port.read(responseBuffer, 0)
// 寫入數(shù)據(jù)
val sendData = byteArrayOf(0x6F)
port.write(sendData, 0)
此時(shí)耕捞,一個(gè)完整的,用于測試我們上述 ESP32 程序的代碼如下:
@Composable
fun SerialScreen() {
val context = LocalContext.current
Column(
Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(onClick = { scanDevice(context) }) {
Text(text = "查找并連接設(shè)備")
}
Button(onClick = { switchLight(true) }) {
Text(text = "開燈")
}
Button(onClick = { switchLight(false) }) {
Text(text = "關(guān)燈")
}
}
}
private fun scanDevice(context: Context) {
val manager = context.getSystemService(Context.USB_SERVICE) as UsbManager
val customTable = ProbeTable()
customTable.addProduct(0x1a86, 0x55d3, CdcAcmSerialDriver::class.java)
val prober = UsbSerialProber(customTable)
val drivers: List<UsbSerialDriver> = prober.findAllDrivers(manager)
if (drivers.isNotEmpty()) {
val driver = drivers[0]
val connection = manager.openDevice(driver.device)
if (connection == null) {
Log.i(TAG, "scanDevice: 連接失敗")
return
}
port = driver.ports[0]
port.open(connection)
port.setParameters(9600, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE)
Log.i(TAG, "scanDevice: Connect success!")
CoroutineScope(Dispatchers.IO).launch {
while (true) {
val responseBuffer = ByteArray(1024)
val len = port.read(responseBuffer, 0)
Log.i(TAG, "scanDevice: recv data = ${responseBuffer.copyOfRange(0, len).contentToString()}")
}
}
}
else {
Log.i(TAG, "scanDevice: 無設(shè)備烫幕!")
}
}
private fun switchLight(isON: Boolean) {
val sendData = if (isON) byteArrayOf(0x6F) else byteArrayOf(0x63)
port.write(sendData, 0)
}
運(yùn)行這個(gè)程序俺抽,并且連接設(shè)備,輸出如下:
可以看到輸出的是 byte 的 101较曼,轉(zhuǎn)換為 ASCII 即為 “e”磷斧。
然后我們點(diǎn)擊 “開燈”、“關(guān)燈” 效果如下:
對(duì)了捷犹,這里發(fā)送的數(shù)據(jù) “0x6F” 即 ASCII “o” 的十六進(jìn)制弛饭,同理,“0x63” 即 “c”萍歉。
可以看到侣颂,可以完美的和我們的 ESP32 開發(fā)版進(jìn)行通信。
實(shí)例
無論使用什么方式與串口通信枪孩,我們?cè)诎沧緼PP的代碼層面能夠拿到的數(shù)據(jù)已經(jīng)是處理好了的數(shù)據(jù)憔晒。
即,在上一篇文章中我們說過串口通信的一幀數(shù)據(jù)包括起始位蔑舞、數(shù)據(jù)位拒担、校驗(yàn)位、停止位攻询。但是我們?cè)诎沧恐惺褂脮r(shí)一般拿到的都只有 數(shù)據(jù)位 的數(shù)據(jù)从撼,其他數(shù)據(jù)已經(jīng)在底層被解析好了,無需我們?nèi)リP(guān)心怎么解析钧栖,或者使用低零。
我們可以直接拿到的就是可用數(shù)據(jù)。
這里舉一個(gè)我之前用過的某型號(hào)驅(qū)動(dòng)版的例子拯杠。
這塊驅(qū)動(dòng)版關(guān)于通信的信息如圖:
可以看到毁兆,它采用了 RS485 的通信方式,波特率支持 9600 或 38400阴挣,8位數(shù)據(jù)位气堕,無校驗(yàn),1位停止位畔咧。
并且茎芭,它還規(guī)定了一個(gè)數(shù)據(jù)協(xié)議。
在它定義的協(xié)議中誓沸,第一位為地址梅桩;第二位為指令;第三位到第N位為數(shù)據(jù)內(nèi)容拜隧;最后兩位為CRC校驗(yàn)宿百。
需要注意的是趁仙,這里定義的協(xié)議是基于串口通信的,不要把這個(gè)協(xié)議和串口通信搞混了垦页,簡單來說就是在串口通信協(xié)議的數(shù)據(jù)位中又定義了一個(gè)自己的協(xié)議雀费。
而且可以看到,雖然定義串口參數(shù)時(shí)沒有指定校驗(yàn)痊焊,但是在它自己的協(xié)議中指定了使用 CRC 校驗(yàn)盏袄。
另外,弱弱的吐槽一句薄啥,這個(gè)驅(qū)動(dòng)版的協(xié)議真的不好使辕羽。
在實(shí)際使用過程中,主機(jī)與驅(qū)動(dòng)版的通信數(shù)據(jù)無法保證一定會(huì)在同一個(gè)數(shù)據(jù)幀中發(fā)送完成垄惧,所以可能會(huì)造成“粘包”刁愿、“分包”現(xiàn)象,也就是說到逊,數(shù)據(jù)可能會(huì)分幾次發(fā)過來酌毡,而且你不好判斷這數(shù)據(jù)是上次沒發(fā)送完的數(shù)據(jù)還是新的數(shù)據(jù)。
我使用過的另外一款驅(qū)動(dòng)版就方便的多蕾管,因?yàn)樗鼤?huì)在幀頭加上開始符號(hào)和數(shù)據(jù)長度枷踏,幀尾加上結(jié)束符號(hào)。
這樣一來掰曾,即使出現(xiàn)“粘包”旭蠕、“分包”我們也能很好的給它解析出來。
當(dāng)然旷坦,它這樣設(shè)計(jì)協(xié)議肯定是有它的道理的掏熬,無非就是減少通信代價(jià)之類的。
我還遇到過一款十分簡潔的驅(qū)動(dòng)版秒梅,直接發(fā)送一個(gè)整數(shù)過去表示執(zhí)行對(duì)應(yīng)的指令旗芬。
驅(qū)動(dòng)版回傳的數(shù)據(jù)同樣非常簡單,就是一個(gè)數(shù)字捆蜀,然后事先約定各個(gè)數(shù)字表示什么意思……
說歸說疮丛,我們還是繼續(xù)來看這款驅(qū)動(dòng)版的通信協(xié)議:
[圖片上傳失敗...(image-91970d-1669728238118)]
這是它的其中一個(gè)指令內(nèi)容,我們發(fā)送指令 “1” 過去后辆它,它會(huì)返回當(dāng)前驅(qū)動(dòng)版的型號(hào)和版本信息給我們誊薄。
因?yàn)槲覀兊闹靼迨嵌ㄖ乒た刂靼澹允褂玫耐ㄐ欧绞绞侵苯佑?android-serialport-api锰茉。
最終發(fā)送與接收回復(fù)也很簡單:
/**
* 將十六進(jìn)制字符串轉(zhuǎn)成 ByteArray
* */
private fun hexStrToBytes(hexString: String): ByteArray {
check(hexString.length % 2 == 0) { return ByteArray(0) }
return hexString.chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
}
private fun isReceivedLegalData(receiveBuffer: ByteArray): Boolean {
val rcvData = receiveBuffer.copyOf() //重新拷貝一個(gè)使用呢蔫,避免原數(shù)據(jù)被清零
if (cmd.cmdId.checkDataFormat(rcvData)) { //檢查回復(fù)數(shù)據(jù)格式
isPkgLost = false
if (cmd.cmdId.isResponseBelong(rcvData)) { //檢查回復(fù)命令來源
if (!AdhShareData.instance.getIsUsingCrc()) { //如果不開啟CRC檢驗(yàn)則直接返回 true
resolveRcvData(cmdRcvDataCallback, rcvData, cmd.cmdId)
coroutineScope.launch(Dispatchers.Main) {
cmdResponseCallback?.onCmdResponse(ResponseStatus.Success, rcvData, 0, rcvData.size, cmd.cmdId)
}
return true
}
if (cmd.cmdId.checkCrc(rcvData)) { //檢驗(yàn)CRC
resolveRcvData(cmdRcvDataCallback, rcvData, cmd.cmdId)
coroutineScope.launch(Dispatchers.Main) {
cmdResponseCallback?.onCmdResponse(ResponseStatus.Success, rcvData, 0, rcvData.size, cmd.cmdId)
}
return true
}
else {
coroutineScope.launch(Dispatchers.Main) {
cmdResponseCallback?.onCmdResponse(ResponseStatus.FailCauseCrcError, ByteArray(0), -1, -1, cmd.cmdId)
}
return false
}
}
else {
coroutineScope.launch(Dispatchers.Main) {
cmdResponseCallback?.onCmdResponse(ResponseStatus.FailCauseNotFromThisCmd, ByteArray(0), -1, -1, cmd.cmdId)
}
return false
}
}
else { //數(shù)據(jù)不符合,可能是遇到了分包飒筑,繼續(xù)等待下一個(gè)數(shù)據(jù)片吊,然后合并
isPkgLost = true
return isReceivedLegalData(cmd)
/*coroutineScope.launch(Dispatchers.Main) {
cmdResponseCallback?.onCmdResponse(ResponseStatus.FailCauseWrongFormat, ByteArray(0), -1, -1, cmd.cmdId)
}
return false */
}
}
// ……省略初始化和連接代碼
// 發(fā)送數(shù)據(jù)
val bytes = hexStrToBytes("0201C110")
outputStream.write(bytes, 0, bytes.size)
// 解析數(shù)據(jù)
val recvBuffer = ByteArray(0)
inputStream.read(recvBuffer)
while (receiveBuffer.isEmpty()) {
delay(10)
}
isReceivedLegalData()
本來打算直接發(fā)我封裝好的這個(gè)驅(qū)動(dòng)版的協(xié)議庫的绽昏,想了想,好像不太合適俏脊,所以就大概抽出了這些不完整的代碼全谤,懂這個(gè)意思就行了,哈哈联予。
總結(jié)
從上面介紹的兩種方式可以看出啼县,兩種方式使用各有優(yōu)缺點(diǎn)材原。
使用 android-serialport-api 可以直接讀取串口數(shù)據(jù)內(nèi)容沸久,不需要轉(zhuǎn)USB接口,不需要驅(qū)動(dòng)支持余蟹,但是需要 ROOT卷胯,適合于定制安卓主板上已經(jīng)預(yù)留了 RS232 或 RS485 接口且設(shè)備已 ROOT 的情況下使用。
而使用 USB host 威酒,可以直接讀取USB接口轉(zhuǎn)接的串口數(shù)據(jù)窑睁,不需要ROOT,但是只支持有驅(qū)動(dòng)的串口轉(zhuǎn)USB芯片葵孤,且只支持使用USB接口担钮,不支持直接連接串口設(shè)備。
各位可以根據(jù)自己的實(shí)際情況靈活選擇使用什么方式來實(shí)現(xiàn)串口通信尤仍。
當(dāng)然箫津,除了現(xiàn)在介紹的這些串口通信,其實(shí)還有一個(gè)通信協(xié)議在實(shí)際使用中用的非常多宰啦,那就是 MODBUS 協(xié)議苏遥。
下一篇文章,我們將介紹 MODBUS赡模。
參考資料
- android-serialport-api
- What is tty?
- Text-Terminal-HOWTO
- Terminal Special Files
- USB host
- Android開啟OTG功能/USB Host API功能
本文轉(zhuǎn)自 https://juejin.cn/post/7171347086032502792田炭,如有侵權(quán),請(qǐng)聯(lián)系刪除漓柑。