智能手環硬件軟件功能模塊設計_預期效果方案構思

2020-09-02 09:05:36分類:聚龙棋牌怎么玩337

  你的一天在做什麽,你的身體呈現什麽狀態?現在的智能穿戴産品就能夠給出答案,其中智能手環是最受大家認可的一款智能穿戴設備,它不僅可以記錄生活的鍛煉、睡眠等實時數據,還具有社交功能,能夠將鍛煉情況和身體健康狀況等發送到社交網絡進行分享。爲我們健康的生活起到指導的作用。


智能手环

  一個智能手環最小系統一般包括:可充電的電源模塊控制模塊(下圖中左邊芯片)、藍牙模塊(右邊芯片)、存儲模塊加速計模塊(上面芯片)。其中加速计是为了获得佩戴者在运动或睡眠过程中的加速度数据,通过分析这些数据则能够判断佩戴者的运动情况和睡眠质量;存儲模塊主要负责将实时数据暂存,接着在适当的时刻借助藍牙模塊将数据同步到手机端。方便起见本次要自制的记步手环将不采用存储器暂存,而是将数据实时地传送到手机端。同时为了便于大家对记步算法的理解,客户端将采用一个折线图的形式实时展示记步手环收集的数据。


智能手环电路板


  2. 如何实现记步

  看了上面的分析大家可能會疑惑——僅僅用一個加速計怎麽能實現記步和睡眠質量檢測呢?其實確實可以!因爲加速計可以實時獲取自身的XYZ三個軸向的加速度。當其靜止時合加速度會在重力加速度附近波動;當佩戴者處于深度睡眠過程中時,其合加速度將呈現出長時間的穩定于重力加速度附近;當其隨著運動的佩戴者手臂而做周期性擺動時,其數據也是有一定規律可循的。這樣,設計時只要通過分析從加速計獲的數據就能實現對運動或睡眠質量的記錄。
 


  3.預期效果構思

  上面已經提到:爲了方便,我們並未采用存儲器實現記步手環的離線記錄,而是實時地將數據發送到客戶端由一個可視化的折線圖動態繪制結果。如下圖所示系統中記步手環部分包含單片機模塊藍牙模塊加速計模塊電源模塊,这样通过单片机的协调可以实现将加速計模塊的数据通过蓝牙实时地传送给客户端程序。在客户端部分则负责将收集到的实时数据以折线图的形式动态地展示出来,此外客户端中也加入一个滑动条来控制记步阈值来真正让大家明白其设计思想(真正商业化的智能手環多數采用的是先將有效數據保存在手環的小型存儲器中,上位機周期性地將數據收集並同步到服務器端)。


预期效果构思图
 


  4.硬件整體設計

  如下图,相比于上一个无线小风扇该硬件构成反而比较简单:藍牙模塊依然采用我们比较熟悉的HC-06模塊,對于加速度的測量采用四周飛行器上常采用的MPU6050模塊。該模塊不僅含有加速計的功能,還具有陀螺儀的功能,其在汽車防側翻相機雲台穩定機器人平衡空中鼠標姿態識別等众多领域都有应用,这里我们只是利用了它的加速计功能。此外要注意:下图所示的單片機模塊的电源引脚被隐藏了,在真正设计连接时一定不要忽略这两个引脚!


硬件整体设计
 


  介紹

  MPU-60X0是全球首例9軸運動處理器。它集成了3軸MEMS陀螺儀,3軸MEMS加速計,以及1個可擴展的數字運動處理器DMP(Digital Motion Processor)。如下图所示轴向是相对于加速计说的,當芯片水平靜止放置時x軸和y軸的加速度分量幾乎爲0,z軸的加速度分量約爲當地的重力加速度;而旋轉極性則是對陀螺儀來說的


數字運動處理器

  为何上面说9轴信号呢?因为 MPU-60X0 可用 I2C 接口连接一个第三方的数字传感器,比如磁力计。扩展之后就可以通过其I2C或SPI接口输出一个9轴的信号。也可以通过其I2C接口连接非惯性的数字传感器,比如压力传感器。(为什么特别提磁力计和压力传感器呢?因为在飞控方面,利用陀螺仪和加速计可以计算飞行器的倾角,从而调节飞行器平衡。但是只是调节平衡对方向没有概念也不能执行复杂任务,因此需要配备磁力计(也即电子罗盘传感器)。此外,由于飞行器在不同高度作业时,其周围的重力加速度也不同,这样会影响倾角的准确性,因此通过气压计计算所处高度然后计算实时加速度达到精确控制的效果。)


