前言
在上一篇文章中我們講解了關于串口的基礎知識,沒有看過的同學推薦先看一下,否則你可能會不太理解這篇文章所述的某些內容。
這篇文章我們將講解安卓端的串口通信實踐,即如何使用串口通信實現安卓設備與其他設備例如PLC主板之間數據交互。
需要注意的是正如上一篇文章所說的,我目前的條件只允許我使用 ESP32 開發版燒錄 Arduino 程序與安卓真機(小米10U)進行串口通信演示。
準備工作
由于我們需要使用 ESP32 燒錄 Arduino 程序演示安卓端的串口通信,所以在開始之前我們應該先把程序燒錄好。
那么燒錄一個怎樣的程序呢?
很簡單,我這里直接燒了一個 ESP32 使用 9600 的波特率進行串口通信,程序內容就是 ESP32 不斷的向串口發送數據 “e” ,并且監聽串口數據,如果接收到數據 “o” 則打開開發版上自帶的 LED 燈,如果接收到數據 “c” 則關閉這個 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 號 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
在這個庫中,通過
// 默認直接初始化,使用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();
初始化串口,然后通過:
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 代碼:
private native FileDescriptor open(String absolutePath, int baudrate, int dataBits, int parity,
int stopBits, int flags);
C 的源碼這里就不放了,只需要知道它做的工作就是打開了 /dev/ttys
文件(準確的說是“終端”),然后通過傳遞進去的這些參數去按串口規則解析數據,最后返回一個 java 的 FileDescriptor
對象。
在 java 中我們再通過這個 FileDescriptor
對象可以拿到輸入/輸出流。
原理說起來是十分的簡單。
看完通信部分的原理后,我們再來看看我們如何查找可用的串口呢?
其實和 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
關于讀取可用串口設備,其實從這里的路徑也可以看出,都是系統路徑,也就是說,如果沒有權限,大概率也是讀取不到東西的。
這就是使用與 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)中添加:
name="android.hardware.usb.host" />
name="android.hardware.usb.action.USB_DEVICE_ATTACHED" /
一個完整的清單文件示例如下:
<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" />
<span class="hljs-name"intent-filter>
<span class="hljs-name"activity>
<span class="hljs-name"application>
<span class="hljs-name"manifest>
聲明好清單文件后,我們就可以查找當前可用的設備信息了:
private fun scanDevice(context: Context) {
val manager = context.getSystemService(Context.USB_SERVICE) as UsbManager
val deviceList: HashMap
將 ESP32 開發版插上手機,運行程序,輸出如下:
可以看到,正確的查找到了我們的 ESP32 開發版。
這里提一下,因為我們的手機只有一個 USB 口,此時已經插上了 ESP32 開發版,所以無法再通過數據線直接連接電腦的 ADB 了,此時我們需要使用無線 ADB,具體怎么使用無線 ADB,請自行搜索。
另外,如果我們想要通過查找到設備后請求連接的方式連接到串口設備的話,還需要額外申請權限。(同理,如果我們直接在清單文件中提前聲明需要連接的設備則不需要額外申請權限,具體可以看看參考資料5,這里不再贅述)
首先聲明一個廣播接收器,用于接收授權結果:
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 中注冊這個廣播接收器:
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)
彈出對話框申請權限。
連接到設備并收發數據
完成上述的準備工作后,我們終于可以連接搜索到的設備并進行數據交互了:
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
使用這個庫的第一步當然是導入依賴:
// 添加倉庫
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
// 添加依賴
dependencies {
implementation 'com.github.mik3y:usb-serial-for-android:3.4.6'
}
添加完依賴同樣需要在清單文件中添加相應字段以及處理權限,因為和上述使用原生API一致,所以這里不再贅述。
-
plc
+關注
關注
5011文章
13299瀏覽量
463398 -
串口通信
+關注
關注
34文章
1626瀏覽量
55530 -
安卓
+關注
關注
5文章
2130瀏覽量
57207 -
ESP32
+關注
關注
18文章
971瀏覽量
17277
發布評論請先 登錄
相關推薦
評論