2014年8月23日 星期六

{ Client 篇 } Bluetooth USB Dongle 初體驗 - Linux 篇 ( BlueZ , Serial Port Profile)

Bluetooth USB Dongle ( USB 藍牙卡 ) 是藍牙入門套件其中的一個配件,可至露天賣場訂購:

經過前面幾篇藍牙配件 ( USB 轉串列介面線、HC-05 主從一體藍牙模組和 USB 藍牙卡 ) 的使用說明網頁,相信各位對於藍牙入門套件中這幾個配件的安裝以及操作上現在應該都不成問題了。

在上一篇 "Bluetooth USB Dongle 初體驗 - Windows 篇 ( 7:免驅;8:BlueSoleil )" 網頁中,重點都著重在 Windows 作業系統中完成 Bluetooth USB Dongle ( USB 藍牙卡,文中簡稱 BTdongle ) ) 虛擬 COM 連接埠的建立以及連線上;而在接下來的兩篇網頁中,切換作業系統到 Linux 環境下,使用 BlueZ 的 Bluetooth Stack 完成 BTdongle 作為 SPP Server 以及 SPP Client 的建立,讓前面介紹的兩個手機程式 ( 藍牙串口助手以及 BTSCmode )可以連接並互傳文字訊息。
本篇,使用 BTSCmode 作為藍牙 SPP Server,介紹在樹莓派環境下使用 BlueZ 函式庫撰寫 BTdongle 的藍牙 SPP Client 端程式的方式,以循序漸進的方式,先取得手機藍牙 SPP Server 所開放的 RFCOMM 通訊埠號碼,再使用這通訊號碼連接上手機。建立連線之後,在樹莓派中就可以使用類似網路 Socket 的方式互傳文字訊息,步驟如下:
  • 安裝 BlueZ 開發工具套件
  • 創建一個 rfcomm socket
  • 取得指定的遠端藍牙裝置 RFCOMM 通訊埠號碼
  • 連接 SPP Server
  • 開始接收與傳送文字訊息

網頁中所使用到的程式碼可以使用下面三種方法取得:
  • 賣場藍牙入門套件的雲端硬碟 ( 購買套件後會提供該連結 )
  • Dropbox ( bt_spp_client.tar.gz )
  • 在樹莓派輸入指令下載
     wget -O - goo.gl/BV3Wlb | tar zxvf -
pi@raspberrypi ~/codes $
pi@raspberrypi ~/codes $ mkdir Bluetooth
pi@raspberrypi ~/codes $ cd Bluetooth/
pi@raspberrypi ~/codes/Bluetooth $ mkdir spp_client
pi@raspberrypi ~/codes/Bluetooth $ cd spp_client/
pi@raspberrypi ~/codes/Bluetooth/spp_client $ wget -O - goo.gl/BV3Wlb | tar zxvf -
...<過程省略>...
pi@raspberrypi ~/codes/Bluetooth/spp_client $
pi@raspberrypi ~/codes/Bluetooth/spp_client $ 
total 48
-rwxr-xr-x 1 pi pi 7455 Aug 13 10:24 rfcomm-port-search
-rw-r--r-- 1 pi pi 2728 Aug 13 10:24 rfcomm-port-search.c
-rwxr-xr-x 1 pi pi 8826 Aug 13 10:24 spp-client
-rw-r--r-- 1 pi pi 6007 Aug 13 23:48 spp-client.c
-rwxr-xr-x 1 pi pi 8057 Aug 13 10:24 spp-client-test01
-rw-r--r-- 1 pi pi 4982 Aug 13 10:24 spp-client-test01.c
pi@raspberrypi ~/codes/Bluetooth/spp_client $


下載解開壓縮之後會有六個檔案:三個是原始碼,另外三個是已經編譯完成的執行檔。

  • rfcomm-port-search:回傳指定的藍牙 SPP Server 的通訊埠號碼
  •      spp-client-test01:連接指定的藍牙 SPP Server,連接成功後傳送 10 次 hello! 字串
  •                 spp-client:完整的 SPP Client 原始碼 ( 上面兩個程式的整合 )

這些檔案執行與編譯需要下面的 BlueZ 開發工具套件,請先安裝之後再編譯。編譯的指令格式,可在每個檔案的最上方找到。


安裝 BlueZ 開發工具套件:

我記得在 "樹莓派中 USB 藍牙卡的驅動與設置" 已經有安裝過一些藍牙相關的軟體套件,但是要撰寫藍牙程式,這些是不夠的 !!! 還需要藍牙開發工具套件 。

輸入下面指令進行藍牙工具套件安裝 ( 可先執行 sudo apt-get update 指令在繼續下面指令 )
pi@raspberrypi ~ $ sudo apt-get install libbluetooth-dev
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following NEW packages will be installed:
  libbluetooth-dev
0 upgraded, 1 newly installed, 0 to remove and 1 not upgraded.
Need to get 118 kB of archives.
After this operation, 350 kB of additional disk space will be used.
Get:1 http://mirrordirector.raspbian.org/raspbian/ wheezy/main libbluetooth-dev armhf 4.99-2 [118 kB]
Fetched 118 kB in 2s (52.2 kB/s)
Selecting previously unselected package libbluetooth-dev.
(Reading database ... 79266 files and directories currently installed.)
Unpacking libbluetooth-dev (from .../libbluetooth-dev_4.99-2_armhf.deb) ...
Setting up libbluetooth-dev (4.99-2) ...
pi@raspberrypi ~ $