数字传感器

  MPU-60X0對陀螺儀和加速計分別用了三個16位的ADC,將其測量的模擬量轉化爲可輸出的數字量。爲了精確跟蹤快速和慢速運動,傳感器的測量範圍是可控的,陀螺儀可測範圍爲±250,±500,±1000,±2000°/秒(dps),加速計可測範圍爲±2,±4,±8,±16g(重力加速度)。如圖是直接從16位ADC中讀出的6軸的數據(從左到右依次爲加速計X軸數據、Y軸數據、Z軸數據、陀螺儀X極數據、Y極數據、Z極數據):

  但是這裏的輸出值並不是真正的加速度和角速度的值,上面說過,MPU是一個16位AD量程可程控的設備,這裏設置的加速度傳感器的測量量程爲正負2g(這裏的g爲重力加速度),陀螺儀的量程爲正負2000°/s。所以要用下面的公式進行轉化:


陀螺仪的量程
 


  6.一個簡單的記步算法設計

  當MPU6050隨著運動的佩戴者手臂而做周期性擺動時,其數據也是有一定規律可循的。簡單起見我們只分析合加速度:一個擺臂周期其合加速度會在重力加速度上下波動,如圖只要選取合適的阈值(黑線代表阈值),每次檢測出合加速度大于該阈值則認爲是一次擺臂,從而可以實現記步的功能。這裏要特別說明下:如果想把你的手環推向市場,就要通過大量分析擺臂數據建立一套更好的記步算法。


计步算法设计
 


  7. I2C总线介绍

  由于51系列單片機將串口通信很多細節都封裝到芯片內部,所以我們即使設計了串口驅動模塊,也並沒有真正了解串口通信的核心思想。其實串口協議的出現是爲了構成一個總線線路這樣單片機只要使用比較少的引腳就能和比較多的設備進行通信了,這裏要用到的I2C總線也具有相同的效果但又有些不同。


i2c总线介绍

  I2C(Inter-Integrated Circuit)总线是由PHILIPS公司开发的两线式串行总线,用于连接微控制器及其外围设备。是微电子通信控制领域广泛采用的一种总线标准。它是同步通信的一种特殊形式,具有接口线少,控制方式简单,器件封装形式小,通信速率较高等优点。如上图采用I2C总线后CPU只要使用2个引脚便可和多个设备进行通信(其实每个采用I2C通信方式的设备都具有唯一的地址码,这样在总线中便能够被唯一识别),从而大大减少了引脚的使用。

  在I2C總線中使用的兩線爲時鍾線SCL和數據線SDA。所有的I2C主從設備都是只被這兩根線連接起來的。每一個設備既可以作爲發送方,也可以作爲接收方,或者既可以作爲發送發也可以作爲接收方。在總線中的主設備一般起産生時鍾信號和初始化通信的作用,從設備則負責響應主設備發出的命令。爲了在總線上區分每一個設備,每一個從設備必須有一個唯一的地址。主設備一般不需要地址(一般爲微處理器),因爲從設備不能發送命令給主設備。


总线中主从设备

  這裏要先介紹I2C總線中幾個專有名詞:

  l 发送者:将数据发送到总线的设备

  l 接收者:从总线接收数据的设备

  l 主设备:产生时钟信号、启动通信、发送I2C命令和终止通信的设备

  l 从设备:监听总线、能被主设备寻址的设备

  l 多主设备:I2C能够拥有多个主设备,而且每个主设备都能够发送命令

  l 仲裁:当多个主设备请求使用总线时,决定哪一个主设备可以占用的一个过程

  l 同步:同步多个设备时钟信号的一个过程

  上面是從宏觀上對I2C總線介紹了下,接下來將深入細節研究其通信過程:

  串行數據傳送:

  在總線備用時SDA和SCL都必須保持高電平狀態,只有關閉I2C總線時才能使SCL鉗位在低電平。在I2C總線數據傳輸時,在時鍾線高電平期間,數據線上必須保持有穩定的邏輯電平(也就是說在數據傳輸期間只有時鍾線低電平期間,才允許數據線上的電平發生變化)。


