android 关于 Wi-Fi 2.4G、5G 混频的扫描方案

前言

目前,在智能家居行业,绝大多数的设备都是使用 Wi-Fi 进行的,但是,大多数的设备连接 Wi-Fi 的时候是只支持 2.4GHz 的 Wi-Fi的。最近公司就有一个需求,若当前连接的 Wi-Fi 或者输入的 Wi-Fi 是 5Ghz 频段的话,就需要阻拦。如果是 2.4Ghz 或者混频的Wi-Fi,那么就允许继续下一步。

那么,唠叨了背景,这里先跟大家普及一下,什么是频率,什么是混频,什么是分频?如何检测?

频率

我们都知道,Wi-Fi 上的数据传输本质上也是一种无线电波,在 5G 频段出来之前,他一般通过工作在 2.4Ghz 这个频段上的,是的,跟蓝牙是用一个频段。但是自从 5G 诞生之后,Wi-Fi 工作的频段就有 2.4Ghz 和 5G 两个所在的频道的。

如何判断这个 Wi-Fi 是 2.4G 还是 5G 的呢?

这个还是比较简单的,根据我们扫描到的 Wi-Fi 对象,去判断当前的频率落在哪个区间即可。如下所示:

kotlin 复制代码
import android.net.wifi.ScanResult

object WiFiUtils {

    /**
     * 判断 Wi-Fi 是否是 2.4GHz 频段
     * @param scanResult Wi-Fi 扫描结果
     * @return 如果是 2.4GHz 频段,则返回 true
     */
    fun is24GHz(scanResult: ScanResult): Boolean {
        return scanResult.frequency in 2400..2500
    }

    /**
     * 判断 Wi-Fi 是否是 5GHz 频段
     * @param scanResult Wi-Fi 扫描结果
     * @return 如果是 5GHz 频段,则返回 true
     */
    fun is5GHz(scanResult: ScanResult): Boolean {
        return scanResult.frequency in 4900..5900
    }
}

分频

什么是混频呢?目前,我们家里所用的 Wi-Fi 路由器,目前都支持两种 Wi-Fi 模式,一种是 2.4G,一种是 5G。我们经常会把我们的 Wi-Fi 名称命名为 「Wi-Fi-xxx」 和 「Wi-Fi-xxx-5G」 通常这种情况下就是分频。实际上就是两个 Wi-Fi,只不过这两个 Wi-Fi 的 mac 地址非常的接近,模拟一个分频的地址(bssid)如下:

「Wi-Fi-xxx」:ab:cd:ef:hi:jk

「Wi-Fi-xxx-5G」:ab:cd:ef:hi:jl

这个地址在术语中,我们称为 bssid,其中 Wi-Fi 名称称为 ssid。 所以,本质上,分频路由器能发出两个 bssid 十分相近的 Wi-Fi,一个工作在 2.4G,一个工作在 5G。

混频

在理解了分频的基础上,我们来了解下,什么是混频,其实混频就是在分频的基础上,把这个上述的两个 Wi-Fi 的名称(ssid)设置为了同一个,让我们觉得是同一个 Wi-Fi,但是实际上也是两个 Wi-Fi,只不过我们通常这种情况下,Wi-Fi 的密码是使用同一个的。

「Wi-Fi-xxx」:ab:cd:ef:hi:jk

「Wi-Fi-xxx」:ab:cd:ef:hi:jl

判断混频就是查看扫描的 Wi-Fi 列表之后,将 Wi-Fi 列表分局频了进行分为 2.4G list 和 5G list,然后判断对应的 ssid 是否在 2.4G 列表以及 5G 列表都存在,如果存在,则代表当前这个 Wi-Fi 为混频。(这里建议使用 map 来降低算法的时间复杂度)

回头操作公司的需求

若当前连接的 Wi-Fi 或者输入的 Wi-Fi 是 5Ghz 频段的话,就需要阻拦。如果是 2.4Ghz 或者混频的Wi-Fi,那么就允许继续操作。

乍一听,这不是很简单,直接调用扫描的方法不就可以了吗,然后通过上述的判断不就好了吗?

