2014年8月30日 星期六

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

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

繼續上一篇 "{ Client 篇 } Bluetooth USB Dongle 初體驗 - Linux 篇 ( BlueZ , Serial Port Profile)" 所談到的 Bluetooth USB Dongle ( USB 藍牙卡,文中簡稱 BTdongle ) 裝設在樹莓派上,如何在 Wheezy-Raspbian 作業系統下使用 BlueZ Stack 來撰寫 SPP-Server 或是 SPP-Client 的程式。


在這一篇中,怎樣建立一個 SPP-Server 讓作為 Client 端藍牙 SPP 裝置能夠連接上,並且互傳訊息。作為 Server 端的好處,就是等待別人的需求,然後在回應需求;如果 Client 端不需要任何 Server 端提供的服務時,就可隨時斷線,等待其他方的連線要求。這可以做什麼呢 ? 例如,Server 端是一個環境監控裝置,每當一早醒來要出門前,我想要知道現在天氣的狀況,只要連線上 Server 端將相關資料載回至 Client 端顯示在螢幕上就可以了;但出門之後,就不需要再繼續連線了吧 ! 也就是可以作為一個提供資料的資料中心。雖然作為 Client 也可以做這些事,但是誰要隨時被你連上傳資料啊 ?

所以這就是為什麼 "藍牙入門套件中" 提供的配件都是可做為藍牙 (SPP) Server / Client 端 ( SPP 用括號的意思是:HC-05 只可作為 SPP Server / Client 端;而 BTdongle 則不只可做為 SPP Server / Client 端,也可作為 A2DP 或規範中的裝置,只要符合協議中的通訊規定。

關於其他詳細如何去實現其他藍牙設備 ( CoD ) 及其服務/通訊協議 ( Service、Protocol ),網路上有更多更專業的文章有討論,本篇網頁只針對在樹莓派中使用 BlueZ Stack 實現藍牙 SPP 協議規範做說明, 若有不詳細或是不清楚之處,請再自行尋找補充的資料。

程式分為幾個步驟,這些步驟都寫成各自的副程式,每個副程式都是各自獨立完成單一項工作,然後再結合在一起完成 SPP Server 的建置
  • 設定 BTdongle 的裝置與服務類別 ( The Class of Device/Service field )
    這裡的 Class 的設定值是: 0x1f00 ( 不是很清楚設定的方式,請看這網頁的介紹 )
  • 登錄 SDP Interoperability Requirements  (SPP_SPEC_V12.pdf, Page 17) 中對於 SPP 所定義的 Service Records
    不管是作為 Server 或是 Client,都是需要將規範 ( Profile ) 裡 SDP Interoperability Requirements 所定義的 Service Records 登錄到 SDP Server。但是對於實作 SPP 規範來說,只有 SPP Server 有定義 Service Rcords 所以需要實作,但 SPP Client 不用。
  • 等待 RFCOMM 的連接
    完成前面兩項之後,Server 與 Client 端的藍牙裝置就可經由所建立 RFCOMM Socket 作連線與溝通。
  • 資料傳送與接收的處理
    連線成功建立之後,資料傳送與接收就跟 Client 篇的處理方式一樣。但有一點要注意的是:不管是 Server 或是 Client,接收資料必須額外處理串流資料的問題 ,不然接收的訊息一但被分段,就無法保證每一次接收都是接收到傳送端所傳送過來的完整一次性資料。

所以作為 SPP Server 端,上面的前三個步驟就是制式的,可以不需要知道以及了解,只要知道怎麼設參數進去就好。而使用者所要注意以及要作的就是第四個步驟所談論到的東西,撰寫資料傳送以及接收時的處理程式。本篇,使用一整串的文字訊息來作資料分析的測試並回傳出來,使用端只要接收這些資料並處理即可,至於實際與週邊應用的例子,會在下一篇作展示。

那麼在說明程式碼之前,先下載一些需要的文件與程式碼吧!!!


文件資料下載:

藍牙裝置所採用的一些規格書,都可以在 bluetooth.org 網站中下載 ( 中文English )。在網頁中按下組合鍵 "Ctrl + F" 尋找 "SPP" ,就會找到可以下載的文件。在右邊有 1.1 與 1.2 版本可以下載,下載 1.2 版本的 ( SPP_SPEC_V12.pdf )。


程式碼下載:

** 直接下載

bt-spp-server.tar.gz


** 使用樹莓派下載

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

pi@raspberrypi ~/codes/Bluetooth $ mkdir spp_server
pi@raspberrypi ~/codes/Bluetooth $ cd spp_server/
pi@raspberrypi ~/codes/Bluetooth/spp_server $ wget -O - goo.gl/MHY7SP | tar zxvf -
...< 過程省略 >...
pi@raspberrypi ~/codes/Bluetooth/spp_server $ ls
spp-sdp-register.h  spp-sdp-register.o  spp-server.c
pi@raspberrypi ~/codes/Bluetooth/spp_server $


** 如何編譯

sudo gcc spp-sdp-register.o see-server.c -lbluetooth -o spp-server


程式碼說明 ( spp-server ):

下面針對 spp-server.c 程式碼做簡單的說明,


** 重要參數說明

整個 spp-server.c 只有這三個位於 line 185 - 188 參數需要注意。以這個程式而言,line 185 不需要修改,Class ( CoD ) 維持 0x1f00 即可;line 186 指定設定的 Class 給 HCI 去處理,等待BTdongle 執行並回覆命令結果的超時時間,以毫秒為單位;line 187 則是設定要開啟給 Client 連接的 RFCOMM 通訊埠號碼,若通訊埠有被佔用的情形發生,可以修改這個地方的參數再重新編譯。
spp-server.c, main(), line 185 - 188
185 unsigned int cls = 0x1f00;  // Serial Port Profile
186 int timeout = 1000;
187 uint8_t channel = 10;       // 開啟的 SPP Server 通道號碼
188 


** main()

在主程式中,程式的結構就如同在開頭所說的一樣,由四個副程式組成,每一個副程式負責處理各自的一個任務,並在與 Client 成功連線之後,進入到一個傳送與接收的無窮迴圈,直到錯誤發生才會結束程式。

line 194 - 197:設定藍牙裝置/服務類別 ( The Class of Device / Service Field ), CoD。
line 199 - 202:登錄 Service Records 到 SDP Server,並指定 RFCOMM 通訊埠號碼( channel)
line 205 - 208:等待 Client 端的連線,並回傳成功連線的 Client 端 socket descriptor
line 211         :Server 與 Client 端傳送與接收的處理,直到錯誤產生跳出程式。
spp-server.c, main(), line 189 - 214
189 int main()
190 {
191  int rfcommsock;
192  int scosock;
193 
194  if (set_class(cls, timeout) < 0)
195  {
196   perror("set_class ");
197  }
198 
199  if (register_sdp(channel) < 0)
200  {
201   perror("register_sdp ");
202   return -1;
203  }
204 
205  if ((rfcommsock = rfcomm_listen(channel)) < 0)
206  {
207   perror("rfcomm_listen ");
208   return -1;
209  }
210 
211   handle_connection(rfcommsock, scosock);
212 
213  return 0;
214 }

上面就是程式的主架構,接著看一下個程式裡面的內容。


** set_class()

先取得主機 ( 這裡是指樹莓派 ) 上面任一個的藍牙裝置的 ID ( line 54 ),利用此 ID 取出 6-byte 的藍牙位址 ( line 58 ),最後轉換 6-byte 藍牙位址為一個以 '\0' 作結尾的字串,這字串只是要作為函式 printf 輸出的參數之用;接著繼續再取得主機 HCI 的 file handle ( line 66 ),利用 file handle 設定藍牙裝置的 Class ( line 70 ),若無錯誤發生,則正常關閉所取得的 file handle ( line 77 ),然後在螢幕上輸出被設定的藍牙裝置位址以及設定的 Class。
spp-server.c, main(), line 44 - 82
 44 int set_class(unsigned int cls, int timeout)
 45 {
 46     int id;
 47     int fh;
 48     bdaddr_t btaddr;
 49     char pszaddr[18];
 50 
 51  // 取得裝置 ID
 52  // 使用 NULL 作為參數,函式成功回傳的
 53  // 所取得的 ID,就是第一個藍牙裝置 ID 
 54  if ((id = hci_get_route(NULL)) < 0)
 55   return -1;
 56 
 57  // 轉換藍牙裝置 ID 為 6-byte 藍牙位址
 58  if (hci_devba(id, &btaddr) < 0)
 59   return -1;
 60 
 61  // 轉換 6-byte 藍牙位址為一般以 '\0' 作結尾的字串
 62  if (ba2str(&btaddr, pszaddr) < 0)
 63   return -1;
 64 
 65  // 取得 HCI 的 file handle
 66  if ((fh = hci_open_dev(id)) < 0)
 67   return -1;
 68 
 69  // 設定藍牙裝置的 Class ( CoD )
 70  if (hci_write_class_of_dev(fh, cls, timeout) != 0)
 71  {
 72   perror("hci_write_class ");
 73   return -1;
 74  }
 75 
 76  // 關閉 file handle
 77  hci_close_dev(fh);
 78 
 79  printf("set device %s to class: 0x%06x\n", pszaddr, cls);
 80 
 81  return 0;
 82 }

這副程式可以被單獨執行,只要加入標頭檔 #include <bluetooth/bluetooth.h> 並在主程式 main() 裡單獨呼叫,並輸入參數 0x1f00 即可;或是直接執行 sudo ./spp-server 也可以。

使用藍牙入門套件的 BTdongle,在樹莓派環境下輸入下面的指令,取出 BTdongle 的藍牙組態資料

hciconfig -a

BTdongle 預設的 Class 是 0x420100 ( 或許會有不同 ),這是值是寫死在 BTdongle 裡的,不過可以暫時變更成使用者設定的數值。

pi@raspberrypi ~ $ hciconfig -a
hci0:   Type: BR/EDR  Bus: USB
        BD Address: 00:11:22:98:76:54  ACL MTU: 1021:4  SCO MTU: 180:1
        UP RUNNING PSCAN ISCAN
        RX bytes:1274 acl:0 sco:0 events:48 errors:0
        TX bytes:458 acl:0 sco:0 commands:47 errors:0
        Features: 0xff 0x3e 0x09 0x76 0x80 0x01 0x00 0x80
        Packet type: DM1 DM3 DM5 DH1 DH3 DH5 HV1 HV2 HV3
        Link policy: RSWITCH HOLD SNIFF
        Link mode: SLAVE ACCEPT
        Name: 'raspberrypi-0'
        Class: 0x420100
        Service Classes: Networking, Telephony
        Device Class: Computer, Uncategorized
        HCI Version: 2.0 (0x3)  Revision: 0x50
        LMP Version: 2.0 (0x3)  Subversion: 0x3
        Manufacturer: Mitel Semiconductor (16)

pi@raspberrypi ~ $


瀏覽一下bluetooth.org Baseband ( 基帶 ) 的網頁,應該不難發現,看不到任何裝置與服務類別是使用 SPP 協議相關的 ( 應該是說 COM 通訊埠 ),所以這裡要怎麼設定呢 ?

回想並搜尋一下在 "HC-05 主從一體藍牙模組初體驗 02 ( AT 指令說明與使用演示、主動角色 )" 裡的 "** 搜尋藍牙裝置" 這部分就可以知道,可以設定為 0x1f00 ( 表示主要裝置類型是:未分類:未指定裝置代碼;次要裝置類型:未分類,未指定設備代碼 )。

先來看一下實際執行 set_class(0x1f00) 之後的變化。輸入指令 sudo ./spp-server,出現 accepting connections on channel : 10 時按下 "Ctrl + C" 結束程式;輸入 hciconfig -a 確認現在的 BTdongle 藍牙組態資料就可發現,Class 已被修改成 0x1f00 ( 重新設定或重開機之後,就會恢復成 0x420100但修改 Class 並不是任何的 USB 藍牙卡都可以做得到的 !!! )
pi@raspberrypi ~/codes/Bluetooth/spp_server $ sudo ./spp-server
set device 00:11:22:98:76:54 to class: 0x001f00
accepting connections on channel: 10   <-- 可按下 "Ctrl + C" 先結束程式
pi@raspberrypi ~/codes/Bluetooth/spp_server $ hciconfig -a
hci0:   Type: BR/EDR  Bus: USB
        BD Address: 00:11:22:98:76:54  ACL MTU: 1021:4  SCO MTU: 180:1
        UP RUNNING PSCAN ISCAN
        RX bytes:1557 acl:0 sco:0 events:52 errors:0
        TX bytes:473 acl:0 sco:0 commands:51 errors:0
        Features: 0xff 0x3e 0x09 0x76 0x80 0x01 0x00 0x80
        Packet type: DM1 DM3 DM5 DH1 DH3 DH5 HV1 HV2 HV3
        Link policy: RSWITCH HOLD SNIFF
        Link mode: SLAVE ACCEPT
        Name: 'raspberrypi-0'
        Class: 0x001f00
        Service Classes: Unspecified
        Device Class: Invalid Device Class!
        HCI Version: 2.0 (0x3)  Revision: 0x50
        LMP Version: 2.0 (0x3)  Subversion: 0x3
        Manufacturer: Mitel Semiconductor (16)

pi@raspberrypi ~/codes/Bluetooth/spp_server $


所以即單獨使用上面的 set_class() 副程式,也可以很方便的設定不同的 CoD。


** register_sdp()

register_sdp() 就是將 SPP service records 登錄到 SDP Server 的副程式,函式只需要輸入一個設定 RFCOMM 通訊埠號碼的參數。根據 SPP_SPEC_V12.pdf, page 16 所說,只有 Server 端需要做 Service Records 登錄的動作,Client 端不用。但 Client 端必須自己去找尋 SPP Server 所開放出來的 RFCOMM 通訊埠號碼才能與連線。

要登錄 SPP Server 的 Service Records 到 SDP Server,必須先知道相關的 Service Records 有哪些。打開 SPP_SPEC_V12.pdf, page 17 的表格 6.1 就是撰寫程式所需要知道的東西。

其中要登錄的 Service Records 根據最上層的描述決定使用的 sdp 函式是哪一個;第二層是要輸入的資料,而這些資料都要經過轉換並且儲存在 sdp_list_t 資料串列中作為第一層 sdp 函式的參數;第三層就是第二層所附加的資料,繼續增加在 sdp_list_t 資料串列中即可,例如 Protocol ID #1 。

表格裡面產生的所有 Service Records 資料,都會在成功與 SDP 連線成功之後,最後利用 sdp_record_register 函式登錄到 SDP Server 去。而為了要能執行這個函式,必須完成這個函式參數的要求,也就是產生一個 sdp_record_t 資料結構並將所有的 SPP 協議規範所要求的 Service Records 的資料,全部填入到這個資料結構中,這個函式的宣告為

  • int sdp_record_register( sdp_session_t *session,
                             sdp_record_t *rec,
                             uint8_t flags )

session SDP_Connection() 函式回傳值,執行 sdp_record_register() 函式前一個指令。

rec 就是程式一開始需要自行先產生的空資料結構,所有的 Service Records 處理之後都必須一起放入在這裡,一開始要使用下面方式產生

sdp_record_t *record = sdp_record_alloc();

若是使用自訂的 UUID,要這樣產生
uint32_t svc_uuid_int[] = { 0, 0, 0, 0x12DF }; // 00000000-0000-0000-0000000012DF
uuid_t svc_class_uuid;
sdp_record_t record = {0};

sdp_uuid128_create( &svc_class_uuid, &svc_uuid_int );
sdp_set_service_id( &record, svc_uuid );

Table 6.1 中,行位 Type/Size 如果是 UUID 保留值,可直接上 Service Discovery ( 服務發現 ) 找到對應的 UUID 值,並使用下面的的函式將其轉換成 uuid_t 格式
  1. uuid_t* sdp_uuid128_create( uuid_t *uuid, const void *data )
  2. uuid_t* sdp_uuid16_create( uuid_t *uuid, const void *data )
  3. uuid_t* sdp_uuid32_create( uuid_t *uuid, const void *data )
1.) 使用在自訂的 128-bit UUID;2.) 和 3.) 使用在保留的 16-bit 與 32-bit 的保留 UUID 轉換上。轉換之後的 uuid_t 會放在是在參數 uuid 裡。