串行数据发送

  因此在如上圖中對于每一個時鍾脈沖期間一比特的數據將會被傳送,SDA只能在時鍾信號爲低電平時才能改變。下面是代碼中發送一字節的函數:在循環體內每次將dat內的最高位移出到CY中,進而賦值給SDA(這時SCL爲低,SDA可改變)。接著拉高SCL並保持5us,最後再拉低SCL實現一個時鍾脈沖將dat中最高位送出。依此循環8次實現將dat全部傳出。

  voidI2C_SendByte(uchar dat){

  uchar i;for(i=0; i<8; i++){

  dat <<=1;

  SDA = CY;

  SCL =1;Delay5us();

  SCL =0;Delay5us();}I2C_RecvACK();}

  開始和結束條件

  命令不會沒有任何預兆直接發送的,每一個I2C命令的發送總是開始于開始條件並結束于終止條件。這裏所謂的開始條件和終止條件起始也是由SCL和SDA組合形成的(如下圖)。


开始和结束条件

  如果時鍾線保持高電平期間,數據線出現由高到低的電平變化,則會啓動I2C總線,此時爲I2C的起始信號:

  voidI2C_Start(){

  SDA =1;

  SCL =1;Delay5us();

  SDA =0;Delay5us();

  SCL =0;}

  若在時鍾線保持高電平期間,數據線出現由低到高的電平變化,則會停止I2C總線的數據傳輸,此時爲I2C的終止信號:

  voidI2C_Stop(){

  SDA =0;

  SCL =1;Delay5us();

  SDA =1;Delay5us();}

  開始條件之後I2C總線被認爲是忙狀態,只有當停止信號之後其他主設備才能使用該總線。此外,當開始條件之後主設備能夠多次發出開始信號。這些開始信號和第一次發出的開始信號類似,他們後面經常會跟從設備的地址。這樣可以方便實現在I2C總線忙期間,當前占線的主設備可以和不同的從設備進行通信。

  7.3 I2C数据传送

  I2C總線上傳送的每一個字節均爲8位,但是每啓動一次I2C總線,其後的數據傳送字節數是沒有限制的。同時每傳送一字節的數據後面都要跟隨一個接收者回應的應答位(低電平爲應答信號,高電平爲非應答信號),當全部數據發送完畢後主設備發送終止信號。



数据传送图

  所以在上面向I2C總線發送一字節的數據的代碼的最後有一個I2C_RecvACK()函數。(如下)該函數負責接收接收者發送過來的應答信號,也即上圖中的第9個時鍾脈沖的期間的相應操作。

  bit I2C_RecvACK(){

  SCL =1;Delay5us();

  CY = SDA;

  SCL =0;Delay5us();return CY;}

  注:所有的數據位包括應答位都需要主設備産生時鍾脈沖。如果從設備沒有應答意味著將沒有更多的數據要傳送或者設備沒有准備好傳送。這時,主設備要麽産生停止信號,要麽重新發出開始條件。


应答信号

  7.4 I2C的7-bit地址

  每一個從設備都應該具有唯一的地址,這樣主設備才能准確的尋址到每一個設備,而這些地址被統一規定爲7比特。但是上面講過I2C總線傳輸數據都是8比特傳送,地址7比特豈不是少一位!其實緊跟地址還有一位用來表示是讀操作還是寫操作的標志位。如果該位爲0表示主設備將要向從設備寫數據,否則表示主設備將要從從設備讀數據。在這8比特被發送後主設備能夠持續地進行讀或者寫。如果主設備想和其他從設備進行通信,只要再次發送一個新的開始信號就可以而不必發送終止信號。


