[智能硬件] 2、三分钟看懂智能硬件原理——智能玩具小风扇制作教程(包括手机应用开发)
恭喜大家顺利通过测试一!在测试一中我们学会了如何利用现有模块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^)/~