-- ServiceClassIDList 所對應到的 sdp 函數是
  • void sdp_set_service_classes ( sdp_record_t *rec,
                                   sdp_list_t *class_list )
  |
  |__ ServiceClass #0 表示要轉換值 SerialPort 的 UUID 為 uuid_t 格式 ( SERIAL_PORT_SCVCLASS_ID, 0x1101 @ sdp.h),並放置到 sdp_list_t 資料串列中,作為 sdp_set_service_classes() 函式的參數


-- ProtocolDescriptorList 所對應到的 sdp 函數是
  • void sdp_set_access_protos( sdp_record_t *rec,
                                sdp_list_t *proto_list )
  |
  |__ Protocol ID #1 表示要轉換值 L2CAP 的 UUID 為 uuid_t 格式 ( L2CAP_UUID, 0x0100  @ sdp.h),並放置到 sdp_list_t 資料串列中,作為 sdp_set_access_protos() 函式的參數
  |
  |__ Protocol ID #2 表示要轉換值 RFCOMM 的 UUID 為 uuid_t 格式 ( RFCOMM_UUID0x0003  @ sdp.h),並放置到 sdp_list_t 資料串列中
      |
      |__ ProtocolSpecificParameter0 表示要將 Server Channel 定義的值 server channel # 繼續新增到與 RFCOMM 的串列 sdp_list_t 中,結合上層的 RFCOMM 串列,作為 sdp_set_access_protos() 函式的參數

