關於藍牙裝置找尋(inquiry, scan)兩三事
當我們拿到手機或平板要和藍牙耳機配對時, 配對程式做的第一件事其實是找尋(scan, inquiry)的動作, 列出周遭附近的藍牙設備名稱或MAC位址, 以供使用者選擇要和那一台配對。在Linux平台上,我們可以透過bluez來做這件事,以C語言來寫,Albert Huang的書[1]給了我們一個簡單的範例程式simplescan.c:
其實沒關係,原本的寫法還是可用的,它會找到所有藍牙設備,不管新或舊。只是沒有加快速度的好處而已。不過,hci_inquiry()函數是blocking mode,也就是程式執行至此就會卡住,必須等到它完成全部的找尋動作才會繼續往下執行。
為了這些原因,有些人會選擇自己實作,原理也不難,就是下HCI指令Inquiry(0x01|0x0001)然後藍牙控制器在收到附近藍牙設備回覆的封包時會回事件,目前可能有以下三種:
有意思的是,我在看DBus的資料時,在Vala DBus ClientSamples的網站看到Bluetooth Discovery範例程式,如下:
我們在終端機下dbus-monitor --system指令,在d-feet執行StartDiscovery(),可以看到有回許多的signal,如下:
如此,可以將vala Bluetooth Discovery範例程式改成如下,以符合bluez 4.101版本:
那麼要用python來寫inquiry的程式也是差不多的,而且好像更容易一些,我結合python, glade, dbus的技術,寫了一支GUI版的藍牙inquiry程式。如Fig.3所示,按下Start按鍵時,叫用StartDiscovery(),按下Stop按鍵時,叫用StopDiscovery()。而當收到device_found信息(signal)時,就將設備的MAC和名稱印出來。
因為python語言的特性,變數不用宣告就可以使用,所以signal handler的參數也不用宣告,那真是好寫多了,也省了很多時間。如上文所述,用vala寫,我光琢磨dict資料型態原來在vala是HashTable<,>就花了很多時間。
使用bluez的DBus API來寫bluetooth inquiry的程式(即後面討論的兩支程式,分別用vala和python寫的),似乎比用libbluetooth.so程式庫的方法好。因為它解決了blocking mode的問題,也不用擔心自己直接使用HCI指令的方式寫,日後規格若增修會造成問題。
後記:用Qt來實做也可以,概念上都一様,和vala比較像的是資料型態的部份都需要程式撰寫者精確的描述。以下程式片斷使用QDbusConnection類别的connect()函數來設定收到signal時要叫用的函數(on_device_found)。重點同様在第二個參數的資料型態,和前文討讑的一様,這是一個字典。在Qt就用QVariantMap[8]即可。其實它就是QMap<QString,QVariant>的同義詞。而QVariant若要印出來還是要轉成字串型别,此時用value<QString>()[9]就可以了。
參考來源:
[1] Albert Huang, An Introduction to Bluetooth Programming
[2] 小白和小叮叮, HFP for Linux 無法和智慧手機配對使用?
[3] wylhistory, 藍牙驅動分析及Bluez使用流程分析Guide
[4] 小白和小叮叮, D-BUS學習筆記
[5] wiki.gnome.org, Vala D-Bus Examples
[6] 小白和小叮叮, python gtk+ glade and dbus work together
[7] GNOME mail services, [Vala] Fwd: Setting up a timer with callback event
[8] jgoday, dbus problem with a compound type Qmap, QtForum.Org, 2007/8/5
[9] sherinrose, QDbusMessage parsing, QtForum.Org, 2011/9/3
#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; }基本上就是透過bluez寫好的程式庫,叫用其提供的函數hci_inquiry(),這個函數會傳回其找到的所有藍牙設備的MAC位址。為了讓使用者分得清那台是那台,程式會再利用這些MAC位址去找到其對應的名稱,同様也是叫用bluez提供的函數hci_read_remote_name()。所以這是兩階段的作法,其實是比較過時的寫法,因為找尋(scan, inquiry)本身就很耗時,找到後還要再一台一台去問名字,那會讓使用者等更久。所以這兩年藍牙規格有新加入一個作法(Extended Inquiry Result, EIR),簡單講就是以前的找尋動作,附近的藍牙設備原本只回覆MAC位址,現在除了回覆MAC位址外,同時還加上名稱等資訊。這様可以一次做完,至少等待時間少了一半。很好,但是舊設備怎縻辦呢?
其實沒關係,原本的寫法還是可用的,它會找到所有藍牙設備,不管新或舊。只是沒有加快速度的好處而已。不過,hci_inquiry()函數是blocking mode,也就是程式執行至此就會卡住,必須等到它完成全部的找尋動作才會繼續往下執行。
為了這些原因,有些人會選擇自己實作,原理也不難,就是下HCI指令Inquiry(0x01|0x0001)然後藍牙控制器在收到附近藍牙設備回覆的封包時會回事件,目前可能有以下三種:
- Inquiry Result
- Inquiry Result with RSSI (0x22)
- Extended Inquiry Result (0x2f)
Fig.1 |
[DBus (name = "org.bluez.Adapter")] interface Bluez : Object { public signal void discovery_started (); public signal void discovery_completed (); public signal void remote_device_found (string address, uint klass, int rssi); public signal void remote_name_updated (string address, string name); public abstract void discover_devices () throws IOError; } MainLoop loop; void on_remote_device_found (string address, uint klass, int rssi) { stdout.printf ("Remote device found (%s, %u, %d)\n", address, klass, rssi); } void on_discovery_started () { stdout.printf ("Discovery started\n"); } void on_remote_name_updated (string address, string name) { stdout.printf ("Remote name updated (%s, %s)\n", address, name); } void on_discovery_completed () { stdout.printf ("Discovery completed\n"); loop.quit (); } int main () { Bluez bluez; try { bluez = Bus.get_proxy_sync (BusType.SYSTEM, "org.bluez", "/org/bluez/hci0"); // Connect to D-Bus signals bluez.remote_device_found.connect (on_remote_device_found); bluez.discovery_started.connect (on_discovery_started); bluez.discovery_completed.connect (on_discovery_completed); bluez.remote_name_updated.connect (on_remote_name_updated); // Async D-Bus call bluez.discover_devices (); } catch (IOError e) { stderr.printf ("%s\n", e.message); return 1; } loop = new MainLoop (); loop.run (); return 0; }這是另一種方式來寫藍牙設備找尋,並不需要用到bluez的程式庫libbluetooth.so,但是要用到bluez的DBus界面。wylhistory[3]寫的文件藍牙驅動分析及Bluez使用流程分析的18頁也提到相同的方法,但是它是用C語言寫的:
scan_bt_device_request = dbus_message_new_method_call(
BT_SERVICE,
BT_REQ_HCI_PATH,
BT_REQ_ADAPTER_INTERFACE,
BT_DISCOVER_DEVICES_REQ
);
PRINT_INFO;
if (scan_bt_device_request == NULL) {
ERROR;
return FALSE;
}
PRINT_INFO;
DBusMessage *reply=NULL;
if ((reply=dbus_connection_send_with_reply_and_block(get_dbus_connection(),
scan_bt_device_request, -1,NULL))==NULL) {
ERROR;
dbus_message_unref(scan_bt_device_request);
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
其中的宏定义如下:
#define BT_SERVICE "org.bluez"
#define BT_REQ_HCI_PATH "/org/bluez/hci0"
#define BT_REQ_ADAPTER_INTERFACE "org.bluez.Adapter"
#define BT_DISCOVER_DEVICES_REQ "DiscoverDevices"
注意到這兩支程式其實都是叫用同一個DBus的函數DiscoverDevices(好啦!名字有點不太一様),很可惜的是,我在Ubuntu 12.10的電腦上跑會出現錯誤訊息,説DiscoverDevices這個函數不存在。其實Ubuntu 12.10用的bluez是4.101版,而wylhistory[3]自己有説他討讑的是bluez 3.22,總之是bluez版本不同的問題。所以用d-feet看一下bluez 4.101的DBus API為何?如Fig.2所示,真的沒有DiscoverDevices,而是StartDiscovery()以及StopDiscovery()。Fig.2 |
signal sender=:1.2 -> dest=(null destination) serial=342 path=/org/bluez/684/hci0; interface=org.bluez.Adapter; member=DeviceFound string "00:26:08:xx:xx:xx" array [ dict entry( string "Address" variant string "00:26:08:xx:xx:xx" ) dict entry( string "Class" variant uint32 5374984 ) dict entry( string "Icon" variant string "audio-card" ) dict entry( string "RSSI" variant int16 -63 ) dict entry( string "Name" variant string "Maxx-0" ) dict entry( string "Alias" variant string "Maxx-0" ) dict entry( string "LegacyPairing" variant boolean false ) dict entry( string "Paired" variant boolean true ) dict entry( string "Trusted" variant boolean true ) dict entry( string "UUIDs" variant array [ string "0000112d-0000-1000-8000-00805f9b34fb" string "0000110c-0000-1000-8000-00805f9b34fb" string "0000110e-0000-1000-8000-00805f9b34fb" string "00001103-0000-1000-8000-00805f9b34fb" string "00001105-0000-1000-8000-00805f9b34fb" string "00001106-0000-1000-8000-00805f9b34fb" ] ) ]這様很明白的是告訴我用法如下:先用org.bluez.Adapter.StartDiscovery()告訴bluez去做找尋動作(Inquiry),而bluez在找到任一藍牙設備時會以signal通知我,當我想結束inquiry時就叫用org.bluez.Adapter.StopDiscovery()就可以了。如果看到這裡你沒有這様的感覺,那縻就是對DBus的用法還不熟悉,可以先參考一下我之前的文章[4] 。
如此,可以將vala Bluetooth Discovery範例程式改成如下,以符合bluez 4.101版本:
[DBus (name = "org.bluez.Adapter")] interface Bluez : Object { public signal void device_found (string address, GLib.HashTable<string,Variant> data); public abstract void StartDiscovery () throws IOError; public abstract void StopDiscovery () throws IOError; } [DBus (name = "org.bluez.Manager")] interface Manager : Object { public abstract string DefaultAdapter () throws IOError; } MainLoop loop; Bluez bluez; Manager man; uint timerID; void on_device_found (string address, GLib.HashTable<string,Variant> data) { stdout.printf ("Remote device found (%s, %s)\n", address, data.get("Name").get_string()); } public bool on_timer_event () { //stdout.printf ("timer event\n"); bluez.StopDiscovery(); loop.quit(); return true; } int main () { try { man = Bus.get_proxy_sync (BusType.SYSTEM, "org.bluez", "/"); string path = man.DefaultAdapter(); bluez = Bus.get_proxy_sync (BusType.SYSTEM, "org.bluez", path); // Connect to D-Bus signals bluez.device_found.connect (on_device_found); // Async D-Bus call bluez.StartDiscovery (); timerID = Timeout.add(10000, on_timer_event); } catch (IOError e) { stderr.printf ("%s\n", e.message); return 1; } loop = new MainLoop (); loop.run (); return 0; }主要的修改為:
- 如上文討論改用StartDiscovery()
- 必須先找到正確的object path
- 加入timeout, 在10秒後叫用StopDiscovery()並結束程式, 見[7]
那麼要用python來寫inquiry的程式也是差不多的,而且好像更容易一些,我結合python, glade, dbus的技術,寫了一支GUI版的藍牙inquiry程式。如Fig.3所示,按下Start按鍵時,叫用StartDiscovery(),按下Stop按鍵時,叫用StopDiscovery()。而當收到device_found信息(signal)時,就將設備的MAC和名稱印出來。
Fig.3 |
#! /usr/bin/env python from gi.repository import Gtk # dbus imports import dbus import dbus.service from dbus.mainloop.glib import DBusGMainLoop class Adapter: def __init__(self): bus = dbus.SystemBus() manager = dbus.Interface(bus.get_object("org.bluez", "/"), "org.bluez.Manager") path = manager.DefaultAdapter() self.adapter = dbus.Interface(bus.get_object("org.bluez", path), "org.bluez.Adapter") def startInquiry(self): self.adapter.StartDiscovery() def stopInquiry(self): self.adapter.StopDiscovery() class SignalRecipient: def __init__(self, window): self.window = window bus = dbus.SystemBus() bus.add_signal_receiver(self.handler, dbus_interface="org.bluez.Adapter", signal_name="DeviceFound") def handler(self, address, data): device = "%s, %s\n" % (address, data.get('Name', 0)) print device self.window.printText(device) class Window: def printText(self, text): self.text_buffer.insert_at_cursor(text) def onDeleteWindow(self, *args): Gtk.main_quit(*args) def onStartButtonPressed(self, button): print "Start inquiry" self.adapter.startInquiry() self.printText("Start inquiry\n") def onStopButtonPressed(self, button): print "Stop inquiry" self.adapter.stopInquiry() self.printText("Stop inquiry\n") def __init__(self, gladeFilePath, adapter): self.adapter = adapter self.builder = Gtk.Builder() self.builder.add_from_file(gladeFilePath) self.builder.connect_signals(self) self.window = self.builder.get_object("window1") self.text_view = self.builder.get_object("textview1") self.text_buffer = self.text_view.get_buffer() self.window.show_all() if __name__=="__main__": DBusGMainLoop(set_as_default=True) adapter = Adapter() gladeFilePath = "inquiry.glade" windowOne = Window(gladeFilePath, adapter) recipient = SignalRecipient(windowOne) Gtk.main()這裡,我寫了三個Class,分別是Adapter用來指到bluez的dbus api並由其負責叫用StartDiscovery()及StopDiscovery(),而Window則專責GUI界面的工作包括在其文字區列印文字和每個按鍵的動作反應,但是如果是要dbus作動的部份則叫用Adapter來工作。第三個Class SignalRecipient則專責接收signal DeviceFound的工作,並叫用Window來列印出相關資訊。更詳細的資訊可以參考我之前的文章[6]。
因為python語言的特性,變數不用宣告就可以使用,所以signal handler的參數也不用宣告,那真是好寫多了,也省了很多時間。如上文所述,用vala寫,我光琢磨dict資料型態原來在vala是HashTable<,>就花了很多時間。
使用bluez的DBus API來寫bluetooth inquiry的程式(即後面討論的兩支程式,分別用vala和python寫的),似乎比用libbluetooth.so程式庫的方法好。因為它解決了blocking mode的問題,也不用擔心自己直接使用HCI指令的方式寫,日後規格若增修會造成問題。
後記:用Qt來實做也可以,概念上都一様,和vala比較像的是資料型態的部份都需要程式撰寫者精確的描述。以下程式片斷使用QDbusConnection類别的connect()函數來設定收到signal時要叫用的函數(on_device_found)。重點同様在第二個參數的資料型態,和前文討讑的一様,這是一個字典。在Qt就用QVariantMap[8]即可。其實它就是QMap<QString,QVariant>的同義詞。而QVariant若要印出來還是要轉成字串型别,此時用value<QString>()[9]就可以了。
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); setCentralWidget(ui->plainTextEdit); QDBusConnection bus = QDBusConnection::systemBus(); bus.connect("","","org.bluez.Adapter","DeviceFound", this, SLOT(on_device_found(QString, QVariantMap))); } void MainWindow::on_device_found(QString address, QVariantMap data) { ui->plainTextEdit->insertPlainText(address + " " + data["Name"].value<QString>() + "\n"); }
參考來源:
[1] Albert Huang, An Introduction to Bluetooth Programming
[2] 小白和小叮叮, HFP for Linux 無法和智慧手機配對使用?
[3] wylhistory, 藍牙驅動分析及Bluez使用流程分析Guide
[4] 小白和小叮叮, D-BUS學習筆記
[5] wiki.gnome.org, Vala D-Bus Examples
[6] 小白和小叮叮, python gtk+ glade and dbus work together
[7] GNOME mail services, [Vala] Fwd: Setting up a timer with callback event
[8] jgoday, dbus problem with a compound type Qmap, QtForum.Org, 2007/8/5
[9] sherinrose, QDbusMessage parsing, QtForum.Org, 2011/9/3
留言