安裝好之後,在 /usr/include/bluetooth 目錄下就會出現這些可使用在藍牙程式上的標頭檔,也就完成藍牙工具套件的安裝
pi@raspberrypi ~ $ ls -l /usr/include/bluetooth/
total 180
-rw-r--r-- 1 root root  3569 May  6  2012 a2mp.h
-rw-r--r-- 1 root root  7457 May  6  2012 bluetooth.h
-rw-r--r-- 1 root root  3743 May  6  2012 bnep.h
-rw-r--r-- 1 root root  1600 May  6  2012 cmtp.h
-rw-r--r-- 1 root root 61945 May  6  2012 hci.h
-rw-r--r-- 1 root root  9917 May  6  2012 hci_lib.h
-rw-r--r-- 1 root root  2087 May  6  2012 hidp.h
-rw-r--r-- 1 root root  6622 May  6  2012 l2cap.h
-rw-r--r-- 1 root root 12653 May  6  2012 mgmt.h
-rw-r--r-- 1 root root  2309 May  6  2012 rfcomm.h
-rw-r--r-- 1 root root  1541 May  6  2012 sco.h
-rw-r--r-- 1 root root 17192 May  6  2012 sdp.h
-rw-r--r-- 1 root root 21653 May  6  2012 sdp_lib.h
-rw-r--r-- 1 root root  1766 May  6  2012 uuid.h
pi@raspberrypi ~ $


如果想要了解更多關於藍牙工具套件的話,建議到 BlueZ 的下載網頁去下載這些藍牙程式與函式庫原始碼。撰寫時遇到困難或不了解的部分,可以參考像是 bluez-utils 裡關於 hcitool、hciconfig、sdptool...等藍牙工具程式的原始碼,相信會很有幫助的。


前置步驟完成之後,就可以開始寫程式了 !

Note: 因為不能直接使用手機以及 HC-05 實際的藍牙位址,因此本網頁使用了下面假設的位址址,( 即便如此,所有的輸出資料都是使用實機測試得到的 ):
  •      Xperia P 手機:12:34:56:78:9A:BC
  •  HC-05 藍牙模組:01:22:03:04:55:06


spp-client 程式說明:

BTdongle 作為 SPP Client 端,連線到遠端的藍牙 SPP Server 端,程式碼的流程可分為下面四個步驟。除了取得 RFCOMM 通訊埠號碼的程式另外拉出來做為一個副程式,其餘的步驟都只是幾行程式而已,這些步驟有:
  • 創建一個 rfcomm socket
  • 取得指定的遠端藍牙裝置 RFCOMM 通訊埠號碼
  • 連接 SPP Server
  • 開始接收與傳送文字訊息

** main(), line 97-171

首先,最重要的參數設定就是:設定要作為 SPP Server 的遠端藍牙裝置的位址,在 line103 處指定給 dest。在這裡指定的是 Xperia P 手機的藍牙位址,看需要再自行修改。
spp-client.c, main(), line 97 - 104
 97 
 98 int main(int argc, char **argv)
 99 {
100     struct sockaddr_rc addr = { 0 };
101     int status, len, rfcommsock;
102     char rfcommbuffer[255];
103     char dest[18] = "12:34:56:78:9A:BC"; // 藍牙 SPP Server 的位址
104      

然後就是程式主要的部份:

line 106:配置或是產生一個藍牙 RFCOMM socket,所以第一次參數就必須設定為 AF_BLUETOTH;第二個參數是傳輸協議 ( Transport Protocol ) 的設定,這裡要設定為 SOCK_STREAM,也就是 Streams-based ( 就像是 TCP );第三個參數是 BTPROTO_RFCOMM,特殊要求一個 RFCOMM 的 socket。對於配置或是產生產生一個藍牙 RFCOMM socket,就是設定這三個參數值,不需要做變更 ! 這裡只是說明一下。

line 108 - 113:配置與指定遠端藍牙 SPP Server 連接的 sockaddr 資料結構數據。
sa_family  :指定 scoket 的家族。直接設定為 AF_BLUETOOTH 就可以了。
rc_channel:指定通訊埠號碼 ( 必須呼叫 get_rfcomm_port_number() 連接到 SDP Server 去詢問要使用哪一個通訊埠做連接;但若是使用 HC-05 作為 Server 端 ( Slave 模式,AT+ROLE=0 ),直接設為 1 就可以了,也不需要另外呼叫 get_rfcomm_port_number() 取得通訊埠號碼 )
 struct socketaddr_rc {
  sa_family_t rc_family;
  bdaddr_t    rc_bdaddr;
  uint8_t     rc_channel;
 };

rc_bdaddr:指定遠端藍牙 SPP Server 的位址。這個位址不能直接使用 dest 所設定的值,必須使用函式 str2ba( const char *str, bdaddr_t *ba ) 轉換藍牙位址字元陣列為 bdaddr_t 資料型態才行。

line 116:因為在 line 112 取得 RFCOMM 通訊埠號碼時已先連接上遠端 SDP Server,因此在與遠端藍牙 SPP Server 連接時,必須先等待一段時間之後再連接上,不然幾乎都是連不上的;若是直接指定通訊埠號碼,則 line 116 可以拿掉不用。

line 117:將 line 106 所取得的 rfcomm_sock、line 109 設定的 rc_family、line 112 所取得的 rc_channel 和 line 113 轉換之後的藍牙位址 rc_bdaddr,集中餵給 connect 函式開始連接藍牙 SPP Server。值得注意的是,第二個參數必須強制型態轉換 rc_bdaddr socketaddr 資料結構,不然編譯時會產生錯誤。

int connect( int sock, const struct sockaddr *server_info, socklen_t infolen );

成功連接的話,就會回傳 0;失敗的話,回傳 -1。
spp-client.c, main(), line 105 - 119
105     // allocate a socket
106     rfcommsock = socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM);
107 
108     // set the connection parameters (who to connect to)
109     addr.rc_family = AF_BLUETOOTH;
110 
111     // 先找到 SPP Server 可被連接的 Port Number ( or channel number )
112     addr.rc_channel = get_rfcomm_port_number(dest);
113     str2ba( dest, &addr.rc_bdaddr );
114 
115     //  等幾秒鐘之後再連接到 SPP Server
116     sleep(5);
117     // 連接 SPP Server
118     status = connect(rfcommsock, (struct sockaddr *)&addr, sizeof(addr));
119 