一个完整的数据读写操作
 


  8. MPU6050驱动设计

  下面將結合MPU6050的驅動進一步講解其原理(該部分的代碼參見工程的部分)。我們首先來看一下它的頭文件:從第6到25行上來就是一大串內部地址的定義,對于初學者可能一頭霧水!如果樓主再引入寄存器等數字電路的知識可能又要說幾頁了,于是這裏准備只用一個簡單的例子闡述下這些地址的作用。

  #include""#define SMPLRT_DIV 0x19 #define CONFIG 0x1A #define GYRO_CONFIG 0x1B #define ACCEL_CONFIG 0x1C #define ACCEL_XOUT_H 0x3B#define ACCEL_XOUT_L 0x3C#define ACCEL_YOUT_H 0x3D#define ACCEL_YOUT_L 0x3E#define ACCEL_ZOUT_H 0x3F#define ACCEL_ZOUT_L 0x40#define TEMP_OUT_H 0x41#define TEMP_OUT_L 0x42#define GYRO_XOUT_H 0x43#define GYRO_XOUT_L 0x44 #define GYRO_YOUT_H 0x45#define GYRO_YOUT_L 0x46#define GYRO_ZOUT_H 0x47#define GYRO_ZOUT_L 0x48#define PWR_MGMT_1 0x6B #define WHO_AM_I 0x75 #define SlaveAddress 0xD0 voidSingle_WriteI2C(uchar REG_Address,uchar REG_data);

  uchar Single_ReadI2C(uchar REG_Address);voidInitMPU6050();intGetData(uchar REG_Address);

  上面講到在I2C總線中主設備可以通過固定的7-bit地址尋找到相應的從設備(這裏的7-bit地址爲第26行的SlaveAddress,想必大家也能夠理解後面注釋的意義了吧~不加1表示緊跟著地址的一位爲0,表示向該設備寫數據;加1則表示緊跟著的一位爲1,表示主設備從從設備讀數據)。雖然采用這種方式能夠准確找到從設備,但是從設備裏面又有比較多的寄存器。這就好比你知道了某個要找的東西在具體的某個大櫃子裏,但是來到大櫃子前又發現有許多小抽屜。這裏的7-bit地址就好像指明了哪個櫃子,而從第6到25行的內部地址就像櫃子上的抽屜編號,而不一樣之處是位于mpu6050內的“小抽屜”一部分存放著其采集的實時數據,另一部分等著外部放一些數據來設置其采樣屬性。

  這樣,如上面的第6行的SMPLRT_DIV(0x19)是用來設置陀螺儀采樣率的寄存器地址,只要向該地址所指的寄存器寫入相應的值則可以設置陀螺儀采樣率。因此下面MPU6050初始化函數就是調用封裝的I2C寫函數向相應的小抽屜內寫屬性數據,設置MPU6050采樣屬性。

  voidInitMPU6050(){Single_WriteI2C(PWR_MGMT_1,0x00);Single_WriteI2C(SMPLRT_DIV,0x07);Single_WriteI2C(CONFIG,0x06);Single_WriteI2C(GYRO_CONFIG,0x18);Single_WriteI2C(ACCEL_CONFIG,0x01);}

  再如第10~11行的ACCEL_XOUT_H、ACCEL_XOUT_L是用來存放最新的陀螺儀X極的數值,因爲采用16位ADC所以這裏需要用兩個寄存器。所以下面合成數據函數負責連續讀取REG_Address開始的兩字節數據組成一個16位數據。當函數的參數爲ACCEL_XOUT_H時,則獲取的是實時的陀螺儀X極的數值,同樣地可以獲得實時的6軸數據。

  intGetData(uchar REG_Address){

  uchar H,L;

  H=Single_ReadI2C(REG_Address);

  L=Single_ReadI2C(REG_Address+1);return(H<<8)+L;}

  **注:**關于MPU6050內部的“小抽屜”的地址和功能需要閱讀其官方的MPU6050寄存器手冊。
 


  9.硬件工程整體介紹

  9.1、打开Keil uVision2,点击Project下的Open Project,打开记步手环.Uv2加载工程。


打开工程

  9.2、待工程加载完毕,大家会在工程窗口中看到图9_2所示文件结构。其中FUNC组下面包含数i2c驱动、mpu6050和串口驱动文件, USER组下是最上层应用程序文件。


文件结构

  9.3、上一章已經把講解了,前幾節也把和mpu6050,c介紹了。這裏直接從對整個工程的流程進行分析:主函數中先初始化串口和MPU6050,接著進入無限循環。循環中每隔一定的時間發送一幀的數據——該幀以‘#’開始以‘$’結束,中間依次是X軸加速度值、Y軸加速度值和Z軸加速度值。

  void main (void){delay(500);InitUART();InitMPU6050();while(1){SendByte('#');SendData(GetData(0x3B));SendData(GetData(0x3D));SendData(GetData(0x3F));SendByte('$');delay(20);}}

  其中调用了串口驱动中的void InitUART(void)串口初始化函数、 void SendByte(unsigned char dat)串口发送一字节函数和 void SendStr(unsigned char *s)串口发送一个字符串函数,以及调用了mpu6050驱动中的void InitMPU6050()初始化函数和int GetData(uchar REG_Address)获取6轴数据函数。

  externvoidInitUART(void);externvoidSendByte(unsignedchar dat);externvoidSendStr(unsignedchar*s);externvoidInitMPU6050();externintGetData(uchar REG_Address);

  这里唯一要特别说明的函数是:void SendData(int value)函数。我们知道直接调用MPU6050的函数int GetData(uchar REG_Address)返回的是int类型的数据,而串口每次只能发送一个8bit的数据,于是这里的SendData则是负责将该int类型的数值转换为串口容易发送的数据再进行发送。

  voidenCode(uchar *s,int temp_data){if(temp_data<0){

  temp_data=-temp_data;*s='-';}else*s=' ';*++s =temp_data/10000+0x30;

  temp_data=temp_data%10000;*++s =temp_data/1000+0x30;

  temp_data=temp_data%1000;*++s =temp_data/100+0x30;

  temp_data=temp_data%100;*++s =temp_data/10+0x30;

  temp_data=temp_data%10;*++s =temp_data+0x30;*++s ='';}voidSendData(int value){enCode(temp, value);SendStr(temp);}

  上面的enCode函數是將輸入的int類型的數據轉換爲第一位爲符號(正用空格代替,負用負號代替),後5位爲數值的字符串,即使不足五位數前面也要填充0。這樣便不難理解SendData的功能:將value編碼並通過串口發送。

  這樣整個工程的作用則是周期性讀取MPU6050三軸的加速度並用下面的幀格式通過藍牙發送出去:


 


  10.客戶端軟件構成模塊

  10.1、打開Eclipse點擊File菜單欄下的Import按鈕准備導入second_test工程(如圖10_1所示)。
