2013年5月28日 星期二

HFP for Linux 無法和智慧手機配對使用?

HFP for Linux 是一個在Linux上實作藍牙Hands-free profile的程式, 簡單講就是可以電腦變成藍牙耳機。這是一個開源碼的專案, 大約在2008-2009年間熱烈地進行, 之後由於主要程式貢獻者Samr7不見了,整個專案可以說是停了下來了。最後一次程式修改是在2010年四月。
專案活動情形可以由http://www.ohloh.net/p/nohands 看到。
我在試這支程式時,發現一個現象:較舊的手機可以正常的運作,但是新的智慧手機就不行。其實也不是不行,而是程式一開始會先進行手機的找尋(Inquiry),在此就找不到,因此後續的功能也自然無法使用。其實手機找尋(Inquiry)的動作是很有討論性的,也是令人混淆的。因為作業系統本身就己經做過一次這個動作了, 不管是使用blueman或者是預設的藍牙管理程式,都是必須先和手機做配對。可是nohands這支程式還是要自己做一次:


而且它的找尋(Inquiry)動作相當的快,一般都要約20秒,它只要5秒。
因為由藍牙規格來說,HFP並不會和硬體有太大的相關,所以我想這支程式雖然是幾年前開發的,但HFP 1.5也還是很有相容性才對。其實我手上有一隻Nokia BH-100的藍牙耳機是2007年買的,它就運作的很正常。因此大膽猜測,應該只是nohands在做手機的找尋(Inquiry)時,也許做了什麼事情,因此把新手機排除在外了!

首先使用hcidump程式來觀察是否底層Inquiry時就出問題,還是nohands本身自己其實有Inquiry到,但是自己另外做了篩選動作:
$ sudo hcidump
HCI sniffer - Bluetooth packet analyzer ver 2.4
device: hci0 snap_len: 1028 filter: 0xffffffff
< HCI Command: Inquiry (0x01|0x0001) plen 5
    lap 0x9e8b33 len 4 num 0
> HCI Event: Command Status (0x0f) plen 4
    Inquiry (0x01|0x0001) status 0x00 ncmd 1
> HCI Event: Extended Inquiry Result (0x2f) plen 255
    bdaddr 00:02:72:xx:xx:xx mode 1 clkoffset 0x6f93 class 0x3e0104 rssi -63
    Unknown type 0x4d with 8 bytes data
    Unknown type 0x00 with 9 bytes data
> HCI Event: Inquiry Result with RSSI (0x22) plen 15
    bdaddr 00:16:xx:xx:xx:EF mode 1 clkoffset 0x74c3 class 0x520204 rssi -52
> HCI Event: Extended Inquiry Result (0x2f) plen 255
    bdaddr 00:15:xx:xx:xx:B6 mode 1 clkoffset 0x69d2 class 0x5e0104 rssi -64
    Unknown type 0x43 with 8 bytes data
    Unknown type 0x15 with 2 bytes data
> HCI Event: Inquiry Complete (0x01) plen 1
    status 0x00
我在nohands這支程式按下search for device的按鍵,要求進行手機的找尋(Inquiry),同時觀察hcidump的輸出,如上,發現其實nohands是下了一般inquiry的hci指令,還沒有看到特别奇特的地方。而且藍牙硬體也正常的把附近的手機都找到了,只是nohands自己只有找到一支舊的手機(00:16:xx...)這支,其他新手機都沒找到。這很明顯是nohands自己的問題,只是問題是它那裡出了問題,是它會去檢查手機的class或者是會去檢查手機使用的HFP版本?
為了找到答案,只好去看程式了(還好它是開源碼)。
首先,我注意到bt.cpp檔裡的BtHci::HciInit()有一段程式:
 hci_filter_clear(&flt);
    hci_filter_set_ptype(HCI_EVENT_PKT, &flt);
    hci_filter_set_event(EVT_CMD_STATUS, &flt);
    hci_filter_set_event(EVT_INQUIRY_RESULT, &flt);
    hci_filter_set_event(EVT_INQUIRY_RESULT_WITH_RSSI, &flt);
    hci_filter_set_event(EVT_EXTENDED_INQUIRY_RESULT, &flt);  // add by ops
    hci_filter_set_event(EVT_INQUIRY_COMPLETE, &flt);
    hci_filter_set_event(EVT_REMOTE_NAME_REQ_COMPLETE, &flt);