kotlin 复制代码
wifiScanner = WiFiScanner(this) 
wifiScanner.setScanResultsListener(object : WiFiScanner.ScanResultsListener { 
    override fun onScanResultsAvailable(results: List<ScanResult>) { 
        for (result in results) { 
            Log.d("MainActivity", "SSID: ${result.SSID} - BSSID: ${result.BSSID}")
            if (WiFiUtils.is24GHz(result)) {
                // 2.4GHz
            } else if (WiFiUtils.is5GHz(result)) {
                // 5G hz
            }
         } 
    } 
    
    override fun onScanFailed() { 
        Log.e("MainActivity", "Wi-Fi scan failed") 
    } 
}) 
wifiScanner.startScanning()

但是你们会发现,Wi-Fi 的每一次的扫描其实并不是非常的精准,假如我们有一个混频的 Wi-Fi A,但是在第一次扫描的时候,我们进扫描到了 5G 的 Wi-Fi A,那么会导致我们判断当前连接的 Wi-Fi 是 5G 的,从而拦截这次的处理,另外有些时候,第一次扫描的 Wi-Fi 列表有20个,但是第二次就变成了18个。

既然一次扫描不到,就多扫描几次呗。多扫描几次之后取并集然后处理就好了,诶~其实我也是这么想的。但是多扫描几次之后,会发现,后面扫描之后,实际上 Wi-Fi 是没有更新的。查了下官方的文档发现,原来为了减少这种扫描频出,google 对 Wi-Fi 扫描做了如下的限制:

所以,我们无脑请求刷新列表就行不通了。要遵循2分钟只能扫描4次的限制。 所以我们就想到了做一个上次的缓存,因为我们只需要判断出来当前连接的 ssid 是不是然后仅记录 2.4G 的列表,然后当扫描开始的时候,我们就判断 5G 列表是否在 2.4G的缓存里面,或者是不是在新扫描的 2.4G 的 Wi-Fi 分组当中即可。

因此,结合上述的方式,就有了以下的流程

sequenceDiagram loop 开始扫描 业务端->>SDK: 发起wifi扫描 SDK-->>SDK: 开启本次扫描 SDK-->>SDK: 处理扫描的结果 Cache->>SDK: 提供 2.4G 的 Wi-Fi 列表 Note over SDK:1. 判断每个 Wi-Fi 的频率分为 2.4G 和 5G 的 Wi-Fi 列表
2. 判断 5G Wi-Fi 里面是否存在同 SSID 的 2.4G 缓存的 Wi-Fi
若存在则进行判定为混频
若不存在再和 当前扫描的 2.4G Wi-Fi 列表进行匹配 SDK->>业务端: 返回扫描之后 Wi-Fi 列表
列表包括仅 2.4G Wi-Fi 列表、
仅 5G Wi-Fi 列表、
2.4G/5G 混频 Wi-Fi 列表 SDK->>Cache: 更新 2.4G Wi-Fi 列表 业务端-->>业务端:根据当前连接的 Wi-Fi 和 SDK 返回
的列表进行判断是否允许继续? 业务端-->>业务端:要是用户对 Wi-Fi 列表存在疑问?则点击刷新按钮 end

但是,即便这样处理,还是没有处理完2分钟扫描4次的限制。 我们先来分析下扫描的能力

扫描能力

