恭喜大家順利通過測試一!在測試一中我們學會了如何利用現有模塊HC-05/06進行簡單的連線來制作一個藍牙防丟器,同時學習了安卓藍牙相關的幾個API並最終制作了一個自己的藍牙防丟客戶端軟件。可能有些專門來看軟硬結合的同學會抱怨“什么呀,感覺就是在開發安卓App嘛!“。不錯!測試一的目的就是讓大家通過了解硬件原理DIY一個簡單的硬件,並學習如何充分利用移動端開發的特點設計一款配套的應用。除此之外樓主還悄悄地為測試二埋下了伏筆,因為測試二將會涉及利用移動端和藍牙模塊的通信功能來實現一個遙控小風扇!如果沒有前面關於藍牙軟硬件知識的鋪墊,直接做這個可能會很吃力。那么現在我們就着手測試二吧![正版請搜索:beautifulzzzz(看樓主博客園官方博客,享高質量生活)嘻嘻!!!]
1 預期效果構思
簡單起見我們實現一個可以通過手機App遙控的可調速小風扇。如圖1_1左邊手機應用部分主要包括1、2、3三個按鈕和4用於顯示風扇速度的文本框;右邊小風扇部分主要包括7風扇模塊和8用於顯示風扇速度的顯示模塊;中間的5、6表示雙方通過藍牙進行無線通信實現遙控功能。
圖1_1 預期效果構思
2 硬件輪廓勾勒
其實整個硬件部分都是要我們自己DIY的。如圖2_1所示1號為51最小系統模塊,起總控作用;2號為電源模塊,用於向整個系統供電;3號為藍牙模塊,用於單片機和智能手機進行藍牙通信;4號為電機模塊(包括電機驅動電路),用於將電能轉換為機械能提供風;5號為數碼管顯示模塊,用於顯示小風扇的當前轉速。
圖 2_1 硬件輪廓勾勒
3 硬件整體電路圖設計
既然輪廓已經勾勒出,接下來要看看我們具體需要哪些元件。首先對於51最小系統模塊(如圖3_1所示)包括晶振電路和89C52單片機(其實為了簡單筆者偷偷地將復位電路去掉了,這樣帶來的直接后果是程序燒不進去。如果大家也一樣學着偷懶,不妨把該最小系統的電源引腳和串口引腳用杜邦線連接到你買來的開發板對應的引腳處,同時把開發板上的單片機拿掉。這樣就可以利用開發板上的復位電路模塊來實現程序的有效燒寫。)
圖 3_1 51最小系統
對於電源模塊,我們可以使用可充電的5V鋰電池或者用3節1.5V的普通電池湊合。藍牙模塊是我們上一章中制作藍牙防丟器的HC-05或HC-06。這里電機模塊要特別說明下:如圖3_2需要用一個ULN2003做驅動,這樣控制信號要從4號引腳輸入以實現對馬達的控制。另外馬達可以選擇玩具四驅車上的那種。
圖 3_2 電機模塊
最后顯示模塊采用的是四位八段共陰數碼管3461AS。如圖3_3每個3461AS有4個數碼管,每個數碼管中有8個LED燈。這樣當我們想使某一個數碼管顯示相應的數字時,只要給4路位選信號和8路段選信號相應的組合電平就能實現功能。需要另外說明的是:3461AS屬於共陰數碼管,如圖3_4其中6、8、9、12為位選引腳,3、5、10、1、2、4、7、11為段選引腳。如果我們想讓第二個數碼管顯示2時,要讓9號引腳置低電平其余位選引腳置高電平,同時要讓11、7、5、1、2置高電平其余段選置低電平。
圖 3_3 3461AS封裝圖
圖 3_4 3461AS內部電路圖
因此在實際電路中(如圖3_5)將P0口作段選信號引腳,同時用P2.3、P2.4、P2.5、P2.6作為位選信號引腳,通過單片機直接驅動即可。此外R1~R8八個上拉電阻不能忽視,起初筆者沒有注意結果燒壞了2個3461AS。
圖3_5 顯示模塊實際驅動電路
最終我們設計的電路圖如下,其中RXD和TXD引腳接HC-05或HC-06的TXD和RXD(要交錯相連)。因為HC-05/06是藍牙串口模塊,也就是說只要單片機采用串口驅動程序並且相應的引腳連接正確,單片機-藍牙模塊通信完全和單片機-串口設備通信一樣。所以圖中的串口模塊也就相當於我們的藍牙模塊,唯一需要注意的是單片機和藍牙模塊的RXD和TXD是交錯相連。
圖 3_6 整體電路圖
4 四位八段共陰數碼管3461AS的驅動程序設計
由上面分析我們知道通過位選信號和段選信號的組合可以實現數碼管顯示功能。如果采用圖3_6所示電路圖,上面想讓第二個數碼管顯示2時,則P0等於0x5b(01011101),P2等於0xdf(11011111)。采用同樣的分析方法我們可以計算出讓八段數碼管顯示從0~F的所有P0對應的賦值:0x3f 0x06 0x5b 0x4f 0x66 0x6d 0x7d 0x07 0x7f 0x6f 0x77 0x7c 0x39 0x5e 0x79 0x71,以及單獨選通第1位到第4位P2的所有賦值:0xbf 0xdf 0xef 0xf7。這樣當我們想讓第3位顯示9只需要給P0、P2分別賦值0x6f和0xef即可。
這時大家可能會有這樣的疑惑:“按照上面的說法似乎每次只能讓某一位顯示一個數字”。其實有這樣的疑惑說明大家學的比較認真,其實生活中很多數碼管的顯示案例中都是每次只顯示一位的!之所以我們看到的情況是一次顯示多個,就在於數碼管驅動程序設計了!而這其中的秘訣則是采用了高頻刷新(也即動態掃描)這一技巧。如果大家對動態掃描沒有感覺,可以想象一下揮舞熒光棒時的樣子——本來只是一根熒光棒,由於揮舞速度比較快而在空中划出一道美麗的弧線。下面結合驅動程序和大家詳細介紹:
1 #include"display_4X8.h" 2 3 unsigned char code DuanMa[16]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07, 4 0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71};// 顯示段碼值0~F 5 unsigned char code WeiMa[]={0xbf,0xdf,0xef,0xf7};//分別對應相應的數碼管點亮,即位碼 6 unsigned char TempData[4]; //存儲顯示值的全局變量 7 8 //------------------------------------------------ 9 //4位8段共陰數碼管顯示函數 10 //第一個參數為0表示從第一個數碼管開始顯示num個數 11 //提前要顯示的數要存在TempData中(TempData[0]表示要顯示的第一個數) 12 //------------------------------------------------ 13 void Display(unsigned char FirstBit,unsigned char Num) 14 { 15 static unsigned char i=0; 16 17 DataPort=0x00; //清空數據,防止有交替重影 18 DataControl=0x00; 19 20 DataPort=TempData[i]; //取顯示數據,段碼 21 DataControl=WeiMa[i+FirstBit]; 22 23 i++; 24 if(i==Num) 25 i=0; 26 }
這里的DuanMa[]和WeiMa[]不再說明,TempData[4]用來存儲要顯示數據。在該驅動中只有一個Display函數,正如第10行提示所述:第一個參數用來表明從哪一個數碼管開始顯示數據,第二個參數表明一共要顯示多少位數據。這樣如果要在數碼管的后兩位顯示一個兩位數則可以用Display(2,2)。這里要特別說明下TempData數組,該數組用於存放數碼管要顯示的數據,千萬不要把該數組和數碼管直接對應。例如同樣是在數碼管后兩位顯示一個兩位數num可以采用下列兩種方案:
① TempData[0]=DuanMa[num/10];
TempData[1]=DuanMa[num%10];
Display(2,2);
② TempData[2]=DuanMa[num/10];
TempData[3]=DuanMa[num%10];
Display(0,4);
其中方案一直接把要顯示的兩位數據存儲在TempData的前兩位,然后調用Display函數從第3個數碼管開始顯示2位來實現功能。方案二其實是把要顯示的數據存放在TempData的后兩位(前兩位默認為0),然后調用Display函數從第1個數碼管開始顯示4位來實現功能。
對於動態掃描這里用了一個很巧妙的方法:注意到第15行定義了一個靜態變量i,其功能在於實現一個周期內實現對需要點亮的數碼管順序點亮。這樣如果Display(0,4)顯示1234,則數碼管的慢動作則為:第一個數碼管顯示1、接着第二個數碼管顯示2、然后第三個數碼管顯示3……由於刷新頻率很高,所以人眼看上去就是4個數碼管同時顯示1234的效果。
5 PWM實現變速小馬達
欲實現直流小馬達的速度控制這里必須先講解下PWM。所謂PWM是“Pulse Width Modulation”的縮寫,簡稱脈寬調制。它是利用微處理器的數字輸出來對模擬電路進行控制的一種非常有效的技術。這里舉個通俗的例子來解釋PWM:假設你是某公司的老板,手下有個奇葩的員工喜歡周期性的在一個小時內干一會休息一會,如果你想多壓榨一下他就會督促讓他在一個周期內多干活少休息。同樣的利用微處理器在一個比較短的周期內設置某個引腳輸出高電平比低電平的持續時間多一點,從宏觀上看則呈現出輸出功率升高的效果,反之輸出功率變低。
圖 5_1不同占空比的輸出脈沖
6 串口驅動程序設計
上面已經介紹過單片機和藍牙模塊的通信方式是采用串口通信,其重要特別注意的是單片機和HC-05/06的RXD引腳和TXD引腳要交錯相連。既然HC-05/06采用的是串口通信方式,所以在給單片機編程時只要按照串口驅動來設計就可以了。
1 #include"uart.h" 2 3 #define Length 8 4 5 unsigned char getByte[Length]; //定義臨時變量 6 unsigned char flag; //接收標記 7 unsigned char point; //指針 8 9 //------------------------------------------------ 10 //串口初始化 11 //------------------------------------------------ 12 void InitUART (void) 13 { 14 flag=0; 15 point=0; 16 SCON = 0x50; // SCON: 模式 1, 8-bit UART, 使能接收 17 TMOD |= 0x20; // TMOD: timer 1, mode 2, 8-bit 重裝 18 TH1 = 0xFD; // TH1: 重裝值 9600 波特率 晶振 11.0592MHz 19 TL1 = 0xFD; 20 TR1 = 1; // TR1: timer 1 打開 21 EA = 1; //打開總中斷 22 ES = 1; //打開串口中斷 23 } 24 25 //------------------------------------------------ 26 //發送一個字節 27 //------------------------------------------------ 28 void SendByte(unsigned char dat) 29 { 30 SBUF = dat; 31 while(!TI); 32 TI = 0; 33 } 34 35 //------------------------------------------------ 36 //發送一個字符串 37 //------------------------------------------------ 38 void SendStr(unsigned char *s) 39 { 40 while(*s!='\0')// \0 表示字符串結束標志,通過檢測是否字符串末尾 41 { 42 SendByte(*s); 43 s++; 44 } 45 } 46 47 //------------------------------------------------ 48 //串口中斷程序 49 //------------------------------------------------ 50 void UART_SER (void) interrupt 4 //串行中斷服務程序 51 { 52 if(RI) //檢測接收完成標志位置1 53 { 54 RI=0; //清零接收完成標志位 55 getByte[point]=SBUF; //讀取接收到的數據 56 57 if(getByte[point++]==0xAA) //遇到可能的結束標志則發送flag 58 flag=1; //再主函數再進行判斷是否為有效幀 59 60 if(point==8) //防止數組越界 61 point=0; 62 } 63 }
在該串口驅動文件里主要包括串口初始化函數InitUART,用來設置串口通信的波特率和接收中斷等。接下來分別是發送一字節函數和發送一個字符串函數。這里單片機向串口設備發送信息采用直接發送,即在程序中用到要發送信息的地方直接調用發送函數發送;但是數據接收則采用中斷的方式,因為在順序執行的程序中不容易處理隨時都可能傳輸過來的信息。在中斷函數中把每次接收來的數據保存在getByte數組中。由於這里采用了數據幀,所以包含了對數據有效性的驗證,這個將在下面詳細分析。
7 硬件工程整體介紹
1) 打開Keil uVision2,點擊Project下的Open Project,打開智能小風扇.Uv2加載工程。
圖 7_1 打開工程
2) 待工程加載完畢,大家會在工程窗口中看到圖7_2所示文件結構。其中FUNC組下面包含數碼管顯示驅動和串口驅動文件,INTE組下包含中斷相關文件,USER組下是最上層應用程序文件。
圖 7_2 文件結構
3) 之前采用的思路是從底向上設計,這次將采用從上向下講解工程。首先看USER組下的main.c文件:
1 #include "../FUNC/display_4X8.h" 2 #include "../FUNC/uart.h" 3 #include "../INTE/inte.h" 4 5 sbit DCOUT = P1^1;//定義電機信號輸出端口 6 //------------------------------------------------ 7 //全局變量 8 //------------------------------------------------ 9 unsigned char PWM_ON; //定義速度等級 10 #define CYCLE 10 //周期 11 12 //變量 13 extern unsigned char code DuanMa[];// 顯示段碼值 14 extern unsigned char TempData[]; //存儲顯示值的全局變量 15 extern unsigned char getByte[]; //定義臨時變量 16 extern unsigned char flag; //接收標記 17 extern unsigned char point; //指針 18 19 //函數 20 extern void Display(unsigned char FirstBit,unsigned char Num);//數碼管顯示函數 21 extern void Init_Timer0(void);//定時器初始化 22 extern void InitUART(void); 23 extern void SendStr(unsigned char *s); 24 extern void SendByte(unsigned char dat); 25 26 //------------------------------------------------ 27 //主函數 28 //------------------------------------------------ 29 void main (void) 30 { 31 //發來的FF EE num AA 或 FF DD num AA返回 AA和FF互換位置 32 unsigned char answer[5]; 33 unsigned char k,data1,data2; 34 answer[0]=0xAA; 35 answer[3]=0xFF; 36 answer[4]='\0'; 37 TempData[2]=DuanMa[0]; //顯示速度等級 38 TempData[3]=DuanMa[0]; 39 PWM_ON=0; 40 41 InitUART(); 42 Init_Timer0(); //初始化定時器0,主要用於數碼管動態掃描 43 44 while (1) //主循環 45 { 46 if(flag==1 && point>3 && getByte[point-4]==0xFF) 47 { 48 ES = 0; //關串口中斷 49 50 answer[1]=0xFF; 51 data1=getByte[point-3]; 52 data2=getByte[point-2]; 53 if(data1==0xEE){ 54 if(0<=data2 && data2<=10){ 55 PWM_ON=data2; 56 TempData[2]=DuanMa[PWM_ON/10]; //顯示速度等級 57 TempData[3]=DuanMa[PWM_ON%10]; 58 answer[1]=0xEE; 59 answer[2]=data2+1; 60 } 61 }else if(data1==0xDD){ 62 answer[1]=0xDD; 63 answer[2]=PWM_ON+1; 64 } 65 SendStr(answer); //應答 66 67 for(k=0;k<8;k++) //清空getByte中數據 68 getByte[k]=0; 69 point=0; //point歸零 70 flag=0; //重置flag標志 71 ES=1; //打開串口中斷 72 } 73 } 74 } 75 76 //------------------------------------------------ 77 //定時器中斷子程序 78 //------------------------------------------------ 79 void Timer0_isr(void) interrupt 1 80 { 81 static unsigned char count; 82 TH0=(65536-2000)/256; //重新賦值 2ms 83 TL0=(65536-2000)%256; 84 85 Display(0,4); // 調用數碼管掃描 86 87 if (count==PWM_ON) 88 { 89 DCOUT = 0; //如果定時等於on的時間, 90 //說明作用時間結束,輸出低電平 91 } 92 count++; 93 if(count == CYCLE) //反之低電平時間結束后返回高電平 94 { 95 count=0; 96 if(PWM_ON!=0) //如果開啟時間是0 保持原來狀態 97 DCOUT = 1; 98 } 99 }
整個工程的功能是遠程安卓設備連接上該小風扇后,通過發送幀FF EE num AA來無線控制風扇轉速(其中num值需滿足0≤num≤10,其中FF和AA是幀頭和幀尾用於驗證是否為有效幀)。若小風扇風速調節成功則會返回給遠程安卓設備AA EE num+1 FF來表明設置成功。此外當遠程設備發送FF DD num AA時將會獲得AA EE num+1 FF,通過這個命令可以獲取當前的轉速。
這里的answer[5]數組是用來存儲小風扇應答信息的,data1、data2用來存儲有效幀的中間兩位,PWM_ON是當前的轉速,CYCLE是一個周期長度。在主函數的32~39行分別對answer固定部分進行初始化、數碼管顯示數據TempData[]初始化、風扇速度PWM_ON初始化。第41、42行主要初始化串口和定時器,接着進入while主循環。在主循環中不斷對收集的數據幀進行判斷是否為有效幀,如果是有效幀則分析是詢問速度命令還是設置速度命令,並分情況作出響應。在主循環的最后(67~71)是一些收尾工作:緩沖區getByte清空、緩沖區指針point清零、接收標志flag重置、以及開中斷。
第76~99行是定時器中斷子程序,每隔2ms觸發一次。在其內實現了對數碼管的高頻動態刷新和PWM。這里PWM是通過一個中間變量count來控制,從而實現在一個CYCLE*2ms的周期內前PWM_ON*2ms時間輸出高電平的效果。
8 客戶端軟件構成模塊
1) 打開Eclipse點擊File菜單欄下的Import按鈕准備導入second_test工程(如圖8_1所示)。
圖 8_1 導入工程
2) 接着在彈出的Select窗口中選擇Android文件夾下的Existing Android Code Into Workspace點擊next(如圖8_2所示)。
圖 8_2 選擇導入類型
3) 接着在彈出的框中點擊右上角的Browse按鈕,找到要導入的second_test所在路徑,並且需要勾選Copy projects into workspace(如圖8_3所示)。
圖 8_3 選擇工程
4) 最終效果如圖8_4所示在src文件夾下有兩個包:其中上面一個是和藍牙相關的類(從下到上依次為藍牙設備搜索相關類、藍牙通信連接相關類和藍牙通信相關類),另一個包是UI相關類(上一章已經講過ui_main.xml負責顯示,UI_Main.java負責顯示背后的邏輯實現)。如果讀者導入過程中出現錯誤,也可以采用上一章的方法新建一個工程,然后把src下的文件、layout下的文件和AndroidManifest.xml文件做相應的新建或修改。
圖 8_4 工程文件結構
9 藍牙通信三劍客詳解
從圖8_4大家可以看出整個工程最重要的部分在於bluetooth包下的藍牙相關的三個類,它們封裝並對外提供藍牙設備搜索、建立藍牙連接以及數據傳輸的基本藍牙功能。這樣在UI_Main.java中只要做簡單的調用即可實現比較繁瑣的藍牙通信功能,下面將針對它們做詳細的介紹。
1)BlueToothSearch主要負責藍牙設備搜索。仔細的讀者可能會發現它與上一章中的Func_BT.java很類似。如下的構造函數除了去掉了表示信號強弱的RSSI向量去掉和在16行實例化並啟動一個BTStateThread的線程外基本沒變。
1 public BlueToothSearch(Activity activity, Handler mHandler) { 2 this.mHandler = mHandler; 3 this.activity = activity; 4 5 mNameVector = new Vector<String>();// 向量 6 mAddrVector = new Vector<String>(); 7 8 IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND); 9 activity.registerReceiver(mReceiver, filter); 10 filter = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); 11 activity.registerReceiver(mReceiver, filter); 12 activity.registerReceiver(mReceiver, filter); 13 14 mBtAdapter = BluetoothAdapter.getDefaultAdapter(); 15 16 new BTStateThread().start();//藍牙狀態監聽 17 }
openBT函數和上一章的略有不同:上一章中打開藍牙設備函數的目的是確保本地藍牙設備打開的情況下進行藍牙搜索,所以上一章中的函數體內還包含了else語句,同時用onActivityResult進行監聽用戶是否授權;本章的openBT函數僅僅是用來在本地藍牙設備沒有開啟時發送一個Intent請求,接着就撒手不管了。
1 public void openBT() { 2 // 如果沒有打開則打開 3 if (!mBtAdapter.isEnabled()) { 4 Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); 5 activity.startActivityForResult(intent, ENABLE_BLUETOOTH); 6 } 7 }
這里的doDIscovery函數並未做修改,仍然是取消正在進行的搜索過程並啟動新的搜索。
1 public void doDiscovery() { 2 if (mBtAdapter.isDiscovering()) { 3 mBtAdapter.cancelDiscovery(); 4 } 5 mBtAdapter.startDiscovery(); 6 }
當上面啟動藍牙搜索后,在此過程中所搜到的藍牙設備將可以在下面的BroadcastReceiver獲得。這里每次發現一個藍牙設備時會獲取該設備的名稱和地址並放入相應的向量中,在最后搜索結束時會通過handler將該消息傳遞給UI_Main.java。
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 = intent 7 .getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 8 mNameVector.add(device.getName()); 9 mAddrVector.add(device.getAddress()); 10 } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED 11 .equals(action)) { 12 // 藍牙搜索完畢發送0x01msg 13 Message msg = new Message(); 14 msg.what = 0x01; 15 mHandler.sendMessage(msg); 16 } 17 } 18 };
在構造函數的第16行有個new BTStateThread().start()語句,其主要功能是周期性檢測本地藍牙設備狀態(如下的BTStateThread類)。此外在run函數內還加入了一旦本地藍牙狀態改變則發送0x10Handler消息,用來及時地通知UI_Main.java當前的本地藍牙設備的狀態。
1 class BTStateThread extends Thread { 2 public void run() { 3 boolean oldBTState; 4 while (true) { 5 try { 6 Thread.sleep(1000); 7 oldBTState=BTState; 8 BTState = mBtAdapter.isEnabled(); 9 if(oldBTState!=BTState){//一旦藍牙狀態改變就發送消息 10 // 藍牙狀態改變發送0x10消息 11 Message msg = new Message(); 12 msg.what = 0x10; 13 mHandler.sendMessage(msg); 14 } 15 } catch (InterruptedException e) {} 16 } 17 } 18 }
2) BlueToothConnect主要負責建立本地和遠程藍牙的Bluetooth Socket連接。由於我們在BlueToothSearch中已經獲得了周邊藍牙設備的名稱和地址,所以(代碼中第3行)這里直接調用getRemoteDevice函數右地址直接獲得遠程藍牙設備。接着(代碼中第5行)通過調用代表目標遠程服務設備的BluetoothDevice對象的createRfcommSocketToServiceRecord方法創建客戶端Bluetooth Socket。
1 public void setDevice(String Addr){ 2 mBtAdapter = BluetoothAdapter.getDefaultAdapter(); 3 mmDevice = mBtAdapter.getRemoteDevice(Addr); 4 try { 5 mmSocket = mmDevice.createRfcommSocketToServiceRecord(MY_UUID); 6 } catch (IOException e) { 7 } 8 }
上面的setDevice函數僅僅通過傳入的地址獲得了Bluetooth Socket,接下來需要調用connect來啟動連接。(如下面代碼所示)啟動連接是放在一個獨立的線程里的,一旦連接建立完畢則通過Handler將該消息通知給activity。
1 public void run() { 2 setName("ConnectThread"); 3 try { 4 mmSocket.connect(); 5 } catch (IOException e) { 6 try { 7 mmSocket.close(); 8 } catch (IOException e2) { 9 10 } 11 return; 12 } 13 //藍牙連接完畢發送0x02msg 14 Message msg=new Message(); 15 msg.what = 0x02; 16 mHandler.sendMessage(msg); 17 }
此外要特別說明下cancel()函數,該函數體內執行關閉藍牙連接的函數。因為在很多時候,比如讀寫文件、網絡socket等,由於建立連接后沒有關閉連接會導致一些意外的錯誤。
1 public void cancel() { 2 try { 3 mmSocket.close(); 4 } catch (IOException e) { 5 } 6 }
3) BlueToothCommunicate主要負責數據傳輸。上面已經解決了連接建立問題,這樣當連接一旦建立,客戶端和服務器設備上都會有Bluetooth Socket。自此之后兩者之間沒有太大的區別,可以使用這兩種設備上的Bluetooth Socket來發送和接收消息(這里因為HC-05/06已經把藍牙通信協議固件化了,所以大家可能不能很好的理解上面一段話的精妙之處,如果大家自己嘗試開發一個手機和手機的藍牙聊天室或者藍牙對戰游戲就能明白我的意思了)。下面是其構造函數,和BlueToothConnect類似負責將Activity的Handler傳入。
1 public BlueToothCommunicate(Handler mHandler) { 2 this.mHandler = mHandler; 3 state=true; 4 }
這里的setSocket主要是根據BlueToothConnect建立的BluetoothSocket來獲取標准輸入輸出流。這樣當本地設備想向遠程設備發送消息時,只要調用標准輸出流的write函數即可實現;當本地設備想讀取遠程設備發送過來的消息時,只要調用標准輸入流的read函數即可實現。
1 public void setSocket(BluetoothSocket socket){ 2 mmSocket = socket; 3 InputStream tmpIn = null; 4 OutputStream tmpOut = null; 5 // 獲取輸入輸出流 6 try { 7 tmpIn = socket.getInputStream(); 8 tmpOut = socket.getOutputStream(); 9 } catch (IOException e) { 10 } 11 mmInStream = tmpIn; 12 mmOutStream = tmpOut; 13 }
和硬件部分藍牙數據傳輸類似:對於本地設備向遠程設備發消息是本地程序可控的,即本地程序控制發送消息的時間點,因此這里僅僅把發送數據封裝成一個write函數,一旦程序需要發送消息直接調用即可;但是對於遠端設備向本地發送過來的消息本地是不可控的,即本地程序不清楚該消息會在什么時候出現,在硬件中我們采用了中斷的方式解決的問題,而在這里我們采用一個獨立的輪訓線程來處理的,這樣一旦有有效信息傳送過來就能夠做出及時的響應(例如可以在有效信息過來時采用Handler將該消息傳送給Activity,本代碼中沒有做進一步優化)。
1 // 利用線程一直收數據 2 public void run() { 3 byte[] buffer = new byte[1024]; 4 int bytes; 5 // 循環一直接收 6 while (state) { 7 try { 8 // bytes是返回讀取的字符數量,其中數據存在buffer中 9 bytes = mmInStream.read(buffer); 10 String readMessage = new String(buffer, 0, bytes); 11 Log.i("beautifulzzzz", "read: " + bytes + " mes: " 12 + readMessage); 13 } catch (IOException e) { 14 break; 15 } 16 } 17 } 18 19 // 發送就直接發送,沒有用線程 20 public void write(byte[] buffer) throws IOException { 21 mmOutStream.write(buffer); 22 }
同樣的這里也需要一個用來關閉BluetoothSocket和標准輸入輸出流的cancel函數。
1 public void cancel(){ 2 try { 3 state=false;//讓死循環停止 4 mmSocket.close(); 5 mmInStream.close(); 6 mmOutStream.close(); 7 } catch (IOException e) { 8 } 9 }
10 客戶端軟件整體邏輯梳理
欲較好地梳理整個安卓工程,一般都是從Activity的onCreate函數開始的,此外通過結合對應的XML文件能夠更快地理解。下面便是ui_main.xml所對應的UI_Main.java中的onCreate函數:該函數中最占篇幅的莫過於三個按鈕監聽了。
如代碼所示第54~86行為對應XML中加減按鈕的監聽,不難看出在mButton2和mButton3中核心是調用mBlueToothCommunicate.write(buffer)函數將數據幀buffer發送給遠程藍牙設備。這里要幫大家回憶一下我們硬件設計時規定的控制命令幀的格式了:(請轉到第七節最后幾段)遠程設備通過發送幀FF EE num AA來無線控制風扇轉速(其中num值需滿足0≤num≤10,其中FF和AA是幀頭和幀尾用於驗證是否為有效幀)。所以在下面代碼中的7~11行是對控制命令幀的設置(這里初始化buffer[2]=0x00,即初始速度為0)。因此,大家也不難理解在加減按鈕監聽中的對buffer[2]范圍的限制以及buffer[2]++和buffer[2]--的用意了。
1 @Override 2 protected void onCreate(Bundle savedInstanceState) { 3 super.onCreate(savedInstanceState); 4 setContentView(R.layout.ui_main); 5 6 //控制命令幀格式(首尾為校驗,第二:0xEE為設置速度,0xDD為獲取速度,第三:速度值) 7 buffer=new byte[4]; 8 buffer[0]=(byte) 0xFF; 9 buffer[1]=(byte) 0xEE; 10 buffer[2]=(byte) 0x00; 11 buffer[3]=(byte) 0xAA; 12 13 //實例化藍牙三劍客(搜索、連接、通信) 14 //myHandler是用來反饋信息的 15 mBlueToothSearch=new BlueToothSearch(this, myHandler); 16 mBlueToothConnect=new BlueToothConnect(myHandler); 17 mBlueToothCommunicate=new BlueToothCommunicate(myHandler); 18 19 mTextView = (TextView)findViewById(R.id.textView1); 20 21 mButton1 = (Button) findViewById(R.id.button_start); 22 if(mBlueToothSearch.getBT()==true) mButton1.setText("連接我的小風扇"); 23 else mButton1.setText("打開藍牙設備"); 24 mButton1.setOnClickListener(new OnClickListener() { 25 @Override 26 public void onClick(View v) { 27 if(mButton1.getText().equals("打開藍牙設備")){ 28 mBlueToothSearch.clearVector(); 29 mBlueToothSearch.openBT(); 30 mButton1.setText("連接我的小風扇"); 31 }else if(mButton1.getText().equals("連接我的小風扇")){ 32 mBlueToothSearch.clearVector(); 33 mBlueToothSearch.doDiscovery(); 34 35 mProgressDialog = ProgressDialog.show(UI_Main.this,"進入搜索藍牙設備階段...", "稍等一下~", true); 36 }else{ 37 if(mBlueToothConnect!=null){ 38 mBlueToothConnect.cancel(); 39 mBlueToothConnect=null; 40 mBlueToothConnect=new BlueToothConnect(myHandler); 41 } 42 if(mBlueToothCommunicate!=null){ 43 mBlueToothCommunicate.cancel(); 44 mBlueToothCommunicate=null; 45 mBlueToothCommunicate=new BlueToothCommunicate(myHandler); 46 } 47 mButton1.setText("連接我的小風扇"); 48 mButton2.setEnabled(false); 49 mButton3.setEnabled(false); 50 } 51 } 52 }); 53 54 mButton2=(Button) findViewById(R.id.button_add); 55 mButton2.setEnabled(false); 56 mButton2.setOnClickListener(new OnClickListener() { 57 @Override 58 public void onClick(View v) { 59 if(buffer[2]<(byte) 0x0A){ 60 buffer[2]++; 61 try { 62 mBlueToothCommunicate.write(buffer); 63 mTextView.setText(new Integer(buffer[2]).toString()); 64 } catch (IOException e) { 65 e.printStackTrace(); 66 } 67 } 68 } 69 }); 70 71 mButton3=(Button) findViewById(R.id.button_cut); 72 mButton3.setEnabled(false); 73 mButton3.setOnClickListener(new OnClickListener() { 74 @Override 75 public void onClick(View v) { 76 if(buffer[2]>(byte) 0x00){ 77 buffer[2]--; 78 try { 79 mBlueToothCommunicate.write(buffer); 80 mTextView.setText(new Integer(buffer[2]).toString()); 81 } catch (IOException e) { 82 e.printStackTrace(); 83 } 84 } 85 } 86 }); 87 }
其實有一點大家可能注意到了:加減按鈕初始化時是被setEnabled(false)的!因為調用藍牙的write函數已經是藍牙搜索、建立連接之后的事情了,而在初始化時我們是不能輕易開放這兩個按鈕中的write功能的。所以在此之前我們必須保證連接已經建立完畢,這就要引出稍微復雜的mButton1按鈕監聽了。
注意到上面代碼的第22、23兩行,首先調用mBlueToothSearch的getBT()行數判斷用戶當前藍牙設備是否打開,如果打開則mButton1的功能直接可設置為“連接我的小風扇”,否則mButton1要設置為“打開藍牙設備”。從mButton1的監聽中可以看出其主要有三個功能:①當本地藍牙設備沒有打開時,負責調用mBlueToothSearch.openBT()函數打開本地藍牙設備,並進入連接小風扇的功能;②當本地藍牙打開並且還未連接遠程小風扇時,負責調用mBlueToothSearch.doDiscovery()函數開始搜索周邊藍牙設備,並啟動一個ProgressDialog告訴用戶稍等;③當連接好了之后需要斷開連接時,負責調用藍牙建立連接和藍牙通信相關函數取消相關操作並讓加減按鈕失效。
圖 10_1 mButton1功能轉換圖
從圖10_1中可以看出有一個過程筆者打了個問號,即從點擊mButton1執行連接小風扇如何變成可控制階段狀態的中間過程被我偷偷跳過了。上面第②點中講到當本地藍牙打開並且還未連接遠程小風扇時,點擊按鈕會執行mBlueToothSearch.doDiscovery()函數,然后似乎就沒有狀態變換了。其實一切的一切都指向了Activity中的myHandler!
1 // 消息句柄(線程里無法進行界面更新,所以要把消息從線程里發送出來在消息句柄里進行處理) 2 public Handler myHandler = new Handler() { 3 @Override 4 public void handleMessage(Message msg) { 5 switch(msg.what){ 6 case 0x00: 7 break;//出現異常或為搜索到設備 8 case 0x01: 9 mProgressDialog.setTitle("進入嘗試連接藍牙設備階段..."); 10 //當搜索完畢自動查找是否是我們的設備然后嘗試連接 11 boolean isFind=false; 12 for(int i=0;i<mBlueToothSearch.mNameVector.size();i++){ 13 if(mBlueToothSearch.mNameVector.get(i).equals("HC-06")){ 14 Log.i("beautifulzzzz",mBlueToothSearch.mNameVector.get(i)); 15 mBlueToothConnect.setDevice(mBlueToothSearch.mAddrVector.get(i)); 16 mBlueToothConnect.start(); 17 isFind=true; 18 break; 19 } 20 } 21 if(isFind!=true)mProgressDialog.dismiss();//等待窗口關閉 22 break;//搜索完畢 23 case 0x02: 24 mProgressDialog.setTitle("進入啟動通信階段..."); 25 //將上一步獲得的socket傳給藍牙通信線程並啟動線程監聽數據 26 mBlueToothCommunicate.setSocket(mBlueToothConnect.mmSocket); 27 mBlueToothCommunicate.start(); 28 29 mProgressDialog.dismiss();//等待窗口關閉 30 mButton1.setText("斷開我的小風扇"); 31 mButton2.setEnabled(true); 32 mButton3.setEnabled(true); 33 break;//連接完畢 34 case 0x03:break; 35 case 0x04:break; 36 case 0x10: 37 if(mBlueToothSearch.getBT()==true 38 && mButton1.getText().equals("打開藍牙設備")){ 39 mButton1.setText("連接我的小風扇"); 40 }else if(mBlueToothSearch.getBT()==false){ 41 if(mBlueToothConnect!=null){ 42 mBlueToothConnect.cancel(); 43 mBlueToothConnect=null; 44 mBlueToothConnect=new BlueToothConnect(myHandler); 45 } 46 if(mBlueToothCommunicate!=null){ 47 mBlueToothCommunicate.cancel(); 48 mBlueToothCommunicate=null; 49 mBlueToothCommunicate=new BlueToothCommunicate(myHandler); 50 } 51 mButton1.setText("打開藍牙設備"); 52 mButton2.setEnabled(false); 53 mButton3.setEnabled(false); 54 } 55 break;//藍牙狀態改變 56 default:break; 57 } 58 } 59 };
這時大家可能會恍然大悟(想想上一節講的藍牙通信三劍客每個構造函數中的Handler,以及時不時地在它們的成員函數內部出現的發送Handler消息):原來mBlueToothSearch.doDiscovery()執行將會啟動藍牙搜索,在其搜索過程中搜索的設備名和設備地址分別存儲在BlueToothSearch的公有成員變量mNameVector和mAddrVector中,然后在本次搜索結束后會向Activity發送一個類型為0x01的Handler消息,而該消息會被Activity中的handleMessage接收到:
圖 10_2 Handler消息之0x01
經過上面一個過程最終位於Activity中的handleMessage接收到0x01消息,請看上面代碼的第8~22行:在case 0x01中遍歷所有找到的藍牙設備是否有name為“HC-06”的藍牙設備(因為我用的藍牙模塊HC-06出廠默認的name就是“HC-06”,此外大家可以參看HC-06的AT指令自行設置其名字)。當找到名為“HC-06”的設備時(第15、16兩行)將會把該設備的地址傳給mBlueToothConnect來獲得遠程藍牙設備,繼而獲得Bluetooth Socket,然后執行獨立線程進行啟動連接(大家可以結合上一節的BlueToothConnect理解)。當然也不排除找不到設備的情況,第21行如果找不到想要的藍牙設備則把mProgressDialog等待窗口關閉。有一點要和大家說一下:這里是為了演示方便而采用name來確定藍牙設備,而name會出現相同的情況,真正應用的時候一定要注意這一點的!
圖 10_3 Handler消息之0x02
上面講到當handleMessage收到0x01消息后,首先找到名為“HC-06”的藍牙設備地址,然后執行圖10_3所示①的操作獲取BluetoothSocket,接着執行②操作啟動線程。這樣等到RUN函數內藍牙通信連接建立完畢后會向Activity發送0x02消息,又重新交給Activity來處理。
請看代碼的第23~33行:在case 0x02中的第26、27兩行,首先調用mBlueToothCommunicate的setSocket方法來將將上一步獲得的socket傳給藍牙通信線程並啟動線程監聽數據,這樣就能實施藍牙無線通信了。所以在接下來的29~32行內關閉了等待窗口並使能加減按鈕,使系統運行的狀態轉換到圖10_1中的可控階段。
圖 10_4 進入可控制狀態
至此,大家把圖10_2、10_3、10_4的圖連起來,然后再換掉圖10_1的帶問號的部分就是整個程序的基本狀態轉換圖。此外,細心的讀者可能會發現在Activity中還有0x10這條消息,其實該消息的發送者來自BlueToothSearch中的BTStateThread線程。在上一章中提到該線程起監視本地藍牙設備狀態的作用,一旦本地藍牙設備的狀態被改變,則會發出0x10的消息。這樣在我們的Activity中一旦發現有0x10這個消息則改變相應的狀態,來提高程序的可靠性(否則中途關掉藍牙可能導致整個狀態機紊亂)。
11 最終成果檢查
怎么樣,上一章玩硬件沒有盡興的同學這回有感覺了嗎?這個看似簡單的小風扇是不是還有點含金量?哈哈哈,給自己評價一下吧:
- 自己焊制出51最小系統並成功給它燒個小程序(+ 20分)
- 明白直流電機電路設計並理解了PWM的51編程(+ 10分)
- 理解了3461AS的原理,並成功設計出自己的數碼管驅動(+ 20分)
- 實現了51串口通信,能對電腦說hello嗎(+ 10分)
- 大致明白安卓藍牙相關API並理解本章介紹的藍牙三劍客(+ 30分)
- 腦袋里走通了整個客戶端軟件的狀態轉換圖(+ 30分)
- 成功DIY出無線小風扇系統(+30)
- 在無線小風扇的基礎上設計出無線小台燈(+40)
- 獲得了超過10個人的贊揚(+20)
- ……
及格分70分,對自己要狠一點哦,否則后面有你受的!哈哈哈!!!
[搜索:beautifulzzzz(看樓主博客園官方博客,享高質量生活)嘻嘻!!!]
[如果有需要制作藍牙防丟器或藍牙室內定位的可以聯系我哦~]
如果您覺得不錯,別忘點個贊讓更多的小伙伴看到\(^o^)/~