安卓與串口通信-實(shí)踐篇

前言

在上一篇文章中我們講解了關(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é)果:

image.png

可以看到原押,確實(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ù)通信埠通。

我們首先來看看初始化串口是怎么做的。

image.png

首先檢查了當(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 功能。

image.png

使用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)行程序霎匈,輸出如下:

image.png

可以看到眶蕉,正確的查找到了我們的 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è)備,輸出如下:

image.png

可以看到輸出的是 byte 的 101较曼,轉(zhuǎn)換為 ASCII 即為 “e”磷斧。

然后我們點(diǎn)擊 “開燈”、“關(guān)燈” 效果如下:

image.png

image.png

對(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)于通信的信息如圖:


image.png

可以看到毁兆,它采用了 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赡模。

參考資料

  1. android-serialport-api
  2. What is tty?
  3. Text-Terminal-HOWTO
  4. Terminal Special Files
  5. USB host
  6. Android開啟OTG功能/USB Host API功能

本文轉(zhuǎn)自 https://juejin.cn/post/7171347086032502792田炭,如有侵權(quán),請(qǐng)聯(lián)系刪除漓柑。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末教硫,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子辆布,更是在濱河造成了極大的恐慌栋豫,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谚殊,死亡現(xiàn)場離奇詭異丧鸯,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)嫩絮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門丛肢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來围肥,“玉大人,你說我怎么就攤上這事蜂怎∧驴蹋” “怎么了?”我有些...
    開封第一講書人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵杠步,是天一觀的道長氢伟。 經(jīng)常有香客問我,道長幽歼,這世上最難降的妖魔是什么朵锣? 我笑而不...
    開封第一講書人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮甸私,結(jié)果婚禮上诚些,老公的妹妹穿的比我還像新娘。我一直安慰自己皇型,他們只是感情好诬烹,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著弃鸦,像睡著了一般绞吁。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上唬格,一...
    開封第一講書人閱讀 51,541評(píng)論 1 305
  • 那天家破,我揣著相機(jī)與錄音,去河邊找鬼西轩。 笑死员舵,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的藕畔。 我是一名探鬼主播马僻,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼注服!你這毒婦竟也來了韭邓?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤溶弟,失蹤者是張志新(化名)和其女友劉穎女淑,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體辜御,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡鸭你,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片袱巨。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡阁谆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出愉老,到底是詐尸還是另有隱情场绿,我是刑警寧澤,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布嫉入,位于F島的核電站焰盗,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏咒林。R本人自食惡果不足惜熬拒,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望映九。 院中可真熱鬧梦湘,春花似錦瞎颗、人聲如沸件甥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽引有。三九已至,卻和暖如春倦逐,著一層夾襖步出監(jiān)牢的瞬間譬正,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來泰國打工檬姥, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留曾我,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓健民,卻偏偏與公主長得像抒巢,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子秉犹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 前言 做了一些Android驅(qū)動(dòng)板的串口通信蛉谜,對(duì)控制卡,繼電器開關(guān)崇堵,麥克風(fēng)型诚,PWM風(fēng)機(jī)等進(jìn)行操作,進(jìn)行一下記錄分享...
    幾圈年輪閱讀 13,268評(píng)論 1 14
  • 出品:1Z實(shí)驗(yàn)室 (1ZLAB: Make Things Easy) 概要 在本節(jié)課程阿凱為大家講解了串口通信的接...
    1Z實(shí)驗(yàn)室阿凱閱讀 14,858評(píng)論 5 6
  • 做了幾個(gè)月的工業(yè)用平板開發(fā)鸳劳,主要是串口通信狰贯。總結(jié)一下: Android 串口開發(fā)筆記01: 應(yīng)用場景、 名詞解釋涵紊、...
    silencefun閱讀 4,361評(píng)論 1 9
  • 前言: 最近在總是看見有人在群里面問一些串口通信相關(guān)的問題还绘,特別是對(duì)于我們這些做APP出生的程序員來說,初次接觸串...
    Roy88閱讀 37,518評(píng)論 30 37
  • 最近涉及到android串口和usb的開發(fā)栖袋,花費(fèi)不少時(shí)間找文章參考拍顷。主要時(shí)間花費(fèi)在串口連接上,由于計(jì)算機(jī)實(shí)際中沒有...
    藍(lán)天_T閱讀 2,933評(píng)論 0 8