客户端软件导入工程

  10.2、接着在弹出的Select窗口中选择Android文件夹下的Existing Android Code Into Workspace点击next(如图10_2所示)。
选择导入类型

  10.3、接着在弹出的框中点击右上角的Browse按钮,找到要导入的third_test所在路径,并且需要勾选Copy projects into workspace(如图10_3所示)。

选择工程

  10.4、最終效果如圖10_4所示在src文件夾下有四個包:其中第一個是和藍牙相關的類(從下到上依次爲藍牙設備搜索相關類、藍牙通信連接相關類和藍牙通信相關類);第二個是繪制折線圖表相關的類(這裏采用開源圖表繪制引擎achartengine,所以在libs裏要添加相應的包);第三個是數據池相關的類,用于實現藍牙數據實時高速處理;另一個包是UI相關類,也是整個工程最核心的部分。如果讀者導入過程中出現錯誤,也可以采用第三章的方法新建一個工程,然後把src下的文件、layout下的文件和文件做相應的新建或修改,同時還要注意引入libs的包以及values裏的。
工程文件结构


 


  11.軟件最終效果預覽

  上面是從模塊構成的角度介紹工程的主要文件,爲了更好的方便分析其內部邏輯,筆者准備先帶領大家預覽下本次應用的最終效果(如圖11_1所示):

  n 第一幅图:是初始打开界面,如果本地蓝牙没有打开最左边的按钮将会显示“打开蓝牙设备”;

  n 第二幅图:是点击“连接我的小手环”后进入蓝牙搜索阶段;

  n 第三幅图:是自动搜索到记步手环后进入的连接蓝牙阶段;

  n 第四幅图:是连接完成后,应用把从手环收集的实时数据(XYZ轴加速度以及合加速度)绘制出;

  n 第五幅图:是通过滑动条调大记步阈值,并选择CheckBox只显示合加速度值的实时折线;

  n 第六幅图:是放大折线图,并点击某个点显示具体信息图。

  其中前三個階段和上一章中的小風扇的控制很類似,都是點擊連接到進入搜索再到進行連接。只不過一個是連接後通過應用向硬件發送命令幀來控制小風扇轉速;一個是不斷從記步手環讀取實時的XYZ三軸的加速度,計算合加速度同時記步,並且將數據實時以折線圖的形式展示出來。


 


  12.一個高效處理數據的數據池設計

  當提到爲什麽需要高效處理的數據池時,其實要從藍牙搜索講起。由于上一章的最後對藍牙搜索、連接、通信的三個過程做了詳細的講解,本次則只從整體上進行梳理一下。

  如圖12_1,當點擊連接小手環按鈕後則執行藍牙搜索類的doDiscovery()函數進行搜索藍牙設備,在其搜索過程中搜索的設備名和設備地址分別存儲在BlueToothSearch的公有成員變量mNameVector和mAddrVector中,然後在本次搜索結束後會向Activity發送一個類型爲0x01的Handler消息,而該消息會被Activity中的handleMessage接收到。

  當Activity中的handleMessage接收類型爲0x01的消息後,程序會遍曆本次藍牙搜索到的周邊設備的名稱找到符合我們的手環的藍牙設備。然後調用藍牙連接的setDevice()函數獲取遠程藍牙通信socket,接著在handleMessage內再觸發藍牙連接的線程進行藍牙連接。當藍牙連接完畢,則會發送0x02類型的消息反饋給Activity中的handleMessage。

  同樣的當Activity中的handleMessage接收類型爲0x02的消息後,程序會調用藍牙通信類的setSocket()函數來獲取標准輸入輸出流。此後,如果想從軟件向硬件發送消息則直接可以調用藍牙通信類的write()函數,而接收數據則是采用啓動一個接收線程來實現實時接收的。