實際使用程式來完成 ProtocolDescriptorList 這一個區塊,則程式碼可以這樣寫。重點是要清楚怎麼將 Service Records 加入到指定函式中
 1  uuid_t l2cap_uuid, rfcomm_uuid;
 2  sdp_list_t *l2cap_list = 0,
 3       *rfcomm_list = 0,
 4       *proto_list = 0,
 5       *access_proto_list = 0;
 6 
 7  sdp_data_t *channel_d = 0;
 8 
 9  sdp_record_t *record = sdp_record_alloc();
10 
11     //*-- Protocol Drectiptor List
12  // set l2cap information
13  sdp_uuid16_create(&l2cap_uuid, L2CAP_UUID);
14  l2cap_list = sdp_list_append( 0, &l2cap_uuid );
15  proto_list = sdp_list_append( 0, l2cap_list );
16     
17  // set rfcomm information
18  sdp_uuid16_create(&rfcomm_uuid, RFCOMM_UUID);
19  channel_d = sdp_data_alloc(SDP_UINT8, &channel);
20  rfcomm_list = sdp_list_append( 0, &rfcomm_uuid );
21 
22  sdp_list_append( rfcomm_list, channel_d );
23  sdp_list_append( proto_list, rfcomm_list );
24 
25     //*-- Bluetooth Profile Descriptor List
26     // attach protocol information to service record
27  access_proto_list = sdp_list_append( 0, proto_list );
28  sdp_set_access_protos( record, access_proto_list );


