智能藍(lán)牙防丟器
藍(lán)牙無線設(shè)備實(shí)現(xiàn)串行通信是通過無線射頻鏈接,利用藍(lán)牙模塊實(shí)現(xiàn)。藍(lán)牙模塊主要由無線收發(fā)單元、鏈路控制單元和鏈路管理及主機(jī)I/O這3個(gè)單元組成。就藍(lán)牙射頻模塊來說,為了在提高收發(fā)性能的同時(shí)減小器件的體積和成本,各公司都采用了自己特有的一些技術(shù),從而使藍(lán)牙射頻模塊的結(jié)構(gòu)都不盡相同。
但就其基本原理來說,藍(lán)牙射頻模塊一般由接收模塊、發(fā)送模塊和合成器這三個(gè)模塊組成。
藍(lán)牙傳輸?shù)脑恚?/p>
1 主從關(guān)系:
藍(lán)牙技術(shù)規(guī)定每一對(duì)設(shè)備之間進(jìn)行藍(lán)牙通訊時(shí),必須一個(gè)為主角色,另一為從角色,才能進(jìn)行通信,通信時(shí),必須由主端進(jìn)行查找,發(fā)起配對(duì),建鏈成功后,雙方即可收發(fā)數(shù)據(jù)。理論上,一個(gè)藍(lán)牙主端設(shè)備,可同時(shí)與7個(gè)藍(lán)牙從端設(shè)備進(jìn)行通訊。一個(gè)具備藍(lán)牙通訊功能的設(shè)備, 可以在兩個(gè)角色間切換,平時(shí)工作在從模式,等待其它主設(shè)備來連接,需要時(shí),轉(zhuǎn)換為主模式,向其它設(shè)備發(fā)起呼叫。一個(gè)藍(lán)牙設(shè)備以主模式發(fā)起呼叫時(shí),需要知道對(duì)方的藍(lán)牙地址,配對(duì)密碼等信息,配對(duì)完成后,可直接發(fā)起呼叫。
2 呼叫過程:
藍(lán)牙主端設(shè)備發(fā)起呼叫,首先是查找,找出周圍處于可被查找的藍(lán)牙設(shè)備。主端設(shè)備找到從端藍(lán)牙設(shè)備后,與從端藍(lán)牙設(shè)備進(jìn)行配對(duì),此時(shí)需要輸入從端設(shè)備的PIN碼,也有設(shè)備不需要輸入PIN碼。配對(duì)完成后,從端藍(lán)牙設(shè)備會(huì)記錄主端設(shè)備的信任信息,此時(shí)主端即可向從端設(shè)備發(fā)起呼叫,已配對(duì)的設(shè)備在下次呼叫時(shí),不再需要重新配對(duì)。已配對(duì)的設(shè)備,做為從端的藍(lán)牙耳機(jī)也可以發(fā)起建鏈請求,但做數(shù)據(jù)通訊的藍(lán)牙模塊一般不發(fā)起呼叫。鏈路建立成功后,主從兩端之間即可進(jìn)行雙向的數(shù)據(jù)或語音通訊。在通信狀態(tài)下,主端和從端設(shè)備都可以發(fā)起斷鏈,斷開藍(lán)牙鏈路。
3 數(shù)據(jù)傳輸
藍(lán)牙數(shù)據(jù)傳輸應(yīng)用中,一對(duì)一串口數(shù)據(jù)通訊是最常見的應(yīng)用之一,藍(lán)牙設(shè)備在出廠前即提前設(shè)好兩個(gè)藍(lán)牙設(shè)備之間的配對(duì)信息,主端預(yù)存有從端設(shè)備的PIN碼、地址等,兩端設(shè)備加電即自動(dòng)建鏈,透明串口傳輸,無需外圍電路干預(yù)。一對(duì)一應(yīng)用中從端設(shè)備可以設(shè)為兩種類型,一是靜默狀態(tài),即只能與指定的主端通信,不被別的藍(lán)牙設(shè)備查找;二是開發(fā)狀態(tài),既可被指定主端查找,也可以被別的藍(lán)牙設(shè)備查找建鏈。
1 什么是智能藍(lán)牙防丟器
所謂智能藍(lán)牙(Smart Bluetooth)防丟器,是采用藍(lán)牙技術(shù)專門為智能手機(jī)設(shè)計(jì)的防丟器。其工作原理主要是通過距離變化來判斷物品是否還控制在你的安全范圍。主要適用于手機(jī)、錢包、鑰匙、行李等貴重物品的防丟,也可用于防止兒童或?qū)櫸锏淖呤?。[請看正版請百度:beautifulzzzz(看樓主博客園官方博客,享高質(zhì)量生活)嘻嘻?。。。?/p>
圖 1-1 藍(lán)牙防丟器應(yīng)用領(lǐng)域
2 藍(lán)牙防丟器的主要構(gòu)造
目前比較成熟的產(chǎn)品一般是采用藍(lán)牙4.0技術(shù),具有低功耗、雙向防丟、自動(dòng)報(bào)警等優(yōu)點(diǎn)。雖然市場上該類產(chǎn)品種類繁多、層出不窮,但其核心構(gòu)成一般包括:藍(lán)牙4.0芯片、藍(lán)牙芯片輔助電路、藍(lán)牙天線、蜂鳴器、開關(guān)、電源等。
圖 2-1 藍(lán)牙防丟器構(gòu)成
3 藍(lán)牙模塊的選擇
由于這是第一個(gè)智能硬件DIY篇,樓主可不想一下子把大家給嚇倒了。所以這里我們先用一個(gè)相對(duì)簡單但常用的藍(lán)牙模塊HC-05/HC-06進(jìn)行DIY 。如下圖該模塊把天線、濾波電路、Soc、晶振都集成在了一起,當(dāng)我們用的時(shí)候只要關(guān)注1、2、12、13、24、26這幾個(gè)引腳就能實(shí)現(xiàn)比較完整的藍(lán)牙通信功能,這樣就為我們制作藍(lán)牙防丟器節(jié)省了很多挑選元件、設(shè)計(jì)電路、焊接制板的功夫,是不是超贊呀?
圖 3-1 藍(lán)牙模塊1
其實(shí)還有更贊的呢!由于考慮到很多讀者在硬件方面還都是新手,初次拿到郵票邊緣式引腳的模塊會(huì)焊接不好,于是樓主又找到了一款封裝更好的藍(lán)牙模塊(其實(shí)就是把上面的模塊加一個(gè)托,然后將VCCGNDTXDRXD四個(gè)關(guān)鍵的引腳引出)。當(dāng)我們只是想把藍(lán)牙模塊作為標(biāo)簽時(shí),只要在VCC和GND之間給它加上相應(yīng)的電壓就行了;當(dāng)想用它進(jìn)行無線數(shù)據(jù)傳輸時(shí),這時(shí)TXD和RXD兩個(gè)引腳就起作用了。
圖 3-2 藍(lán)牙模塊2
4 開始制作一個(gè)簡易的藍(lán)牙防丟器
上面說了這么多了,那么我們的藍(lán)牙防丟器的設(shè)計(jì)方案到底是什么樣的呢?簡單起見,咱么僅僅實(shí)現(xiàn)通過距離變化來判斷物品是否還控制在你的安全范圍內(nèi)的具有核心功能的防丟器,對(duì)于節(jié)能功能、雙向報(bào)警功能甚至是自拍功能咱們就先不考慮了。哈哈,可能說到這里大家還是對(duì)咱們要做的防丟器一點(diǎn)想法都沒有,其實(shí)通過上面的鋪墊樓主有信心大家可以在一分鐘之內(nèi)知道怎么完成它!
圖 4-1 簡易藍(lán)牙防丟器
相信很多看完上面圖片同學(xué)會(huì)恍然大悟——不是嘛,只要將藍(lán)牙模塊接上相應(yīng)的電源模塊就能夠做成一個(gè)簡單的可以發(fā)出藍(lán)牙信號(hào)的防丟器了!對(duì)的,由于我們沒有加入復(fù)雜的通信功能,所以我們僅僅把藍(lán)牙模塊通上電做成一個(gè)藍(lán)牙標(biāo)簽就可以了。但是大家不要高興太早,雖然是第一個(gè)DIY,樓主也不會(huì)這么水吧~(沒發(fā)現(xiàn)上面圖片中手機(jī)屏幕里的應(yīng)用還是一片空白嗎?哈哈)。
5 如何找到并學(xué)習(xí)要用到的API
上面制作藍(lán)牙防丟器的硬件部分讓大家覺得沒什么挑戰(zhàn)性,那么接下來的東西可就有一定難度了!記得樓主當(dāng)時(shí)學(xué)安卓藍(lán)牙開發(fā)的時(shí)候費(fèi)了好大力氣的。這里樓主強(qiáng)烈建議大家著手了解安卓某個(gè)功能的應(yīng)用時(shí)最好去安卓開發(fā)者社區(qū),但是隨著Google被屏Android Developer也不能被訪問了。雖然樓主在MIT網(wǎng)站上又找到了一個(gè)類似的網(wǎng)頁,但是訪問速度不是那么流暢,為了方便,LZ挑出了和本節(jié)相關(guān)的知識(shí)幫助大家理解和運(yùn)用。
圖 5-1 Android Developer頁面
6 安卓藍(lán)牙編程小知識(shí)
安卓平臺(tái)支持藍(lán)牙協(xié)議棧,允許一臺(tái)設(shè)備和其他設(shè)備進(jìn)行無線數(shù)據(jù)交換,當(dāng)大家想使用藍(lán)牙時(shí)可以通過調(diào)用相應(yīng)的API實(shí)現(xiàn)功能。這些API提供的主要功能有:
△ 掃描搜索其他藍(lán)牙設(shè)備(Scan for other Bluetooth devices)
△ 查詢本地藍(lán)牙適配器配對(duì)的藍(lán)牙設(shè)備(Query the local Bluetooth adapter for paired Bluetooth devices)
△ 建立RFCOMM通道(Establish RFCOMM channels)
△ 連接其他設(shè)備(Connect to other devices through service discovery)
△ 與其他設(shè)備進(jìn)行數(shù)據(jù)交換(Transfer data to and from other devices)
△ 管理多組連接(Manage multiple connections)
當(dāng)準(zhǔn)備在應(yīng)用程序中使用藍(lán)牙時(shí),首先需要在manifest文件中包含BLUETOOTH和BLUETOOTH_ADMIN權(quán)限:
《uses-permission android:name=“android.permission.BLUETOOTH_ADMIN” /》
《uses-permission android:name=“android.permission.BLUETOOTH” /》
接著涉及藍(lán)牙的操作主要有:1、開啟藍(lán)牙 2、關(guān)閉藍(lán)牙 3、能被搜到 4、獲取配對(duì)設(shè)備 5、傳輸數(shù)據(jù)。由于本節(jié)只涉及到搜索周邊設(shè)備,所以配對(duì)并傳輸數(shù)據(jù)暫時(shí)不介紹。
7 安卓藍(lán)牙編程主要操作
要想通過編程操作藍(lán)牙設(shè)備,首先我們得了解一下有可供我們使用的相關(guān)類及其成員函數(shù)有哪些,如下圖:藍(lán)牙設(shè)備包括本地設(shè)備和遠(yuǎn)程設(shè)備,本地設(shè)備對(duì)應(yīng)的類為BluetoothAdapter,遠(yuǎn)程設(shè)備對(duì)應(yīng)的類為BluetoothDevice,兩者的成員函數(shù)基本相同,樓主將在接下來詳細(xì)講解。
圖 7-1 藍(lán)牙設(shè)備相關(guān)函數(shù)
7_1 打開本地藍(lán)牙設(shè)備
我們要做帶有藍(lán)牙的應(yīng)用,首先要保證用戶的本地藍(lán)牙設(shè)備是打開的,否則什么都不管直接使用搜索、連接、通信等函數(shù)肯定會(huì)得到和預(yù)期不一樣的效果。但是有時(shí)候用戶為了節(jié)省電池電量會(huì)把藍(lán)牙關(guān)閉,那我們該怎么辦呢?
其實(shí),遇到這種情況大家也不用擔(dān)心!請看下面的解決方案:其中第1行調(diào)用BluetoothAdapter的getDefaultAdapter()方法獲得本地藍(lán)牙設(shè)備,接著調(diào)用isEnabled()判斷本地藍(lán)牙設(shè)備是否被打開,如果被打開了就執(zhí)行接下來的操作,否則可以發(fā)送Intent消息,請求打開本地藍(lán)牙設(shè)備,接著待用戶打開藍(lán)牙后再進(jìn)入接下來的操作。
1 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
2 if (!mBluetoothAdapter.isEnabled()) {
3 Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
4 startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
5 } else {
6 nextOperation();
7 }
這時(shí)候有些同學(xué)可能要吐槽樓主了“你上面發(fā)送、請求、接著、然后說的挺輕巧,我怎么知道我發(fā)送完Intent消息后系統(tǒng)到底干了什么?用戶又如何打開藍(lán)牙的?應(yīng)用程序又在哪里等待用戶完成打開藍(lán)牙事件,然后在哪里執(zhí)行nextOperation()函數(shù)的?”哈哈,樓主知道大家動(dòng)手心切啦!下面將給大家詳細(xì)介紹這一過程。
想解答大家的這些問題還得看上面代碼:其中第4行startActIvityForResult會(huì)啟動(dòng)一個(gè)系統(tǒng)Preference Activity并將ACTION_REQUEST_ENABLE靜態(tài)常量作為其動(dòng)作字符串,得到的Preference Activity如下圖:
該Activity提醒用戶是否授予權(quán)限打開本地藍(lán)牙設(shè)備,當(dāng)用戶點(diǎn)擊“是”或者“否”的時(shí)候,該Activity將會(huì)關(guān)閉。我們可以使用onActivityResult處理程序中返回的結(jié)果代碼參數(shù)來確定是否操作成功。
正如上面介紹,當(dāng)用戶點(diǎn)擊“是”授予藍(lán)牙權(quán)限請求后,確認(rèn)請求的Activity會(huì)關(guān)閉,在下面的函數(shù)中將會(huì)收到此操作的消息,這樣我們就能很瀟灑的在第4行安全的進(jìn)入接下來的操作了。
1 protected void onActivityResult(int requestCode,int resultCode,Intent data){
2 if(requestCode==ENABLE_BLUETOOTH){
3 if(resultCode==RESULT_OK){
4 nextOperation();
5 }
6 }
7 }
7_2 搜索周邊藍(lán)牙設(shè)備
上面解決了藍(lán)牙設(shè)備打開問題,接下來我們就要嘗試調(diào)用相關(guān)函數(shù)搜索周邊的藍(lán)牙設(shè)備,這樣我們就能知道我們制作的藍(lán)牙防丟器是否在我們周邊了(是不是很興奮呢?)。
這里我們要用到BluetoothAdapter的成員函數(shù)startDiscovery,該方法可以執(zhí)行一個(gè)異步方式獲得周邊藍(lán)牙設(shè)備,因?yàn)槭且粋€(gè)異步的方法所以我們不需要考慮線程被阻塞問題,整個(gè)過程大約需要12秒時(shí)間。這時(shí)我們可以注冊一個(gè)BroadcastReceiver 對(duì)象來接收查找到的藍(lán)牙設(shè)備信息,我們通過Filter來過濾ACTION_FOUND這個(gè)Intent動(dòng)作以獲取每個(gè)遠(yuǎn)程設(shè)備的詳細(xì)信息,過濾ACTION_DISCOVERY_FINISHED這個(gè)Intent動(dòng)作以獲取整個(gè)藍(lán)牙搜索過程結(jié)束的信息。
1 // Register for broadcasts when a device is discovered
2 IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
3 this.registerReceiver(mReceiver, filter);
4 // Register for broadcasts when discovery has finished
5 filter = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
6 this.registerReceiver(mReceiver, filter);
上面講的可能有點(diǎn)專業(yè),下面俺用一種簡單非專業(yè)的方式向大家介紹一下:每次我們想編程搜索周邊的藍(lán)牙設(shè)備時(shí),只要簡單調(diào)用BluetoothAdapter的成員函數(shù)startDiscovery就可以了。所謂的異步方法大家可以這樣理解——start方法就好像一個(gè)總司令告訴情報(bào)搜查員去搜查情報(bào),然后自己繼續(xù)做自己的事,情報(bào)員去收集各種情報(bào);這里的filter就好像總司令告訴情報(bào)搜查員,我只要ACTION_FOUND和ACTION_DISCOVERY_FINISHED信息;那么這里的mReceiver就是總司令指定的情報(bào)搜查員了。
接下來就讓咱們神奇的情報(bào)搜查員登場!如下,相信大家一看就明白了,咱們的情報(bào)搜查員會(huì)一絲不茍地將總司令下達(dá)的任務(wù)執(zhí)行。這樣當(dāng)他收到FOUND動(dòng)作信息時(shí)就用第6~8行的方法將每個(gè)發(fā)現(xiàn)的藍(lán)牙設(shè)備的名字和地址存儲(chǔ)進(jìn)一個(gè)Vector中,這樣等自己完成任務(wù)時(shí),就能告訴總司令任務(wù)完成,所有周邊藍(lán)牙情報(bào)都在xxx向量中(嘻嘻,多么讓領(lǐng)導(dǎo)喜歡的員工呀!)。
1 private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
2 @Override
3 public void onReceive(Context context, Intent intent) {
4 String action = intent.getAction();
5 if (BluetoothDevice.ACTION_FOUND.equals(action)) {
6 BluetoothDevice device =
7 intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
8 mDevicesVector.add(device.getName() + “ ” + device.getAddress());
9 } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
10 //。。。
11 }
12 }
13 };
7_3 獲取藍(lán)牙設(shè)備信號(hào)強(qiáng)度
到目前想必很多人已經(jīng)能夠一氣呵成自己的簡單藍(lán)牙防丟器了,因?yàn)榇蠹乙呀?jīng)掌握了硬件部分的設(shè)計(jì)方法、擁有軟件藍(lán)牙開發(fā)打開和搜索周邊藍(lán)牙的編程技巧。但是如果心急的同學(xué)只用前面介紹的一點(diǎn)小知識(shí)的話,肯定設(shè)計(jì)的小東西很不盡人意(自己都不好意思給自己及格)。是不是出現(xiàn)了藍(lán)牙防丟器放到很遠(yuǎn)很遠(yuǎn)時(shí)應(yīng)用程序才會(huì)報(bào)告東西已丟?如果是這樣請耐心地看完下面的介紹(也許能給你更多的啟發(fā)(⊙o⊙)哦)。
想必大家也想到了問題所在——距離沒控制好!如果我們能夠知道藍(lán)牙防丟器距離我們手機(jī)的距離那就好了,這樣我們就能設(shè)定一個(gè)范圍值,當(dāng)它超出這個(gè)范圍時(shí)手機(jī)就發(fā)出丟失警告,這樣就不會(huì)出現(xiàn)防丟器要放很遠(yuǎn)才能被手機(jī)察覺已丟失的問題了。要解決這個(gè)問題就要向大家介紹一下無線傳感器網(wǎng)絡(luò)中的RSSI了!
RSSI全名Received Signal Strength Indication,翻譯過來是接收的信號(hào)強(qiáng)度指示。在我們的經(jīng)驗(yàn)中一般離得越近信號(hào)越強(qiáng),所以通過這個(gè)RSSI能夠大致估計(jì)接收點(diǎn)與發(fā)射點(diǎn)之間的距離。而在無線傳感器網(wǎng)絡(luò)中經(jīng)常會(huì)設(shè)置多個(gè)固定的發(fā)射源(也是所謂的標(biāo)簽),然后根據(jù)一定的算法來確定移動(dòng)目標(biāo)的空間位置信息。例如下圖左分別在A、B、C三點(diǎn)放置三個(gè)發(fā)射功率相同的信號(hào)源(標(biāo)簽),這樣移動(dòng)點(diǎn)D通過收集A、B、C三點(diǎn)的RSSI并估計(jì)距離rA、rB、rC,由于這個(gè)距離并不是完全精準(zhǔn),所以三個(gè)圓并不一定交于一點(diǎn),大多數(shù)情況是下面交于一個(gè)公共區(qū)域的情況,而移動(dòng)點(diǎn)D的當(dāng)前位置很有可能在該區(qū)域。
圖 7-2 藍(lán)牙標(biāo)簽定位
我們這里只要簡單地用到通過RSSI估計(jì)手機(jī)和防丟器之間的距離就行了,上面的定位技術(shù)相信大家也能舉一反三輕松搞定((^o^)/~其實(shí)沒那么簡單,哈哈)。那么在安卓編程時(shí)如何獲得RSSI呢?其實(shí)并不難,我們可以利用和獲取周邊藍(lán)牙設(shè)備的名稱和地址類似的方法獲取RSSI:
1 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
2 mDevicesVector.add(device.getName() + “ ” + device.getAddress());
3 short rssi = intent.getExtras().getShort(BluetoothDevice.EXTRA_RSSI);
4 mRSSIVector.add(rssi);
8 著手開發(fā)我們的APP
1) 使用Eclipse創(chuàng)建一個(gè)安卓項(xiàng)目,命名為first_test,并將Activity Name命名為UI_Main,Layout Name命名為ui_main(如圖8-1所示)。
圖8-1 新建安卓項(xiàng)目
2) 展開res文件夾下的layout文件夾,雙擊ui_main.xml文件,選擇xml編輯模式,并將其代碼改為:
1 《?xml version=“1.0” encoding=“utf-8”?》
2 《LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”
3 android:layout_width=“fill_parent”
4 android:layout_height=“fill_parent”
5 android:orientation=“vertical” 》
6
7 《SurfaceView
8 android:id=“@+id/surfaceView”
9 android:layout_width=“match_parent”
10 android:layout_height=“wrap_content”
11 android:layout_weight=“0.75” /》
12
13 《Button
14 android:id=“@+id/button_add”
15 android:layout_width=“match_parent”
16 android:layout_height=“wrap_content”
17 android:text=“添加藍(lán)牙防丟器” /》
18
19 《Button
20 android:id=“@+id/button_start”
21 android:layout_width=“match_parent”
22 android:layout_height=“wrap_content”
23 android:text=“開始防丟” /》
24
25 《/LinearLayout》
操作說明:這里修改的ui_main.xml為應(yīng)用的主界面(如圖8-2所示)。該頁面采用LinearLayout布局模式,從上到下依次為用于動(dòng)態(tài)顯示藍(lán)牙信號(hào)強(qiáng)弱的SurfaceView控件、用于添加防丟設(shè)備的按鈕和用于開始防丟控制的按鈕。
圖 8-2 ui_main.xml效果
3) 右擊layout,依次選擇New|Android XML File新建安卓XML文件。
4) 將新建的Android XML File命名為ui_list,點(diǎn)擊Finish按鈕。接著同第二步將新建的ui_list.xml修改為:
1 《?xml version=“1.0” encoding=“utf-8”?》
2 《LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”
3 android:layout_width=“fill_parent”
4 android:layout_height=“fill_parent”
5 android:orientation=“vertical” 》
6
7 《ListView
8 android:id=“@+id/listView1”
9 android:layout_width=“match_parent”
10 android:layout_height=“wrap_content”
11 android:layout_weight=“1” 》
12 《/ListView》
13
14 《Button
15 android:id=“@+id/button_search”
16 android:layout_width=“match_parent”
17 android:layout_height=“wrap_content”
18 android:text=“search” /》
19
20 《Button
21 android:id=“@+id/button_ok”
22 android:layout_width=“match_parent”
23 android:layout_height=“wrap_content”
24 android:text=“OK” /》
25
26 《/LinearLayout》
操作說明:這里新建的ui_list.xml為搜索并添加藍(lán)牙防丟器界面。該界面同樣采用LinearLayout布局模式,包含一個(gè)用于開始搜索的按鈕、一個(gè)用于列出搜索結(jié)果的list和一個(gè)用于確認(rèn)返回的OK按鈕。
5) 右擊src文件夾下的包名,依次選擇New|Class命令(如圖8-3所示)
圖 8-3 新建類
6) 新建類文件命名為My_BTS,點(diǎn)擊Finish按鈕。
7) 將My_BTS.java文件修改為:
1 package com.example.first_test;
2
3 import java.util.Vector;
4
5 public class My_BTS {
6 public String mName;
7 public String mAddr;
8 public Vector《Short》 mRSSIVector;
9
10 public My_BTS() {
11 mName = new String();
12 mAddr = new String();
13 mRSSIVector = new Vector《Short》();
14 }
15
16 public My_BTS(String name, String addr) {
17 mName = name;
18 mAddr = addr;
19 mRSSIVector = new Vector《Short》();
20 }
21 }
操作說明:該類表示藍(lán)牙防丟器。其中mName和mAddr分別表示藍(lán)牙防丟器的名字和地址;mSSIVector用來存放一段時(shí)間檢測到該藍(lán)牙防丟器的RSSI值(之所以保留多組數(shù)據(jù),是方便今后大家擴(kuò)展)。
8) 采用同樣的方法新建一個(gè)Func_Draw.java文件,并將文件修改為:
1 package com.example.first_test;
2
3 import java.util.Vector;
4
5 import android.graphics.Canvas;
6 import android.graphics.Color;
7 import android.graphics.Paint;
8 import android.graphics.Paint.Style;
9 import android.view.SurfaceHolder;
10
11 public class Func_Draw {
12 private static Vector《Paint》 mPaint = new Vector《Paint》();
13 public static Integer times = 0;// 防丟搜索次數(shù)
14 public static float Bei = 200;// 繪制圖形時(shí)放大倍數(shù)
15
16 public static void initPaint() {
17 Paint paint0 = new Paint();
18 paint0.setAntiAlias(true);
19 paint0.setStyle(Style.STROKE);
20 paint0.setColor(Color.RED);
21 mPaint.add(paint0);
22 Paint paint1 = new Paint();
23 paint1.setAntiAlias(true);
24 paint1.setStyle(Style.STROKE);
25 paint1.setColor(Color.GREEN);
26 mPaint.add(paint1);
27 Paint paint2 = new Paint();
28 paint2.setAntiAlias(true);
29 paint2.setStyle(Style.STROKE);
30 paint2.setColor(Color.BLUE);
31 mPaint.add(paint2);
32 Paint paint3 = new Paint();
33 paint3.setAntiAlias(true);
34 paint3.setStyle(Style.STROKE);
35 paint3.setColor(Color.YELLOW);
36 mPaint.add(paint3);
37 Paint paint4 = new Paint();
38 paint4.setAntiAlias(true);
39 paint4.setStyle(Style.STROKE);
40 paint4.setColor(Color.WHITE);
41 mPaint.add(paint4);
42 Paint paint5 = new Paint();
43 paint5.setAntiAlias(true);
44 paint5.setStyle(Style.STROKE);
45 paint5.setColor(Color.LTGRAY);
46 mPaint.add(paint5);
47 Paint paint6 = new Paint();
48 paint6.setAntiAlias(true);
49 paint6.setStyle(Style.STROKE);
50 paint6.setColor(Color.CYAN);
51 mPaint.add(paint6);
52 }
53
54 public static void draw(SurfaceHolder mHolder) {
55 Canvas canvas = mHolder.lockCanvas();
56 canvas.drawRGB(0, 0, 0);
57 for (int i = 0; i 《 UI_Main.mBTSArrayList.size(); i++) {
58 boolean find = false;
59 short rssi = 0;
60 for (int j = 0; j 《 UI_Main.mFuncBT.mAddrVector.size(); j++) {
61 if (UI_Main.mBTSArrayList.get(i).mAddr
62 .equals(UI_Main.mFuncBT.mAddrVector.get(j))) {
63 find = true;
64 rssi = UI_Main.mFuncBT.mRSSIVector.get(j);
65 }
66 }
67 if (find == false) {
68 canvas.drawText(
69 times + “: NOT_FIND ”
70 + UI_Main.mBTSArrayList.get(i).mName, 5,
71 i * 10 + 12, mPaint.get(i));
72 } else {
73 float power = (float) ((Math.abs(rssi) - 59) / (10 * 2.0));
74 float dis = (float) Math.pow(10, power);
75
76 canvas.drawText(
77 times + “: FIND ” + UI_Main.mBTSArrayList.get(i).mName
78 + “ dis: ” + new Float(dis).toString()
79 + “ rssi: ” + rssi, 5, i * 10 + 12,
80 mPaint.get(i));
81 canvas.drawCircle(canvas.getWidth() / 2,
82 canvas.getHeight() / 2, Bei * dis, mPaint.get(i));//畫圓圈
83 }
84 }
85 times++;
86 mHolder.unlockCanvasAndPost(canvas);// 更新屏幕顯示內(nèi)容
87 UI_Main.mFuncBT.mRSSIVector.clear();
88 UI_Main.mFuncBT.mNameVector.clear();
89 UI_Main.mFuncBT.mAddrVector.clear();
90 }
91 }
操作說明:該類提供在SurfaceView上繪制功能。其中靜態(tài)方法initPaint對(duì)畫筆進(jìn)行初始化,draw函數(shù)負(fù)責(zé)繪制。
draw函數(shù)的核心在于canvas繪圖,canvas繪圖的過程和我們在白紙上繪繪圖的過程很像,如:
l 第55行鎖定canvas相當(dāng)于得到一張紙;
l 第56行用RGB為0的顏色來刷新canvas相當(dāng)于用橡皮擦把紙上原來的東西擦掉;
l 第68和76行drawText相當(dāng)于在紙的相應(yīng)位置寫文字;
l 第81行drawCircle相當(dāng)于在紙的相應(yīng)位置繪制一個(gè)指定的圓;
l 第86行的nlockCanvasAndPost相當(dāng)于你把繪制好的作品拿出來展示給別人看;
正是因?yàn)閏anvas的加鎖和解鎖這一機(jī)制,才保證了繪制過程中屏幕正確地顯示。
接著再來理解這里draw函數(shù)的功能:mBTSArrayList是一個(gè)My_BTS類型的數(shù)組,保存我們想要防丟的藍(lán)牙防丟器設(shè)備的名稱、地址等信息;mFuncBT是一個(gè)可以實(shí)時(shí)搜索周邊藍(lán)牙設(shè)備的一個(gè)對(duì)象,其靜態(tài)變量mNameVector、mAddrVector、mRSSIVector保存著實(shí)時(shí)搜索結(jié)果;這樣核心部分的功能便是通過兩層循環(huán)遍歷待防丟設(shè)備是否在本次搜索中,如果不在則顯示“NOT_FIND”,如果在則由RSSI計(jì)算距離。(效果如圖8-4)
圖 8-4 找到藍(lán)牙設(shè)備圖
9) 新建一個(gè)Func_BT.java文件,并修改為:
1 package com.example.first_test;
2
3 import java.util.Vector;
4
5 import android.app.Activity;
6 import android.bluetooth.BluetoothAdapter;
7 import android.bluetooth.BluetoothDevice;
8 import android.content.BroadcastReceiver;
9 import android.content.Context;
10 import android.content.Intent;
11 import android.content.IntentFilter;
12 import android.os.Bundle;
13 import android.os.Handler;
14 import android.os.Message;
15
16 public class Func_BT {
17 private BluetoothAdapter mBtAdapter;// 藍(lán)牙適配器
18 private static final int ENABLE_BLUETOOTH = 1;
19 // 分別用于存儲(chǔ)設(shè)備名地址名稱和RSSI的向量
20 public Vector《String》 mNameVector;
21 public Vector《String》 mAddrVector;
22 public Vector《Short》 mRSSIVector;
23
24 private Handler myHandler;
25 private Activity activity;
26
27 public Func_BT(Activity activity, Handler myHandler) {
28 this.myHandler = myHandler;
29 this.activity = activity;
30
31 mNameVector = new Vector《String》();// 向量
32 mAddrVector = new Vector《String》();
33 mRSSIVector = new Vector《Short》();
34
35 IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
36 activity.registerReceiver(mReceiver, filter);
37 filter = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
38 activity.registerReceiver(mReceiver, filter);
39 activity.registerReceiver(mReceiver, filter);
40
41 mBtAdapter = BluetoothAdapter.getDefaultAdapter();
42 }
43
44 private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
45 @Override
46 public void onReceive(Context context, Intent intent) {
47 String action = intent.getAction();
48 if (BluetoothDevice.ACTION_FOUND.equals(action)) {
49 BluetoothDevice device = intent
50 .getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
51 short rssi = intent.getExtras().getShort(
52 BluetoothDevice.EXTRA_RSSI);
53 mNameVector.add(device.getName());
54 mAddrVector.add(device.getAddress());
55 mRSSIVector.add(rssi);
56 } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED
57 .equals(action)) {
58 /*if (mNameVector.size() != 0) {
59 Message msg = new Message();// 消息
60 msg.what = 0x01;// 消息類別
61 myHandler.sendMessage(msg);
62 }*/
63 }
64 }
65 };
66
67 public void doDiscovery() {
68 if (mBtAdapter.isDiscovering()) {
69 mBtAdapter.cancelDiscovery();
70 }
71 mBtAdapter.startDiscovery();
72 new TimeLimitThread().start();
73 }
74
75 public void openBT() {
76 // 如果沒有打開則打開
77 if (!mBtAdapter.isEnabled()) {
78 Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
79 activity.startActivityForResult(intent, ENABLE_BLUETOOTH);
80 } else {
81 doDiscovery();
82 }
83 }
84
85 protected void onActivityResult(int requestCode, int resultCode, Intent data){
86 if (requestCode == ENABLE_BLUETOOTH) {
87 if (resultCode == Activity.RESULT_OK) {
88 doDiscovery();
89 }
90 }
91 }
92
93 public void setHandler(Handler myHandler) {
94 this.myHandler = myHandler;
95 }
96
97 public void setFunc_BT(Activity activity, Handler myHandler) {
98 this.myHandler = myHandler;
99 this.activity = activity;
100 }
101
102 class TimeLimitThread extends Thread{
103 public void run() {
104 try {
105 sleep(3000);
106 if (mBtAdapter.isDiscovering()) {
107 mBtAdapter.cancelDiscovery();
108 }
109 Message msg = new Message();// 消息
110 msg.what = 0x01;// 消息類別
111 myHandler.sendMessage(msg);
112 } catch (InterruptedException e) {
113 e.printStackTrace();
114 }
115 }
116 }
117 }
操作說明:該類用來實(shí)時(shí)搜索周邊藍(lán)牙設(shè)備,并把搜索結(jié)果保存在其靜態(tài)成員變量mNameVector、mAddrVector、mRSSIVector中。此外這里還使用Handler用于向調(diào)用其搜索方法的Activity發(fā)送本次搜索完畢的消息。具體介紹如下:
l 第27到42行為構(gòu)造函數(shù),主要負(fù)責(zé)成員變量初始化、注冊filter、獲取本地藍(lán)牙設(shè)備;
l 第44到65行為BroadcastReceiver,主要負(fù)責(zé)異步搜索周邊藍(lán)牙設(shè)備的廣播的接收;
l 第67到73行為開始搜索周邊藍(lán)牙設(shè)備函數(shù);
l 第75到83行為帶有檢查本地藍(lán)牙是否開啟并能夠發(fā)出開啟請求的搜索周邊藍(lán)牙設(shè)備函數(shù);
l 第85到91行為接收用戶是否授權(quán)打開藍(lán)牙設(shè)備的Activity的結(jié)果函數(shù);
l 第102到116行是一個(gè)線程類主要用于限定搜索周邊藍(lán)牙設(shè)備的最長時(shí)間(默認(rèn)為12s左右);
正是因?yàn)榧恿霜?dú)立用于限定搜索時(shí)長的線程,才讓搜索過程的時(shí)間長短便于我們控制,但是sleep的時(shí)間也不要設(shè)置的過小,否則會(huì)出現(xiàn)一個(gè)藍(lán)牙設(shè)備也搜索不到的情況。(建議最好參照第七部分——“安卓藍(lán)牙編程主要操作”來理解本部分的代碼)
10) 修改UI_Main.java為:
1 package com.example.first_test;
2
3 import java.util.ArrayList;
4 import android.app.Activity;
5 import android.content.Intent;
6 import android.os.Bundle;
7 import android.os.Handler;
8 import android.os.Message;
9 import android.view.SurfaceHolder;
10 import android.view.SurfaceHolder.Callback;
11 import android.view.SurfaceView;
12 import android.view.View;
13 import android.view.View.OnClickListener;
14 import android.widget.Button;
15
16 public class UI_Main extends Activity implements Callback {
17
18 public Func_Draw mFuncDraw;
19 public SurfaceHolder mHolder;
20 public static Func_BT mFuncBT;
21 // 防丟設(shè)備
22 public static ArrayList《My_BTS》 mBTSArrayList = new ArrayList《My_BTS》();
23
24 // 消息句柄(線程里無法進(jìn)行界面更新,
25 // 所以要把消息從線程里發(fā)送出來在消息句柄里進(jìn)行處理)
26 public Handler myHandler = new Handler() {
27 @Override
28 public void handleMessage(Message msg) {
29 if (msg.what == 0x01) {
30 Func_Draw.draw(mHolder);
31 }
32 mFuncBT.doDiscovery();
33 }
34 };
35
36 @Override
37 protected void onCreate(Bundle savedInstanceState) {
38 super.onCreate(savedInstanceState);
39 setContentView(R.layout.ui_main);
40
41 Func_Draw.initPaint();
42
43 SurfaceView mSurface = (SurfaceView) findViewById(R.id.surfaceView);
44 mHolder = mSurface.getHolder();
45 mHolder.addCallback(this);
46
47 mFuncBT = new Func_BT(this, myHandler);
48
49 Button mButton1 = (Button) findViewById(R.id.button_start);
50 mButton1.setOnClickListener(new OnClickListener() {
51 @Override
52 public void onClick(View v) {
53 Func_Draw.times = 0;
54 mFuncBT.openBT();
55 }
56 });
57
58 Button mButton2 = (Button) findViewById(R.id.button_add);
59 mButton2.setOnClickListener(new OnClickListener() {
60 @Override
61 public void onClick(View v) {
62 startActivity(new Intent(UI_Main.this, UI_List.class));
63 }
64 });
65 }
66
67 @Override
68 public void surfaceCreated(SurfaceHolder holder) {
69 // TODO Auto-generated method stub
70
71 }
72
73 @Override
74 public void surfaceChanged(SurfaceHolder holder, int format, int width,
75 int height) {
76 // TODO Auto-generated method stub
77
78 }
79
80 @Override
81 public void surfaceDestroyed(SurfaceHolder holder) {
82 // TODO Auto-generated method stub
83
84 }
85 }
操作說明:上面第2步時(shí)我們修改了ui_main.xml并做出應(yīng)用程序主頁面的效果,而該類則是其對(duì)應(yīng)的邏輯實(shí)現(xiàn)。
nn 首先看第37到65行的onCreate函數(shù):
l 第39行設(shè)置要顯示的頁面為ui_main.xml所展示的效果;
l 第41行調(diào)用Func_Draw的靜態(tài)initPaint()方法對(duì)畫筆進(jìn)行初始化;
l 第43到45行負(fù)責(zé)獲取并設(shè)置SurfaceView;
l 第47行實(shí)例化一個(gè)Func_BT;
l 第49到56行是給開始防丟按鈕綁定一個(gè)監(jiān)聽器,一旦點(diǎn)擊該按鈕則執(zhí)行onClick內(nèi)代碼;
l 第58到65行是給添加藍(lán)牙防丟器綁定一個(gè)監(jiān)聽器,一旦點(diǎn)擊則啟動(dòng)另一個(gè)Activity;
nn 第24到34行實(shí)例化一個(gè)Handler用于接收Func_BT一次搜索結(jié)束時(shí)發(fā)回結(jié)束的消息。在這里一旦收到本次周邊藍(lán)牙設(shè)備搜索結(jié)束的消息就調(diào)用Func_Draw.draw(mHolder)進(jìn)行繪制,然后繼續(xù)調(diào)用mFuncBT.doDiscovery()實(shí)現(xiàn)周期性搜索/防丟。
nn 第67到84行是自動(dòng)生成的,暫且不用管(也不要?jiǎng)h除?。?。
11) 新建一個(gè)UI_List.java文件,并修改代碼為:
1 package com.example.first_test;
2
3 import java.util.ArrayList;
4 import android.annotation.SuppressLint;
5 import android.app.Activity;
6 import android.app.ProgressDialog;
7 import android.content.Intent;
8 import android.os.Bundle;
9 import android.os.Handler;
10 import android.os.Message;
11 import android.util.Log;
12 import android.view.View;
13 import android.view.View.OnClickListener;
14 import android.widget.ArrayAdapter;
15 import android.widget.Button;
16 import android.widget.ListView;
17
18 public class UI_List extends Activity {
19
20 private ArrayList《String》 Items;
21 private ListView myListView;
22 private ArrayAdapter《String》 aa;
23 private boolean getDIV;
24
25 @SuppressLint(“InlinedApi”)
26 protected void onCreate(Bundle savedInstanceState) {
27 super.onCreate(savedInstanceState);
28 setContentView(R.layout.ui_list);
29
30 UI_Main.mFuncBT.setFunc_BT(this, myHandler);
31
32 // 獲取listview并設(shè)置為多選
33 myListView = (ListView) findViewById(R.id.listView1);
34 myListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
35 myListView.setTextFilterEnabled(true);
36 // 設(shè)置listview數(shù)組并綁定
37 Items = new ArrayList《String》();
38 aa = new ArrayAdapter《String》(this,
39 android.R.layout.simple_list_item_checked, Items);
40 myListView.setAdapter(aa);
41
42 // 獲取OK按鈕,并遍歷選擇的設(shè)備,返回主Activity
43 Button myButton1 = (Button) findViewById(R.id.button_ok);
44 myButton1.setOnClickListener(new OnClickListener() {
45 @Override
46 public void onClick(View v) {
47 int num = 0;
48 for (int i = 0; i 《 myListView.getCount(); i++) {
49 if (myListView.isItemChecked(i)) {
50 String item = myListView.getItemAtPosition(i)
51 .toString();
52 String name = item.substring(0, item.indexOf(“ ”));
53 String addr = item.substring(item.indexOf(“ ”) + 1);
54 Log.i(“UI_LIST”, name + “ ” + addr);
55 UI_Main.mBTSArrayList
56 .add(num++, new My_BTS(name, addr));
57 }
58 }
59 Intent mIntent = new Intent(UI_List.this, UI_Main.class);
60 mIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
61 startActivity(mIntent);
62 }
63 });
64 // 獲取Search按鈕,設(shè)置監(jiān)聽事件
65 Button myButton2 = (Button) findViewById(R.id.button_search);
66 myButton2.setOnClickListener(new OnClickListener() {
67 @Override
68 public void onClick(View v) {
69 getDIV = false;
70 UI_Main.mFuncBT.openBT();
71 final ProgressDialog dialog = ProgressDialog.show(UI_List.this,
72 “搜索藍(lán)牙設(shè)備”, “稍等一下~”, true);
73 new Thread(new Runnable() {
74 public void run() {
75 while (getDIV == false);
76 dialog.dismiss();
77 }
78 }).start();
79 }
80 });
81 }
82
83 // 消息句柄(線程里無法進(jìn)行界面更新,
84 // 所以要把消息從線程里發(fā)送出來在消息句柄里進(jìn)行處理)
85 public Handler myHandler = new Handler() {
86 @Override
87 public void handleMessage(Message msg) {
88 if (msg.what == 0x01) {
89 Items.clear();
90 for (int i = 0; i 《 UI_Main.mFuncBT.mNameVector.size(); i++) {
91 Items.add(i, UI_Main.mFuncBT.mNameVector.get(i) + ‘ ’
92 + UI_Main.mFuncBT.mAddrVector.get(i));
93 aa.notifyDataSetChanged();// 通知數(shù)據(jù)變化
94 }
95 getDIV = true;
96 UI_Main.mFuncBT.mRSSIVector.clear();
97 UI_Main.mFuncBT.mNameVector.clear();
98 UI_Main.mFuncBT.mAddrVector.clear();
99 }
100 }
101 };
102 }
操作說明:這個(gè)類和ui_list.xml也是配套的。因此:
nn 在onCreate中:
l 第28行設(shè)置要顯示的頁面為ui_list.xml所展示的效果;
l 第30行負(fù)責(zé)將周邊藍(lán)牙搜索對(duì)象的Handler設(shè)置為本Activity內(nèi)的Handler;
l 第32到40行是list相關(guān);
l 第42到80行是兩個(gè)按鈕點(diǎn)擊事件監(jiān)聽相關(guān);
nn 第85到101行的Handler則同UI_Main.java中的Handler類似負(fù)責(zé)接收周期性藍(lán)牙搜索結(jié)束消息。在這里當(dāng)接到藍(lán)牙搜索結(jié)束的消息后是將搜索到的設(shè)備信息放入list中待用戶選擇。(沒有再次調(diào)用藍(lán)牙搜索)
nn 此外兩個(gè)按鈕監(jiān)聽中執(zhí)行部分要特別說明下:
l 當(dāng)點(diǎn)擊OK按鈕時(shí)程序?qū)⒂脩粼趌ist中選擇的設(shè)備的信息存放在UI_Main.mBTSArrayList中,然后結(jié)束當(dāng)前Activity并啟動(dòng)UI_Main所對(duì)應(yīng)的Activity。
l 當(dāng)點(diǎn)擊search按鈕時(shí)會(huì)啟動(dòng)周邊藍(lán)牙設(shè)備搜索并打開一個(gè)等待對(duì)話框,其中的Thread負(fù)責(zé)等待直到搜索完畢。
12) 最后(如圖8-5)找到AndroidMainFest.xml文件修改為:
圖 8-5 AnDroidManifest.xml文件
1 《?xml version=“1.0” encoding=“utf-8”?》
2 《manifest xmlns:android=“http://schemas.android.com/apk/res/android”
3 package=“com.example.first_test”
4 android:versionCode=“1”
5 android:versionName=“1.0” 》
6
7 《uses-sdk
8 android:minSdkVersion=“8”
9 android:targetSdkVersion=“19” /》
10
11 《uses-permission android:name=“android.permission.BLUETOOTH_ADMIN” /》
12 《uses-permission android:name=“android.permission.BLUETOOTH” /》
13
14 《application
15 android:allowBackup=“true”
16 android:icon=“@drawable/ic_launcher”
17 android:label=“@string/app_name”
18 android:theme=“@style/AppTheme” 》
19 《activity
20 android:name=“.UI_Main”
21 android:label=“@string/app_name” 》
22 《intent-filter》
23 《action android:name=“android.intent.action.MAIN” /》
24 《category android:name=“android.intent.category.LAUNCHER” /》
25 《/intent-filter》
26 《/activity》
27
28 《activity
29 android:name=“.UI_List”
30 android:label=“@string/app_name” 》
31 《intent-filter》
32 《action android:name=“android.intent.action.UI_List” /》
33 《category android:name=“android.intent.category.DEFAULT” /》
34 《/intent-filter》
35 《/activity》
36
37 《/application》
38
39
40 《/manifest》
操作說明:主要是在11、12行添加藍(lán)牙權(quán)限以及在28到35行添加一個(gè)activity。
非常好我支持^.^
(2) 100%
不好我反對(duì)
(0) 0%
相關(guān)閱讀:
- [電子說] 信馳達(dá)RF-BM-2340x系列BLE藍(lán)牙模塊正式登錄TI官網(wǎng) 2023-10-23
- [電子說] 信馳達(dá)RF-BM-2340x系列BLE藍(lán)牙模塊正式通過TI認(rèn)證 2023-10-20
- [電子說] BLE藍(lán)牙模塊功能應(yīng)用②——定位功能 2023-10-18
- [電子說] 海凌科BLE低功耗藍(lán)牙模塊物聯(lián)網(wǎng)應(yīng)用 2023-10-16
- [電子說] 智能物聯(lián)網(wǎng)解決方案:藍(lán)牙IOT主控模塊打造高效監(jiān)測和超低功耗 2023-10-13
- [制造/封裝] 貿(mào)澤電子開售Laird Connectivity Lyra 24系列低功耗藍(lán)牙模塊 2023-10-08
- [電子說] wifi模塊的作用 wifi模塊和藍(lán)牙模塊的區(qū)別 2023-10-05
- [電子說] BLE藍(lán)牙模塊功能應(yīng)用① — 主從一體 2023-09-09
( 發(fā)表人:李倩 )