从点击连接小手环到完成蓝牙连接全过程流程图

  現在我們的思維已經跟著轉到了上圖中最後一個無限輪詢收數據階段。同時我們知道從小手環發來的數據是比較高速的(硬件工程中寫的是每次發送完畢delay(20),應該算是比較短的時間了)。那麽問題就來了:如果我們不能及時地將手環傳來的數據進行處理,很有可能導致大量的數據滯留在緩沖區。這樣進一步會導致每次獲得的數據都不是最新的數據,而表現出動態繪制折線圖滯後糟糕的效果。

  綜上由于下位機10ms發送一次20byte的數據,上位機一方面要做好接收工作,保證數據不擁擠在串口接收緩沖區;另一方面也要實時獲取當前從串口讀到的最新數據。如果采用傳統多線程+鎖的機制是可以的,但是當多線程中加入鎖勢必會影響程序執行效率,通過綜合分析該問題筆者最終抽象出一個特殊的數據模型——自動更新的環形棧:

自动更新的环形栈

  如圖12_2所謂自動更新的環形棧本質上是一個基于環形數組的特殊數據結構。圖中環形代表數據池,也是一個環形數組(普通數組,采用一定技巧將首尾連接),p_write指示當前數據插入位置,每次插入一個數據p_write順時針移動一格,從而實現新數據覆蓋老數據的自動更新功能。而這裏最精妙的地方在于每次取數據的方式:從p_write所指的位置逆時針取40個數據(因爲有效幀包含的數據長度爲20,一次取40保證至少有一個有效幀),然後從這40個數據中找出有效信息,賦值給公有成員X,Y,Z。這樣通過適當調節環的容量,保證取數據時該段數據不被覆蓋的前提下,又能根據p_write指示獲取最新的下位機發來的有效幀,將存和取有效地分離從而完美達到了我們的需求。

  具体在程序中的onCreate函数中声明并实例化一个大小为20000的数据池mDataPool = new DataPool(20000)。接着在BlueToothCommunicate的轮询接收数据的线程中(也即图12_1的最后一环节的read中)对于每次新收到的数据调用mDataPool的push_back(buffer, bytes)函数将其存储在数据池中。当每次需要取最新数据时只要先调用mDataPool的ask()函数,接着便可直接通过访问DataPool的公有成员XYZ获取最新三轴加速度的值了。

  publicvoidrun(){byte[] buffer =newbyte[1024];int bytes;while(state){try{

  bytes = mmInStream.read(buffer);

  String readMessage =newString(buffer,0, bytes);

  Log.i("beautifulzzzz","read: "+ bytes +" mes: "+ readMessage);

  UI_Main.mDataPool.push_back(buffer, bytes);}catch(IOException e){break;}}}
 


  13.一個開源的折線圖繪制方案

  在第10節客戶端軟件構成模塊中曾提到本項目中采用了開源圖表繪制引擎AChartEngine。它是一個安卓系統上制作圖表的框架,支持折線圖、面積圖、分區圖、對比圖、散點圖、柱狀圖、餅圖等(如下圖所示)。