-- BluetoothProfileDescriptotList 所對應到的 sdp 函數是
  • void sdp_set_profile_descs( sdp_record_t *rec,
                                sdp_list_t *profile_list )
  |
  |__ Profile ID #0 表示要轉換 Support Profiles 定義的值 SerialPortProfile 的 UUID 為 uuid_t 格式 ( SERIAL_PORT_PROFILE_ID0x1101 @ sdp.h),並放置到 sdp_list_t 資料串列中
      |
      |__ Parameter #0 表示要將 Profile Version 定義的值 0x0102 繼續新增到上面的串列 sdp_list_t 後面,作為 sdp_ser_profile_descs() 函式的參數,下面是實作的程式碼
 1  sdp_profile_desc_t desc;
 2 
 3  sdp_list_t *root_list = 0;
 4 
 5  sdp_record_t *record = sdp_record_alloc(); 
 6 
 7  sdp_profile_desc_t desc;
 8 
 9     //*-- Bluetooth Profile Descriptor List
10  sdp_uuid16_create(&desc.uuid, SERIAL_PORT_PROFILE_ID);
11 
12  // set the Profile Version to 0x0102
13  desc.version = 0x0100;
14 
15  if (!(root_list = sdp_list_append(NULL, &desc)))
16   return -1;
17 
18  if (sdp_set_profile_descs(record, root_list) < 0)
19   return -1;

-- ServiceName 所對應到的 sdp 函數是
  • void sdp_set_info_attr( sdp_record_t *rec,
                            const char *name,
                            const char *provider,
                            const char *description )
要將 Displayable text name 所自定義的值 ( 這些數值定義在下面的程式碼 ),使用下面的函式新增到 record 裡,下面是如何實作這一段的程式碼
1     const char *service_name = "SPP Service";
2  const char *service_dsc = "SPP";
3  const char *service_prov = "Alu-Studio ";
4 
5  sdp_record_t *record = sdp_record_alloc(); 
6 
7     // set the name, provider, and description
8     sdp_set_info_attr(record, service_name, service_prov, service_dsc);

完成所有的 Service Records 的動作之後,這些屬性與值就會全部存在於 record 資料結構中,只要連上 SDP Server 並將其登錄,成功之後就完成 SPP Server 的建立。
1  // connect to the local SDP server and register the service record
2  session = sdp_connect( BDADDR_ANY, BDADDR_LOCAL, SDP_RETRY_IF_BUSY );
3  err = sdp_record_register(session, record, 0);

因為程式產生很多暫時的 sdp_list_t 與 sdp_data_t 資料串列,因此在結束前,請先釋放記憶體
1  // cleanup
2  sdp_data_free( channel_d );
3  sdp_list_free( l2cap_list, 0 );
4  sdp_list_free( rfcomm_list, 0 );
5  sdp_list_free( root_list, 0 );
6  sdp_list_free( access_proto_list, 0 );

source: bluetooth.org, SPP_SPEC_V12.pdf, page 17

完成了之後,要如何確認所建立的 SDP Server ????

請下指令看一下預設的 BTdongle 的 SDP Service Records 有哪些 ? ( 如下表左邊 )

sdptool browse local

繼續輸入 sudo ./spp-server & 將指令推至背景執行,然後再下一次

 sdptool browse local

這時在最下面的輸出資料就會看到多了 SPP Service 這一項,而緊接的數據是不是就是我們使用 Table 6.1 所建立的 Service Records。

ps. 要退出輸入的指令,請下 fg 然後按下 "Ctrl + C" 中斷 spp-server 指令

預設值
sudo ./spp-server
pi@raspberrypi ~ $ sdptool browse local
Browsing FF:FF:FF:00:00:00 ...
Service Name: SIM Access Server
Service RecHandle: 0x10000
Service Class ID List:
  "SIM Access" (0x112d)
  "Generic Telephony" (0x1204)
Protocol Descriptor List:
  "L2CAP" (0x0100)
  "RFCOMM" (0x0003)
    Channel: 8
Profile Descriptor List:
  "SIM Access" (0x112d)
    Version: 0x0101

…<< 中間省略 >>…

Service Name: Dial-Up Networking
Service RecHandle: 0x10005
Service Class ID List:
  "Dialup Networking" (0x1103)
  "Generic Networking" (0x1201)
Protocol Descriptor List:
  "L2CAP" (0x0100)
  "RFCOMM" (0x0003)
    Channel: 1