是的,很明顯nohands對於要收送的hci指令下了篩選,此時我注意到(其實之前就懷疑)它接收的hci事件中,少了extended inquiry result,而新的手機都是回這種封包。也正因為如此,新手機都無法被nohands找到。解決方法,如上,加入我新增的那行即可。你問我怎縻知道要加入這行呢?其實,去查看一下/usr/include/bluetooth/hci.h,就會知道。
只要這様就行了嗎?當然不是!還有一個地方要跟著一起改:
同様在bt.cpp檔裡的BtHci::HciDataReadyNot()
switch (hdr->evt) {
    case EVT_CMD_STATUS:
    case HCI_COMMAND_DISALLOWED:
    case EVT_INQUIRY_RESULT: {
    case EVT_INQUIRY_RESULT_WITH_RSSI: {
    case EVT_EXTENDED_INQUIRY_RESULT: {    // ---- add by ops --------//
        uint8_t *countp;
        countp = (uint8_t *) (hdr + 1);
        ext_infop = (extended_inquiry_info *) (countp + 1);
        ret = 1;
        if (hdr->plen < ret)
            goto invalid_struct;
        count = *countp;
        ret = 1 + (count * sizeof(*ext_infop));
        if (hdr->plen != ret)
            goto invalid_struct;

        //inq_result_rssi = true;
        inq_result = EXTEND_INQ_RESULT;

    do_next_inq_extend:
        if (!count)
            break;
        listp = m_hci_tasks.next;
        while (listp != &m_hci_tasks) {
            taskp = GetContainer(listp, HciTask, m_hcit_links);
            listp = listp->next;

            if (taskp->m_tasktype == HciTask::HT_INQUIRY) {
                taskp->m_complete = false;
                bacpy(&taskp->m_bdaddr, &ext_infop->bdaddr);
                taskp->m_pscan_rep = ext_infop->pscan_rep_mode;
                taskp->m_clkoff = ext_infop->clock_offset;
                taskp->m_devclass =
                    (ext_infop->dev_class[2] << 16) |
                    (ext_infop->dev_class[1] << 8) |
                    ext_infop->dev_class[0];
                taskp->m_hcit_links.UnlinkOnly();
                tasks_done.AppendItem(taskp->m_hcit_links);
            }
        }
        break;
    }
    case EVT_INQUIRY_COMPLETE:
    case EVT_REMOTE_NAME_REQ_COMPLETE: 
 可以注意到這裡的程式結構和BtHci::HciInit()是相呼應的。之前篩選要接收的事件,在這裡都要處理一下,基本上就是把收到的封包拆解一下,取出要的資料,放入nohands自己的結構(物件)去,以便利後續的工作。
OK! 這様大致就完成了(其實還有一些小地方,例如變數的宣告,還有那個奇怪的label是要做什縻的呢?不過,主要的修改精神就只有如此)
此時,再去試試按下search for device的按鍵,神奇的事就出現了,所有新手機就出現了。
想想,其實也沒有什縻,個人猜想samr7當時可能是因為自己下hci指令可以設定timeout時間,這様不用等20秒那縻久;或者是想配合它的事件架構,讓程式可以順利的並行作業。所以才自己實作用hci指令去inquiry.但這様的缺點是,一旦有新的規格變更,就會不相容了。
所以其實也可以將程式改成使用標準的inquiry函數hci_inquiry(), 來取得手機資訊也可以解決這個問題,只是如此,可能會動到nohands程式的架構,也許工程會更大些。
又或者可以由作業系統取得己配對的手機資訊,不用自己再去inquiry一次,這様不是又更快了嗎?而且使用者也不會覺得很混淆,為什縻要inquiry兩次?這其實也不會太困難,只要問bluetoothd程式就可以了:
$ dbus-send --system --print-reply --dest=org.bluez /org/bluez/830/hci0 org.bluez.Adapter.ListDevices |awk '/object path/ {print $3}' | sed s/\"//g | sed s/_/:/g
如上,簡單地用dbus-send指令,配合awk,sed指令就可以做到,若要直接在C++中做,也是可以的,只是要研究一下dbus的溝通方式。


張貼留言