折现图绘制

  此外其所有支持的圖表類型,都可以包含多個系列,都支持水平(默認)或垂直方式展示圖表。並且支持許多其他的自定義功能。所有圖表都可以建立爲一個view,也可以建立爲一個用于啓動activity的intent(顯然上面前兩幅圖是采用view的形式,其他幾個是采用intent啓動的)。

  一般突然提到某某開源包或者調用別的接口初學者可能會頭大,而且這裏更讓多數人頭痛的是筆者竟突然亮出了這麽多炫酷的UI,豈不是更加難以使用!于是可能會有很多人准備自己DIY折線圖了。然而事實卻是這個開源的框架用起來十分方便:大家可以把所有的chart都想象成由兩層組成,一部分是Renderer(如XYMultipleSeriesRenderer,用于對圖表樣框架樣式的說明),另一部分是Dataset(如XYMultipleSeriesDataset,用于對視圖數值的處理)。所以在類的開始就定義並聲明這兩種類型的私有成員:【第一步:數據層和顯示層定義並實例化】

  private XYMultipleSeriesDataset mDataset =newXYMultipleSeriesDataset();private XYMultipleSeriesRenderer mRenderer =newXYMultipleSeriesRenderer();

  因爲mRenderer用于對圖表框架樣式的說明,所以在setChartSettings函數裏調用了多個其成員函數用來對圖表整體樣式屬性進行設置。例如7、8兩行是設置X軸和Y軸的標題,9到12行設置初始X軸和Y軸所表示的範圍,22到24行用來設置放大縮小的控件和屬性(就像地圖控件裏的放大縮小按鈕)。這樣下層的X軸、Y軸等就都設置好了。【第二步:設置顯示層顯示樣式】

  publicvoidsetChartSettings(String xTitle, String yTitle,double xMin,double xMax,double yMin,double yMax,int axesColor,int labelsColor){

  mRenderer.setXTitle(xTitle);

  mRenderer.setYTitle(yTitle);

  mRenderer.setXAxisMin(xMin);

  mRenderer.setXAxisMax(xMax);

  mRenderer.setYAxisMin(yMin);

  mRenderer.setYAxisMax(yMax);

  mRenderer.setAxesColor(axesColor);

  mRenderer.setLabelsColor(labelsColor);

  mRenderer.setShowGrid(true);

  mRenderer.setGridColor(Color.GRAY);

  mRenderer.setXLabels(16);

  mRenderer.setYLabels(20);

  mRenderer.setYLabelsAlign(Align.RIGHT);

  mRenderer.setPointSize((float)2);

  mRenderer.setShowLegend(true);

  mRenderer.setZoomEnabled(true,false);

  mRenderer.setPanEnabled(true,false);}

  當表格框架設置好之後,接下來就是向框架內填充折線,並且在此過程中把每一條折線的數據層放入總的數據層中。如下setLineSettings函數循環4次,每次首先實例化一個標題爲titles[i]的坐標序列,然後將該序列放入總的數據層mDataset中。同樣的每次實例化一個XYSeriesRenderer(因爲每個折線也有自己的樣式),並將其加入總的圖標層mRenderer中。這樣就能夠將4條分別表示X軸加速度、Y軸加速度、Z軸加速度和合加速度的折線圖設置好。【第三步:設置4個折線數據序列並加入數據層,設置4個折線層並加入顯示層】

  publicvoidsetLineSettings(){for(int i =0; i < titles.length; i++){

  mCurrentSeries[i]=newXYSeries(titles[i]);

  mDataset.addSeries(mCurrentSeries[i]);

  renderer[i]=newXYSeriesRenderer();

  mRenderer.addSeriesRenderer(renderer[i]);

  renderer[i].setPointStyle(styles[i]);

  renderer[i].setColor(colors[i]);

  renderer[i].setFillPoints(true);

  renderer[i].setDisplayChartValues(false);

  renderer[i].setDisplayChartValuesDistance(10);}}

  此时mDataset里存放着当前要显示的折线的所有XYSeries,每个折线XY序列存放在mCurrentSeries[i]中,如果想在该折线图上增加一个数据只要调用mCurrentSeries[i].add(x, y)即可;如果想显示或隐藏某个折线图只要调用图表层的mRenderer和数据层mDataset移出对应的折线和折线序列即可。【提前一步(5):如何往对应的折线中增加数据,以及如何显示隐藏某条折线】

  publicvoidshowLine(int i){

  mDataset.addSeries(mCurrentSeries[i]);

  mRenderer.addSeriesRenderer(renderer[i]);}publicvoidhideLine(int i){

  mDataset.removeSeries(mCurrentSeries[i]);

  mRenderer.removeSeriesRenderer(renderer[i]);}publicvoidaddData(int i,double x,double y){

  mCurrentSeries[i].add(x, y);}

  上面說過所有圖表都可以建立爲一個view,也可以建立爲一個用于啓動activity的intent。這裏由于我們需要在中添加其他控件,所以采用view的方式新建圖表。如下setChartViewSetting函數負責當圖表沒有建立時分別實例化layout和mChartView,並將新建的mChartView加入中圖表所在的layout中,這樣我們就可以看到基本的圖表了。此外,第10行是給圖表加的點擊監聽,用于顯示點擊點的詳細信息(圖11_1軟件最終效果的第6張圖)。【第四步:將數據層和顯示層合成爲圖表加入UI中】

  publicvoidsetChartViewSetting(final Activity activity){if(mChartView == null){

  LinearLayout layout =(LinearLayout) activity

  .findViewById(R.id.chart);

  mChartView = ChartFactory.getLineChartView(activity, mDataset,

  mRenderer);

  mRenderer.setClickEnabled(true);

  mRenderer.setSelectableBuffer(10);

  mChartView.setOnClickListener(newView.OnClickListener(){publicvoidonClick(View v){

  ……(略)}});

  layout.addView(mChartView,newLayoutParams(

  LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));}else{

  mChartView.repaint();}}
 


  14 整体逻辑梳理

  其實仔細觀察的讀者會發現:本次的和上次的大致相同。前一階段都是點擊按鈕來連接遠程藍牙設備。而不同之處在于上一章是通過加減按鈕向小風扇發送速度控制命令來控制速度,這一章是不斷讀取手環的實時數據並用折線圖繪制出來。整體業務邏輯還是在控件的點擊事件和handleMessage之間有序進行,下面將著重說明數據的實時顯示及一些用于優化操作的細節。

  在onCreate中首先實例化藍牙三劍客,接著實例化數據池和折線圖表,然後調用折線圖類的成員函數對折線圖做前期設置,最後啓動ChartThread線程。

  mBlueToothSearch =newBlueToothSearch(this, myHandler);

  mBlueToothConnect =newBlueToothConnect(myHandler);

  mBlueToothCommunicate =newBlueToothCommunicate(myHandler);

  mDataPool =newDataPool(20000);

  mChartLine =newChartLine();

  mChartLine.setChartSettings("Time","",0,100,-20000,20000, Color.WHITE, Color.WHITE);

  mChartLine.setLineSettings();

  ChartThread.start();

  在此之後便是對連接手環按鈕做的相關設置,這裏和上一章中的連接風扇幾乎一樣,關鍵在于理解藍牙三劍客通過線程啓動並通過handler將消息反饋的機制。