Profile Descriptor List:
  "Dialup Networking" (0x1103)
    Version: 0x0100

pi@raspberrypi ~ $
pi@raspberrypi ~ $ sdptool browse local
Browsing FF:FF:FF:00:00:00 ...
Service Name: SIM Access Server
Service RecHandle: 0x10000
Service Class ID List:
  "SIM Access" (0x112d)
  "Generic Telephony" (0x1204)
Protocol Descriptor List:
  "L2CAP" (0x0100)
  "RFCOMM" (0x0003)
    Channel: 8
Profile Descriptor List:
  "SIM Access" (0x112d)
    Version: 0x0101

…<< 中間省略 >>…

Service Name: Dial-Up Networking
Service RecHandle: 0x10005
Service Class ID List:
  "Dialup Networking" (0x1103)
  "Generic Networking" (0x1201)
Protocol Descriptor List:
  "L2CAP" (0x0100)
  "RFCOMM" (0x0003)
    Channel: 1
Profile Descriptor List:
  "Dialup Networking" (0x1103)
    Version: 0x0100

Service Name: SPP Service
Service Description: SPP
Service Provider: Alu-Studio
Service RecHandle: 0x10006
Service Class ID List:
  "Serial Port" (0x1101)
Protocol Descriptor List:
  "L2CAP" (0x0100)
  "RFCOMM" (0x0003)
    Channel: 10
Profile Descriptor List:
  "Serial Port" (0x1101)
    Version: 0x0100

pi@raspberrypi ~ $
















































完成 Table 6.1 SDP Service Records 的登錄,就完成了 SPP Server 的建置,而接下來要作的,就是等待 Client 的連線。


** rfcomm_listen()

建立藍牙客戶端 ( Client ) 與服務端 ( Server ) 的連線可分為下面圖示的四個步驟:
  1. 產生一個藍牙 socket
    剛開始這個 socket 是沒什麼用的,就像擁有一個插頭一樣,沒通上電能幹什麼 !
  2. 將 socket 與本地 ( local ) 的藍牙裝置作連接;也就是 BTdongle。所以在綁定的時候要順帶設定 RFCOMM 通訊埠號碼。
  3. 將服務端設置在等待客戶端連接的狀態。
  4. 一但客戶端進行連結,服務端若接受連線,就會產生一個新的 socket 與之連線。
客戶端與服務端連線建立的步驟

若熟悉網路程式開發的話,下面的程式碼應該很熟悉,這就是藍牙服務端與客戶端連線的建立方式
spp-server.c, rfcomm_listen), line 86 - 129
 86 int rfcomm_listen(uint8_t channel)
 87 {
 88  int sock; // socket descriptor for local listener
 89  int client; // socket descriptor for remote client
 90  unsigned int len = sizeof(struct sockaddr_rc);
 91 
 92  struct sockaddr_rc remote; // local rfcomm socket address
 93  struct sockaddr_rc local; // remote rfcomm socket address
 94  char pszremote[18];
 95 
 96  // 藍牙 socket 初始化
 97  sock = socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM);
 98 
 99  local.rc_family = AF_BLUETOOTH;
100 
101  // 如果使用 Bluetooth USB Dongle 的位址已知,可直接在 rd_bdaddr 輸入位址
102  // 不然就輸入 *BDADDR_ANY ( 由系統自己選擇 )
103  local.rc_bdaddr = *BDADDR_ANY;
104  local.rc_channel = channel;
105 
106  // 綁定 socket 到藍牙裝置 ( 這裡指的是 Bluetooth USB Dongle )
107  if (bind(sock, (struct sockaddr *)&local, sizeof(struct sockaddr_rc)) < 0)
108   return -1;
109 
110  // 設定 listen 序列的長度 ( 通常設為 1 就可以了 )
111  if (listen(sock, 1) < 0)
112   return -1;
113 
114  printf("accepting connections on channel: %d\n", channel);
115 
116  // 接受接入的連線;此連線是一個阻絕式呼叫 ( a blocking call )
117  client = accept(sock, (struct sockaddr *)&remote, &len);
118 
119  ba2str(&remote.rc_bdaddr, pszremote);
120 
121  printf("received connection from: %s\n", pszremote);
122 
123  // 關掉阻絕式呼叫
124  if (fcntl(client, F_SETFL, O_NONBLOCK) < 0)
125   return -1;
126 
127  // 返回 client 端的 socket descriptor
128  return client;
129 }

上面的程式碼很簡單的就是使用在連線建立上,在 SPP Server 的建立上,沒有什麼特別的參數需要在作說明,直接用就可以了。若是要硬抓出使用上讓我比較不便的就是 RFCOMM 通訊埠號碼的選定在這個副程式裡面是固定的 !

有沒有其他方式可以動態來作 RFCOMM 通訊埠號碼的指定呢 ? 這樣就不必遇到通訊埠被佔用的情況產生了。

方法是有的:那就是每一次都由第一個 RFCOMM 通訊埠號碼開始綁定,若成功就不再往下作;若綁定不成功,換一個號碼繼續綁定直到最後一個。

修改上面 line 104 - 108 為下面這樣
1 for( port = 1; port <= 30; port++ )
2 {
3  local.rc_channel = port;
4  if( bind( sock, (struct sockaddr *)&local, sizeof( struct sockaddr_rc )  ) == 0 )
5   break;
6 }

並且將函式 int rfcomm_listen(uint8_t channel) 改成 int rfcomm_listen() 就可以動態自行變更 RFCOMM 通訊埠號碼。

Note: RFCOMM 可用的通訊埠號碼為 1 至 30,其餘的設定都是不對的。


** handle_connection()

函式 handle_connection() 與在上一篇 { Client 篇 } 主程式裡面處理訊息的程式相同,因此 rfcomm_listen() 函式成功回傳 Client 端的 socket descriptor 之後,就會開始接收 Client 端的文字訊息,並在成功接收之後回傳 ATOK。

spp-server.c, handle_connection(), line 133 - 179
133 int handle_connection(int rfcommsock, int scosock)
134 {
135    char hello_str[] = "SPP Server: Hello, Client !!";
136  char rfcommbuffer[255];
137  int len;
138     
139     // Server 端傳送 hello 字串到 Client 端
140     len = send(rfcommsock, hello_str, sizeof(hello_str), 0);
141     if( len < 0 )
142     {
143         perror("rfcomm send ");
144         return -1;
145     }
146     
147     
148  while (1)
149  {
150   // 從 RFCOMM socket 讀取資料
151   // 此 socket 已經關掉阻絕式呼叫,所以即使沒有可用數據也不會被阻絕
152   len = recv(rfcommsock, rfcommbuffer, 255, 0);
153 
154   // EWOULDBLOCK indicates the socket would block if we had a
155   // blocking socket.  we'll safely continue if we receive that
156   // error.  treat all other errors as fatal
157   if (len < 0 && errno != EWOULDBLOCK)
158   {
159    perror("rfcomm recv ");
160    break;
161   }
162   else if (len > 0)
163   {
164    // 將接收到的文字訊息最後面加上 '\0' 結束字元
165    // 並將其列印到螢幕上,並回傳 ATOK 給 Client 端
166    rfcommbuffer[len] = '\0';
167 
168    printf("rfcomm received: %s\n", rfcommbuffer);
169    send(rfcommsock, "ATOK\r\n", 6, 0);
170   }
171 
172  }
173 
174  close(rfcommsock);
175 
176  printf("client disconnected\n");
177 
178  return 0;
179 }

雖然 spp-server 可以接收到 Client 端傳送過來的文字訊息,但是 ! 若是還記得我們在上一篇的最後所談論到接收的問題,那就不難發現 handle_connection() 同樣也存在接收文字訊息會分段的問題。但是在這裡的解決方式已不再使用上一篇使用的方法作解決,而是讓使用者可以直接取得解析出的文字 ( 儲存在二維陣列中 ) 作後續處理。


程式碼說明 ( spp-server-fixed ):

程式碼在此:下載
-rw-r--r-- pi/pi           209 2014-08-29 20:05 btrecv.h
-rw-r--r-- pi/pi          3248 2014-08-29 19:41 btrecv.o
-rw-r--r-- pi/pi           145 2014-08-26 10:38 mydefine.h
-rw-r--r-- pi/pi           264 2014-08-29 19:42 spp-sdp-register.h
-rw-r--r-- pi/pi          2652 2014-08-29 19:42 spp-sdp-register.o
-rw-r--r-- pi/pi          6748 2014-08-29 19:46 spp-server.c

編譯指令:

sudo gcc spp-server.c btrecv.o spp-sdp-register.o
         -lbluetooth -o spp-serverfixed


此修正的程式主要修改的部分在 handle_connection() 函式無窮迴圈處,使用額外加入的 btrecv.h 來處理這些分析所得的資料
FIXED: spp-server.c, handle_connection(), line 139 - 190
139 int handle_connection(int rfcommsock, int scosock)
140 {
141    char hello_str[] = "SPP Server: Hello, Client !!";
142  char rfcommbuffer[RECVSTRINGSIZE];
143  int i, n, len;
144     char **strlist;
145     
146     // Server 端傳送 hello 字串到 Client 端
147     len = send(rfcommsock, hello_str, sizeof(hello_str), 0);
148     if( len < 0 )
149     {
150         perror("rfcomm send ");
151         return -1;
152     }
153  while( ( i = btRecvHandle( rfcommsock, rfcommbuffer, DELIMITER ) ) >= 0 )
154     {
155         if( i == 1 )
156         {
157             //printf("handle_connect: while 1\n");
158             // for link list, 取回分段的文字訊息
159             /* 顯示取得的資料*/
160             n = getIdx();
161             // 動態指定二維陣列
162             strlist = (char**)malloc( n * sizeof(char*) );        
163             for( i = 0; i < n; i++ )
164             {
165                 strlist[i] = (char*)malloc( RECVSTRINGSIZE * sizeof(char) );
166             }
167             getItem( strlist );        
168             
169             // 若要處理取得的資料,就是在這裡處理這些字串
170             for( i = 0; i < n; i++ )
171             {            
172                 printf( "GET[%d]: %s\n", i+1, strlist[i] );            
173             }
174         } 
175     }
176 
177  close(rfcommsock);    
178     
179     freeItem();
180     
181     // 釋放動態二微陣列
182     for( i = 0; i < n; i++ )
183     {
184         free(strlist[i]);
185     }    
186     
187  printf("client disconnected\n");
188 
189  return 0;
190 }

下面是 btrecv.h 新增的處理函式
FIXED: btrecv.h, line 1 - 9
1 #ifndef BTRECV_H_
2 #define BTRECV_H_
3 
4 int btRecvHandle( const int btsocket, char *buffer, const char delimiter );
5 void getItem( char **recvItems );
6 void freeItem();
7 unsigned char getIdx();
8 
9 #endif // BTRECV_H_


** btRecvHandle()

這是用來處理接收到的文字訊息處理的函式,由 recv() 函式所接收到的資料會一直存放 buffer 中,然後經過資料的處理找出 deliniter 所定義的分段字元 ( DELIMITER, mydefine.h ),將字串分段截取出來並放置在一個字串串列中;處理完的字串會從 buffer 被移出,未處理再繼續處理。

由於我們只限定使用者一次最多輸入 RECVSTRINGSIZE 的字元 ( mydefine.h ),但不限定一次輸入的字串裡面包含多少個分段字元,意味著使用者可以分段輸入不同資料來區隔,但一次最多輸入加上分段字元不可以超過 RECVSTRINGSIZE。所以為了儲存分析之後不確定的字串數目,所以使用串列 ( link list ) 來儲存這些字串。

每一次拆解之後字串的數目,可以使用 getIdx() 函式取出,然後就可用它來產生一個二維字元陣列取出這些拆解之後的字串作後續的處理。

