基於BlueZ的C語言藍芽編程
有很多理由促使我們選用C替代其他進階語言來例如Python來開發藍芽應用程式。Python環境可能並不適合於嵌入式系統。因為嵌入式系統對程式的大小,運行速度,和佔用的儲存空間有嚴格的限制,這些都使得像Python之類的解釋性語言無法在嵌入式系統上應用。程式員需要對本地的藍芽適配器進行更好的控制,或者需要建立一套動態連結程式庫以便於其他應用程式的連結以取代單一的應用程式。就像上述描述的這些,BlueZ是一款強大的藍芽通訊協定棧,它擴充的API使得使用者方便操縱大量的藍芽資源。但是BlueZ沒有官方的描述文檔,甚至非官方的文檔也寥寥無幾。初學者在BlueZ的官方郵件清單上請求相關的文檔,通常的得到的回複是被告知請通過仔細閱讀原始碼來瞭解API的功能。閱讀BlueZ的原始碼對於初學者來說是一項相當費時的工作,在短期內取得的進展是相當有限的,很可能成為很多藍芽編程初學者的攔路虎。
本章簡要敘述了基於BlueZ的C語言藍芽編程的方法。本章為C程式員進一步闡述了第二章中涉及的知識點。
4.1 選擇一個通訊的對象
Example 4-1是一個尋找周邊藍牙裝置的簡單應用程式。程式首先擷取系統的藍牙裝置號,掃描周邊的藍牙裝置,然後尋找每一個被搜尋到的藍牙裝置的名稱。後邊有對資料結構和函數的詳細描述。
Example 4-1. simplescan.c
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char **argv)
{
inquiry_info *ii = NULL;
int max_rsp, num_rsp;
int dev_id, sock, len, flags;
int i;
char addr[19] = { 0 };
char name[248] = { 0 };
dev_id = hci_get_route(NULL);
sock = hci_open_dev( dev_id );
if (dev_id < 0 || sock < 0) {
perror("opening socket");
exit(1);
}
len = 8;
max_rsp = 255;
flags = IREQ_CACHE_FLUSH;
ii = (inquiry_info*)malloc(max_rsp * sizeof(inquiry_info));
num_rsp = hci_inquiry(dev_id, len, max_rsp, NULL, &ii, flags);
if( num_rsp < 0 ) perror("hci_inquiry");
for (i = 0; i < num_rsp; i++) {
ba2str(&(ii+i)->bdaddr, addr);
memset(name, 0, sizeof(name));
if (hci_read_remote_name(sock, &(ii+i)->bdaddr, sizeof(name),
name, 0) < 0)
strcpy(name, "[unknown]");
printf("%s %s/n", addr, name);
}
free( ii );
close( sock );
return 0;
}
4.1.1 編譯
編譯需要使用gcc連結libbluetooth這個庫。
# gcc -o simplescan simplescan.c -lbluetooth
4.1.2. 解釋
typedef struct {
uint8_t b[6];
} __attribute__((packed)) bdaddr_t;
藍牙裝置的地址採用結構體bdaddr_t來描述,BlueZ中隊藍芽地址的儲存和操縱都使用bdaddr_t結構體,BlueZ提供兩個函數來進行字串到藍芽地址的轉換。
int str2ba( const char *str, bdaddr_t *ba );
int ba2str( const bdaddr_t *ba, char *str );
str2ba把形如XX:XX:XX:XX:XX:XX(XX標識48位藍芽地址的16進位的一個位元組)的字串轉化6位元組的bdaddr_t結構, ba2str完成相反的功能。
本地藍芽適配器被分配一個從0開始的識別號碼。程式在分配系統資源時必須指定使用那一個藍芽適配器,通常的話系統只有一個藍芽適配器,把參數NULL傳給hci_get_route可以獲得第一個有效藍芽適配器識別號。
int hci_get_route( bdaddr_t *bdaddr );
int hci_open_dev( int dev_id );
[note]將適配器的裝置號指定為0是不恰當的,因為它並不總代表第一個可用的藍芽適配器。例如系統有兩個藍芽適配器,第一個被disable掉了,那麼第一個有效裝置號就是2。
如果存在多個藍芽適配器,選擇"01:23:45:67:89:AB"作為藍芽適配器的地址, 將指示這個地址的指標char *representation傳給hci_devid函數,用這個函數替代hci_get_route。
很多藍芽操作都需要開啟一個套介面, hci_open_dev函數可以開啟特定資源號的一個套介面,確切的說hci_open_dev開啟的通訊端建立了一條和本地藍芽適配器控制器的串連,而不是和遠端藍牙裝置的串連。使用這個套介面發送命令到藍芽控制器可以實現底層的藍芽操作,這部分在4.5中有詳細的討論。
選擇好本地藍芽適配器並進行系統資源分派後,程式就可以開始掃描周邊的藍牙裝置了,在這個常式中,hci_inquiry函數完成對藍牙裝置的搜尋,並將返回的裝置資訊資料記錄在變數ii中。遇到錯誤時,它將返回-1並設定errno變數。
int hci_inquiry(int dev_id, int len, int max_rsp, const uint8_t *lap,
inquiry_info **ii, long flags);
hci_inquiry 的參數需要使用裝置資源號而非套介面,所以我們使用hci_get_route函數的傳回值dev_id傳遞給它。查詢時間最長持續1.28 * len秒。max_rsp個設別返回的資訊都被儲存在變數ii中,這個變數必須有足夠的空間來儲存max_rsp返回的結果。我們推薦max_rsp取值 255來完成標準10.24秒的查詢工作。
如果標誌位flag設定為IREQ_CACHE_FLUSH,那麼在進行查詢操作時會把先前一次查詢記錄的cache重新整理,否則flag設定為0的話,即便先前查詢的裝置已經不處於有效範圍內,先前查詢的記錄也將被返回。
inquiry_info結構體定義如下
typedef struct {
bdaddr_t bdaddr;
uint8_t pscan_rep_mode;
uint8_t pscan_period_mode;
uint8_t pscan_mode;
uint8_t dev_class[3];
uint16_t clock_offset;
} __attribute__ ((packed)) inquiry_info;
在大多數場合,我們僅用到成員bdaddr,它標識了裝置的藍芽地址。有些場合我們也會用到成員dev_class,它標識了被檢測到的藍牙裝置的一些資訊(例如,識別這個裝置是列印裝置,電話,個人電腦等),詳細地對應關係可以參見藍牙裝置分配號[3]。其餘的成員在用於底層通訊,一般情況並不常用。感興趣的讀者可以閱讀藍芽核心規範[4]擷取更多的資訊。一旦周圍的藍牙裝置和其藍芽地址被檢測到,程式可以將此裝置的名稱提供給使用者,hci_read_remote_name函數可以完成這個功能。
int hci_read_remote_name(int sock, const bdaddr_t *ba, int len,
char *name, int timeout)
hci_read_remote_name函數在規定的逾時時間內使用套介面通過藍芽地址ba去擷取藍牙裝置的名稱,成功返回0,並將擷取的藍牙裝置名稱存入name中;失敗時返回-1並設定相應的errno。
Notes
[1]http://www.bluez.org/lists.html
[2] for the curious, it makes a call to socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_HCI), followed by a call to bind with the specified resource number.
[3]https://www.bluetooth.org/foundry/assignnumb/document/baseband
[4]http://www.bluetooth.org/spec