点击连接设备到通信建立

  這樣當點擊連接手環的按鈕之後,然後在handler的溝通下上位機和下位機最終實現可通信。此時下位機一旦有數據傳送上來,上位機便快速的將其放入數據池內。那麽程序是在成麽時候取數據並更新UI的呢?秘密就在于()!

  private Thread ChartThread =newThread(){publicvoidrun(){while(true){try{sleep(100);

  Message msg =newMessage();

  msg.what =0x04;

  myHandler.sendMessage(msg);}catch(InterruptedException e){}}}};

  从上面的可以看出ChartThread主要负责周期性发送类别为0x04的消息,而在handleMessage的case 0x04中则是负责获取实时数据并更新UI的。之所以这样绕个弯是因为UI更新一旦放在ChartThread中就会导致程序运行异常。这里的数据获取和更新也比较容易理解:首先调用数据池的ask函数从p_write向后找40个数据寻找并解析有效帧,如果成功则最新的XYZ三轴的加速度已经保存在mDataPool的公有成员XYZ中。下面第3行是计算合加速度(减去16000是为了方便显示),接着6到9行负责分别将三轴加速度及其合速度值加入折线图。第10到13行便是我们简单的记步算法了,即当合加速值超过设定的记步阈值时记步数加一。第15、16行是控制折线图滚动到最新的位置并刷新ChartView。

  case0x04:if(mDataPool.ask()==true){int all =(int) Math.sqrt(mDataPool.X * mDataPool.X

  + mDataPool.Y * mDataPool.Y + mDataPool.Z

  * mDataPool.Z)-16000;

  mChartLine.addData(0, mTime, mDataPool.X);

  mChartLine.addData(1, mTime, mDataPool.Y);

  mChartLine.addData(2, mTime, mDataPool.Z);

  mChartLine.addData(3, mTime, all);if(all > mUpperLimit){

  mNum++;

  mTextView2.setText("当前记步数为: "+ mNum);}

  mTime +=1;

  mChartLine.letChartMove(mTime);

  mChartLine.mChartView.repaint();}break;

  綜上,當建立藍牙通信後,整個應用程序中主要有三個線程:

  ①用于不斷讀取串口數據並將其存入數據池的數據線程;

  ②用于周期性發送0x04消息的信號線程;

  ③隱蔽而重要的主線程(UI更新等操作)。

  如图14_2所示:一方面数据线程不断读取数据存入数据池,另一方面信号线程周期性发送0x04消息触发handleMessage的case 0x04执行ask读数据函数,当成功解析到有效数据时会在主线程中记步并更新UI。
三线程运作

  此外,還有一些其他的控件用于提高交互性,如表14_1所示:開始/停止按鈕用于控制折線圖是否動態滾動,當停止折線圖動態滾動時折線圖的數據增加並未被中止,此時可以方便用戶拖動折線圖查看曆史或觀察細節。四個CheckBox用于控制顯示哪一個折線圖,這樣便于單獨分析。滾動條是用來動態設置記步阈值的,這樣便于大家深入理解我們的簡單的記步算法。
 


原文出處:beautifulzzzz
侵刪

 

上一篇:下一篇: