Files
Road_roller/src/utils/wifi.ts
2026-09-14 10:42:40 +08:00

605 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* WiFi 工具类 (Android App 专用)
* 用于 uni-app Android 平台 WiFi 操作
*
* ======================== 使用说明 ========================
*
* import wifi from '@/utils/wifi'
*
* // 检查 WiFi 是否可用
* const available = await wifi.isWifiAvailable()
*
* // 获取当前连接信息
* const info = await wifi.getConnectedWifi()
*
* // 扫描附近 WiFi
* const networks = await wifi.scanWifi()
*
* // 连接指定 WiFi
* await wifi.connectWifi({ SSID: 'MyWiFi', password: '12345678' })
*
* // 断开当前连接
* await wifi.disconnectWifi()
*
* // 连接已保存的 WiFi(免密)
* await wifi.connectSavedWifi({ SSID: 'MyWiFi' })
*
* // 获取已保存的 WiFi 列表
* const savedList = await wifi.getSavedWifiList()
*
* // 移除已保存的 WiFi
* await wifi.removeSavedWifi({ SSID: 'MyWiFi' })
*
* // 监听 WiFi 状态变化
* const unwatch = wifi.onWifiStateChanged((state) => {
* console.log(state.isConnected ? '已连接' : '未连接')
* })
* unwatch() // 取消监听
*
* // 监听扫描结果
* const unwatch = wifi.onWifiScan((networks) => {
* console.log('扫描到', networks.length, '个网络')
* })
* unwatch() // 取消监听
*
* // 获取信号强度描述
* wifi.getSignalDescription(-55) // "强"
*
* // 判断频段
* wifi.is5GHz(5180) // true
*
* ======================== 前置条件 ========================
*
* - 仅支持 Android App 环境
* - 需要在 manifest.json 配置权限:
* - android.permission.ACCESS_WIFI_STATE
* - android.permission.CHANGE_WIFI_STATE
* - android.permission.ACCESS_NETWORK_STATE
* - android.permission.ACCESS_FINE_LOCATION
* - android.permission.ACCESS_COARSE_LOCATION
* - Android 6+ 还需运行时授予定位权限,且系统「位置信息」开关需开启
*/
// ======================== 类型定义 ========================
/** WiFi 网络信息 */
export interface WifiInfo {
SSID: string
BSSID: string
signalStrength: number
securityType?: string
frequency?: number
connected?: boolean
}
/** 连接 WiFi 参数 */
export interface ConnectWifiParams {
SSID: string
password?: string
timeout?: number
}
/** WiFi 状态变化回调 */
export type WifiStateCallback = (state: {
isConnected: boolean
wifiInfo: WifiInfo | null
}) => void
/** WiFi 扫描结果回调 */
export type WifiScanCallback = (networks: WifiInfo[]) => void
// ======================== 内部工具 ========================
const listeners = {
stateChange: new Map<string, WifiStateCallback>(),
scan: new Map<string, WifiScanCallback>(),
}
let receiverId = 0
/** 获取 WiFi Manager 实例 */
function getWifiManager() {
const ctx = (plus.android as any).importClass('android.content.Context')
const activity = (plus.android as any).runtimeMainActivity()
return activity.getSystemService(ctx.WIFI_SERVICE)
}
/** 调用 Android 方法 */
function invoke(obj: any, method: string, ...args: any[]) {
return (plus.android as any).invoke(obj, method, ...args)
}
/** 导入 Android 类 */
function importClass(name: string) {
return (plus.android as any).importClass(name)
}
/** 读取 Java 对象字段(ScanResult.SSID 等是字段不是方法) */
function getAttr(obj: any, name: string) {
try {
return (plus.android as any).getAttribute(obj, name)
}
catch {
return invoke(obj, name)
}
}
/** 申请定位权限(Android 扫描 WiFi 必需) */
function requestLocationPermission(): Promise<boolean> {
return new Promise((resolve) => {
try {
;(plus.android as any).requestPermissions(
[
'android.permission.ACCESS_FINE_LOCATION',
'android.permission.ACCESS_COARSE_LOCATION',
],
(result: { granted?: string[], deniedAlways?: string[], deniedPresent?: string[] }) => {
const granted = result?.granted || []
const ok = granted.includes('android.permission.ACCESS_FINE_LOCATION')
|| granted.includes('android.permission.ACCESS_COARSE_LOCATION')
console.log('定位权限申请结果:', result)
resolve(ok)
},
(err: unknown) => {
console.error('定位权限申请失败:', err)
resolve(false)
},
)
}
catch (err) {
console.error('requestPermissions 不可用:', err)
resolve(false)
}
})
}
/** 系统定位开关是否开启 */
function isLocationEnabled(): boolean {
try {
const LocationManager = importClass('android.location.LocationManager')
const Context = importClass('android.content.Context')
const activity = (plus.android as any).runtimeMainActivity()
const lm = activity.getSystemService(Context.LOCATION_SERVICE)
return !!(
invoke(lm, 'isProviderEnabled', LocationManager.GPS_PROVIDER)
|| invoke(lm, 'isProviderEnabled', LocationManager.NETWORK_PROVIDER)
)
}
catch {
return true
}
}
/** 解析 getScanResults 列表 */
function parseScanResults(wm: any): WifiInfo[] {
const results = invoke(wm, 'getScanResults')
if (!results)
return []
const size = invoke(results, 'size') || 0
const networks: WifiInfo[] = []
for (let i = 0; i < size; i++) {
const r = invoke(results, 'get', i)
const ssid = String(getAttr(r, 'SSID') || '').replace(/"/g, '')
if (!ssid || ssid === '<unknown ssid>')
continue
networks.push({
SSID: ssid,
BSSID: String(getAttr(r, 'BSSID') || ''),
signalStrength: Number(getAttr(r, 'level') || 0),
securityType: String(getAttr(r, 'capabilities') || ''),
frequency: Number(getAttr(r, 'frequency') || 0),
connected: false,
})
}
return networks
}
// ======================== WiFi 工具 ========================
const wifi = {
/** 检查 WiFi 是否可用 */
isWifiAvailable(): Promise<boolean> {
return new Promise((resolve) => {
try {
const wm = getWifiManager()
resolve(!!invoke(wm, 'isWifiEnabled'))
}
catch {
resolve(false)
}
})
},
/** 获取当前 WiFi 连接信息 */
getConnectedWifi(): Promise<WifiInfo | null> {
return new Promise((resolve, reject) => {
try {
const wm = getWifiManager()
const info = invoke(wm, 'getConnectionInfo')
const ssid = invoke(info, 'getSSID')
const bssid = invoke(info, 'getBSSID')
const rssi = invoke(info, 'getRssi')
const networkId = invoke(info, 'getNetworkId')
if (!ssid || ssid === '<unknown ssid>' || networkId === -1) {
resolve(null)
return
}
resolve({
SSID: ssid.replace(/"/g, ''),
BSSID: bssid || '',
signalStrength: rssi,
connected: true,
})
}
catch (err) {
reject(err)
}
})
},
/** 扫描附近 WiFi 网络 */
async scanWifi(): Promise<WifiInfo[]> {
const wm = getWifiManager()
const isEnabled = invoke(wm, 'isWifiEnabled')
console.log('WiFi 是否开启:', isEnabled)
if (!isEnabled)
throw new Error('请先开启 WiFi')
const hasPermission = await requestLocationPermission()
if (!hasPermission)
throw new Error('需要定位权限才能扫描 WiFi,请在系统设置中允许')
if (!isLocationEnabled())
throw new Error('请先打开系统「位置信息/定位」开关,否则无法扫描 WiFi')
return new Promise((resolve, reject) => {
let settled = false
let receiver: any = null
let timer: ReturnType<typeof setTimeout> | undefined
const activity = (plus.android as any).runtimeMainActivity()
const finish = (networks: WifiInfo[], err?: Error) => {
if (settled)
return
settled = true
if (timer)
clearTimeout(timer)
try {
if (receiver)
activity.unregisterReceiver(receiver)
}
catch {}
if (err)
reject(err)
else
resolve(networks)
}
try {
const Filter = importClass('android.content.IntentFilter')
const Receiver = importClass('android.content.BroadcastReceiver')
const filter = new Filter()
filter.addAction('android.net.wifi.SCAN_RESULTS')
receiver = new Receiver({
onReceive(_ctx: any, intent: any) {
const updated = invoke(intent, 'getBooleanExtra', 'resultsUpdated', false)
console.log('收到 SCAN_RESULTS, resultsUpdated:', updated)
const networks = parseScanResults(wm)
console.log('解析后网络数量:', networks.length)
finish(networks)
},
})
activity.registerReceiver(receiver, filter)
const started = invoke(wm, 'startScan')
console.log('WiFi 扫描启动:', started)
// startScan 失败时仍尝试读取缓存结果(限流时常见)
if (!started) {
const cached = parseScanResults(wm)
console.log('startScan 失败,缓存网络数量:', cached.length)
if (cached.length > 0) {
finish(cached)
return
}
finish([], new Error('WiFi 扫描被拒绝(可能被系统限流,请稍后再试)'))
return
}
}
catch (err) {
finish([], err instanceof Error ? err : new Error(String(err)))
return
}
timer = setTimeout(() => {
const networks = parseScanResults(wm)
console.log('扫描超时,当前网络数量:', networks.length)
if (networks.length > 0)
finish(networks)
else
finish([], new Error('扫描超时,未发现可用网络'))
}, 8000)
})
},
/** 连接指定 WiFi */
connectWifi(params: ConnectWifiParams): Promise<void> {
return new Promise((resolve, reject) => {
const { SSID, password, timeout = 10000 } = params
if (!SSID) {
reject(new Error('SSID 不能为空'))
return
}
try {
const wm = getWifiManager()
const Config = importClass('android.net.wifi.WifiConfiguration')
const config = new Config()
invoke(config, 'SSID', `"${SSID}"`)
config.networkId = -1
const keyMgmt = invoke(config, 'allowedKeyManagement')
if (password) {
config.preSharedKey = `"${password}"`
invoke(keyMgmt, 'set', 1) // WPA_PSK
}
else {
invoke(keyMgmt, 'set', 0) // NONE
}
const networkId = invoke(wm, 'addNetwork', config)
if (networkId === -1) {
const networks = invoke(wm, 'getConfiguredNetworks')
const size = invoke(networks, 'size')
for (let i = 0; i < size; i++) {
const existing = invoke(networks, 'get', i)
if (invoke(existing, 'SSID') === `"${SSID}"`) {
invoke(wm, 'updateNetwork', existing, config)
invoke(wm, 'disconnect')
invoke(wm, 'enableNetwork', invoke(existing, 'networkId'), true)
invoke(wm, 'reconnect')
resolve()
return
}
}
reject(new Error('无法连接到网络'))
return
}
invoke(wm, 'disconnect')
invoke(wm, 'enableNetwork', networkId, true)
invoke(wm, 'reconnect')
let elapsed = 0
const timer = setInterval(() => {
const info = invoke(wm, 'getConnectionInfo')
if (invoke(info, 'getSupplicantState') === 3) {
clearInterval(timer)
resolve()
return
}
elapsed += 500
if (elapsed >= timeout) {
clearInterval(timer)
reject(new Error('连接超时'))
}
}, 500)
}
catch (err) {
reject(err)
}
})
},
/** 断开当前 WiFi 连接 */
disconnectWifi(): Promise<void> {
return new Promise((resolve, reject) => {
try {
const wm = getWifiManager()
invoke(wm, 'disableNetwork', -1)
const result = invoke(wm, 'disconnect')
result ? resolve() : reject(new Error('断开连接失败'))
}
catch (err) {
reject(err)
}
})
},
/** 连接已保存的 WiFi(免密) */
connectSavedWifi(params: { SSID: string }): Promise<void> {
return new Promise((resolve, reject) => {
const { SSID } = params
try {
const wm = getWifiManager()
const networks = invoke(wm, 'getConfiguredNetworks')
const size = invoke(networks, 'size')
for (let i = 0; i < size; i++) {
const config = invoke(networks, 'get', i)
if (invoke(config, 'SSID') === `"${SSID}"`) {
invoke(wm, 'disconnect')
invoke(wm, 'enableNetwork', invoke(config, 'networkId'), true)
invoke(wm, 'reconnect')
resolve()
return
}
}
reject(new Error(`未找到已保存的网络: ${SSID}`))
}
catch (err) {
reject(err)
}
})
},
/** 获取已保存的 WiFi 列表 */
getSavedWifiList(): Promise<WifiInfo[]> {
return new Promise((resolve, reject) => {
try {
const wm = getWifiManager()
const networks = invoke(wm, 'getConfiguredNetworks')
const size = invoke(networks, 'size')
const result: WifiInfo[] = []
for (let i = 0; i < size; i++) {
const config = invoke(networks, 'get', i)
const ssid = invoke(config, 'SSID').replace(/"/g, '')
if (!ssid)
continue
result.push({
SSID: ssid,
BSSID: invoke(config, 'BSSID') || '',
signalStrength: 0,
connected: false,
})
}
resolve(result)
}
catch (err) {
reject(err)
}
})
},
/** 移除已保存的 WiFi 网络 */
removeSavedWifi(params: { SSID: string }): Promise<void> {
return new Promise((resolve, reject) => {
const { SSID } = params
try {
const wm = getWifiManager()
const networks = invoke(wm, 'getConfiguredNetworks')
const size = invoke(networks, 'size')
for (let i = 0; i < size; i++) {
const config = invoke(networks, 'get', i)
if (invoke(config, 'SSID') === `"${SSID}"`) {
invoke(wm, 'removeNetwork', invoke(config, 'networkId'))
invoke(wm, 'saveConfiguration')
resolve()
return
}
}
reject(new Error(`未找到已保存的网络: ${SSID}`))
}
catch (err) {
reject(err)
}
})
},
/** 监听 WiFi 状态变化 */
onWifiStateChanged(callback: WifiStateCallback): () => void {
const id = `state_${++receiverId}`
listeners.stateChange.set(id, callback)
try {
const activity = (plus.android as any).runtimeMainActivity()
const Filter = importClass('android.content.IntentFilter')
const Receiver = importClass('android.content.BroadcastReceiver')
const filter = new Filter()
filter.addAction('android.net.wifi.STATE_CHANGE')
const receiver = new Receiver({
onReceive(_ctx: any, intent: any) {
const action = invoke(intent, 'getAction')
if (action !== 'android.net.wifi.STATE_CHANGE')
return
const networkInfo = invoke(intent, 'getParcelableExtra', 'networkInfo')
const state = invoke(networkInfo, 'getState')
if (state === 2) {
wifi.getConnectedWifi().then(info => callback({ isConnected: true, wifiInfo: info }))
}
else {
callback({ isConnected: false, wifiInfo: null })
}
},
})
activity.registerReceiver(receiver, filter)
}
catch {}
return () => listeners.stateChange.delete(id)
},
/** 监听 WiFi 扫描结果 */
onWifiScan(callback: WifiScanCallback): () => void {
const id = `scan_${++receiverId}`
listeners.scan.set(id, callback)
try {
const activity = (plus.android as any).runtimeMainActivity()
const Filter = importClass('android.content.IntentFilter')
const Receiver = importClass('android.content.BroadcastReceiver')
const filter = new Filter()
filter.addAction('android.net.wifi.SCAN_RESULTS')
const receiver = new Receiver({
onReceive() {
const wm = getWifiManager()
callback(parseScanResults(wm))
},
})
activity.registerReceiver(receiver, filter)
}
catch {}
return () => listeners.scan.delete(id)
},
/** 获取信号强度等级 (0-4) */
getSignalLevel(rssi: number): number {
if (rssi >= -50)
return 4
if (rssi >= -60)
return 3
if (rssi >= -70)
return 2
if (rssi >= -80)
return 1
return 0
},
/** 获取信号强度描述 */
getSignalDescription(rssi: number): string {
const level = this.getSignalLevel(rssi)
return ['无信号', '弱', '一般', '强', '非常强'][level] || '未知'
},
/** 判断是否为 5GHz 频段 */
is5GHz(frequency: number): boolean {
return frequency >= 5000 && frequency <= 5900
},
/** 获取频段描述 */
getFrequencyDescription(frequency: number): string {
if (this.is5GHz(frequency))
return '5GHz'
if (frequency >= 2400 && frequency <= 2500)
return '2.4GHz'
return '未知'
},
}
export default wifi