在專題中,我們企劃使用定期的掃瞄周遭的Wi-Fi AP訊號來判別目前使用者所在的位置,可以用來分析日常活動的狀況,可能是一天去廁所的次數;在客廳停留的時間;去廚房的次數等。
要做的一樣的事情,比較簡單的方式是使用BLE beacon來做,也比較精確,缺點是成本較高。不過我們喜歡挑戰不一樣的東西。
Linkit 7697 / Arduino 裡面有提供一個ScanNetworks的sketch來幫助做掃描Wi-Fi AP的這件事情。裡面其實只有一行程式碼就完成Wi-Fi的掃瞄。這裡提醒一下,從Arduino IED開啟的ScanNetworks時要確定是由Linkit範例程式庫開啟的,而不是Arduino程式範例。
int numSsid = WiFi.scanNetworks();
掃描完成以後回傳掃描到的AP數量,然後把結果存在WiFi這個物件裡面。這對多數人來講是件好事,因為很簡單就完成事情,但對於我來說不是好事,因為他掃描一次至少需要占用系統時間2 sec以上,運氣不好會被佔用20 sec。20秒的時間對嵌入式系統來說算是災難等級了。
測試程式碼,變數宣告的部分都省略了,應該看得懂:
startMillis = millis();
int numSsid = WiFi.scanNetworks();
currentMillis = millis();
Serial.print("WiFi.scanNetworks() uses millis: ");
Serial.println(currentMillis - startMillis);
輸出結果
** Scan Networks **
WiFi.scanNetworks() uses millis: 20026
number of available networks:0
Scanning available networks…
** Scan Networks **
WiFi.scanNetworks() uses millis: 2030
number of available networks:3
0) kts Signal: -65 dBm Encryption: 1) HITRON-3580 Signal: -89 dBm Encryption: WPA2
2) 18-10 Signal: -90 dBm Encryption: Scanning available networks…
當掃描不到AP時會使用超過20秒的時間,而掃描到AP的時候會使用2秒的時間。不管時間長短,都不符合我們的需求。因此,我需要追蹤程式碼去看看是哪裡需要花費掉20秒的時間,去找到並打開函式庫裡面的WiFi.cpp檔案,搜尋scanNetworks()
int8_t WiFiClass::scanNetworks()
{
uint8_t attempts = 10;
uint8_t numOfNetworks = 0;
if (WiFiDrv::startScanNetworks() == WL_FAILURE)
return WL_FAILURE;
do
{
delay(2000);
numOfNetworks = WiFiDrv::getScanNetworks();
}
while (( numOfNetworks == 0)&&(--attempts>0));
return numOfNetworks;
}
程式碼第10行裡面就有一個delay(2000)的敘述,因此可以知道最短2秒的原因了,然後第3行定義了一個計數器,在第13行裡面計算,如果超過10次還沒有掃描到AP就跳出迴圈,因此也知道了20秒延遲的來源。
很顯然,WiFi.scanNetworks()這個方法不適合我們使用,不過他也揭露出在第6行的WiFiDrv::startScanNetworks()
和第11行的WiFiDrv::getScanNetworks()
可能可以提供一些幫助。
ps: Arduino communicates with the WiFi shield using the SPI bus.
雖然 MT7697 晶片支援硬體 SPI 運作,但由於架構設計的緣故,無法完全相容於 Arduino 定義的 SPI API,因此 LinkIt 7697 BSP 內建的 SPI 函式庫是透過呼叫 GPIO API 完成實作的 (亦即為軟體 SPI),不過這對大多數 Arduino 的應用情境並不造成影響。若要使用 LinkIt 7697 的硬體 SPI 功能,請參考 LinkIt SDK 相關文件說明。
接下來繼續搜尋WiFiDrv::startScanNetworks()
和WiFiDrv::getScanNetworks()
這兩隻程式,這裏出了一點錯誤,但是運氣很好,很快就發現了錯誤,然後修正。
LWiFi.h將整個library的外觀包裝的跟WiFi.h一模一樣,這個目的是為了能夠讓Arduino的使用者無痛地轉移到Linkit來,然而因為底層硬體的不同,後續所使用的函式庫就不同。在追蹤程式碼的過程,我所使用的工具將程式碼導向Arduino所使用的wifi_drv.h和wifi_drv.cpp這兩隻程式。他們的函式內容是這樣的:
int8_t WiFiDrv::startScanNetworks()
{
WAIT_FOR_SLAVE_SELECT();
// Send Command
SpiDrv::sendCmd(START_SCAN_NETWORKS, PARAM_NUMS_0);
//Wait the reply elaboration
SpiDrv::waitForSlaveReady();
// Wait for reply
uint8_t _data = 0;
uint8_t _dataLen = 0;
if (!SpiDrv::waitResponseCmd(START_SCAN_NETWORKS, PARAM_NUMS_1, &_data, &_dataLen))
{
WARN("error waitResponse");
_data = WL_FAILURE;
}
SpiDrv::spiSlaveDeselect();
return (_data == WL_FAILURE)? _data : WL_SUCCESS;
}
uint8_t WiFiDrv::getScanNetworks()
{
WAIT_FOR_SLAVE_SELECT();
// Send Command
SpiDrv::sendCmd(SCAN_NETWORKS, PARAM_NUMS_0);
//Wait the reply elaboration
SpiDrv::waitForSlaveReady();
// Wait for reply
uint8_t ssidListNum = 0;
SpiDrv::waitResponse(SCAN_NETWORKS, &ssidListNum, (uint8_t**)_networkSsid, WL_NETWORKS_LIST_MAXNUM);
SpiDrv::spiSlaveDeselect();
return ssidListNum;
}
然而在搜尋WAIT_FOR_SLAVE_SELECT()的使用方法的時候,意外地發現了另外一組wifi_drv.h和wifi_drv.cpp,包含在Linkit的下載資料包裡面
C:\…\ArduinoData\packages\LinkIt\hardware\linkit_rtos\0.10.18\libraries\LWiFi\src\utility
程式碼相對簡潔:
int8_t WiFiDrv::startScanNetworks()
{
return start_scan_net();
}
uint8_t WiFiDrv::getScanNetworks()
{
return get_reply_scan_networks();
}
現在的問題是:哪一個程式才是真正被執行的程式碼呢? 我提供一個簡單的方法,可以在兩支程式碼裡面隨便打一些錯誤指令,然後將專案進行編譯,這一定會產出編譯錯誤,這時候只需要去看一下error message是由哪一隻程式碼發出來的就可以辨認了。
實驗發現,真正被使用到的是linkit HDK所提供的函式庫。幸好發現得早,省掉一些可能的時間上的浪費。
因為發現上述的問題,所以之後每一次找到一支新的程式碼都會使用上面的方法去判斷一下是否為正確被使用的程式碼,以免花了時間卻得到錯誤的理解。
C:\…\ArduinoData\packages\LinkIt\hardware\linkit_rtos\0.10.18\libraries\LWiFi\src\utility\ard_mtk.c
static uint8_t getscan = 0;
int8_t start_scan_net(void)
{
int8_t status = 0, i;
init_global_connsys();
getscan = 0;
for (i = 0; i < WL_NETWORKS_LIST_MAXNUM; i++) {
memset(&(_ap_list[i]), 0, sizeof(wifi_scan_list_item_t));
}
if(wifi_connection_scan_init(_ap_list, WL_NETWORKS_LIST_MAXNUM) < 0){
pr_debug("wifi_connection_scan_init failed\r\n");
}
if((status = wifi_connection_register_event_notifier(WIFI_EVENT_IOT_SCAN_COMPLETE,
get_scan_list)) < 0){
pr_debug("register failed\r\n");
}
if((status = wifi_connection_start_scan(NULL, 0, NULL, 0, 0)) < 0) {
pr_debug("start scan failed\r\n");
}
//MsgQueue = xQueueCreate(5, sizeof(uint8_t));
//xQueueReceive(MsgQueue, &scannum, portMAX_DELAY); // portMAX_DELAY = BLOCk, but can set timeout-timer
//vQueueDelete(MsgQueue);
//wifi_connection_unregister_event_notifier(WIFI_EVENT_IOT_SCAN_COMPLETE, get_scan_list);
return (status >= 0)? WL_SUCCESS : WL_FAILURE;
}
uint8_t get_reply_scan_networks(void)
{
if(getscan){
wifi_connection_unregister_event_notifier(WIFI_EVENT_IOT_SCAN_COMPLETE, get_scan_list);
wifi_connection_stop_scan();
wifi_connection_scan_deinit();
return getscan;
}
return 0;
}
很快地瀏覽一下程式碼,因為我們的目標是弄清楚Wi-Fi AP scan的流程,追蹤到這裡也差不多了,如果以後還需要更精確的操作再繼續追蹤下去。所以相關的程式碼功能就可以用猜的,自己覺得合理就差不多了,如果你是一個訓練過的程設人員,你覺得合理大概就是正確的。前提是:「如果你是一個訓練過的程設人員…」。
第17行這裡註冊了一個監聽器,依照我們學習Android的經驗,看到註冊監聽器大概就知道接下來會發生什麼事。–> 掃描完成 –> 呼叫callback。
第22行開始掃描程序。
第37行,這已經在準備接收掃描結果的副程式裡面,這裡利用getscan作為判斷,就是當getscan > 0的時候我就接收結果,getscan = 0的時候就離開。
getscan的數值的設定並沒有在這兩隻程式碼裡面,但可以猜到他的功能,所以也就不詳細追蹤。這邊的程式邏輯是「雖然你有callbak可以使用,但我不想那麼麻煩,所以我就過一段時間來看看,如果掃瞄有了結果我就把結果讀回去,如果沒有那就下次再來。」
下一步…
回到ScanNetwors這個範例程式,它利用WiFi.SSID()、WiFi.RSSI()等函式印出掃描結果,可以推測LWiFi.h中提供一系列的API來完成資料讀取的工作。秉持著追到底的信念,在WiFi.cpp中可以找到
char* WiFiClass::SSID(uint8_t networkItem)
{
return WiFiDrv::getSSIDNetworks(networkItem);
}
int32_t WiFiClass::RSSI(uint8_t networkItem)
{
return WiFiDrv::getRSSINetworks(networkItem);
}
uint8_t WiFiClass::encryptionType(uint8_t networkItem)
{
return WiFiDrv::getEncTypeNetworks(networkItem);
}
這一個系列…
然後繼續,在wifi_drv.cpp中
char* WiFiDrv::getSSIDNetworks(uint8_t networkItem)
{
get_idx_ssid(networkItem, _networkSsid[networkItem]);
return _networkSsid[networkItem];
}
uint8_t WiFiDrv::getEncTypeNetworks(uint8_t networkItem)
{
return get_idx_enct(networkItem);
}
int32_t WiFiDrv::getRSSINetworks(uint8_t networkItem)
{
return get_idx_rssi(networkItem);
}
在ard_mtk.c
void get_idx_ssid(uint8_t networkItem, char *netssid)
{
memset(netssid, 0, WL_SSID_MAX_LENGTH);
memcpy(netssid, Ssid[networkItem], strlen(Ssid[networkItem]));
}
int32_t get_idx_rssi(uint8_t networkItem)
{
return Rssi[networkItem];
}
uint8_t get_idx_enct(uint8_t networkItem)
{
return encrypt_map_arduino(Encr[networkItem]);
}
整個系列瀏覽下來,大概都是記憶體讀取的工作,中間有一些轉換是為了要包裝成和Arduino相容。追蹤到這裡我的腦力就消耗得差不多了,想休息…。
重構ScanNetwors()…
先測試一下Linkit 7697掃描一次Wi-Fi AP需要多少時間,去修改int8_t WiFiClass::scanNetworks()
這隻程式碼裡面的delay()和attempt參數,逐步縮小delay間隔來發現7697掃描一次所需要的時間。
舉例來說,以下是delay(700)的結果
Scanning available networks…
** Scan Networks **
ScanNetworks uses millis: 1430
number of available networks:3
0) CHT7483 Signal: -85 dBm Encryption: WPA
1) HITRON-3580 Signal: -92 dBm Encryption: WPA2
2) SweetHome-New Signal: -93 dBm Encryption: WPA2
Scanning available networks…
** Scan Networks **
ScanNetworks uses millis: 1430
number of available networks:1
0) CHT7483 Signal: -88 dBm Encryption: WPA
這表示需要兩次詢問才能得到結果,間接表示一次掃描的時間是大於700ms的,如果設定delay(800),結果顯示
Scanning available networks…
** Scan Networks **
ScanNetworks uses millis: 829
number of available networks:2
0) kts Signal: -69 dBm Encryption: WPA-PSK/WPA2-PSK
1) HITRON-3580 Signal: -92 dBm Encryption: WPA2
Scanning available networks…
** Scan Networks **
ScanNetworks uses millis: 1629
number of available networks:1
0) huhome Signal: -86 dBm Encryption: WPA2
Scanning available networks…
** Scan Networks **
ScanNetworks uses millis: 1630
number of available networks:2
0) kts Signal: -66 dBm Encryption: WPA-PSK/WPA2-PSK
1) CHT7483 Signal: -85 dBm Encryption: WPA
表示有些時候在800ms可以完成,但也會超過800ms。當delay設為900時,大部分的掃瞄都會在這個時間內完成。
Scanning available networks…
** Scan Networks **
ScanNetworks uses millis: 930
number of available networks:4
0) kts Signal: -65 dBm Encryption: WPA-PSK/WPA2-PSK
1) Tracy Signal: -86 dBm Encryption: WPA2
2) HITRON-3580 Signal: -89 dBm Encryption: WPA2
3) apple369 Signal: -94 dBm Encryption: WPA-PSK/WPA2-PSK
使用時間930的推論是,延遲900ms,讀取掃描結果需要30ms。在程式使用時一般還會取一個安全閥值,另外,環境中所有的Wi-Fi AP的數量也會影響掃描所需要的時間,AP數量越多,掃描所需要的時間越久,所以在WiFi.scanNetworks()裡面設定為2000ms。
結論
1. 在setup()階段,WiFi.scanNetworks()是可以使用的,2000ms的延遲我們等的起,然而在loop()階段,2000ms太長了,我們需要自己撰寫程式個別呼叫WiFiDrv::startScanNetworks()和WiFiDrv::getScanNetworks(),以免太長的延遲影響程式運作。
2. 另外,讀取一次WiFiDrv::getScanNetworks()需要耗時30ms,這比三軸資料的讀取週期還要長,因此需要思考如何規避統計上的錯誤,或者再更深入的追蹤程式碼,把30ms的時間再進一步的拆解成更小的時間單位。
3. Linkit 7697的Wi-Fi AP的掃瞄很不精確,常常掃不到AP,用來做設計時這個特性(缺點)必須要被考慮。