當 connect() 函式成功連接上 ( 回傳 0 ) 之後,line 125 傳送 hello! 字串到遠端的藍牙 SPP Server 端表示連接成功,可以開始互傳文字訊息了。

line 125:傳送文字訊息到遠端。send 函式可以跟 write 互換使用,前面的參數都一樣。但是 send ( recv 也一樣 ) 多了最後一項 flags,正常使用之下,flags 設定為 0,效果就跟使用 write 是相同的,可以直接改寫試試看 !

ssize_t send( int sock, const void *buf, size_t len, int flags );
成功的話,回傳傳送的數據大小;失敗的話,回傳 -1;回傳 0,表示通訊斷線,或是沒有資料可再傳送或是接收。

line 133 - 163這是個處理傳送與接收文字訊息的無窮迴圈,也是整個程式需要使用者根據自己的需求做修改的部分。這段程式裡沒有很複雜的指令,只要設定好傳送 ( send()write() ) 與接收 ( recv()read() ) 在什麼條件下被啟動,例如:接收到溫度低於 10 度 C 的資料時,傳送開啟加熱器的命令給遠端的藍牙裝置;或是接收到光照度小於 200 Lux 時,傳送開啟燈光的命令...等等,就可以很容易的完成藍牙裝置控制程式。

line 164 -167:connect() 函式沒有連接成功就會輸出相對應的錯誤字串。

line 169:關閉開啟的藍牙 RFCOMM socket。
spp-client.c, main(), line 120 - 171
120 //--------------------------------------------------------------------------
121     // send/receive messages
122     if( status == 0 ) {
123         
124         // say hello to client side
125         status = send(rfcommsock, "hello!", 6, 0);
126         if( status < 0 )
127         {
128             perror( "rfcomm send " );
129             close(rfcommsock);
130             return -1;
131         }
132         
133         while(1)
134         {
135            // 從 RFCOMM socket 讀取資料
136            // this socket has blocking turned off so it will never block,
137            // even if no data is available
138            len = recv(rfcommsock, rfcommbuffer, 255, 0);
139     
140            // EWOULDBLOCK indicates the socket would block if we had a
141            // blocking socket.  we'll safely continue if we receive that
142            // error.  treat all other errors as fatal
143            if (len < 0 && errno != EWOULDBLOCK)
144            {
145                perror("rfcomm recv ");
146                break;
147            }
148            else if (len > 0)
149            {
150                // received a message; print it to the screen and
151                // return ATOK to the remote device
152                rfcommbuffer[len] = '\0';
153     
154                printf("rfcomm received: %s\n", rfcommbuffer);
155                status = send(rfcommsock, "ATOK\r\n", 6, 0);
156                if( status < 0 )
157                {
158                     perror("rfcomm send ");
159                     break;
160                }
161            }
162         }
163     }
164     else if( status < 0 ) 
165     {
166         perror("uh oh");
167     }
168 
169     close(rfcommsock);
170     return 0;
171 }


**  uint8_t get_rfcomm_port_number( const char bta[] ), line 43 - 96

get_rfcomm_port_number() 函式會回傳所指定的遠端藍牙裝置 RFCOMM 通訊埠號碼。

int sdp_connect( const bdaddr_t *src,
                 const bdaddr_t *dsc, uint32_t flags );

使用 line 56sdp_connect( BDADDR_ANY, &target, 0 ) 函式,連接位址是 target 的藍牙 SDP Server。由於 src 參數設定是 BDADDR_ANY,表示不管樹莓派上面插了多少顆 BTdongle,由程式自己選擇其中一顆來做為連線到 SDP Server 的藍牙裝置,所以若是不指定的話,就使用現在設定的這個參數,不然就直接指定當地藍牙裝置位址也可以;另外還有 BDADDR_LOCALBDARRD_ALL 兩個全局常數可以使用。由於我們不去控制與 SDP Server 連線的事情,所以設定為 0 就可以了;想知道可以設定的參數有哪些,可以查看 /usr/include/bluetooth/sdp_lib.h 並尋找常數 SDP_RETRY_IF_BUSY

spp-client.c, main(), line 55 - 57
55     // 連接運行在遠端機器的 SDP Server
56     session = sdp_connect( BDADDR_ANY, &target, 0 );
57    

連接上 SDP Server 之後,我們必須提供兩個查詢的資料串列:第一個,要查詢的 UUID;第二個,要查詢的服務紀錄 ( service records ) 中的屬性-值對 ( attribute/value pairs ) 串列。

UUID 可以是自己定義的 ( 可以使用 uuidgen 指令產生 ),或是使用 Bluetooth 規範內建保留使用的 UUID ( 上 Service Discovery 或 "服務發現" 查詢所保留的協議 );因為要查詢 RFCOMM 通訊序列埠的號碼,所以使用保留的協議 RFCOMM ( 0x0003 )。依照 /usr/include/bluetooth/sdp.h 裡面所定義的保留 UUID 的常數定義,選用 RFCOMM_UUID 作為函式 line 58 sdp_uuid16_create() 第二個參數,然後傳回 128-bit 的 UUID 在 uuid_t 資料結構在第一個參數,供下面其他函式使用。若是要同時搜尋多個 UUID,就使用 sdp_list_append 繼續將其他 UUID 串列加上即可。

由於我們想要取得所有 SDP Server 裡跟 RFCOMM_UUID 相關的服務紀錄中的屬性-值對串列,所以在 line 50 宣告了一個 32-bit 的帶正負號的整數值 0x0000ffff,並且將這數值在 line 60 轉換為 sdp_list_t 的資料串列。
spp-client.c, get_rfcomm_port_number(), line 
 50     uint32_t range = 0x0000ffff;

