網頁最後修改時間:2014/08/23
在上一篇 "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 56 的 sdp_connect( BDADDR_ANY, &target, 0 ) 函式,連接位址是 target 的藍牙 SDP Server。由於 src 參數設定是 BDADDR_ANY,表示不管樹莓派上面插了多少顆 BTdongle,由程式自己選擇其中一顆來做為連線到 SDP Server 的藍牙裝置,所以若是不指定的話,就使用現在設定的這個參數,不然就直接指定當地藍牙裝置位址也可以;另外還有 BDADDR_LOCAL 和 BDARRD_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 端的連線。
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 |
樹莓派 ( 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 的配對密碼
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
abcd\nabcd\nabcd\nabcd\n1233333\n123
最後一個字串只輸入 123 不加分段字元
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
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
請輸入下面指令直接下載測試程式碼
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 一模一樣了 !
<< 部落格藍牙相關網頁連結 >>
- 樹莓派中 USB 藍牙卡的驅動與設置
- HC-05 主從一體藍牙模組初體驗 01 ( 硬體接線、從動角色 )
- HC-05 主從一體藍牙模組初體驗 02 ( AT 指令說明與使用演示、主動角色 )
- HC-05 主從一體藍牙模組初體驗 03 ( BTSCmode.apk Eclipse 編譯環境說明 )
- Bluetooth USB Dongle 初體驗 - Windows 篇 ( 7:免驅;8:BlueSoleil )
- { Server 篇 } Bluetooth USB Dongle 初體驗 - Linux 篇 ( BlueZ , Serial Port Profile )
<<樹莓派編輯環境設置系列文章>>
- 在 Windows 設置 Raspberry Pi (樹莓派) 遠端編輯環境 [第一發] 工欲善其事,必先利其器
- 把 Windows 桌面當作是 Raspberry Pi (樹莓派) 的螢幕 [第二發] 工欲善其事,必先利其器
- 遠端桌面 - VNC [第三發] 工欲善其事,必先利其器
Sublime Text 2 與遠端 (樹莓派) 資料夾連線畫面 |
MobaXterm: Windows 作業系統下開啟樹莓派作業系統的 LXDE 桌面 |
VNC |
資料終於順利顯示出來了,謝謝版主!
回覆刪除另外還有測試程式碼下載O後面好像少了一槓
wget -O - goo.gl/b62lYJ | tar zxvf -
多加後即可順利下載
您好想請問版主一些問題
回覆刪除1.請問收到的訊息是存在樹莓派的什麼地方呢?
2.如果我想要把收到的資料去做演算法或數學運算,該從哪裡下手呢?
spp-client-hc-05.c, line 63:rfcommbuffer 陣列中就是鳩收到的字串訊息。
刪除如果要做運算就需要從字串中取出所需要的訊息 ( 注意到訊息分段的問題 ),上網找一下切割字串的資料!
您好版主想再請教一些問題非常感謝
回覆刪除1.想請問為何spp-client-fixed的程式碼中沒有printf但卻有輸出呢?
(想用atof的方法字串轉數值去做一些簡單的大於小於比較)
2.如果需要再多加另外兩個藍芽hc05去接收資料的話想請問要修改呢?
1. 那是因為藍牙接收到的文字資料,是由 btRecvHandle 處理分割以 \n 結尾為一個字串但這部分沒有開放原始碼。如果要做的話,必須要由 spp-client.c 去修改,只要處理 line 133 - line 163 之間的程式碼即可,rfcommbuffer 就是接收到的文字訊息。
刪除2. HC-05 應該不具有多對一的藍牙連接,若要多對一可能要用其他方式克服,這需要您自己找一下資料!
不好意思版主還想請問您一點小細節非常感謝
刪除1.修改spp-client.c的line133-line163並編譯之後,是spp-client-fixed也會跟著變動嗎?
還是說只能在spp-client.c做修改,但字串被切割的問題還是會在?
2.不好意思我上面問題的說明不夠詳細,我是想問說如果我想讓我的BTdongle
同時與3個hc-05做傳輸並接收資料,想請問程式該如何做修改呢?
(已試過可以同時與兩個hc-05做連線配對)
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 之後,剩下的就看第一點的網頁連結,解決字串被切割的問題。
去看一下 http://ruten-proteus.blogspot.com/2014/08/Bluetooth-Kit-tutorial-04-Linux-BlueZ-02-Server.html 最下面的回覆,雖然沒有程式碼,但是建立了 rfcomm 通道之後,九可以像是開當雨讀黨的方式撰寫傳送與接收的程式。
刪除首先再次感謝版主的回覆
刪除另外想請問可否再貼一次您說三種解決方法的網址,因為上面的網址好像錯誤了
網頁中的連結沒貼好,之後再改!
刪除網址http://faq.cprogramming.com/cgi-bin/smartfaq.cgi?id=1044780608&answer=1108255660
版主您好:有幾項問題想請問
回覆刪除1.有沒有辦法直接從程式端發送訊息而不透過SSCOM3.2呢?
2.有辦法用樹梅派當(client)同時連接好幾個arduino(HM-10)呢?
謝謝
1. SSCOM 就是一個 COM Port 通訊的程式,所以只要程式能夠與電腦的 COM Port 通訊就可以與藍牙模組溝通。
刪除2. 可以! 去找一下關於 RFCOMM 的資料。Linux 環境中有相關的工具程式可以使用,設定好 RFCOMM 之後,使用 Serial 通訊的就可以與藍牙互傳資料。
板主您好:想再問個問題
回覆刪除若要用樹莓派or筆電內建藍芽發送資料(一個訊號),同時讓多個藍芽裝置一起收到訊號,也是從RFCOMM裡面下手嗎?
我試過用bluetoothctl,但只能一次連接一個發送指令。
RFCOMM 只能一次指定一個傳,這不是廣播! 參考一下下面網頁中的問與答,或許會有幫助
刪除http://ruten-proteus.blogspot.tw/2014/08/Bluetooth-Kit-tutorial-04-Linux-BlueZ-02-Server.html
不好意思,想跟板主您情叫慣字串回傳分段,有點看不懂怎解決的
回覆刪除使用的方法與說明都在上面說明的兩個網頁中。
刪除分段就是傳送的時候會將字串拆成好幾個小字串傳輸,可以使用下面兩個網頁中的方法去解決。
--------------------------------------------
"Reading data from a socket"
這網頁要先看。幾近詳細的說明造成 recv() 接收卻讓文字訊息分段的理由,以及提出三種解決方法的說明,而解決這問題所採用的方法就是網頁中的 solution 1。
"c recv() read until newline occurs"
上面網頁 solution 1 的參考程式碼。
---------------------------------------------