安卓與串列埠通訊-實踐篇
前言
在上一篇文章中我們講解了關於串列埠的基礎知識,沒有看過的同學推薦先看一下,否則你可能會不太理解這篇文章所述的某些內容。
這篇文章我們將講解安卓端的串列埠通訊實踐,即如何使用串列埠通訊實現安卓裝置與其他裝置例如PLC主機板之間資料互動。
需要注意的是正如上一篇文章所說的,我目前的條件只允許我使用 ESP32 開發版燒錄 Arduino 程式與安卓真機(小米10U)進行串列埠通訊演示。
準備工作
由於我們需要使用 ESP32 燒錄 Arduino 程式演示安卓端的串列埠通訊,所以在開始之前我們應該先把程式燒錄好。
那麼燒錄一個怎樣的程式呢?
很簡單,我這裡直接燒了一個 ESP32 使用 9600 的波特率進行串列埠通訊,程式內容就是 ESP32 不斷的向串列埠傳送資料 “e” ,並且監聽串列埠資料,如果接收到資料 “o” 則開啟開發版上自帶的 LED 燈,如果接收到資料 “c” 則關閉這個 LED 燈。
程式碼如下:
```arduino
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 號 Pin 是這塊開發版的 LED。
使用 Arduino自帶串列埠監視器測試結果:
可以看到,確實如我們設想的通過串列埠不斷的傳送字元 “e”,並且在接收到字元 “o” 後點亮了 LED。
安卓實現串列埠通訊
原理概述
眾所周知,安卓其實是基於 Linux 的作業系統,所以在安卓中對於串列埠的處理與 Linux 一致。
在 Linux 中串列埠會被視為一個“裝置”,並體現為 /dev/ttys
檔案。
/dev/ttys
又被稱為字元終端,例如 ttys0
對應的是 DOS/Windows 系統中的 COM1 串列埠檔案。
通常,我們可以簡單理解,如果我們插入了某個串列埠裝置,則這個裝置與 Linux 的通訊會由 /dev/ttys
檔案進行 “中轉”。
即,如果 Linux 想要傳送資料給串列埠裝置,則可以通過往 /dev/ttys
檔案中直接寫入要傳送的資料來實現,如:
echo test > /dev/ttyS1
這個命令會將 “test” 這串字元傳送給串列埠裝置。
如果想讀取串列埠傳送的資料也是一樣的,可以通過讀取 /dev/ttys
檔案內容實現。
所以,如果我們在安卓中想要實現串列埠通訊,大概率也會想到直接讀取/寫入這個特殊檔案。
android-serialport-api
在上文中我們說到,在安卓中也可以通過與 Linux 一樣的方式--直接讀寫 /dev/ttys
實現串列埠通訊。
但是其實並不需要我們自己去處理讀寫和資料的解析,因為谷歌官方給出了一個解決方案:android-serialport-api
為了便於理解,我們會大致說一下這個解決方案的原始碼,但是就不上示例了,至於為什麼,同學們往下看就知道了。另外,雖然這個方案歷史比較悠久,也很長時間沒有人維護了,但是並不意味著不能使用了,只是使用條件比較苛刻,當然,我司目前使用的還是這套方案(哈哈哈哈)。
不過這裡我們不直接看 android-serialport-api 的原始碼,而是通過其他大佬二次封裝的庫來看: Android-SerialPort-API
在這個庫中,通過
```java // 預設直接初始化,使用8N1(8資料位、無校驗位、1停止位),path為串列埠路徑(如 /dev/ttys1),baudrate 為波特率 SerialPort serialPort = new SerialPort(path, baudrate);
// 使用可選引數配置初始化,可配置資料位、校驗位、停止位 - 7E2(7資料位、偶校驗、2停止位) SerialPort serialPort = SerialPort .newBuilder(path, baudrate) // 校驗位;0:無校驗位(NONE,預設);1:奇校驗位(ODD);2:偶校驗位(EVEN) // .parity(2) // 資料位,預設8;可選值為5~8 // .dataBits(7) // 停止位,預設1;1:1位停止位;2:2位停止位 // .stopBits(2) .build(); ```
初始化串列埠,然後通過:
java
InputStream in = serialPort.getInputStream();
OutputStream out = serialPort.getOutputStream();
獲取到輸入/輸出流,通過讀取/寫入這兩個流來實現與串列埠裝置的資料通訊。
我們首先來看看初始化串列埠是怎麼做的。
首先檢查了當前是否具有串列埠檔案的讀寫許可權,如果沒有則通過 shell 命令更改許可權為 666
,更改後再次檢查是否有許可權,如果還是沒有就丟擲異常。
注意這裡的執行 shell 時使用的 runtime 是 Runtime.getRuntime().exec(sSuPath);
也就是說,它是通過 root 許可權來執行這段命令的!
換句話說,如果想要通過這種方式實現串列埠通訊,必須要有 ROOT 許可權!這就是我說我不會給出示例的原因,因為我手頭的裝置無法 ROOT 啊。至於為啥我司還能繼續使用這種方案的原因也很簡單,因為我們工控機的安卓裝置都是定製版的啊,擁有 ROOT 許可權不是基本操作?
確定許可權可用後通過 open
方法拿到一個型別為 FileDescriptor
的變數 mFd
,最後通過這個 mFd
拿到輸入輸出流。
所以核心在於 open
方法,而 open 方法是一個 native 方法,即 C 程式碼:
java
private native FileDescriptor open(String absolutePath, int baudrate, int dataBits, int parity,
int stopBits, int flags);
C 的原始碼這裡就不放了,只需要知道它做的工作就是打開了 /dev/ttys
檔案(準確的說是“終端”),然後通過傳遞進去的這些引數去按串列埠規則解析資料,最後返回一個 java 的 FileDescriptor
物件。
在 java 中我們再通過這個 FileDescriptor
物件可以拿到輸入/輸出流。
原理說起來是十分的簡單。
看完通訊部分的原理後,我們再來看看我們如何查詢可用的串列埠呢?
其實和 Linux 上也一樣:
```java
public Vector
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
配置檔案後過濾:
java
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;
}
關於讀取可用串列埠裝置,其實從這裡的路徑也可以看出,都是系統路徑,也就是說,如果沒有許可權,大概率也是讀取不到東西的。
這就是使用與 Linux 一樣的方式去讀取串列埠資料的基本原理,那麼問題來了,既然我說這個方法使用條件比較苛刻,那麼更易用的替代方案是什麼呢?
我們下面就會介紹,那就是使用安卓的 USB host
(USB主機)的功能。
USB host
Android 3.1(API 級別 12)或更高版本的平臺直接支援 USB 配件和主機模式。USB 配件模式還作為外掛庫向後移植到 Android 2.3.4(API 級別 10)中,以支援更廣泛的裝置。裝置製造商可以選擇是否在裝置的系統映像中新增該外掛庫。
在安卓 3.1 版本開始,支援將USB作為主機模式(USB host)使用,而我們如果想要通過 USB 讀取串列埠資料則需要依賴於這個主機模式。
在正式開始介紹USB主機模式前,我們先簡要介紹一下安卓上支援的USB模式。
安卓上的USB支援三種模式:裝置模式、主機模式、配件模式。
裝置模式即我們常用的直接將安卓裝置連線至電腦上,此時電腦上顯示為 USB 外設,即可以當成 “U盤” 使用拷貝資料,不過現在安卓普遍還支援 MTP模式(作為攝像頭)、檔案傳輸模式(即當U盤用)、網絡卡模式等。
主機模式即將我們的安卓裝置作為主機,連線其他外設,此時安卓裝置就相當於上面裝置模式中的電腦。此時安卓裝置可以連線鍵盤、滑鼠、U盤以及嵌入式應用USB轉串列埠、轉I2C等裝置。但是如果想要將安卓裝置作為主機模式可能需要一條支援 OTG 的資料線或轉接頭。(Micro-USB 或 USB type-c 轉 USB-A 口)
而在 USB 配件模式下,外部 USB 硬體充當 USB 主機。配件示例可能包括機器人控制器、擴充套件塢、診斷和音樂裝置、自助服務終端、讀卡器等等。這樣,不具備主機功能的 Android 裝置就能夠與 USB 硬體互動。Android USB 配件必須設計為與 Android 裝置相容,並且必須遵守 Android 配件通訊協議。
裝置模式與配件模式的區別在於在配件模式下,除了 adb 之外,主機還可以看到其他 USB 功能。
使用USB主機模式與外設互動資料
在介紹完安卓中的三種USB模式後,下面我們開始介紹如何使用USB主機模式。當然,這裡只是大概介紹原生APi的使用方法,我們在實際使用中一般都都是直接使用大佬編寫的第三方庫。
準備工作
在開始正式使用USB主機模式時我們需要先做一些準備工作。
首先我們需要在清單檔案(AndroidManifest.xml)中新增:
```xml
一個完整的清單檔案示例如下:
xml
<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>
宣告好清單檔案後,我們就可以查詢當前可用的裝置資訊了:
kotlin
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 開發版插上手機,執行程式,輸出如下:
可以看到,正確的查詢到了我們的 ESP32 開發版。
這裡提一下,因為我們的手機只有一個 USB 口,此時已經插上了 ESP32 開發版,所以無法再通過資料線直接連線電腦的 ADB 了,此時我們需要使用無線 ADB,具體怎麼使用無線 ADB,請自行搜尋。
另外,如果我們想要通過查詢到裝置後請求連線的方式連線到串列埠裝置的話,還需要額外申請許可權。(同理,如果我們直接在清單檔案中提前宣告需要連線的裝置則不需要額外申請許可權,具體可以看看參考資料5,這裡不再贅述)
首先宣告一個廣播接收器,用於接收授權結果:
```kotlin 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 {
// 已授權,可以在這裡開始請求連線
connectDevice(context, device)
}
} else {
Log.d(TAG, "permission denied for device $device")
}
}
}
}
} ```
宣告好之後在 Acticity 的 OnCreate 中註冊這個廣播接收器:
kotlin
permissionIntent = PendingIntent.getBroadcast(this, 0, Intent(ACTION_USB_PERMISSION), FLAG_MUTABLE)
val filter = IntentFilter(ACTION_USB_PERMISSION)
registerReceiver(usbReceiver, filter)
最後,在查詢到裝置後,呼叫 manager.requestPermission(deviceList.values.first(), permissionIntent)
彈出對話方塊申請許可權。
連線到裝置並收發資料
完成上述的準備工作後,我們終於可以連線搜尋到的裝置並進行資料互動了:
```kotlin 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
打開了指定的裝置,即連線到裝置。
然後通過 bulkTransfer
接收資料,它會將接收到的資料寫入緩衝陣列 bytes
中,並返回成功接收到的資料長度。
執行程式,連線裝置,日誌列印如下:
可以看到,輸出的資料並不是我們預料中的資料。
這是因為這是非常原始的資料,如果我們想要讀取資料,還需要針對不同的串列埠轉USB晶片或協議編寫驅動程式才能獲取到正確的資料。
順道一提,如果想要將資料寫入串列埠資料的話可以使用 controlTransfer()
。
所以,我們在實際生產環境中使用的都是基於此封裝好的第三方庫。
這裡推薦使用 usb-serial-for-android
usb-serial-for-android
使用這個庫的第一步當然是匯入依賴:
groovy
// 新增倉庫
allprojects {
repositories {
...
maven { url 'http://jitpack.io' }
}
}
// 新增依賴
dependencies {
implementation 'com.github.mik3y:usb-serial-for-android:3.4.6'
}
新增完依賴同樣需要在清單檔案中新增相應欄位以及處理許可權,因為和上述使用原生API一致,所以這裡不再贅述。
和原生 API 不同的是,因為我們此時已經知道了我們的 ESP32 主機板的裝置資訊,以及使用的驅動(CDC),所以我們就不使用原生的查詢可用裝置的方法了,我們這裡直接指定我們已知的這個裝置(當然,你也可以繼續使用原生API的查詢和連線方法):
```kotlin private fun scanDevice(context: Context) { val manager = context.getSystemService(Context.USB_SERVICE) as UsbManager
val customTable = ProbeTable()
// 新增我們的裝置資訊,三個引數分別為 vendroId、productId、驅動程式
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)
}
else {
Log.i(TAG, "scanDevice: 無裝置!")
}
} ```
連線到裝置後,下一步就是和資料互動,這裡封裝的十分方便,只需要獲取到 UsbSerialPort
後,直接呼叫它的 read()
和 write()
即可讀寫資料:
```kotlin port = driver.ports[0] // 大多數裝置都只有一個 port,所以大多數情況下直接取第一個就行
port.open(connection) // 設定連線引數,波特率9600,以及 “8N1” port.setParameters(9600, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE)
// 讀取資料 val responseBuffer = ByteArray(1024) port.read(responseBuffer, 0)
// 寫入資料 val sendData = byteArrayOf(0x6F) port.write(sendData, 0) ```
此時,一個完整的,用於測試我們上述 ESP32 程式的程式碼如下:
```kotlin @Composable fun SerialScreen() { val context = LocalContext.current
Column(
Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(onClick = { scanDevice(context) }) {
Text(text = "查詢並連線裝置")
}
Button(onClick = { switchLight(true) }) {
Text(text = "開燈")
}
Button(onClick = { switchLight(false) }) {
Text(text = "關燈")
}
}
}
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: 無裝置!")
}
}
private fun switchLight(isON: Boolean) { val sendData = if (isON) byteArrayOf(0x6F) else byteArrayOf(0x63)
port.write(sendData, 0)
} ```
執行這個程式,並且連線裝置,輸出如下:
可以看到輸出的是 byte 的 101,轉換為 ASCII 即為 “e”。
然後我們點選 “開燈”、“關燈” 效果如下:
對了,這裡傳送的資料 “0x6F” 即 ASCII “o” 的十六進位制,同理,“0x63” 即 “c”。
可以看到,可以完美的和我們的 ESP32 開發版進行通訊。
例項
無論使用什麼方式與串列埠通訊,我們在安卓APP的程式碼層面能夠拿到的資料已經是處理好了的資料。
即,在上一篇文章中我們說過串列埠通訊的一幀資料包括起始位、資料位、校驗位、停止位。但是我們在安卓中使用時一般拿到的都只有 資料位 的資料,其他資料已經在底層被解析好了,無需我們去關心怎麼解析,或者使用。
我們可以直接拿到的就是可用資料。
這裡舉一個我之前用過的某型號驅動版的例子。
這塊驅動版關於通訊的資訊如圖:
可以看到,它採用了 RS485 的通訊方式,波特率支援 9600 或 38400,8位資料位,無校驗,1位停止位。
並且,它還規定了一個數據協議。
在它定義的協議中,第一位為地址;第二位為指令;第三位到第N位為資料內容;最後兩位為CRC校驗。
需要注意的是,這裡定義的協議是基於串列埠通訊的,不要把這個協議和串列埠通訊搞混了,簡單來說就是在串列埠通訊協議的資料位中又定義了一個自己的協議。
而且可以看到,雖然定義串列埠引數時沒有指定校驗,但是在它自己的協議中指定了使用 CRC 校驗。
另外,弱弱的吐槽一句,這個驅動版的協議真的不好使。
在實際使用過程中,主機與驅動版的通訊資料無法保證一定會在同一個資料幀中傳送完成,所以可能會造成“粘包”、“分包”現象,也就是說,資料可能會分幾次發過來,而且你不好判斷這資料是上次沒傳送完的資料還是新的資料。
我使用過的另外一款驅動版就方便的多,因為它會在幀頭加上開始符號和資料長度,幀尾加上結束符號。
這樣一來,即使出現“粘包”、“分包”我們也能很好的給它解析出來。
當然,它這樣設計協議肯定是有它的道理的,無非就是減少通訊代價之類的。
我還遇到過一款十分簡潔的驅動版,直接傳送一個整數過去表示執行對應的指令。
驅動版回傳的資料同樣非常簡單,就是一個數字,然後事先約定各個數字表示什麼意思……
說歸說,我們還是繼續來看這款驅動版的通訊協議:
這是它的其中一個指令內容,我們傳送指令 “1” 過去後,它會返回當前驅動版的型號和版本資訊給我們。
因為我們的主機板是定製工控主機板,所以使用的通訊方式是直接用 android-serialport-api。
最終傳送與接收回復也很簡單:
```kotlin /* * 將十六進位制字串轉成 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() //重新拷貝一個使用,避免原資料被清零
if (cmd.cmdId.checkDataFormat(rcvData)) { //檢查回覆資料格式
isPkgLost = false
if (cmd.cmdId.isResponseBelong(rcvData)) { //檢查回覆命令來源
if (!AdhShareData.instance.getIsUsingCrc()) { //如果不開啟CRC檢驗則直接返回 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)) { //檢驗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 { //資料不符合,可能是遇到了分包,繼續等待下一個資料,然後合併
isPkgLost = true
return isReceivedLegalData(cmd)
/*coroutineScope.launch(Dispatchers.Main) {
cmdResponseCallback?.onCmdResponse(ResponseStatus.FailCauseWrongFormat, ByteArray(0), -1, -1, cmd.cmdId)
}
return false */
}
}
// ……省略初始化和連線程式碼
// 傳送資料 val bytes = hexStrToBytes("0201C110") outputStream.write(bytes, 0, bytes.size)
// 解析資料 val recvBuffer = ByteArray(0) inputStream.read(recvBuffer)
while (receiveBuffer.isEmpty()) { delay(10) }
isReceivedLegalData() ```
本來打算直接發我封裝好的這個驅動版的協議庫的,想了想,好像不太合適,所以就大概抽出了這些不完整的程式碼,懂這個意思就行了,哈哈。
總結
從上面介紹的兩種方式可以看出,兩種方式使用各有優缺點。
使用 android-serialport-api 可以直接讀取串列埠資料內容,不需要轉USB介面,不需要驅動支援,但是需要 ROOT,適合於定製安卓主機板上已經預留了 RS232 或 RS485 介面且裝置已 ROOT 的情況下使用。
而使用 USB host ,可以直接讀取USB介面轉接的串列埠資料,不需要ROOT,但是隻支援有驅動的串列埠轉USB晶片,且只支援使用USB介面,不支援直接連線串列埠裝置。
各位可以根據自己的實際情況靈活選擇使用什麼方式來實現串列埠通訊。
當然,除了現在介紹的這些串列埠通訊,其實還有一個通訊協議在實際使用中用的非常多,那就是 MODBUS 協議。
下一篇文章,我們將介紹 MODBUS。
參考資料
- android-serialport-api
- What is tty?
- Text-Terminal-HOWTO
- Terminal Special Files
- USB host
- Android開啟OTG功能/USB Host API功能
本文正在參加「金石計劃 . 瓜分6萬現金大獎」
- 安卓與串列埠通訊-校驗篇
- 安卓與串列埠通訊-實踐篇
- 為 Kotlin 的函式新增作用域限制(以 Compose 為例)
- 安卓與串列埠通訊-基礎篇
- Compose For Desktop 實踐:使用 Compose-jb 做一個時間水印助手
- 初探 Compose for Wear OS:實現一個簡易選擇APP
- Compose太香了,不想再寫傳統 xml View?教你如何在已有View專案中混合使用Compose
- 在安卓中壓縮GIF的幾種方法(附例項程式碼)
- 魔改車鑰匙實現遠端控車:(1)整體思路及控制方案實現
- 跟我一起使用 compose 做一個跨平臺的黑白棋遊戲(3)狀態與遊戲控制邏輯
- 跟我一起使用 compose 做一個跨平臺的黑白棋遊戲(2)介面佈局
- 以不同的形式在安卓中建立GIF動圖
- 跟我一起使用 compose 做一個跨平臺的黑白棋遊戲(4)移植到compose-jb實現跨平臺
- 羨慕大勞星空頂?不如跟我一起使用 Jetpack compose 繪製一個星空背景(帶流星動畫)
- 魔改車鑰匙實現遠端控車:(4)基於compose和經典藍芽編寫一個控制APP