使用 Go 和 Raspberry Pi 解决 WiFi 问题

构建一个有趣的 WiFi 扫描器。
86 位读者喜欢这篇文章。
Selfcare, drinking tea on the porch

opensource.com

去年夏天,我和妻子卖掉了我们所有的东西,带着我们的两条狗搬到了夏威夷。这里的一切都和我们想象的一样:美丽的阳光、温暖的沙滩、凉爽的海浪——应有尽有。我们也遇到了一些我们没有预料到的事情:WiFi 问题。

现在,这不是夏威夷的问题。这仅限于我们租住的公寓。我们住在一个与房东公寓相连的单间公寓里。房租的一部分包括免费互联网!耶!但是,所谓的互联网是由房东公寓里的 WiFi 路由器提供的。呜!

老实说,它工作得还算可以。勉强可以。好吧,它工作得不太好,我也不确定为什么。路由器实际上就在墙的另一边,但是我们的信号断断续续,并且在保持连接方面遇到了一些麻烦。在家里,我们的 WiFi 路由器的信号穿过了许多墙壁和一些楼层。当然,它覆盖的面积比我们居住的 600 平方英尺的公寓还要大!

在这种情况下,一个优秀的科技爱好者会做什么?当然是调查!

幸运的是,我们在搬到这里之前卖掉的“我们所有的东西”不包括我们的 Raspberry Pi Zero W。如此小巧!如此便携!当然,我把它带到了夏威夷。我的好主意是使用 Pi 及其内置的 WiFi 适配器,用 Go 编写一个小程序来测量从路由器接收到的 WiFi 信号强度,并显示该输出。我打算让它超级简单、快速和粗糙,稍后再担心如何改进它。我只是想知道 WiFi 到底是怎么回事,该死的!

在 Google 上搜索一分钟,找到一个相对有用的 Go 包,用于处理 WiFi,mdlayher/wifi。听起来很有希望!

获取有关 WiFi 接口的信息

我的计划是查询 WiFi 接口统计信息并返回信号强度,所以我需要找到设备上的接口。幸运的是,mdlayher/wifi 包有一个查询它们的方法,所以我可以通过创建一个名为 main.go 的文件来做到这一点

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)
	}

}

那么,这里发生了什么?导入它之后,mdlayher/wifi 模块可以在 main 函数中使用,以创建一个新的 Client(类型为 *Client)。新的客户端(名为 c)然后可以使用 c.Interfaces() 获取系统上接口的列表。然后它可以循环遍历 Interface 指针的切片并打印有关它们的信息。

通过在 %+v 中添加“+”,它也会打印 *Interface 结构中字段的名称,这有助于我识别我看到的内容,而无需查阅文档。

运行上面的代码会提供我的机器上 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 得到确认。Go wifi 模块的文档 指出 PHY 是接口所属的物理设备。

第一个接口没有名称,并且是 TYPE:P2P。第二个接口名为 wpl2s0,是 TYPE:Station。wifi 模块文档列出了 不同类型的接口 并描述了它们是什么。根据文档,“P2P”类型表示“接口是点对点客户端网络中的设备”。我相信,如果我错了,请在评论中纠正我,这个接口用于 WiFi Direct,这是一种允许两个 WiFi 设备在没有中间接入点的情况下连接的标准。

“Station”类型表示“接口是具有控制接入点的客户端设备托管基本服务集 (BSS) 的一部分。” 这是大多数人习惯的无线设备的标准功能——作为连接到接入点的客户端。这是用于测试 WiFi 质量的接口。

从接口获取 Station 信息

使用此信息,我可以更新接口上的循环以检索我要查找的信息

	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——Station 接口(这是本练习中唯一重要的类型)。这是一个不幸的命名冲突——接口“类型”不是 Golang 意义上的“类型”。事实上,我在这里做的是一个名为 InterfaceType 的 Go type,用于表示接口的类型。哇,这花了我一分钟才弄清楚!

因此,假设接口是正确的类型,可以使用 c.StationInfo(x) 使用客户端 StationInfo() 方法来检索 Station 信息,以获取有关接口 x 的信息。

这会返回 *StationInfo 指针的切片。我不确定为什么会有一个切片。也许接口可以有多个 StationInfo 响应?无论如何,我可以循环遍历切片并使用相同的 +%v 技巧来打印 StationInfo 结构的键和值。

运行上面的代码返回

&{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”和“BeaconLoss”。信号以 dBm(或分贝毫瓦)为单位报告。

一个小插曲:如何读取 WiFi dBm

根据 MetaGeek

  • –30 是最佳信号强度——既不现实也不必要
  • –67 非常好;适用于需要可靠数据包传输的应用,例如流媒体
  • –70 还可以,最低可靠数据包传输,适用于电子邮件和网页
  • –80 很差,绝对基本的连接,不可靠的数据包传输
  • –90 不可用,接近“噪声底限”

请注意,dBm 是对数刻度:-60 比 -30 低 1,000 倍

将其变成真正的“扫描器”

因此,看看我上面的信号:–79。糟糕,不太好。但是这个单一的结果并没有特别大的帮助。这只是一个时间点的参考,并且仅对 WiFi 网络适配器在那个时刻所在的特定物理空间有效。更有用的是连续读取,这样就可以看到信号随着 Raspberry Pi 的移动而发生的变化。可以再次调整 main 函数来实现这一点

	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。由于它在循环之外,我可以使用它来存储接口信息。在循环内部创建的任何变量都无法在该循环范围之外访问。

然后,我可以将循环分成两个。第一个循环遍历 c.Interfaces() 返回的接口,如果该接口是 Station 类型,则将其存储在之前创建的 i 变量中并跳出循环。

第二个循环是一个无限循环,所以它会一遍又一遍地运行,直到我按下 Ctrl+C 结束程序。此循环获取该接口信息并检索 Station 信息(如前所述),并打印出信号信息。然后它休眠一秒钟,再次运行,一遍又一遍地打印信号信息,直到我退出。

所以,运行它

[chris@marvin wifi-monitor]$ go run main.go
Signal: -81
Signal: -81
Signal: -79
Signal: -81

哎呀。不太好。

绘制公寓地图

至少这些信息很有用。使用附加的屏幕或 E Ink 显示屏和电池(或长长的延长线),我可以带着 Pi 在公寓里走动,并绘制出死角在哪里。

剧透警告:由于房东的接入点在隔壁的公寓里,我的大死角是从单间公寓厨房区域的冰箱发出的锥形区域……冰箱与房东的公寓共用一堵墙!

我认为用《龙与地下城》的行话来说,这是一个“静默之锥”。或者至少是一个“网络差之锥”。

无论如何,这段代码可以直接在 Raspberry Pi 上使用 go build -o wifi_scanner 编译,生成的二进制文件 wifi_scanner 可以与任何其他 ARM 设备(相同版本)共享。或者,它可以在具有 ARM 设备正确库的常规系统上编译。

祝您 Pi 扫描愉快!愿您的 WiFi 路由器不在您的冰箱后面!您可以在 我的 GitHub 仓库 中找到用于此项目的代码。

接下来阅读什么
Chris Collins
Chris Collins 是 Red Hat 的 SRE 和 OpenSource.com 的通讯员,对自动化、容器编排及其周围的生态系统充满热情,并且喜欢在家中重现企业级技术以获得乐趣。

评论已关闭。

Creative Commons License本作品根据知识共享署名-相同方式共享 4.0 国际许可协议获得许可。
© . All rights reserved.