使用 Go 和樹莓派排查 WiFi 問題
去年夏天,我和妻子變賣了家產(chǎn),帶著我們的兩只狗移居了夏威夷。這里有美麗的陽光、溫暖的沙灘、涼爽的沖浪等你能想到的一切。我們同樣遇到了一些意料之外的事:WiFi 問題。
不過,這不是夏威夷的問題,而是我們租住公寓的問題。我們住在一個單身公寓里,與房東的公寓僅一墻之隔。我們的租房協(xié)議中包含了免費的網(wǎng)絡(luò)連接!好耶!只不過,它是由房東的公寓里的 WiFi 提供的,哇哦……
說實話,它的效果還不錯……吧?好吧,我承認(rèn)它不盡如人意,并且不知道是哪里的問題。路由器明明就在墻的另一邊,但我們的信號就是很不穩(wěn)定,經(jīng)常會自動斷開連接。在家的時候,我們的 WiFi 路由器的信號能夠穿過層層墻壁和地板。事實上,它所覆蓋的區(qū)域比我們居住的 600 平方英尺(大約 55 平方米)的公寓還要大。
在這種情況下,一個優(yōu)秀的技術(shù)人員會怎么做呢?既然想知道為什么,當(dāng)然是開始排查咯!
幸運的是,我們在搬家之前并沒有變賣掉樹莓派 Zero W。它是如此小巧便攜! 我當(dāng)然就把它一起帶來了。我有一個機(jī)智的想法:通過樹莓派和它內(nèi)置的 WiFi 適配器,使用 Go 語言編寫一個小程序來測量并顯示從路由器收到的 WiFi 信號。我打算先簡單快速地把它實現(xiàn)出來,以后再去考慮優(yōu)化。真是麻煩!我現(xiàn)在只想知道這個 WiFi 是怎么回事!
谷歌搜索了一番后,我發(fā)現(xiàn)了一個比較有用的 Go 軟件包 ??mdlayher/wifi??,它專門用于 WiFi 相關(guān)操作,聽起來很有希望!
獲取 WiFi 接口的信息
我的計劃是查詢 WiFi 接口的統(tǒng)計數(shù)據(jù)并返回信號強(qiáng)度,所以我需要先找到設(shè)備上的接口。幸運的是,??mdlayher/wifi?
?? 包有一個查詢它們的方法,所以我可以創(chuàng)建一個 ??main.go?
? 來實現(xiàn)它,具體代碼如下:
package main
import (
"fmt"
"github.com/mdlayher/wifi"
)
func main() {
c, err := wifi.New()
defer c.Close()
if err != nil {
panic(err)
}
interfaces, err := c.Interfaces()
for _, x := range interfaces {
fmt.Printf("%+v\n", x)
}
}
讓我們來看看上面的代碼都做了什么吧!首先是導(dǎo)入依賴包,導(dǎo)入后,我就可以使用 ??mdlayher/wifi?
?? 模塊就在 ??main?
?? 函數(shù)中創(chuàng)建一個新的客戶端(類型為 ??*Client?
??)。接下來,只需要調(diào)用這個新的客戶端(變量名為 ??c?
??)的 ??c.Interfaces()?
? 方法就可以獲得系統(tǒng)中的接口列表。接著,我就可以遍歷包含接口指針的切片(變長數(shù)組),然后打印出它們的具體信息。
注意到 ??%+v?
?? 中有一個 ??+?
?? 了嗎?它意味著程序會詳細(xì)輸出 ??*Interface?
? 結(jié)構(gòu)體中的屬性名,這將有助于我標(biāo)識出我看到的東西,而不用去查閱文檔。
運行上面的代碼后,我得到了機(jī)器上的 WiFi 接口列表:
&{Index:0 Name: HardwareAddr:5c:5f:67:f3:0a:a7 PHY:0 Device:3 Type:P2P device Frequency:0}
&{Index:3 Name:wlp2s0 HardwareAddr:5c:5f:67:f3:0a:a7 PHY:0 Device:1 Type:station Frequency:2412}
注意,兩行輸出中的 MAC 地址(??HardwareAddr?
??)是相同的,這意味著它們是同一個物理硬件。你也可以通過 ??PHY: 0?
?? 來確認(rèn)。查閱 Go 的 ??wifi 模塊文檔???,??PHY?
? 指的就是接口所屬的物理設(shè)備。
第一個接口沒有名字,類型是 ??TYPE: P2P?
??。第二個接口名為 ??wpl2s0?
??,類型是 ??TYPE: Station?
??。??wifi?
?? 模塊的文檔列出了 ??不同類型的接口???,以及它們的用途。根據(jù)文檔,??P2P?
??(點對點傳輸) 類型表示“該接口屬于點對點客戶端網(wǎng)絡(luò)中的一個設(shè)備”。我認(rèn)為這個接口的用途是 ??WiFi 直連?? ,這是一個允許兩個 WiFi 設(shè)備在沒有中間接入點的情況下直接連接的標(biāo)準(zhǔn)。
??Station?
?(基站)類型表示“該接口是具有控制接入點controlling access point的客戶端設(shè)備管理的基本服務(wù)集basic service set(BSS)的一部分”。這是大眾熟悉的無線設(shè)備標(biāo)準(zhǔn)功能:作為一個客戶端來連接到網(wǎng)絡(luò)接入點。這是測試 WiFi 質(zhì)量的重要接口。
利用接口獲取基站信息
利用該信息,我可以修改遍歷接口的代碼來獲取所需信息:
for _, x := range interfaces {
if x.Type == wifi.InterfaceTypeStation {
// c.StationInfo(x) returns a slice of all
// the staton information about the interface
info, err := c.StationInfo(x)
if err != nil {
fmt.Printf("Station err: %s\n", err)
}
for _, x := range info {
fmt.Printf("%+v\n", x)
}
}
}
首先,這段程序檢查了 ??x.Type?
??(接口類型)是否為 ??wifi.InterfaceTypeStation?
??,它是一個基站接口(也是本練習(xí)中唯一涉及到的類型)。不幸的是名字出現(xiàn)了沖突,這個接口“類型”并不是 Golang 中的“類型”。事實上,我在這里使用了一個叫做 ??interfaceType?
? 的 Go 類型來代表接口類型。呼,我花了一分鐘才弄明白!
然后,假設(shè)接口的類型正確,我們就可以調(diào)用 ??c.StationInfo(x)?
?? 來檢索基站信息,??StationInfo()?
?? 方法可以獲取到關(guān)于這個接口 ??x?
? 的信息。
這將返回一個包含 ??*StationInfo?
?? 指針的切片。我不大確定這里為什么要用切片,或許是因為接口可能返回多個 ??StationInfo?
???不管怎么樣,我都可以遍歷這個切片,然后使用之前提到的 ??+%v?
?? 技巧格式化打印出 ??StationInfo?
? 結(jié)構(gòu)的屬性名和屬性值。
運行上面的程序后,我得到了下面的輸出:
&{HardwareAddr:70:5a:9e:71:2e:d4 Connected:17m10s Inactive:1.579s ReceivedBytes:2458563 TransmittedBytes:1295562 ReceivedPackets:6355 TransmittedPackets:6135 ReceiveBitrate:2000000 TransmitBitrate:43300000 Signal:-79 TransmitRetries:2306 TransmitFailed:4 BeaconLoss:2}
我感興趣的是 ??Signal?
??(信號)部分,可能還有 ??TransmitFailed?
??(傳輸失?。┖?nbsp;??BeaconLoss?
?(信標(biāo)丟失)部分。信號強(qiáng)度是以 dBm(分貝-毫瓦decibel-milliwatts)為單位來報告的。
簡短科普:如何讀懂 WiFi dBm
根據(jù) ??MetaGeek?? 的說法:
- -30 最佳,但它既不現(xiàn)實也沒有必要
- -67 非常好,它適用于需要可靠數(shù)據(jù)包傳輸?shù)膽?yīng)用,例如流媒體
- -70 還不錯,它是實現(xiàn)可靠數(shù)據(jù)包傳輸?shù)牡拙€,適用于電子郵件和網(wǎng)頁瀏覽
- -80 很差,只是基本連接,數(shù)據(jù)包傳輸不可靠
- -90 不可用,接近“背景噪聲noise floor”
注意:dBm 是對數(shù)尺度,-60 比 -30 要低 1000 倍。
使它成為一個真的“掃描器”
所以,看著上面輸出顯示的我的信號:-79。哇哦,感覺不大好呢。不過單看這個結(jié)果并沒有太大幫助,它只能提供某個時間點的參考,只對 WiFi
網(wǎng)絡(luò)適配器在特定物理空間的某一瞬間有效。一個連續(xù)的讀數(shù)會更有用,借助于它,我們觀察到信號隨著樹莓派的移動而變化。我可以再次修改 ??main?
? 函數(shù)來實現(xiàn)這一點。
var i *wifi.Interface
for _, x := range interfaces {
if x.Type == wifi.InterfaceTypeStation {
// Loop through the interfaces, and assign the station
// to var x
// We could hardcode the station by name, or index,
// or hardwareaddr, but this is more portable, if less efficient
i = x
break
}
}
for {
// c.StationInfo(x) returns a slice of all
// the staton information about the interface
info, err := c.StationInfo(i)
if err != nil {
fmt.Printf("Station err: %s\n", err)
}
for _, x := range info {
fmt.Printf("Signal: %d\n", x.Signal)
}
time.Sleep(time.Second)
}
首先,我命名了一個 ??wifi.Interface?
?? 類型的變量 ??i?
?。因為它在循環(huán)的范圍外,所以我可以用它來存儲接口信息。循環(huán)內(nèi)創(chuàng)建的任何變量在該循環(huán)的范圍外都是不可訪問的。
然后,我可以把這個循環(huán)一分為二。第一個遍歷了 ??c.Interfaces()?
?? 返回的接口切片,如果元素是一個 ??Station?
?? 類型,它就將其存儲在先前創(chuàng)建的變量 ??i?
? 中,并跳出循環(huán)。
第二個循環(huán)是一個死循環(huán),它將不斷地運行,直到我按下 ??Ctrl + C?
? 來結(jié)束程序。和之前一樣,這個循環(huán)內(nèi)部獲取接口信息、檢索基站信息,并打印出信號信息。然后它會休眠一秒鐘,再次運行,反復(fù)打印信號信息,直到我退出為止。
運行上面的程序后,我得到了下面的輸出:
[chris@marvin wifi-monitor]$ go run main.go
Signal: -81
Signal: -81
Signal: -79
Signal: -81
哇哦,感覺不妙。
繪制公寓信號分布圖
不管怎么說,知道這些信息總比不知道要好。讓樹莓派連接上顯示器或者電子墨水屏,并接上電源,我就可以讓它在公寓里移動,并繪制出信號死角的位置。
劇透一下:由于房東的接入點在隔壁的公寓里,對我來說最大的死角是以公寓廚房的冰箱為頂點的一個圓錐體形狀區(qū)域......這個冰箱與房東的公寓靠著一堵墻!
我想如果用《龍與地下城》里的黑話來說,它就是一個“沉默之錐Cone of Silence”?;蛘咧辽偈且粋€“糟糕的網(wǎng)絡(luò)連接之錐Cone of Poor Internet”。
總之,這段代碼可以直接在樹莓派上運行 ??go build -o wifi_scanner?
?? 來編譯,得到的二進(jìn)制文件 ??wifi_scanner?
? 可以運行在其他同樣的ARM 設(shè)備上。另外,它也可以在常規(guī)系統(tǒng)上用正確的 ARM 設(shè)備庫進(jìn)行編譯。
祝你掃描愉快!希望你的 WiFi 路由器不在你的冰箱后面!你可以在 ??我的 GitHub 存儲庫?? 中找到這個項目所用的代碼。