前言
在上一篇文章中我們講解了關(guān)于串口的基礎(chǔ)知識(shí),沒(méi)有看過(guò)的同學(xué)推薦先看一下,否則你可能會(huì)不太理解這篇文章所述的某些內(nèi)容。
這篇文章我們將講解安卓端的串口通信實(shí)踐,即如何使用串口通信實(shí)現(xiàn)安卓設(shè)備與其他設(shè)備例如PLC主板之間數(shù)據(jù)交互。
需要注意的是正如上一篇文章所說(shuō)的,我目前的條件只允許我使用 ESP32 開(kāi)發(fā)版燒錄 Arduino 程序與安卓真機(jī)(小米10U)進(jìn)行串口通信演示。
準(zhǔn)備工作
由于我們需要使用 ESP32 燒錄 Arduino 程序演示安卓端的串口通信,所以在開(kāi)始之前我們應(yīng)該先把程序燒錄好。
那么燒錄一個(gè)怎樣的程序呢?
很簡(jiǎn)單,我這里直接燒了一個(gè) ESP32 使用 9600 的波特率進(jìn)行串口通信,程序內(nèi)容就是 ESP32 不斷的向串口發(fā)送數(shù)據(jù) “e” ,并且監(jiān)聽(tīng)串口數(shù)據(jù),如果接收到數(shù)據(jù) “o” 則打開(kāi)開(kāi)發(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 是這塊開(kāi)發(fā)版的 LED。
使用 Arduino自帶串口監(jiān)視器測(cè)試結(jié)果:
可以看到,確實(shí)如我們?cè)O(shè)想的通過(guò)串口不斷的發(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è)備,則可以通過(guò)往 /dev/ttys
文件中直接寫(xiě)入要發(fā)送的數(shù)據(jù)來(lái)實(shí)現(xiàn),如:
echo test > /dev/ttyS1
這個(gè)命令會(huì)將 “test” 這串字符發(fā)送給串口設(shè)備。
如果想讀取串口發(fā)送的數(shù)據(jù)也是一樣的,可以通過(guò)讀取 /dev/ttys
文件內(nèi)容實(shí)現(xiàn)。
所以,如果我們?cè)诎沧恐邢胍獙?shí)現(xiàn)串口通信,大概率也會(huì)想到直接讀取/寫(xiě)入這個(gè)特殊文件。
android-serialport-api
在上文中我們說(shuō)到,在安卓中也可以通過(guò)與 Linux 一樣的方式--直接讀寫(xiě) /dev/ttys
實(shí)現(xiàn)串口通信。
但是其實(shí)并不需要我們自己去處理讀寫(xiě)和數(shù)據(jù)的解析,因?yàn)楣雀韫俜浇o出了一個(gè)解決方案:android-serialport-api
為了便于理解,我們會(huì)大致說(shuō)一下這個(gè)解決方案的源碼,但是就不上示例了,至于為什么,同學(xué)們往下看就知道了。另外,雖然這個(gè)方案歷史比較悠久,也很長(zhǎng)時(shí)間沒(méi)有人維護(hù)了,但是并不意味著不能使用了,只是使用條件比較苛刻,當(dāng)然,我司目前使用的還是這套方案(哈哈哈哈)。
不過(guò)這里我們不直接看 android-serialport-api 的源碼,而是通過(guò)其他大佬二次封裝的庫(kù)來(lái)看: Android-SerialPort-API
在這個(gè)庫(kù)中,通過(guò)
// 默認(rèn)直接初始化,使用8N1(8數(shù)據(jù)位、無(wú)校驗(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:無(wú)校驗(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();
初始化串口,然后通過(guò):
InputStream in = serialPort.getInputStream();
OutputStream out = serialPort.getOutputStream();
獲取到輸入/輸出流,通過(guò)讀取/寫(xiě)入這兩個(gè)流來(lái)實(shí)現(xiàn)與串口設(shè)備的數(shù)據(jù)通信。
我們首先來(lái)看看初始化串口是怎么做的。
首先檢查了當(dāng)前是否具有串口文件的讀寫(xiě)權(quán)限,如果沒(méi)有則通過(guò) shell 命令更改權(quán)限為 666
,更改后再次檢查是否有權(quán)限,如果還是沒(méi)有就拋出異常。
注意這里的執(zhí)行 shell 時(shí)使用的 runtime 是 Runtime.getRuntime().exec(sSuPath);
也就是說(shuō),它是通過(guò) root 權(quán)限來(lái)執(zhí)行這段命令的!
換句話說(shuō),如果想要通過(guò)這種方式實(shí)現(xiàn)串口通信,必須要有 ROOT 權(quán)限!這就是我說(shuō)我不會(huì)給出示例的原因,因?yàn)槲沂诸^的設(shè)備無(wú)法 ROOT 啊。至于為啥我司還能繼續(xù)使用這種方案的原因也很簡(jiǎn)單,因?yàn)槲覀児た貦C(jī)的安卓設(shè)備都是定制版的啊,擁有 ROOT 權(quán)限不是基本操作?
確定權(quán)限可用后通過(guò) open
方法拿到一個(gè)類型為 FileDescriptor
的變量 mFd
,最后通過(guò)這個(gè) mFd
拿到輸入輸出流。
所以核心在于 open
方法,而 open 方法是一個(gè) native 方法,即 C 代碼:
private native FileDescriptor open(String absolutePath, int baudrate, int dataBits, int parity,
int stopBits, int flags);
C 的源碼這里就不放了,只需要知道它做的工作就是打開(kāi)了 /dev/ttys
文件(準(zhǔn)確的說(shuō)是“終端”),然后通過(guò)傳遞進(jìn)去的這些參數(shù)去按串口規(guī)則解析數(shù)據(jù),最后返回一個(gè) java 的 FileDescriptor
對(duì)象。
在 java 中我們?cè)偻ㄟ^(guò)這個(gè) FileDescriptor
對(duì)象可以拿到輸入/輸出流。
原理說(shuō)起來(lái)是十分的簡(jiǎn)單。
看完通信部分的原理后,我們?cè)賮?lái)看看我們?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;
}
也是通過(guò)直接遍歷 /dev
下的文件,只不過(guò)這里做了一些額外的過(guò)濾。
或者也可以通過(guò)讀取 /proc/tty/drivers
配置文件后過(guò)濾:
Vector
關(guān)于讀取可用串口設(shè)備,其實(shí)從這里的路徑也可以看出,都是系統(tǒng)路徑,也就是說(shuō),如果沒(méi)有權(quán)限,大概率也是讀取不到東西的。
這就是使用與 Linux 一樣的方式去讀取串口數(shù)據(jù)的基本原理,那么問(wèn)題來(lái)了,既然我說(shuō)這個(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 版本開(kāi)始,支持將USB作為主機(jī)模式(USB host)使用,而我們?nèi)绻胍ㄟ^(guò) USB 讀取串口數(shù)據(jù)則需要依賴于這個(gè)主機(jī)模式。
在正式開(kāi)始介紹USB主機(jī)模式前,我們先簡(jiǎn)要介紹一下安卓上支持的USB模式。
安卓上的USB支持三種模式:設(shè)備模式、主機(jī)模式、配件模式。
設(shè)備模式即我們常用的直接將安卓設(shè)備連接至電腦上,此時(shí)電腦上顯示為 USB 外設(shè),即可以當(dāng)成 “U盤(pán)” 使用拷貝數(shù)據(jù),不過(guò)現(xiàn)在安卓普遍還支持 MTP模式(作為攝像頭)、文件傳輸模式(即當(dāng)U盤(pán)用)、網(wǎng)卡模式等。
主機(jī)模式即將我們的安卓設(shè)備作為主機(jī),連接其他外設(shè),此時(shí)安卓設(shè)備就相當(dāng)于上面設(shè)備模式中的電腦。此時(shí)安卓設(shè)備可以連接鍵盤(pán)、鼠標(biāo)、U盤(pán)以及嵌入式應(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ò)展塢、診斷和音樂(lè)設(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模式后,下面我們開(kāi)始介紹如何使用USB主機(jī)模式。當(dāng)然,這里只是大概介紹原生APi的使用方法,我們?cè)趯?shí)際使用中一般都都是直接使用大佬編寫(xiě)的第三方庫(kù)。
準(zhǔn)備工作
在開(kāi)始正式使用USB主機(jī)模式時(shí)我們需要先做一些準(zhǔn)備工作。
首先我們需要在清單文件(AndroidManifest.xml)中添加:
name="android.hardware.usb.host" />
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" />
<span class="hljs-name"intent-filter>
<span class="hljs-name"activity>
<span class="hljs-name"application>
<span class="hljs-name"manifest>
聲明好清單文件后,我們就可以查找當(dāng)前可用的設(shè)備信息了:
private fun scanDevice(context: Context) {
val manager = context.getSystemService(Context.USB_SERVICE) as UsbManager
val deviceList: HashMap
將 ESP32 開(kāi)發(fā)版插上手機(jī),運(yùn)行程序,輸出如下:
可以看到,正確的查找到了我們的 ESP32 開(kāi)發(fā)版。
這里提一下,因?yàn)槲覀兊氖謾C(jī)只有一個(gè) USB 口,此時(shí)已經(jīng)插上了 ESP32 開(kāi)發(fā)版,所以無(wú)法再通過(guò)數(shù)據(jù)線直接連接電腦的 ADB 了,此時(shí)我們需要使用無(wú)線 ADB,具體怎么使用無(wú)線 ADB,請(qǐng)自行搜索。
另外,如果我們想要通過(guò)查找到設(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),可以在這里開(kāi)始請(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
打開(kāi)了指定的設(shè)備,即連接到設(shè)備。
然后通過(guò) bulkTransfer
接收數(shù)據(jù),它會(huì)將接收到的數(shù)據(jù)寫(xiě)入緩沖數(shù)組 bytes
中,并返回成功接收到的數(shù)據(jù)長(zhǎng)度。
運(yùn)行程序,連接設(shè)備,日志打印如下:
可以看到,輸出的數(shù)據(jù)并不是我們預(yù)料中的數(shù)據(jù)。
這是因?yàn)檫@是非常原始的數(shù)據(jù),如果我們想要讀取數(shù)據(jù),還需要針對(duì)不同的串口轉(zhuǎn)USB芯片或協(xié)議編寫(xiě)驅(qū)動(dòng)程序才能獲取到正確的數(shù)據(jù)。
順道一提,如果想要將數(shù)據(jù)寫(xiě)入串口數(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一致,所以這里不再贅述。
-
plc
+關(guān)注
關(guān)注
5011文章
13297瀏覽量
463376 -
串口通信
+關(guān)注
關(guān)注
34文章
1626瀏覽量
55529 -
安卓
+關(guān)注
關(guān)注
5文章
2130瀏覽量
57203 -
ESP32
+關(guān)注
關(guān)注
18文章
971瀏覽量
17276
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論