国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

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

這篇具有很好參考價(jià)值的文章主要介紹了安卓與串口通信-實(shí)踐篇。希望對(duì)大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請(qǐng)大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問。

前言

在上一篇文章中我們講解了關(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è)怎樣的程序呢?

很簡(jiǎn)單,我這里直接燒了一個(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)視器測(cè)試結(jié)果:

android 串口發(fā)送指令,android,linux,單片機(jī)

可以看到,確實(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 串口文件。

通常,我們可以簡(jiǎn)單理解,如果我們插入了某個(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è)方案歷史比較悠久,也很長(zhǎng)時(shí)間沒有人維護(hù)了,但是并不意味著不能使用了,只是使用條件比較苛刻,當(dāng)然,我司目前使用的還是這套方案(哈哈哈哈)。

不過這里我們不直接看 android-serialport-api 的源碼,而是通過其他大佬二次封裝的庫(kù)來看: Android-SerialPort-API

在這個(gè)庫(kù)中,通過

// 默認(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ù)通信。

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

android 串口發(fā)送指令,android,linux,單片機(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ù)使用這種方案的原因也很簡(jiǎn)單,因?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ì)象可以拿到輸入/輸出流。

原理說起來是十分的簡(jiǎn)單。

看完通信部分的原理后,我們?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 配件模式還作為插件庫(kù)向后移植到 Android 2.3.4(API 級(jí)別 10)中,以支持更廣泛的設(shè)備。設(shè)備制造商可以選擇是否在設(shè)備的系統(tǒng)映像中添加該插件庫(kù)。

在安卓 3.1 版本開始,支持將USB作為主機(jī)模式(USB host)使用,而我們?nèi)绻胍ㄟ^ USB 讀取串口數(shù)據(jù)則需要依賴于這個(gè)主機(jī)模式。

在正式開始介紹USB主機(jī)模式前,我們先簡(jiǎn)要介紹一下安卓上支持的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 功能。

android 串口發(fā)送指令,android,linux,單片機(jī)

使用USB主機(jī)模式與外設(shè)交互數(shù)據(jù)

在介紹完安卓中的三種USB模式后,下面我們開始介紹如何使用USB主機(jī)模式。當(dāng)然,這里只是大概介紹原生APi的使用方法,我們?cè)趯?shí)際使用中一般都都是直接使用大佬編寫的第三方庫(kù)。

準(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)行程序,輸出如下:

android 串口發(fā)送指令,android,linux,單片機(jī)

可以看到,正確的查找到了我們的 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ù)長(zhǎng)度。

運(yùn)行程序,連接設(shè)備,日志打印如下:

android 串口發(fā)送指令,android,linux,單片機(jī)

可以看到,輸出的數(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)境中使用的都是基于此封裝好的第三方庫(kù)。

這里推薦使用 usb-serial-for-android

usb-serial-for-android

使用這個(gè)庫(kù)的第一步當(dāng)然是導(dǎo)入依賴:

// 添加倉(cāng)庫(kù)
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è)完整的,用于測(cè)試我們上述 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è)備,輸出如下:

android 串口發(fā)送指令,android,linux,單片機(jī)

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

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

android 串口發(fā)送指令,android,linux,單片機(jī)

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

android 串口發(fā)送指令,android,linux,單片機(jī)

可以看到,它采用了 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é)議和串口通信搞混了,簡(jiǎn)單來說就是在串口通信協(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ù)長(zhǎng)度,幀尾加上結(jié)束符號(hào)。

這樣一來,即使出現(xiàn)“粘包”、“分包”我們也能很好的給它解析出來。

當(dāng)然,它這樣設(shè)計(jì)協(xié)議肯定是有它的道理的,無非就是減少通信代價(jià)之類的。

我還遇到過一款十分簡(jiǎn)潔的驅(qū)動(dòng)版,直接發(fā)送一個(gè)整數(shù)過去表示執(zhí)行對(duì)應(yīng)的指令。

驅(qū)動(dòng)版回傳的數(shù)據(jù)同樣非常簡(jiǎn)單,就是一個(gè)數(shù)字,然后事先約定各個(gè)數(shù)字表示什么意思……

說歸說,我們還是繼續(xù)來看這款驅(qū)動(dòng)版的通信協(xié)議:

android 串口發(fā)送指令,android,linux,單片機(jī)