講到這裡,我並不打算再繼續往下講,其餘的部分都跟上一節 spp-server 相同 ! 現在就讓我們直接進入到程式測試吧 !


測試藍牙 SPP Server:

mydefine.h 中定義了兩個常數,其中分段字元選用 '\n' ( 換行字元,0x0A )
1 #ifndef MYDEFINE_H_
2 #define MYDEFINE_H_
3 
4 #define RECVSTRINGSIZE  255
5 #define DELIMITER       '\n'    // LF, 換行符號
6 
7 #endif  // MY_DEFINE_H_


這裡的測試,樹莓派使用經過修正的程式 spp-server-fixed;手機 APP 使用藍牙串口助手;HC-05 則在 Windows 作業系統使用 AccessPort。


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



** 藍牙串口助手

首先,請進入到樹莓派的桌面 ( 建議使用 VNC ) 並打開 Bluetooth Manager,按下 "Search" 按鈕搜尋手機 ( 記得手機藍牙開啟之後還要設定為可搜尋 ),在出現的手機圖示按一下滑鼠右鍵選擇 "Pair"

桌面會出現輸入密碼的畫面 ( 1234 );然後手機端也會出現要輸入配對密碼的要求,同樣也是輸入 1234。完成雙方的配對之後,就會看到原本的手機圖上左上角出現一把鑰匙,表示配對成功。

在樹莓派輸入下面指令 ( 要先切換到 spp-server-fixed 的目錄下 )
pi@raspberrypi ~/codes/Bluetooth/spp_server_fixed $ sudo ./spp-server-fixed
set device 00:11:22:98:76:54 to class: 0x001f00
accepting connections on channel: 11