查詢的結果都會由 SDP Server 回傳到 Client 端的 response_list 串列中,所以一開始在 line 63 先將其設為 NULL。最後將 line 58 - 63  轉換之後的參數全部填入到 line 64 函式裡。

int sdp_service_serarch_attr_req( sdp_session_t *session, 
      const sdp_list_t *uuid_list, sdp_attrreq_type_t reqtype, 
      const sdp_list_t attrid_list, sdp_list_t **response_list );

第三個是 sdp_attrreq_type_t 的列舉型別,參數說明可至 { bluez-#.## }/ lib / sdp.c 中尋找,在這裡直接設為 SDP_ATTR_REQ_RANGE,表示查詢的範圍由 0x0000 - 0xffff
 typedef enum {
  SDP_ATTR_REQ_INDIVIDUAL = 1,
  SDP_ATTR_REQ_RANGE
 } sdp_attrreq_type_t;

spp-client.c, get_rfcomm_port_number(), line 57 - 66 
 57 
 58     sdp_uuid16_create( &svc_uuid, RFCOMM_UUID );
 59     search_list = sdp_list_append( 0, &svc_uuid );
 60     attrid_list = sdp_list_append( 0, &range );
 61 
 62     // 取得擁有 RFCOMM_UUID 服務紀錄 (service records) 的裝置清單
 63     response_list = NULL;
 64     status = sdp_service_search_attr_req( session, search_list,
 65             SDP_ATTR_REQ_RANGE, attrid_list, &response_list);
 66 

sdp_service_search _attr_req() 函式一但執行成功,就會返回 0 進入到 if 條件判斷式的內部,在內部會對所有的屬性-值對進行確認 ( line 72 - 82 ) 直到符合,並且返回通訊埠號碼。
spp-client.c, get_rfcomm_port_number(), line 67 - 85  
 67     if( status == 0 ) {
 68         sdp_list_t *proto_list = NULL;
 69         sdp_list_t *r = response_list;
 70         
 71         // 遍歷每一個裝置的服務紀錄,並取得所有屬性資料
 72         for (; r; r = r->next ) {
 73             sdp_record_t *rec = (sdp_record_t*) r->data;
 74             
 75             // get a list of the protocol sequences
 76             if( sdp_get_access_protos( rec, &proto_list ) == 0 ) {
 77 
 78                 // get the RFCOMM port number !!!!
 79                 port = sdp_get_proto_port( proto_list, RFCOMM_UUID );
 80 
 81                 sdp_list_free( proto_list, 0 );
 82             }
 83             sdp_record_free( rec );
 84         }
 85     }

返回的通訊埠號碼不可以為 0 ,若符合條件就輸出取得的數據。並且在程式結束之前,回傳通訊埠號碼。
spp-client.c, get_rfcomm_port_number(), line 91 - 93
 91     if( port != 0 ) {
 92         printf("found service running on RFCOMM port %d\n", port);
 93     }

完整的 get_rfcomm_port_number() 如下所示:
spp-client.c, get_rfcomm_port_number(), line 
 41 //-- 搜尋遠端 SPP Server 所使用的 RFCOMM Port Number
 42 // 回傳 RFCOMM Port Number
 43 uint8_t get_rfcomm_port_number( const char bta[] )
 44 {
 45     int status;
 46     bdaddr_t target;
 47     uuid_t svc_uuid;
 48     sdp_list_t *response_list, *search_list, *attrid_list;
 49     sdp_session_t *session = 0;
 50     uint32_t range = 0x0000ffff;
 51     uint8_t port = 0;
 52 
 53     str2ba( bta, &target );
 54 
 55     // 連接運行在遠端機器的 SDP Server
 56     session = sdp_connect( BDADDR_ANY, &target, 0 );
 57 
 58     sdp_uuid16_create( &svc_uuid, RFCOMM_UUID );
 59     search_list = sdp_list_append( 0, &svc_uuid );
 60     attrid_list = sdp_list_append( 0, &range );
 61 
 62     // 取得擁有 RFCOMM_UUID 服務紀錄 (service records) 的裝置清單
 63     response_list = NULL;
 64     status = sdp_service_search_attr_req( session, search_list,
 65             SDP_ATTR_REQ_RANGE, attrid_list, &response_list);
 66 
 67     if( status == 0 ) {
 68         sdp_list_t *proto_list = NULL;
 69         sdp_list_t *r = response_list;
 70         
 71         // 遍歷每一個裝置的服務紀錄,並取得所有屬性資料
 72         for (; r; r = r->next ) {
 73             sdp_record_t *rec = (sdp_record_t*) r->data;
 74             
 75             // get a list of the protocol sequences
 76             if( sdp_get_access_protos( rec, &proto_list ) == 0 ) {
 77 
 78                 // get the RFCOMM port number !!!!
 79                 port = sdp_get_proto_port( proto_list, RFCOMM_UUID );
 80 
 81                 sdp_list_free( proto_list, 0 );
 82             }
 83             sdp_record_free( rec );
 84         }
 85     }
 86     sdp_list_free( response_list, 0 );
 87     sdp_list_free( search_list, 0 );
 88     sdp_list_free( attrid_list, 0 );
 89     sdp_close( session );
 90 
 91     if( port != 0 ) {
 92         printf("found service running on RFCOMM port %d\n", port);
 93     }
 94     
 95     return port;
 96 }
 97 


上面就是 spp-client.c 程式碼的簡單說明。接下來我們將會實際使用這個程式,連接到做為 SPP-Server 的手機程式 ( 藍牙串口助手和 BTSCmode ) 與 HC-05。

由於 HC-05 經過測試,不一定需要與 SDP Server 連線,所以不用 get_rfcomm_port_number() 函式來取得通訊埠號碼,只要直接指定通訊埠號碼即可,這部分在連線到 HC-05 時會再做說明。


SPP-Client 連接手機 APP ( 藍牙串口助手、BTSCmode ):

手機與樹莓派做藍牙通訊需要輸入配對碼 ( 如果之前沒有配對過的話 ),請先參考網頁最下方 "遠端桌面 - VNC" 網頁安裝 VNC Server,以及下載 VNC Viewer 軟體。

使用 SSH 連線至樹莓派配,並且輸入下面指令開啟 VNC Server  ( 指令後方式螢幕解析度設定,請自行變更;另外前面不需要加上 sudo )

vncserver :1 -geometry 16x900

完成指令輸入之後,使用 VNC Viewer 軟體連線到樹莓派桌面,並且開啟 Bluetooth Manager
開啟 Bluetooth Management


** 可惡的藍牙配對

使用 spp-client 與 BTSCmode @ Server Mode 連線,是在兩者都沒有配對的情況之下連線成功並互傳資料。但有時候搞了很久,即使兩邊都做了配對,spp-client 還是不能正常的連上 BTSCmode @ Server Mode。

為了在測試這個程式時,能夠順利 ! 請先將手機端以及樹莓派中藍牙相互配對的裝置移除或是解除配對,使兩者都在互相不認識的狀態之下再執行各自的程式 ( 沒有限制一定不能配對,這只是為了配合現在測試的狀態 )。

經過一系列的步驟測試,我是在兩者都沒有配對的情況之下,BTSCmode @ Server Mode 再執行 sudo ./spp-client 程式,沒有出現任何錯誤的情況之下,等待差不多 10 秒鐘左右,手機端就會收到 hello! 文字訊息。

手機端 BTSCmode 每次按下 "AS Server" ,再執行 sudo ./spp-client 所取得的 RFCOMM 通訊埠號碼應該都會不一樣。通訊埠號碼的範圍在 1 - 30,若同時兩次執行取得相同的數值,那有可能通訊不被之前的連結占用住了。若重複執行 sudo ./spp-client 還是一樣,若有配對的話就直接移除配對裝置在重新下指令就可以了。

另外就是在執行 sudo ./spp-client 時出現 uh oh: #$%^&*( 的錯誤訊息。有時手機重新開啟一次 SPP Server 再重新執行 sudo ./spp-client 指令就正常了。但若是不正常呢 ? 試試使用 BTSCmode "Close Bluetooth" 再 "Open Bluetooth" ,重新 "AS Server" 進入 SPP Server 畫面,再下一次 sudo ./spp-client 指令就可以成功連線。

正常建立連線成功是不會出現要輸入配對密碼的視窗的,雙方也不需要事先配對 !!!!!!!!!

測試藍牙裝置有時很快,有時連都連不上,所以測試時請多點耐心,一定可以成功的 !!!



開啟手機藍牙,再開啟 BTSCmode 手機程式。於手機畫面中點選 "AS Server" 按鈕,讓手機變成藍牙 SPP Server 端,等待樹莓派 Client 端的連線。

在 SSH 連線的視窗或是在樹莓派桌面開啟 LXTerminal,輸入下面指令 ( 假設下載的藍牙程式檔案是放在 /home/pi/codes/bluetooth/spp_client )

sudo ./spp-client

命令執行之後,會出現幾種情況:SPP Server 沒有開啟;重複連線到 SPP Server;沒有正常回應...等,都會出現相對應的錯誤訊息。
spp-client 執行時遇到的情況

BTSCmode 並非用於可同時接收多個 SPP Client 端的連接,所以一但樹莓派出現連線不成功的情況之下,手機端必須先跳回主畫面再重新按下 "AS Server" 的按鈕,讓手機端再次等待 Client 端的連線。因此只要出現不成功的情況,手機端必須跳回主畫面再進入 Server 等待的畫面,然後再次輸入指令 ( sudo ./spp-client ) 重新連線到 SPP Server 才會成功。

另外一種情況是,剛剛開啟手機藍牙且馬上下指令要樹莓派連上,這時也常會發生失敗的情況。所以建議下指令前就先開啟手機端的藍牙一段時間再連線,就會一切正常了 !!!

如畫面最下面一個指令。下達之後,Server 端會傳回 Client 端查詢的 RFCOMM 通訊埠號碼,然後開始與遠端 ( 也就是手機端 ) 進行連線。連線若是成功,差不多在 10 秒鐘左右,就會在手機端收到 hello! 的文字訊息,這時就表示可以開始由手機端傳送訊息給樹莓派。
樹莓派成功連線到 BTSCmode


這時在手機端輸入文字並按下 "Send" 按鈕,就會將文字訊息傳送到樹莓派,並且收到樹莓派傳回 ATOK 的文字訊息,表示正常接收。完整的 spp-client 與 BTSCmode 之間成功的通訊如下所示。
樹莓派 ( SPP Client ) 連線到手機 ( SPP Server )

BTSCmode 在傳送與接收期間,只要畫面跳出,就必須重新從樹莓派端重新下達指令再次連接,之前的通訊就會斷線,使用時請特別注意有這種情況 !!!


SPP-Client 連接 HC-05:

與 HC-05 的連線就顯得比較簡單,因為 HC-05 的 RFCOMM 所使用的通訊埠號碼是固定的。

修改 spp-client.c 程式碼 line 103,指定 HC-05 主從一體藍牙裝置的位址
spp-client.c, main(), line 97 - 104
 97 
 98 int main(int argc, char **argv)
 99 {
100     struct sockaddr_rc addr = { 0 };
101     int status, len, rfcommsock;
102     char rfcommbuffer[255];
103     char dest[18] = "01:22:03:04:55:06"; // HC-05 的藍牙位址
104      

直接下達 sudo ./spp-client 指令後會發現所搜尋到的 RFCOMM 通訊埠號碼都是 1,這不是錯誤而是 HC-05 把這個通道寫死了,因此不管再重複執行幾次都是一樣的結果 !

pi@raspberrypi ~/codes/Bluetooth/spp_client $ sudo ./spp-client
found service running on RFCOMM port 1
uh oh: Connection refused <-- 取消樹莓派中輸入配對密碼產生的錯誤
pi@raspberrypi ~/codes/Bluetooth/spp_client $ sudo ./spp-client
found service running on RFCOMM port 1
uh oh: Connection refused <-- 取消樹莓派中輸入配對密碼產生的錯誤
pi@raspberrypi ~/codes/Bluetooth/spp_client $


既然結果都是一樣的,spp-client 裡的副程式 get_rfcomm_port_number() 就完全不需要執行。所以若是只跟 HC-05 做連線,可以將不需要的程式碼取消掉,整個 spp-client 程式就可以簡化成下面的程式碼 ( 附件裡面沒有 )
spp-client-hc05.c
 1 #include <stdio.h>
 2 #include <errno.h>
 3 #include <stdlib.h>
 4 #include <unistd.h>
 5 #include <bluetooth/bluetooth.h>
 6 #include <bluetooth/sdp.h>
 7 #include <bluetooth/sdp_lib.h>
 8 #include <sys/socket.h>
 9 #include <bluetooth/rfcomm.h>
10 
11 int main(int argc, char **argv)
12 {
13     struct sockaddr_rc addr = { 0 };
14     int status, len, rfcommsock;
15     char rfcommbuffer[255];
16     char dest[18] = "01:22:03:04:55:06"; // HC-05 的藍芽位址
17 
18     // allocate a socket
19     rfcommsock = socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM);
20 
21     // set the connection parameters (who to connect to)
22     addr.rc_family = AF_BLUETOOTH;
23     addr.rc_channel = 1;
24     str2ba( dest, &addr.rc_bdaddr );
25 
26     // 連接 SPP Server
27     status = connect(rfcommsock, (struct sockaddr *)&addr, sizeof(addr));
28 
29 //--------------------------------------------------------------------------
30     // send/receive messages
31     if( status == 0 ) {
32         
33         // say hello to client side
34         status = send(rfcommsock, "hello!", 6, 0);
35         if( status < 0 )
36         {
37             perror( "rfcomm send " );
38             close(rfcommsock);
39             return -1;
40         }
41         
42         while(1)
43         {
44             // 從 RFCOMM socket 讀取資料
45             // this socket has blocking turned off so it will never block,
46             // even if no data is available
47             len = recv(rfcommsock, rfcommbuffer, 255, 0);
48     
49             // EWOULDBLOCK indicates the socket would block if we had a
50             // blocking socket.  we'll safely continue if we receive that
51             // error.  treat all other errors as fatal
52             if (len < 0 && errno != EWOULDBLOCK)
53             {
54                 perror("rfcomm recv ");
55                 break;
56             }
57             else if (len > 0)
58             {
59                 // received a message; print it to the screen and
60                 // return ATOK to the remote device
61                 rfcommbuffer[len] = '\0';
62     
63                 printf("rfcomm received: %s\n", rfcommbuffer);
64                 status = send(rfcommsock, "ATOK\r\n", 6, 0);
65                if( status < 0 )
66                 {
67                     perror("rfcomm send ");
68                     break;
69                 }
70             }
71         }
72     }
73     else if( status < 0 ) 
74     {
75         perror("uh oh");
76     }
77 
78     close(rfcommsock);
79     return 0;
80 }

修改 line 16 HC-05 的藍牙位址後,編譯程式成執行檔

sudo gcc spp-client-hc05.c -lbluetooth -o spp-client-hc05


** spp-client-hc05 連線 HC-05 主從一體藍牙模組

先將 HC-05 與 USB 轉串列介面線接好並插上電腦,開啟電腦的裝置管理員確認新增的 COM 通訊埠號碼是多少,再將這號碼輸入到 SSCOM 進行連線的預備動作。

在連線之前,請確認 HC-05 是在 slave 模式下 ( AT+ROLE=0;不懂 !!  看這篇網頁中的說明:"HC-05 主從一體藍牙模組初體驗 02 ( AT 指令說明與使用演示、主動角色 )" )、樹莓派桌面現在是開啟的情況下,輸入下面指令連線到 HC-05
pi@raspberrypi ~/codes/Bluetooth/spp_client $ sudo ./spp-client-hc05


輸入指令之後,切換到樹莓派桌面等待一下,就會出現要求輸入連線到 HC-05 的配對密碼


輸入 1234,按下確定

配對成功之後,就會出現加了鑰匙的 HC-05 圖示

同時,在 SSCOM 的文字欄中也會出現連線成功所接收到的 client 端訊息:hello!

一但在 SSCOM 收到樹莓派送過來的 hello! 字串,就可以開始從 SSCOM 傳送文字訊息過去樹莓派

但很不幸的,從 SSCOM 傳送過去的字串,不知怎麼的?硬是被拆成了好幾段 !!!!
pi@raspberrypi ~/codes/Bluetooth/spp_client $ sudo ./spp-client-hc05
rfcomm received: I
rfcomm received:  am SSCOM
rfcomm received:  @ Win8


雖然一開始並不完全清楚搞什麼小朋友 ! 但是靜下來找一些資料看之後就發現,這是 Stream-Based Socket 資料傳輸所會出現的問題。

由於藍牙的程式碼與網路 TCP 程式碼類似,上網找了之後發現還真是很多類似的問題出現。可惜專門說藍牙部分的很少,網路 TCP 的卻是不少,其中兩篇幫助我釐清這問題以及程式碼修改的參考網頁列在下面:

  • Reading data from a socket
    這網頁要先看。幾近詳細的說明造成 recv() 接收卻讓文字訊息分段的理由,以及提出三種解決方法的說明,而解決這問題所採用的方法就是網頁中的 solution 1。
  • c recv() read until newline occurs
    上面網頁 solution 1 的參考程式碼。

修改之後的程式,所選用的分段字元是 LF ( Line feed, 0Ah, '\n' ),而且可以一次輸入包含多個 LF 的字串。而且即使上一次輸入沒有輸入完成,只要在下次輸入有 LF 字元的字串,就會與上次沒完成的字串完整取出。為了展示這個功能,不使用 SSCOM,改用 AccessPort。

修改之後的程式,命名為 spp-client-fixed spp-client-hc05-fixed ( 執行時可不加 sudo )。連線成功之後,在 AccessPort 輸入下面的字串 ( \n 要使用 hex 畫面輸入 0A 在每一段文字的後面,再繼續輸入其他的字串 )

abcd\nabcd\nabcd\nabcd\n1233333\n123

最後一個字串只輸入 123 不加分段字元

命令視窗中只會出現 5 個解出的字串
pi@raspberrypi ~/codes/Bluetooth/spp_client $ ./spp-client-fixed
 rfcomm recvived: abcd
  rfcomm recvived: abcd
  rfcomm recvived: abcd
  rfcomm recvived: abcd
  rfcomm recvived: 1233333



接著清除掉 AccessPort 剛剛輸入的字串,改輸入下面的字串

abcd\n


就可以將第一次輸入的最後一個字串 123 與第二次輸入的字串 abcd\n 解析出來
pi@raspberrypi ~/codes/Bluetooth/spp_client $ ./spp-client-hc05-fixed
 rfcomm recvived: abcd
  rfcomm recvived: abcd
  rfcomm recvived: abcd
  rfcomm recvived: abcd
  rfcomm recvived: 1233333
  rfcomm recvived: 123abcd



修改之後的程式,一樣適用於網頁中的各項測試。


** spp-client-fixed spp-client-hc05-fixed 測試程式碼下載

請輸入下面指令直接下載測試程式碼

wget -O goo.gl/b62lYJ | tar zxvf -

pi@raspberrypi ~ $ cd codes/Bluetooth/
pi@raspberrypi ~/codes/Bluetooth $ 
pi@raspberrypi ~/codes/Bluetooth $ mkdir spp_client_fixed
pi@raspberrypi ~/codes/Bluetooth $ cd spp_client_fixed
pi@raspberrypi ~/codes/Bluetooth/spp_client_fixed $ wget -O goo.gl/b62lYJ | tar zxvf -
...<過程省略>....
pi@raspberrypi ~/codes/Bluetooth/spp_client_fixed $ ls -l
total 12
-rw-r--r-- 1 pi pi  112 Aug 23 21:50 btrecv.h
-rw-r--r-- 1 pi pi 2136 Aug 23 21:50 btrecv.o
-rw-r--r-- 1 pi pi 2289 Aug 23 21:50 spp-client-fixed.c
pi@raspberrypi ~/codes/Bluetooth/spp_client_fixed $

*********************************************************************************
Note: btrecv 只提供標頭檔以及 OBJ 檔,其中函式的說明如下:

btRecvHandle( const int btsocket, char *buffer )

處理藍牙所接收到的文字訊息。處理時以 \n 作為訊息分段的依據,所以可一次輸入一整串的資料並以 \n 做區隔送出,解析之後會將文字訊息分段列印到螢幕上。
  • btsocket:取得的藍牙 socket;spp-client-fixed.c, line 40
  • buffer    :放置經由藍牙所取得的文字訊息的緩衝區
*********************************************************************************

使用之前,先修改 line 36 遠端藍牙裝置的位址
30 
31 int main(int argc, char **argv)
32 {
33     struct sockaddr_rc addr = { 0 };
34     int status, len, rfcommsock;
35     char rfcommbuffer[255];
36     char dest[18] = "01:22:03:04:55:06"; // HC-05, slave, WORK!!

然後輸入下面指令進行編譯,就可以產生執行檔 ( 執行時並不一定要加上 sudo )

sudo gcc spp-client-fixed.c btrecv.o -lbluetooth -o spp-client-fixed


以上,就是 Bluetooth USB Dongle 在樹莓派環境下,使用 BlueZ software Stack 所撰寫的藍牙 SPP Client 的程式以及連線測試。


結論:

Client 端的藍牙程式相對於 Server 端簡單許多,如果就是作為藍牙串口來用,這個 Client 端的程式碼就是使用 BlueZ 撰寫藍牙 SPP Client 的樣板。只要設定好欲連接的藍牙裝置的位址,然後考慮主程式無窮迴圈中傳送與接收的程式碼,根據自己設定的條件來傳送或是接收像是:溫度、溼度、速度、照度...等資料或是控制數據,就可以直接來撰寫屬於自己客製的藍牙 Client 程式,即便你都不懂藍牙程式也可以寫出自己的資料傳送接收程式。

由於一開始撰寫程式以及做測試的時候,並沒有特別注意接收時所產生的文字分段問題,直到在測試輸入一整串數據時才發現。為了之後要使用藍牙來處理環境偵測數據的傳送,若是沒有處理好這文字分段的問題,將會影響到之後其他藍牙的實驗。還好,最後解決了這問題 !

下一篇就是關於藍牙 SPP Server 的實作與測試。基本程式的架構差不多,但是需要加入幾個特定處理某些像是 Class 宣告、Service Records 登錄到 SDP Server、RFCOMM 處理接入的連結...等,搞定了 SPP Server 基本架構之後,一但 Client 端連上線,溝通的方式就跟 Client 一模一樣了 !




<< 部落格藍牙相關網頁連結 >>

<<樹莓派編輯環境設置系列文章>>

16 則留言:

  1. 資料終於順利顯示出來了,謝謝版主!
    另外還有測試程式碼下載O後面好像少了一槓
    wget -O - goo.gl/b62lYJ | tar zxvf -
    多加後即可順利下載

    回覆刪除
  2. 您好想請問版主一些問題
    1.請問收到的訊息是存在樹莓派的什麼地方呢?
    2.如果我想要把收到的資料去做演算法或數學運算,該從哪裡下手呢?

    回覆刪除
    回覆
    1. spp-client-hc-05.c, line 63:rfcommbuffer 陣列中就是鳩收到的字串訊息。
      如果要做運算就需要從字串中取出所需要的訊息 ( 注意到訊息分段的問題 ),上網找一下切割字串的資料!

      刪除
  3. 您好版主想再請教一些問題非常感謝
    1.想請問為何spp-client-fixed的程式碼中沒有printf但卻有輸出呢?
    (想用atof的方法字串轉數值去做一些簡單的大於小於比較)
    2.如果需要再多加另外兩個藍芽hc05去接收資料的話想請問要修改呢?

    回覆刪除
    回覆
    1. 1. 那是因為藍牙接收到的文字資料,是由 btRecvHandle 處理分割以 \n 結尾為一個字串但這部分沒有開放原始碼。如果要做的話,必須要由 spp-client.c 去修改,只要處理 line 133 - line 163 之間的程式碼即可,rfcommbuffer 就是接收到的文字訊息。
      2. HC-05 應該不具有多對一的藍牙連接,若要多對一可能要用其他方式克服,這需要您自己找一下資料!

      刪除
    2. 不好意思版主還想請問您一點小細節非常感謝
      1.修改spp-client.c的line133-line163並編譯之後,是spp-client-fixed也會跟著變動嗎?
      還是說只能在spp-client.c做修改,但字串被切割的問題還是會在?
      2.不好意思我上面問題的說明不夠詳細,我是想問說如果我想讓我的BTdongle
      同時與3個hc-05做傳輸並接收資料,想請問程式該如何做修改呢?
      (已試過可以同時與兩個hc-05做連線配對)

      刪除
    3. 1. 網頁中的內容很長需要花一點時間去看!spp-client.c 使用手機 APP 與 BT-dongle 連線;spp-cleint-hc05.c 使用 BT-dongle 與 HC-05 連線;spp-client-fixed.c 是修正 spp-client-hc05.c 會截斷文字的問題。三個都是不一樣的程式,網頁有提供這些程式可以做測試,且也有解壓縮後的資料列表,可以去看一下。
      另外,關於字串被切割的問題,參考的網頁也有在網頁中,上去點連結去看一下 !
      *****
      Reading data from a socket
      這網頁要先看。幾近詳細的說明造成 recv() 接收卻讓文字訊息分段的理由,以及提出三種解決方法的說明,而解決這問題所採用的方法就是網頁中的 solution 1。
      c recv() read until newline occurs
      上面網頁 solution 1 的參考程式碼。
      *****
      2. 假設,BT-dongle 可以與多個藍牙裝置做連線開啟 RFOMM 通道,那就分別產生三個 rfcommsock 並對應到那三個要連接的 HC-05,只要建立連結成功之後,就可以傳送資料出去了 !
      我沒這部分的應用例,但如果要做,spp-client-hc05.c 去改,能夠一次傳送字串給三個 HC-05 之後,剩下的就看第一點的網頁連結,解決字串被切割的問題。

      刪除
    4. 去看一下 http://ruten-proteus.blogspot.com/2014/08/Bluetooth-Kit-tutorial-04-Linux-BlueZ-02-Server.html 最下面的回覆,雖然沒有程式碼,但是建立了 rfcomm 通道之後,九可以像是開當雨讀黨的方式撰寫傳送與接收的程式。

      刪除
    5. 首先再次感謝版主的回覆
      另外想請問可否再貼一次您說三種解決方法的網址,因為上面的網址好像錯誤了

      刪除
    6. 網頁中的連結沒貼好,之後再改!
      網址http://faq.cprogramming.com/cgi-bin/smartfaq.cgi?id=1044780608&answer=1108255660

      刪除
  4. 版主您好:有幾項問題想請問
    1.有沒有辦法直接從程式端發送訊息而不透過SSCOM3.2呢?
    2.有辦法用樹梅派當(client)同時連接好幾個arduino(HM-10)呢?
    謝謝

    回覆刪除
    回覆
    1. 1. SSCOM 就是一個 COM Port 通訊的程式,所以只要程式能夠與電腦的 COM Port 通訊就可以與藍牙模組溝通。
      2. 可以! 去找一下關於 RFCOMM 的資料。Linux 環境中有相關的工具程式可以使用,設定好 RFCOMM 之後,使用 Serial 通訊的就可以與藍牙互傳資料。

      刪除
  5. 板主您好:想再問個問題
    若要用樹莓派or筆電內建藍芽發送資料(一個訊號),同時讓多個藍芽裝置一起收到訊號,也是從RFCOMM裡面下手嗎?
    我試過用bluetoothctl,但只能一次連接一個發送指令。

    回覆刪除
    回覆
    1. RFCOMM 只能一次指定一個傳,這不是廣播! 參考一下下面網頁中的問與答,或許會有幫助
      http://ruten-proteus.blogspot.tw/2014/08/Bluetooth-Kit-tutorial-04-Linux-BlueZ-02-Server.html

      刪除
  6. 不好意思,想跟板主您情叫慣字串回傳分段,有點看不懂怎解決的

    回覆刪除
    回覆
    1. 使用的方法與說明都在上面說明的兩個網頁中。
      分段就是傳送的時候會將字串拆成好幾個小字串傳輸,可以使用下面兩個網頁中的方法去解決。
      --------------------------------------------
      "Reading data from a socket"
      這網頁要先看。幾近詳細的說明造成 recv() 接收卻讓文字訊息分段的理由,以及提出三種解決方法的說明,而解決這問題所採用的方法就是網頁中的 solution 1。
      "c recv() read until newline occurs"
      上面網頁 solution 1 的參考程式碼。
      ---------------------------------------------

      刪除