若存在 targetSsid,sdk 会扫描附近的 Wi-Fi 列表

  • 纯2.4G 判断,根据返回的结果列表中 2.4G List 是否存在当前传入的 targetSsid (有可能不准确,因为此时可能没有扫描到当对应 5G 频段的 Wi-Fi ,但是不妨碍继续进行)
  • 混频判断,根据返回的结果列表中 2.4G List 和 5G List 是否都存在当前传入的 targetSsid (准确
  • 纯 5G 判断,根据返回对结果列表中,是否 5G List 存在,但是2.4G List 不存 targetSsid(有可能不准确,因为此时可能没有扫描到当对应 2.4G 频段的 Wi-Fi )
  • 分频 5G 判断,根据返回到结果列表中,查看是否 5G List 是否存在当前的 targetSsid,然后查看当前 targetSsid 对应的 bssid 在 2.4G 列表里面是否存在,若存在则代表当前是分频 5G (有可能不准确,有可能不准确,因为此时可能没有扫描到当对应 2.4G 频段的 Wi-Fi )

因此,我们应该尽量保证扫描到纯 2.4G 和混频的是精准的。然后结合扫描的限制,理所应当,我们会想到如下的方式:

由于扫描的策略为2分钟不超过4次,这个限制放在业务层使用并不是非常合理,因此,将这个逻辑下层到 sdk。

sdk 的扫描策略如下:

当有业务层发起 Wi-Fi 扫描的请求的时候,我们一次完整4次的扫描周期为120秒,那么平均分配的扫描时间应该为30秒。即如下所示:

1th 2nd 3rd 4th
30s 30s 30s 30s

但是这样的扫描效果非常的差,如果第一次扫描的时候,无法分辨出混频、分频的情况的话,会导致第二次的扫描等待时长过长。通过测试,我们发现,在公司比较复杂的网络环境下,进行混频/分频的扫描的结果如下所示。可以看到,有大概率的第一次的扫描是会失败的,但是基本上前面2次就可以把混频/分频的结果正确拿到,因此,我们更正一下之前的策略,只要保证2分钟的扫描次数不超过4次即可,另外,其实到了第三次第四次的时候,扫描的结果基本上不会发生太大的变化,因此,我们可以考虑将前面2次甚至前面3次的扫描时间紧凑一点。基本上,每次扫描的时间都差不多再5秒左右。

因此,将扫描的时间片分为以下的方式,假设:

  • 第一次扫描的时间是x秒

  • 第二次扫描的时间为y秒

  • 第三次扫描的时间为z秒

  • 第四次扫描的时间为w秒

其中,x + y + z + w <= 120,这里你们可能有个疑问,为什么能保证4者之和一定小于120秒呢,因为实测的结果4次是远远小于120的。

那么我让第一次和第二次直接无延迟串行扫描,第三次则进行适当低延迟扫描,第四次的话,我们进行高延迟扫描。

1th 2nd delay-1 3rd delay-1 4th sleep
x y delay1 z delay2 w 120 - x - y - delay1 -z - delay2
  • x:第一次扫描花费的时间

  • y:第二次扫描花费的时间

  • delay1: 第二次扫描之后到第三次扫描间隔的时间

  • z:第三次扫描花费的时间

  • delay2:第三次扫描之后到第四次扫描的间隔时间

  • w:第四次扫描花费的时间。(这里会预留30秒的时间给第四次扫描)

  • sleep status:休眠状态,不会处罚扫描。

以上是自动扫描的策略,那么当业务层多次频繁触发扫描动作的时候,该怎么处理?

后面有进行触发扫描的动作,

  • 若当前正处于第一次、第二次、第三次、第四次扫描,则直接不做处理。
  • 若当前处于延迟-1的阶段,则立马进行第三次扫描。同理,若当前处于延迟-2的情况,那么立马进行第四次扫描。
  • 若处于休眠态,则不做处理,直接返回之前的扫描结果。

我们正常期望的扫描流程如下:

但是用户可以主动停止,可能会导致重新开始的扫描点次数加上下一个周期的扫描次数超过了2分钟4次。如下:

因此,我们规定如下点,在一个周期,前三次扫描要落在前1分钟,第四次扫描一定要或者后1分钟.

但是,即便如此,还是有可能出现扫描超出限制的,因此当现扫描出现限制的时候,则直接将上一次的结果进行返回。通过测试,系统会将这个扫描结果是否返回还回来,因此直接使用系统的管理方式即可。

这里仅仅提供了一种解决 Wi-Fi 扫描限制的思路,具体的代码这边先不贴了,希望能帮到大家。

最后,android 在 Wi-Fi 扫描的时候,新增了一个API,来帮我们提前判断是否可以进行 Wi-Fi 扫描。

scss 复制代码
WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
    if (mWifiManager.isScanThrottleEnabled()){
        // 代表不允许扫描 Wi-Fi
    }
}

developer.android.com/reference/a...

相关推荐
学习使我快乐013 小时前
JS进阶 3——深入面向对象、原型
开发语言·前端·javascript
bobostudio19953 小时前
TypeScript 设计模式之【策略模式】
前端·javascript·设计模式·typescript·策略模式
黄尚圈圈4 小时前
Vue 中引入 ECharts 的详细步骤与示例
前端·vue.js·echarts
浮华似水5 小时前
简洁之道 - React Hook Form
前端
正小安7 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
_.Switch9 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光9 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   9 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   9 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d