2013年9月18日 星期三

關於藍牙裝置找尋(inquiry, scan)兩三事

當我們拿到手機或平板要和藍牙耳機配對時, 配對程式做的第一件事其實是找尋(scan, inquiry)的動作, 列出周遭附近的藍牙設備名稱或MAC位址, 以供使用者選擇要和那一台配對。在Linux平台上,我們可以透過bluez來做這件事,以C語言來寫,Albert Huang的書[1]給了我們一個簡單的範例程式simplescan.c
#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)
如果是收到EIR,那其封包本身就含有名稱資訊,就不用再去問人家一次了。如果是收到其他兩種事件就要再去問一次名稱。同時也可以寫成nonblocking mode的架構,在下完HCI Inquiry指今後,就交回程式控制權,讓叫用的程式可以繼續往下執行;再利用某種機制,讓原叫用者可以收到HCI Inquiry Result事件的結果(也就是MAC, 名稱)。在Linux上實作HFP的程式HFP for Linux就是自己實作找尋,可是自己實作的缺點在於,若藍牙規格有增修時,就會出問題,我之前的文章[2]就有討讑過。其實Pybluez也有相同的問題,因為我們使用Ubuntu的apt-get來完裝Pybluez,結果它的套件裝到0.18版的Pybluez,而EIR的修正是0.19版才有。所以在試asynchronous-inquiry.py的程式時,也是發生新手機都找不到,只找到舊手機的情況,後來還自己去改了Pybluez的bluez.py程式,解決了這個問題。但後來想想,去検查其原始程式,果然最新版的Pybluez已經有修正了。
Fig.1
有意思的是,我在看DBus的資料時,在Vala DBus ClientSamples的網站看到Bluetooth Discovery範例程式,如下:
[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
我們在終端機下dbus-monitor --system指令,在d-feet執行StartDiscovery(),可以看到有回許多的signal,如下:
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;
}
主要的修改為:
  1. 如上文討論改用StartDiscovery()
  2. 必須先找到正確的object path
  3. 加入timeout, 在10秒後叫用StopDiscovery()並結束程式, 見[7]
這裡比較困難的地方在device_found()這個signal的第二個參數是一個字典(由d-feet工具觀察到的),那在vala中是對應到什麼資料型態呢?我在Vala D-Bus Server Example[5]的網頁找到一個Data Type表格,其中給了答案,就是GLib.HashTable<,>

那麼要用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
張貼留言