出現上面的圖示之後,就可以開啟手機上的藍牙串口助手。藍牙助手開啟之後就會開始搜尋附近的藍牙裝置,( 如果沒有變更 BTdongle 的名稱,應該會是 raspberryp[i-0 ),在出現的藍牙裝置中點選 raspberrypi-0

進入之後,等個幾秒鐘,在 Service's UUID: 處就會出現所有關於 BTdongle 相關的服務列表出現 ( 重點是要出現第一個 00001101 開頭的服務,表示 Serial Port Profile ),等待大約 5 秒鐘之後,按下 "連接設備" 按鈕,開始連接

藍牙串口助手若是成功連接上 SPP Server,就會在命令列螢幕上輸出手機藍牙的位址
pi@raspberrypi ~/codes/Bluetooth/spp_server_fixed $ sudo ./spp-server-fixed
set device 00:11:22:98:76:54 to class: 0x001f00
accepting connections on channel: 11
received connection from: 12:34:56:78:9A:BC



並且在手機的畫面下方出現三個可供選擇的通訊工作模式,選擇 "命令行模式"。在這模式下,我們可以自由設定伴隨輸出字串後面要接的字元,而且是用 16 進制的方式來作設定。

進入到 "命令行模式" 的畫面之後,按下手機的選項按鈕 ( 手機面板上的按鈕 ),在出現的選單中,選擇 "設置結束符" 選項

這邊的設定照畫面中輸入即可,下面我們簡單解釋一下怎麼設定。

看一下 ASCII 碼表格:數字 0 - 9,16 進制由 0x30 - 0x39;大寫字母 A - Z,16 進制由 0x41 - 0x5A;小寫字母 a - z,16 進制由 0x61 - 0x7A;換行字元 ( '\n' ) 是 0x0A。所以就可以知道畫面中的 16 進制數值

65 66 0A 65 66 0A 70 71 0A 就等於 ef\nef\npq\n

當輸入 test,再加上剛剛設定的一長串結束字元,則完整的輸出就會變成

testef\nef\npq\n

按下傳送之後

這次只會在手機螢幕上看到輸出 test 字串

但是在樹莓派的螢幕上卻可以看到,剛剛的一整串文字使用換行字元作為分行符號後,已被拆成三個字串並顯示在螢幕上
pi@raspberrypi ~/codes/Bluetooth/spp_server_fixed $ sudo ./spp-server-fixed
set device 00:11:22:98:76:54 to class: 0x001f00
accepting connections on channel: 11
received connection from: 12:34:56:78:9A:BC
GET[1]: testef
GET[2]: ef
GET[3]: pq



所以利用這個方式,就可以利用不同的分行符號達到一次傳送多筆代表不同意思的資料出去,至於實際使用在硬體資料傳送上的例子,就請等下篇吧 !!!

原本還要繼續使用 HC-05 搭配 AccessPort 做測試的部分,若是了解 16 進制字元怎麼設定,以及前面幾篇文章的介紹,就不應該會難倒看到這裡的朋友,自己試試吧 ! 有問題再提出來一起討論。


結論:

藍牙入門套件的使用說明到這邊已經完成,接下來就只剩下一個應用範例的介紹,希望這幾篇關於 bluetooth USB dongle 與 HC-05 主從一體藍牙模組的使用介紹,能夠讓真正想入門藍牙的朋友真的上手了,這也是寫這些東西最大的成就與收穫 !



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

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

10 則留言:

  1. 你好 請問一下 如果我是要把camera的影像用藍芽傳輸android或pc上 也是需要用hc-05和藍芽usb一起嗎?? 幾篇看下來 有點搞不清楚是兩個都要用 還是可以只選其中一個使用我想做的內容

    回覆刪除
    回覆
    1. 先了解一下藍牙協議吧!網頁中所實現的都是使用 SPP 協議,要傳送影像我沒試過,但應該要去實現藍牙的影像傳輸協議,或是以其他方式實現這部分的功能。

      看一下 http://ruten-proteus.blogspot.tw/2014/07/Bluetooth-Kit-tutorial-02-hc-05-02.html 這一篇"HC - 05 @ 主動模式連線到手機" 這一節對於 CoD 的解釋。如果要用手機傳輸影像,必須要藍牙主從雙方都有支援這個功能才能做溝通。

      HC-05 是採用藍牙 SPP 協議,所以可以將藍牙轉成串口做使用,但因為 USB 藍牙卡在樹莓派中沒有這功能必須自己寫程式去實現 ( 除非直接使用檔案設定 ),所以才能由手機互傳文字訊息到樹莓派,不論是主動模式或是被動模式。

      要使用藍牙傳送影像到樹莓派 ( Linux 系統 ),有些藍牙卡不支援的協議必須自己寫程式去實現它,不然就網路找看看是否有已經寫好的程式可以做參考,而且還必須要懂藍牙的一些規格與專用名詞,才不至於瞎子摸象!

      要用藍牙做影像傳輸,USB 藍芽卡是比較好的選擇,但是如何傳輸與那些協議程式要自己寫,要自己再花點時間用功一下,網頁中的實作方式可以做參考,但實際如何做要找資料研究研究一下!

      刪除
  2. 你好 我是藍芽方面的初學者,我從購買你的整組藍芽套件後,成功完成兩端連接並互傳資料,接者我想試者以多個藍芽dongle(usb)做出可以一對多連線傳輸的實現,所以把server端的程式拿去作改良,把rfcomm_listen給他做兩遍,使得可以實現2對1連線,並分別產生兩個rfcommsock,然後再handle_connection多傳一個rfcommsock進去,使之可以由另外兩端的藍芽usb傳值過來,但我第一次的rfcomm_listen可以連接成功,不過第二次的卻在顯示 printf("accepting connections on channel: %d\n", channel);完後不知為何就卡住,所以想像版主請教關於程式方面要如何實現藍芽一對多的連線,或者是對於我的作法有何建議或看法可以指教我,感謝~

    回覆刪除
    回覆
    1. 今天更新了系統並重新安裝了樹莓派藍牙卡的驅動 http://ruten-proteus.blogspot.com/2014/07/Bluetooth-Kit-tutorial-01.html ) 嘗試使用 HC-05 兩個與 HC-06 共三個與樹莓派做通訊,但我沒時間寫程式測試,我使用下面這兩種方式來測試:
      HC-05 設置為 Slave ( AT+ROLE=0),其他的使用預設。
      <<1>>. 直接使用樹莓派的 bluetooth Manager " (藍牙裝置) 搜尋"三個藍牙模組,每個模組都要使用 USB 轉 TTL 線接好電源、TXD 和 RXD 共四條線並設置好模式 ( HC-05 需要手動設置 ),應該就會在螢幕上看到三個藍牙模組出現。選擇好其中一個藍牙模組,(1) 接著按下上面"加號"圖示加入裝置,(2) 按下"鑰匙" 圖示配對,要輸入連線密碼,(3) 再按下"星形"圖示加入信任 ( Trusted ),完成之後就會看到在藍牙模組的對應圖示上有鑰匙與星形圖案在上面。
      最後在該藍牙圖示上面按下滑鼠右鍵,選擇像是 VGA 接頭的圖示那個選項(或是"序列部服務"文字,我系統改成中文,所以寫中文)按下去,成功之後就會在下方出現像是 "序列埠連接建立: /dev/rfcomm0" 文字訊息出現 ( 是系統選擇號碼會不一樣 );然後重複設定其餘裝置即可建立多個連結通道。
      另一邊就是直接開起三個 SSCOM32E 並選擇相對應的 COM 號碼就可以直接與樹莓派做溝通。
      樹莓派部分要開啟三個終端機視窗,然後分別下 cat /dev/rfcomm0,cat /dev/rfcomm1 ... 到不同視窗中,就可以接收來自 SSCOM32E 的文字訊息;若要從樹莓派輸出文字訊息,就直接下 echo "文字訊息" /dev/rfcomm0 就可以傳送至與 /dev/rfcomm0 建立連結的藍牙裝置,也就是 SSCOM32E 其中一個會出現接收到的訊息。
      <<2>> 使用命令列設置,因為就是指令而以,所以自己試試,有問題上網路去者說明一下
      假設樹莓派 BT dongle = 11:22:33:44:55:66,HC-05 ( AT+ROLE=0) = 00:22:44:66:88:AA
      -- 用這個指令查樹莓派接的藍牙位址
      ## hcitool dev
      -- 用這個指令查詢遠端的藍牙裝置位址
      ## hcitool scan
      -- 開始!可使用樹莓派桌面做確認
      -- 下面指令要輸入配對密碼
      # bluez-simple-agent hci0 00:22:44:66:88:AA
      # bluez-test-device trusted 00:22:44:66:88:AA
      # rfcomm /dev/rfcomm0 00:22:44:66:88:AA 1
      然後接下來的都跟 <<1>> 一樣,直接使用 cat 與 echo 接收與傳送文字訊息就可以了。
      ******
      上面是今天剛測試可以動作的,先頂著用吧!詳細的說明網頁和程式碼有時間再整理和撰寫。

      刪除
  3. 您好我有在您的賣場購買藍芽裝置

    但是關於
    sdptool browse local

    這時在最下面的輸出資料就會看到多了 SPP Service 這一項,而緊接的數據是不是就是我們使用 Table 6.1 所建立的 Service Records。

    我做不出來

    可否有範例程是可以參考呢?

    回覆刪除
  4. 您好:
    所有關於套件所會使用程式碼都在雲端硬碟中。每個部落格網頁中所使用到的範例程式不是可由部落格網頁中所提供的連結下載就是可在雲端硬碟中找到。

    要建立 SPP Service 必須要將 SPP 登錄到 SDP Server 去,而登錄的這一部分則是在 spp_server.c 程式裡實現,執行這個程式然後再 sdptool browser local 就會看到新增的項目。

    回覆刪除
  5. 您好, 想請問您 spp-sdp-register.o這個副程式的程式碼可以在哪裡找到呢?因為下載下來就已經是.o檔.想了解他是怎麼寫的. 謝謝您!

    回覆刪除
    回覆
    1. 您好:
      spp-sdp-register.o 這程式碼可以在此網頁中找到,雖沒有提供原始碼,但是有說明如何撰寫,請自己嘗試跟據網頁中的說明去實現它。

      刪除