編輯:關於Android編程
關鍵詞:藍牙blueZ UART HCI_UART H4 HCI L2CAP RFCOMM
版本:基於android4.2之前版本 bluez
內核:linux/linux3.08
系統:android/android4.1.3.4
作者:xubin341719(歡迎轉載,請注明作者,請尊重版權謝謝)
歡迎指正錯誤,共同學習、共同進步!!
一、Android Bluetooth Architecture藍牙代碼架構部分(google 官方藍牙框架)
Android的藍牙系統,自下而上包括以下一些內容如上圖所示:
1、串口驅動
Linux的內核的藍牙驅動程、Linux的內核的藍牙協議的層
2、BlueZ的適配器
BlueZ的(藍牙在用戶空間的函式庫)
bluez代碼結構 類名 作用 BluetoothAdapter 本地藍牙設備的適配類,所有的藍牙操作都要通過該類完成 BluetoothClass 用於描述遠端設備的類型,特點等信息 BluetoothDevice 藍牙設備類,代表了藍牙通訊過程中的遠端設備 BluetoothServerSocket 藍牙設備服務端,類似ServerSocket BluetoothSocket 藍牙設備客戶端,類似Socket BluetoothClass.Device 藍牙關於設備信息 BluetoothClass.Device.Major 藍牙設備管理 BluetoothClass.Service 藍牙相關服務 同樣下圖也是一張比較經典的藍牙代碼架構圖(google官方提供) 二、藍牙通過Hciattach啟動串口流程: 2、展訊hciattach代碼實現流程: 三、具體代碼分析
Bluetooth協議棧BlueZ分為兩部分:內核代碼和用戶態程序及工具集。
(1)、內核代碼:<喎?/kf/ware/vc/" target="_blank" class="keylink">vc3Ryb25nPtPJQmx1ZVq6y9DE0K3S6brNx/22r7PM0PLX6bPJPGJyIC8+CUJsdWV0b290aNCt0unKtc/W1NrE2rrL1LS0+sLrIGtlcm5lbC9uZXQvYmx1ZXRvb3Ro1tCho7D8wKhoY2ksbDJjYXAsaGlko6xyZmNvbW0sc2NvLFNEUCxCTkVQtcjQrdLptcTKtc/WoaM8YnIgLz48c3Ryb25nPqOoMqOpoaLH/bavs8zQ8qO6PC9zdHJvbmc+a2VybmVsL2RyaXZlci9ibHVldG9vdGjW0Cyw/LqsTGludXhrZXJuZWy21Lj31ta907/atcQ8YnIgLz4JQmx1ZXRvb3RoIGRldmljZbXEx/22ryzI56O6VVNCvdO/2qOstK6/2rXIoaM8YnIgLz48c3Ryb25nPqOoM6OpoaLTw7unzKyzzNDyvLC5pL7fvK+jujxiciAvPjwvc3Ryb25nPgmw/MCo06bTw7PM0PK907/aus1CbHVlWrmkvt+8r6GjQmx1ZVrM4bmpuq/K/b/i0tS8sNOm08OzzNDyvdO/2qOssePT2rPM0PLUsb+qt6JibHVldG9vdGjTptPDs8zQ8qGjQmx1ZVogdXRpbHPKx9b30qq5pL7fvK+jrMq1z9a21GJsdWV0b290aMnosbi1xLP1yry7r7rNv9jWxqGjPC9wPjxwPjxzdHJvbmc+M6GiwLbRwM/gudi1xNOm08OzzNDyvdO/2jxiciAvPjwvc3Ryb25nPglBbmRyb2lkLmJ1bGV0b290aLD81tC1xLj3uPZDbGFzc6OowLbRwNTav/K83LLjtcTE2sjdLS0tLS1qYXZho6k8L3A+PHRhYmxlIGJvcmRlcj0="1" cellspacing="0" cellpadding="0">
1、hciattach總體流程
1、initrc中定義
idh.code\device\sprd\sp8830ec_nwcn\init.sc8830.rcservice hciattach /system/bin/hciattach -n /dev/sttybt0 sprd_shark
socket bluetooth stream 660 bluetooth bluetooth
user bluetooth
group wifi bluetooth net_bt_admin net_bt inet net_raw net_admin system
disabled
oneshot
adb 下/dev/ttybt0(不同平台有所不同)
PS 進程中:hicattch
2、/system/bin/hciattach 執行的Main函數
idh.code\external\bluetooth\bluez\tools\hciattach.c
service hciattach /system/bin/hciattach -n /dev/sttybt0 sprd_shark
傳進兩個參數,/dev/sttybt0 和 sprd_shark
nt main(int argc, char *argv[]) { ……………… for (n = 0; optind < argc; n++, optind++) { char *opt; opt = argv[optind]; switch(n) { case 0://(1)、解析驅動的位置; dev[0] = 0; if (!strchr(opt, '/')) strcpy(dev, "/dev/"); strcat(dev, opt); break; case 1://(2)、解析串口的配置相關參數; if (strchr(argv[optind], ',')) { int m_id, p_id; sscanf(argv[optind], "%x,%x", &m_id, &p_id); u = get_by_id(m_id, p_id); } else { u = get_by_type(opt); } if (!u) { fprintf(stderr, "Unknown device type or id\n"); exit(1); } break; case 2://(3)、通過對前面參數的解析,把uart[i]中的數值初始化; u->speed = atoi(argv[optind]); break; case 3: if (!strcmp("flow", argv[optind])) u->flags |= FLOW_CTL; else u->flags &= ~FLOW_CTL; break; case 4: if (!strcmp("sleep", argv[optind])) u->pm = ENABLE_PM; else u->pm = DISABLE_PM; break; case 5: u->bdaddr = argv[optind]; break; } } ……………… if (init_speed)//初始化串口速率; u->init_speed = init_speed; ……………… n = init_uart(dev, u, send_break, raw);//(4)、初始化串口; ……………… return 0; }
(1)、解析驅動的位置;
if (!strchr(opt, '/')) strcpy(dev, "/dev/"); service hciattach /system/bin/hciattach -n /dev/sttybt0 sprd_shark dev = /dev/ttyb0
(2)、解析串口的配置相關參數;獲取參數對應的結構體;
u = get_by_id(m_id, p_id); static struct uart_t * get_by_id(int m_id, int p_id) { int i; for (i = 0; uart[i].type; i++) { if (uart[i].m_id == m_id && uart[i].p_id == p_id) return &uart[i]; } return NULL; }
這個函數比較簡單,通過循環對比,如傳進了的參數sprd_shark和uart結構體中的對比,找到對應的數組。如果是其他藍牙芯片,如博通、RDA、BEKN等著到其相對應的初始化配置函數。
struct uart_t uart[] = { { "any", 0x0000, 0x0000, HCI_UART_H4, 115200, 115200, FLOW_CTL, DISABLE_PM, NULL, NULL }, { "sprd_shark", 0x0000, 0x0000, HCI_UART_H4, 115200, 115200, FLOW_CTL, DISABLE_PM, NULL, init_sprd_config }, { "ericsson", 0x0000, 0x0000, HCI_UART_H4, 57600, 115200, FLOW_CTL, DISABLE_PM, NULL, ericsson }, ……………… { "bk3211", 0x0000, 0x0000, HCI_UART_BCSP, 115200, 921600, 0, DISABLE_PM, NULL, beken_init, NULL}, { NULL, 0 } };
注意:init_sprd_config這個函數在uart_init中用到,這個函數其實對我們具體芯片的初始化配置。
注釋:HCI_UART_H4和HCI_UART_BCSP的區別如下圖。
(3)、通過對前面參數的解析,把uart[i]中的數值初始化;
u->speed = atoi(argv[optind]); break;
(4)、初始化串口;
n = init_uart(dev, u, send_break, raw); idh.code\external\bluetooth\bluez\tools\hciattach.c /* Initialize UART driver */ int init_uart(char *dev, struct uart_t *u, int send_break) { struct termios ti; int fd, i; fd = open(dev, O_RDWR | O_NOCTTY);//打開串口設備,其中標志 //O_RDWR,可以對此設備進行讀寫操作; //O_NOCTTY:告訴Unix這個程序不想成為“控制終端”控制的程序,不說明這個標志的話,任何輸入都會影響你的程序。 //O_NDELAY:告訴Unix這個程序不關心DCD信號線狀態,即其他端口是否運行,不說明這個標志的話,該程序就會在DCD信號線為低電平時停止。 //但是不要以控制 tty 的模式,因為我們並不希望在發送 Ctrl-C 後結束此進程 if (fd < 0) { perror(“Can’t open serial port”); return -1; } //drop fd’s data; tcflush(fd, TCIOFLUSH);//清空數據線 if (tcgetattr(fd, &ti) < 0) { perror(“Can’t get port settings”); return -1; } cfmakeraw(&ti); cfmakeraw sets the terminal attributes as follows://此函數設置串口終端的以下這些屬性, termios_p->c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP |INLCR|IGNCR|ICRNL|IXON); termios_p->c_oflag &= ~OPOST; termios_p->c_lflag &= ~(ECHO|ECHONL|ICANON|ISIG|IEXTEN); termios_p->c_cflag &= ~(CSIZE|PARENB) ; termios_p->c_cflag |=CS8; ti.c_cflag |= CLOCAL;//本地連接,無調制解調器控制 if (u->flags & FLOW_CTL) ti.c_cflag |= CRTSCTS;//輸出硬件流控 else ti.c_cflag &= ~CRTSCTS; if (tcsetattr(fd, TCSANOW, &ti) < 0) {//啟動新的串口設置 perror(“Can’t set port settings”); return -1; } /* Set initial baudrate */ if (set_speed(fd, &ti, u->init_speed) < 0) {//設置串口的傳輸速率bps, 也可以使 //用 cfsetispeed 和 cfsetospeed 來設置 perror(“Can’t set initial baud rate”); return -1; } tcflush(fd, TCIOFLUSH);//清空數據線 if (send_break) tcsendbreak(fd, 0); //int tcsendbreak ( int fd, int duration );Sends a break for //the given time.在串口線上發送0值,至少維持0.25秒。 //If duration is 0, it transmits zero-valued bits for at least 0.25 seconds, and //not more than 0.5seconds. //where place register u’s init function; if (u->init && u->init(fd, u, &ti) < 0) //所有bluez支持的藍牙串口設備類型構成了一個uart結構數組,通過 //查找對應的uart類型,這個uart的init成員顯示了它的init調用方法; struct uart_t uart[] = { { "any", 0x0000, 0x0000, HCI_UART_H4, 115200, 115200,FLOW_CTL, DISABLE_PM, NULL, NULL }, { "sprd_shark", 0x0000, 0x0000, HCI_UART_H4, 115200, 115200,FLOW_CTL, DISABLE_PM, NULL, init_sprd_config }, { "ericsson", 0x0000, 0x0000, HCI_UART_H4, 57600, 115200,FLOW_CTL, DISABLE_PM, NULL, ericsson }, ……………… { "bk3211", 0x0000, 0x0000, HCI_UART_BCSP, 115200, 921600, 0, DISABLE_PM, NULL, beken_init, NULL}, { NULL, 0的init函數名為bcsp,定義在本文件中**; return -1; tcflush(fd, TCIOFLUSH);//清空數據線 /* Set actual baudrate */ if (set_speed(fd, &ti, u->speed) < 0) { perror(“Can’t set baud rate”); return -1; } /* Set TTY to N_HCI line discipline */ i = N_HCI; if (ioctl(fd, TIOCSETD, &i) < 0) {// TIOCSETD int *ldisc//改變到 i 行規,即hci行規 Change to the new line discipline pointed to by ldisc. The available line disciplines are listed in /* ioctl (fd, TIOCSERGETLSR, &result) where result may be as below */ /* line disciplines */ #define N_TTY 0 …… #define N_HCI 15 /* Bluetooth HCI UART */ perror(“Can’t set line discipline”); return -1; } if (ioctl(fd, HCIUARTSETPROTO, u->proto) < 0) { //設置hci設備的proto操作函數集為hci_uart操作集; perror(“Can’t set device”); return -1; } return fd; }
這裡一個重要的部分是:u->init指向init_sprd_config
4、uart具體到芯片的初始化init_sprd_config(這部分根據不同的芯片,對應進入其相應初始化部分)
idh.code\external\bluetooth\bluez\tools\hciattach_sprd.c
int sprd_config_init(int fd, char *bdaddr, struct termios *ti) { int i,psk_fd,fd_btaddr,ret = 0,r,size=0,read_btmac=0; unsigned char resp[30]; BT_PSKEY_CONFIG_T bt_para_tmp; char bt_mac[30] = {0}; char bt_mac_tmp[20] = {0}; uint8 bt_mac_bin[32] = {0}; fprintf(stderr,"init_sprd_config in \n"); //(1)、這部分檢查bt_mac,如果存在,從文件中讀取,如果不存在,隨機生成,並寫入相應文件; if(access(BT_MAC_FILE, F_OK) == 0) {//這部分檢查bt_mac LOGD("%s: %s exists",__FUNCTION__, BT_MAC_FILE); fd_btaddr = open(BT_MAC_FILE, O_RDWR);// #define BT_MAC_FILE "/productinfo/btmac.txt" if(fd_btaddr>=0) { size = read(fd_btaddr, bt_mac, sizeof(bt_mac));//讀取BT_MAC_FILE中的地址; LOGD("%s: read %s %s, size=%d",__FUNCTION__, BT_MAC_FILE, bt_mac, size); if(size == BT_RAND_MAC_LENGTH){ LOGD("bt mac already exists, no need to random it"); fprintf(stderr, "read btmac ok \n"); read_btmac=1; } ………… }else{//如果不存在,就隨機生成一個bt_mac地址,寫入/productinfo/btmac.txt fprintf(stderr, "btmac.txt not exsit!\n"); read_btmac=0; mac_rand(bt_mac); LOGD("bt random mac=%s",bt_mac); printf("bt_mac=%s\n",bt_mac); write_btmac2file(bt_mac); fd_btaddr = open(BT_MAC_FILE, O_RDWR); if(fd_btaddr>=0) { size = read(fd_btaddr, bt_mac, sizeof(bt_mac)); LOGD("%s: read %s %s, size=%d",__FUNCTION__, BT_MAC_FILE, bt_mac, size); if(size == BT_RAND_MAC_LENGTH){ LOGD("bt mac already exists, no need to random it"); fprintf(stderr, "read btmac ok \n"); read_btmac=1; } close(fd_btaddr); ………… } /* Reset the BT Chip */ memset(resp, 0, sizeof(resp)); memset(&bt_para_tmp, 0, sizeof(BT_PSKEY_CONFIG_T) ); ret = getPskeyFromFile( (void *)(&bt_para_tmp) );//ret = get_pskey_from_file(&bt_para_tmp);//(2)、PSKey參數、射頻參數的設定; if(ret != 0){//參數失敗處理 fprintf(stderr, "get_pskey_from_file faill \n"); /* Send command from hciattach*/ if(read_btmac == 1){ memcpy(bt_para_setting.device_addr, bt_mac_bin, sizeof(bt_para_setting.device_addr));// (3)、讀取失敗,把bt_para_setting中defaut參數寫入; } if (write(fd, (char *)&bt_para_setting, sizeof(BT_PSKEY_CONFIG_T)) != sizeof(BT_PSKEY_CONFIG_T)) { fprintf(stderr, "Failed to write reset command\n"); return -1; } }else{//getpskey成功處理 /* Send command from pskey_bt.txt*/ if(read_btmac == 1){ memcpy(bt_para_tmp.device_addr, bt_mac_bin, sizeof(bt_para_tmp.device_addr)); } ………… return 0; }
(1)、這部分檢查bt_mac,如果存在,從文件中讀取,如果不存在,隨機生成,並寫入相應文件/productinfo/btmac.txt;
(2)、PSKey參數、射頻參數的設定;
get_pskey_from_file(&bt_para_tmp);這個函數後面分析;
(3)、讀取失敗,把bt_para_setting中defaut參數寫入;頻率、主從設備設定等……
// pskey file structure default value BT_PSKEY_CONFIG_T bt_para_setting={ 5, 0, 0, 0, 0, 0x18cba80, 0x001f00, 0x1e, {0x7a00,0x7600,0x7200,0x5200,0x2300,0x0300}, ………… };
5、get_pskey_from_file 解析相關射頻參數
idh.code\external\bluetooth\bluez\tools\pskey_get.c
int getPskeyFromFile(void *pData) { ………… char *BOARD_TYPE_PATH = "/dev/board_type";//(1)、判斷PCB的版本; int fd_board_type; char board_type_str[MAX_BOARD_TYPE_LEN] = {0}; int board_type; char *CFG_2351_PATH_2 = "/productinfo/2351_connectivity_configure.ini";//(2)、最終生成ini文件存儲的位置; char *CFG_2351_PATH[MAX_BOARD_TYPE]; (3)、針對不同PCB版本,不同的ini配置文件; CFG_2351_PATH[0] = "/system/etc/wifi/2351_connectivity_configure_hw100.ini"; CFG_2351_PATH[1] = "/system/etc/wifi/2351_connectivity_configure_hw102.ini"; CFG_2351_PATH[2] = "/system/etc/wifi/2351_connectivity_configure_hw104.ini";
(4)、下面函數就不做具體分析,大致意識是,根據/dev/board_type中,讀取的PCB類型,設置不同的ini文件。
……………… ret = chmod(CFG_2351_PATH_2, 0644); ALOGE("chmod 0664 %s ret:%d\n", CFG_2351_PATH_2, ret); if(pBuf == pBuf2) free(pBuf1); ……………… }
(1)、判斷PCB的版本;
char *BOARD_TYPE_PATH = "/dev/board_type";
(2)、最終生成ini文件存儲的位置,就是系統運行時讀取ini文件的地方;
char *CFG_2351_PATH_2 ="/productinfo/2351_connectivity_configure.ini";
(3)、針對不同PCB版本,不同的ini配置文件;
CFG_2351_PATH[0] = "/system/etc/wifi/2351_connectivity_configure_hw100.ini"; CFG_2351_PATH[1] = "/system/etc/wifi/2351_connectivity_configure_hw102.ini"; CFG_2351_PATH[2] = "/system/etc/wifi/2351_connectivity_configure_hw104.ini";
(4)、下面函數就不做具體分析,大致意識是,根據/dev/board_type中,讀取的PCB類型,設置不同的ini文件。 覆蓋到(2)中的文件。
四、HCI_UART_H4和H4層的加入
uart->hci_uart->Uart-H4->hci:從uart開始分析,介紹整個驅動層數據流(涉及tty_uart中斷, 線路層ldisc_bcsp、tasklet、work queue、skb_buffer的等)
這是數據的流動過程,最底層的也就是和硬件打交道的是uart層了,它的存在和起作用是通過串口驅動來保證的,這個請參閱附錄,但是其它的層我們都不知道什麼時候work的,下面來看。
1、idh.code\kernel\drivers\bluetooth\hci_ldisc.c
static int __init hci_uart_init(void) { static struct tty_ldisc_ops hci_uart_ldisc; int err; /* Register the tty discipline */ memset(&hci_uart_ldisc, 0, sizeof (hci_uart_ldisc)); hci_uart_ldisc.magic = TTY_LDISC_MAGIC; hci_uart_ldisc.name = "n_hci"; hci_uart_ldisc.open = hci_uart_tty_open; hci_uart_ldisc.close = hci_uart_tty_close; hci_uart_ldisc.read = hci_uart_tty_read; hci_uart_ldisc.write = hci_uart_tty_write; hci_uart_ldisc.ioctl = hci_uart_tty_ioctl; hci_uart_ldisc.poll = hci_uart_tty_poll; hci_uart_ldisc.receive_buf = hci_uart_tty_receive; hci_uart_ldisc.write_wakeup = hci_uart_tty_wakeup; hci_uart_ldisc.owner = THIS_MODULE; if ((err = tty_register_ldisc(N_HCI, &hci_uart_ldisc))) {//(1)、這部分完成ldisc的注冊; BT_ERR("HCI line discipline registration failed. (%d)", err); return err; } #ifdef CONFIG_BT_HCIUART_H4 h4_init();//(2)、我們藍牙芯片用的是H4,這部分完成H4的注冊; #endif #ifdef CONFIG_BT_HCIUART_BCSP bcsp_init(); #endif ……………… return 0; }
(1)、這部分完成ldisc的注冊;
tty_register_ldisc(N_HCI,&hci_uart_ldisc)
注冊了一個ldisc,這是通過把新的ldisc放在一個ldisc的數組裡面實現的,tty_ldiscs是一個全局的ldisc數組裡面會根據序號對應一個ldisc,這個序號就是上層通過ioctl來指定的,比如我們在前面已經看到的:
i = N_HCI;
ioctl(fd, TIOCSETD, &i) < 0
可以看到這裡指定的N_HCI剛好就是這裡注冊的這個號碼15;
(2)、藍牙芯片用的是H4,這部分完成H4的注冊;
h4_init();
hci_uart_proto結構體的初始化:
idh.code\kernel\drivers\bluetooth\hci_h4.c
static struct hci_uart_proto h4p = { .id = HCI_UART_H4, .open = h4_open, .close = h4_close, .recv = h4_recv, .enqueue = h4_enqueue, .dequeue = h4_dequeue, .flush = h4_flush, };
H4的注冊:
idh.code\kernel\drivers\bluetooth\hci_h4.c
int __init h4_init(void) { int err = hci_uart_register_proto(&h4p); if (!err) BT_INFO("HCI H4 protocol initialized"); else BT_ERR("HCI H4 protocol registration failed"); return err; }
這是通過hci_uart_register_proto(&bcsp)來完成的,這個函數非常簡單,本質如下:
hup[p->id]= p;其中static struct hci_uart_proto*hup[HCI_UART_MAX_PROTO];也就是說把對應於協議p的id和協議p連接起來,這樣設計的好處是hci uart層本身可以支持不同的協議,包括h4、bcsp等,通過這個數組連接這些協議,等以後有數據的時候調用對應的協議來處理,這裡比較關鍵的是h4裡面的這些函數。
五、HCI層的加入
hci的加入是通過hci_register_dev函數來做的,這時候用戶通過hciconfig就可以看到有一個接口了,通過這個接口用戶可以訪問底層的信息了,hci0已經生成;至於它在何時被加入的,我們再看看hciattach在內核裡面的處理過程;
1、TIOCSEATD的處理流程
Ioctl的作用是設置一個新的ldisc;
2、HCIUARTSETPROTO的處理流程:
這部分比較重要,注冊生成hci0, 初始化3個工作隊列,hci_rx_work、hci_tx_work、hci_cmd_work;完成hci部分數據、命令的接收、發送。
六、數據在驅動的傳遞流程
1、uart數據接收
這部分流程比較簡單,其實就是注冊一個tty驅動程序和相對應的函數,注冊相應的open\close\ioctl等方法,通過應用open /dev/ttyS*操作,注冊中斷接收函數,接收處理藍牙模塊觸發中斷的數據。
在這個中斷函數裡面會接受到來自於藍牙模塊的數據;在中斷函數裡面會先讀取串口的狀態寄存器判斷是否是data准備好,如果准備好就調用serial_sprd_rx_chars函數來接收數據,下面看看這個函數是如何處理的:
那就是把數據一個個的加入到uart層的緩沖區,直到底層不處於dataready狀態,或者讀了maxcount個數,當讀完後就調用tty層的接口把數據傳遞給tty層,tty層則把數據交給了ldisc,於是控制權也就交給了hci_uart層;
七、Hci_uart的數據接收
它基本上就是要個二傳手,通過:
spin_lock(&hu->rx_lock); hu->proto->recv(hu,(void *) data, count); hu->hdev->stat.byte_rx+= count; spin_unlock(&hu->rx_lock);把數據交給了在它之上的協議層,對於我們的設置來說實際上就交給了h4層;
如圖:
九、HCI以上的處理
這裡的hci_rx_work前面已經看到它了,它是一個工作隊列用來處理hci層的數據接收的;先看是否有進程打開hci的socket用來監聽數據,如果有的話,就把數據的一個copy發送給它,然後根據包的類型調用不同的處理函數,分別對應於event、acl、sco處理;
hci_event_packet是對於事件的處理,裡面包含有包括掃描,信號,授權,pin碼,總之基本上上層所能收到的事件,基本都是在這裡處理的,它的很多信息都是先存起來,等待上層的查詢然後才告訴上層;
hci_acldata_packet是一個經常的情況,也就是說上層通常都是使用的是l2cap層的接口,而l2cap就是基於這個的,如下圖所示:
到這裡如果有基於BTPROTO_L2CAP的socket,那麼這個socket就可以收到數據了;再看看BTPROTO_RFCOMM的流程:
十、 數據流程的總結
簡單總結一下,數據的流程,
"基本上是:
1, uart口取得藍牙模塊的數據;
2, uart口通過ldisc傳給hci_uart;
3, hci_uart傳給在其上的h4;
4, h4傳給hci層;
5, hci層傳給l2cap層
6, l2cap層再傳給rfcomm;
[Android]仿京東手機端類別頁京東手機端的類別標簽頁, 是一個左側滑動可選擇類別, 右側一個類別明細的列表聯動頁面. 當用戶選擇左側選項, 可在右側顯示更多選項來選
當我們直接在布局文件中寫三個listview的時候,會出現三個滾動條,並且每個listview都只顯示一個item,要改動才顯示更多。怎麼做才好了? 辦法是有得:用一
簡介本文介紹一個Android手勢密碼開源庫的使用及實現的詳細過程,該開源庫主要實現以下幾個功能: 支持手勢密碼的繪制,並支持密碼保存功能,解鎖時自動比對密碼給出結果
擴展自定義相機應用程序 在我看來,Android 上的內置相機應用程序缺少幾個基本特征。其中之一是,延遲一小段時間,10或者30秒,之後進行拍攝。此種功能對於那些可以安裝