這是它的其中一個(gè)指令內(nèi)容,我們發(fā)送指令 “1” 過去后,它會(huì)返回當(dāng)前驅(qū)動(dòng)版的型號(hào)和版本信息給我們。

因?yàn)槲覀兊闹靼迨嵌ㄖ乒た刂靼澹允褂玫耐ㄐ欧绞绞侵苯佑?android-serialport-api。

最終發(fā)送與接收回復(fù)也很簡(jiǎn)單:

/**
 * 將十六進(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é)議庫(kù)的,想了想,好像不太合適,所以就大概抽出了這些不完整的代碼,懂這個(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。文章來源地址http://www.zghlxwxcb.cn/news/detail-593938.html

參考資料

  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功能

到了這里,關(guān)于安卓與串口通信-實(shí)踐篇的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場(chǎng)。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請(qǐng)注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請(qǐng)點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • 「ML 實(shí)踐篇」模型訓(xùn)練

    「ML 實(shí)踐篇」模型訓(xùn)練

    在訓(xùn)練不同機(jī)器學(xué)習(xí)算法模型時(shí),遇到的各類訓(xùn)練算法大多對(duì)用戶都是一個(gè)黑匣子,而理解它們實(shí)際怎么工作,對(duì)用戶是很有幫助的; 快速定位到合適的模型與正確的訓(xùn)練算法,找到一套適當(dāng)?shù)某瑓?shù)等; 更高效的執(zhí)行錯(cuò)誤調(diào)試、錯(cuò)誤分析等; 有助于理解、構(gòu)建和訓(xùn)練神經(jīng)

    2023年04月16日
    瀏覽(25)
  • 程序員職業(yè)規(guī)劃-實(shí)踐篇

    程序員職業(yè)規(guī)劃-實(shí)踐篇

    你是否認(rèn)真思考過3-5年、10年: 你想成為什么樣的人 ? 作為一名技術(shù)人,我們應(yīng)認(rèn)真規(guī)劃自己的職業(yè)發(fā)展,不再焦慮、為自己加速~ 一塊留言來聊聊吧~ 你該去什么樣的公司、做什么樣的事情、拿多少錢,都取決于一個(gè)問題: 你想成為什么樣的人 ? 你是否認(rèn)真思考過3-5年、

    2024年02月05日
    瀏覽(28)
  • 【實(shí)踐篇】推薦算法PaaS化探索與實(shí)踐 | 京東云技術(shù)團(tuán)隊(duì)

    【實(shí)踐篇】推薦算法PaaS化探索與實(shí)踐 | 京東云技術(shù)團(tuán)隊(duì)

    作者:京東零售 崔寧 目前,推薦算法部支持了主站、企業(yè)業(yè)務(wù)、全渠道等20+業(yè)務(wù)線的900+推薦場(chǎng)景,通過梳理大促運(yùn)營(yíng)、各垂直業(yè)務(wù)線推薦場(chǎng)景的共性需求,對(duì)現(xiàn)有推薦算法能力進(jìn)行沉淀和積累,并通過算法PaaS化打造通用化的推薦能力,提升各業(yè)務(wù)場(chǎng)景推薦賦能效率,高效賦

    2024年02月15日
    瀏覽(26)
  • 「ML 實(shí)踐篇」分類系統(tǒng):圖片數(shù)字識(shí)別

    「ML 實(shí)踐篇」分類系統(tǒng):圖片數(shù)字識(shí)別

    目的 :使用 MNIST 數(shù)據(jù)集,建立數(shù)字圖像識(shí)別模型,識(shí)別任意圖像中的數(shù)字; MNIST ,一組由美國(guó)高中生和人口調(diào)查局員工手寫的 70000 個(gè)數(shù)字圖片;每張圖片都用其代表的數(shù)字標(biāo)記;因廣泛被應(yīng)用于機(jī)器學(xué)習(xí)入門,被稱作機(jī)器學(xué)習(xí)領(lǐng)域的 Hello World ;也可用于測(cè)試新分類算法的

    2023年04月08日
    瀏覽(28)
  • 微服務(wù)實(shí)戰(zhàn)系列之ZooKeeper(實(shí)踐篇)

    微服務(wù)實(shí)戰(zhàn)系列之ZooKeeper(實(shí)踐篇)

    關(guān)于 ZooKeeper ,博主已完整的通過庖丁解牛式的 “解法” ,完成了概述。我想掌握了這些基礎(chǔ)原理和概念后,工作的問題自然迎刃而解,甚至offer也可能手到擒來,真實(shí)一舉兩得,美極了。 為了更有直觀的體驗(yàn),強(qiáng)化概念,博主特別獻(xiàn)上一篇實(shí)踐文章。理論聯(lián)系實(shí)踐,才能學(xué)

    2024年01月21日
    瀏覽(32)
  • 【實(shí)踐篇】領(lǐng)域驅(qū)動(dòng)設(shè)計(jì):DDD工程參考架構(gòu)

    不同團(tuán)隊(duì)落地DDD所采取的應(yīng)用架構(gòu)風(fēng)格可能不同,并沒有統(tǒng)一的、標(biāo)準(zhǔn)的DDD工程架構(gòu)。有些團(tuán)隊(duì)可能遵循經(jīng)典的DDD四層架構(gòu),或改進(jìn)的DDD四層架構(gòu),有些團(tuán)隊(duì)可能綜合考慮分層架構(gòu)、整潔架構(gòu)、六邊形架構(gòu)等多種架構(gòu)風(fēng)格,有些在實(shí)踐中可能引入CQRS解決讀模型與寫模型的差異

    2024年02月05日
    瀏覽(26)
  • Redis【實(shí)踐篇】之RedisTemplate基本操作

    Redis【實(shí)踐篇】之RedisTemplate基本操作

    在SpringBoot中,可以使用RedisTemplate來操作Redis數(shù)據(jù)庫(kù)。RedisTemplate是Spring Data Redis提供的一個(gè)強(qiáng)大的Redis客戶端,它支持各種Redis數(shù)據(jù)結(jié)構(gòu),并提供了許多方便的方法來操作這些數(shù)據(jù)結(jié)構(gòu)。下面是一些RedisTemplate的用法示例: 在此示例中,創(chuàng)建了一個(gè)RedisTemplate對(duì)象,并設(shè)置了key和

    2024年02月16日
    瀏覽(31)
  • 瑞芯微RK3568開發(fā):GPIO實(shí)踐篇

    瑞芯微RK3568開發(fā):GPIO實(shí)踐篇

    ? ? ? ? SOC平臺(tái)各類GPIO構(gòu)建原理是大道一統(tǒng)的,在各個(gè)諸如狀態(tài)、數(shù)據(jù)、中斷和屏蔽等寄存器具體含義用法,有少許差異。玩好RK的GPIO,需要先理解這類通用接口的框架。 ? ? ? ? 介紹RK3568的GPIO,認(rèn)為講2類重要地址和記錄幾種編程實(shí)踐方法即可。 一、2類地址 ? ? ? ? RK

    2024年02月10日
    瀏覽(163)
  • 【實(shí)踐篇】DDD腳手架及編碼規(guī)范

    我們團(tuán)隊(duì)一直在持續(xù)推進(jìn)業(yè)務(wù)系統(tǒng)的體系化治理工作,在這個(gè)過程中我們沉淀了自己的DDD腳手架項(xiàng)目。腳手架項(xiàng)目是體系化治理過程中比較重要的一環(huán),它的作用有兩點(diǎn): (1)可以對(duì)新建的項(xiàng)目進(jìn)行統(tǒng)一的規(guī)范; (2)對(duì)于指導(dǎo)老項(xiàng)目進(jìn)行DDD的改造提供指導(dǎo)。 本文主要是梳

    2024年02月11日
    瀏覽(28)
  • 【Redis實(shí)踐篇】使用Redisson 優(yōu)雅實(shí)現(xiàn)項(xiàng)目實(shí)踐過程中的5種場(chǎng)景

    【Redis實(shí)踐篇】使用Redisson 優(yōu)雅實(shí)現(xiàn)項(xiàng)目實(shí)踐過程中的5種場(chǎng)景

    Redisson是一個(gè)基于Redis的分布式Java對(duì)象和數(shù)據(jù)結(jié)構(gòu)庫(kù),它提供了豐富的功能和易于使用的API,使開發(fā)人員能夠輕松地在分布式環(huán)境中操作和管理數(shù)據(jù)。 作為一個(gè)分布式對(duì)象和數(shù)據(jù)結(jié)構(gòu)庫(kù),Redisson提供了許多常見的數(shù)據(jù)結(jié)構(gòu)和算法的實(shí)現(xiàn),包括通用對(duì)象桶、二進(jìn)制流、地理空間

    2024年02月13日
    瀏覽(27)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請(qǐng)作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包