AI_UIAutomation/utils/common/network.helper.ts

191 lines
8.9 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.

/**
* 双协议网络前置:按 PROTO 切手机蓝牙/WiFi用于必测项控制用例的 BLE/WiFi 两种模式。
*
* - ble 模式: 开蓝牙、关 WiFi → app 走 BLE 直连
* - wifi 模式: 关蓝牙、开 WiFi → app 走 WiFi/云
*
* 平台能力(已在三星真机实测):
* - Android WiFi : adb `svc wifi enable/disable` 可行(dumpsys 实测真关/开)
* - Android 蓝牙 : adb `svc bluetooth` 被新系统禁用(exit 137)、`cmd bluetooth_manager` 无实现、
* `settings put bluetooth_on` 不动 radio → 改为 `am start 蓝牙设置` + 点 switch_widget(实测可切 ON↔BLE_ON)
* - iOS : 无公开 API,走系统设置(com.apple.Preferences)UI;locator 需 iOS 真机校准
*/
import { execSync } from 'child_process';
import { DeviceDriver } from '../../drivers/types';
import { APP_CONFIG } from '../../config/app.config';
import { sleep } from './element.helper';
function adbShell(cmd: string): string {
return execSync(`adb shell ${cmd}`, { encoding: 'utf-8', timeout: 20000 });
}
// ---------- Android(纯 adb,已实测) ----------
async function androidSetWifi(on: boolean): Promise<void> {
adbShell(`svc wifi ${on ? 'enable' : 'disable'}`);
await sleep(2000);
}
async function androidSetBluetooth(on: boolean): Promise<void> {
adbShell('am start -a android.settings.BLUETOOTH_SETTINGS');
await sleep(2500);
adbShell('uiautomator dump /sdcard/ui.xml');
const xml = adbShell('cat /sdcard/ui.xml');
// 三星实测节点: resource-id="com.android.settings:id/switch_widget" class=Switch checked=.. bounds=[x1,y1][x2,y2]
const m = xml.match(/switch_widget"[^>]*?checked="(true|false)"[^>]*?bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
if (!m) {
console.warn('[network] 未找到蓝牙开关 switch_widget,跳过(请真机校准 locator)');
return;
}
const isOn = m[1] === 'true';
if (isOn !== on) {
const cx = Math.round((Number(m[2]) + Number(m[4])) / 2);
const cy = Math.round((Number(m[3]) + Number(m[5])) / 2);
adbShell(`input tap ${cx} ${cy}`);
await sleep(3000);
}
}
// ---------- iOS(系统设置 UI,locator 已在 iPhone/iOS26.5 实测) ----------
async function iosSetBluetooth(driver: DeviceDriver, on: boolean): Promise<void> {
await driver.activateApp('com.apple.Preferences');
await sleep(2000);
// 进入蓝牙二级页:设置首页 cell(文案本地化,中文"蓝牙"/英文"Bluetooth")
const entry =
(await driver.findElementRaw('accessibility id', '蓝牙').catch(() => null)) ||
(await driver.findElementRaw('accessibility id', 'Bluetooth').catch(() => null));
if (entry) {
await driver.tapElement(entry);
await sleep(2000);
}
// 蓝牙开关:name="BLUETOOTH"(与语言无关),value "1"=开 / "0"=关
const sw = await driver.findElementRaw('accessibility id', 'BLUETOOTH').catch(() => null);
if (!sw) {
console.warn('[network] iOS 未找到蓝牙开关(accessibility id=BLUETOOTH),跳过');
return;
}
const val = await driver.getElementAttribute(sw, 'value').catch(() => '');
const isOn = val === '1' || val === 'true';
if (isOn !== on) {
await driver.tapElement(sw);
await sleep(2500);
}
}
// iOS WiFi(无 adb,走系统设置;locator 已在 iOS26.5 实测:开关 name="无线局域网" value 1/0)
async function iosSetWifi(driver: DeviceDriver, on: boolean): Promise<void> {
await driver.activateApp('com.apple.Preferences');
await sleep(2000);
// 进入 WiFi 页:cell 文案本地化(中文"无线局域网" / 英文"Wi-Fi"/"WLAN")
const entry =
(await driver.findElementRaw('accessibility id', '无线局域网').catch(() => null)) ||
(await driver.findElementRaw('accessibility id', 'Wi-Fi').catch(() => null)) ||
(await driver.findElementRaw('accessibility id', 'WLAN').catch(() => null));
if (entry) {
await driver.tapElement(entry);
await sleep(2000);
}
// WiFi 开关:name="无线局域网"(本地化), value "1"=开 / "0"=关
const sw =
(await driver.findElementRaw('accessibility id', '无线局域网').catch(() => null)) ||
(await driver.findElementRaw('accessibility id', 'Wi-Fi').catch(() => null));
if (!sw) {
console.warn('[network] iOS 未找到 WiFi 开关,跳过');
return;
}
const val = await driver.getElementAttribute(sw, 'value').catch(() => '');
const isOn = val === '1' || val === 'true';
if (isOn !== on) {
await driver.tapElement(sw);
await sleep(2500);
}
}
/**
* 按协议设置手机网络状态,然后切回 SwitchBot app。
* 无人值守可用(Android 全自动;iOS 走设置 UI)。
*/
export async function applyProtoNetwork(driver: DeviceDriver, proto: 'ble' | 'wifi'): Promise<void> {
const wantBluetooth = proto === 'ble';
const wantWifi = proto === 'wifi';
if (driver.platform === 'android') {
await androidSetWifi(wantWifi);
await androidSetBluetooth(wantBluetooth);
await driver.activateApp(APP_CONFIG.android.appPackage);
} else {
await iosSetWifi(driver, wantWifi);
await iosSetBluetooth(driver, wantBluetooth);
await driver.activateApp(APP_CONFIG.ios.bundleId);
}
await sleep(2000);
}
/**
* 关→开手机蓝牙,复位蓝牙栈。BLE 扫描/配对偶发卡死(吸顶灯 Pro "FAIL: connection timeout"、
* 重跑就过)的根治。用于:跑 BLE 控制前、灯类 BLE 配对添加前。失败不抛(非致命)。
*
* Android 直接用 `adb svc bluetooth disable/enable` —— 本机(三星)实测可真切 radio(bluetooth_on 1→0→1,
* exit=0),不走设置 UI、不与 Appium 冲突、前台 app 不变。(早期机器 svc 被禁才绕设置 UI,本机不需要。)
*/
export async function resetBluetooth(driver: DeviceDriver): Promise<void> {
try {
if (driver.platform === 'android') {
adbShell('svc bluetooth disable');
await sleep(2500);
adbShell('svc bluetooth enable');
await sleep(5000); // 等蓝牙栈重新就绪
} else {
// iOS 无 adb,走系统设置 UI 关再开,完成后切回 SwitchBot app
await iosSetBluetooth(driver, false);
await sleep(1500);
await iosSetBluetooth(driver, true);
await sleep(4000);
await driver.activateApp(APP_CONFIG.ios.bundleId);
await sleep(2000);
}
console.log('已重置手机蓝牙(关→开)');
} catch (e: any) {
console.log(`重置蓝牙失败(非致命): ${e.message}`);
}
}
/** 首页"响应度":设备卡数 + 带实时状态(On/Off/℃/%/open 等)的卡数。BLE 未连上时卡片多为无状态/无响应。 */
async function homepageResponsiveness(driver: DeviceDriver): Promise<{ cards: number; stateHits: number }> {
const src = await driver.getSource().catch(() => '');
const cards = driver.platform === 'android'
? (src.match(/resource-id="[^"]*\/(nameText|nameTextMeter)"/g) || []).length
: (src.match(/XCUIElementTypeCell[^>]*?name="/g) || []).length;
// 实时状态关键词(连上 BLE 才会刷出来):开关/温湿度/窗帘/传感器等
const stateHits = (src.match(/(On\b|Off\b|Fully open|Fully closed|Partially open|Opened|Closed|℃|°C|\d%|ppm|Detected|unoccupied|Locked|Unlocked)/g) || []).length;
return { cards, stateHits };
}
/**
* 切到"仅 BLE"模式并**校验设备已连上**:关手机 WiFi → 复位蓝牙 → 下拉刷新首页 → 看卡片是否有响应。
* 大部分卡无响应(BLE 没连上)就重试整套切换(最多 maxRetries 次)。根治"中途切 BLE 后首页卡片全无响应 → BLE 控制整片失败"。
* 仅 Android(BLE 控制阶段);校验是启发式(状态关键词/卡数比例),非致命,失败只告警。
*/
export async function switchToBleAndVerify(driver: DeviceDriver, maxRetries = 3): Promise<boolean> {
if (driver.platform !== 'android') return true;
adbShell('svc wifi disable'); // 关 WiFi 强制 BLE 直连
await sleep(3000);
const { width: w, height: h } = await driver.getWindowSize().catch(() => ({ width: 1080, height: 2280 }));
for (let attempt = 1; attempt <= maxRetries; attempt++) {
await resetBluetooth(driver); // 关→开蓝牙复位
await driver.activateApp(APP_CONFIG.android.appPackage).catch(() => {});
await sleep(2000);
await driver.goBackToHomepage().catch(() => {});
await sleep(1500);
// 下拉刷新首页(从上方下拉),触发 App 重新建立 BLE 连接
await driver.swipe(Math.round(w / 2), Math.round(h * 0.22), Math.round(w / 2), Math.round(h * 0.72), 0.6).catch(() => {});
await sleep(10000); // 等 BLE 重连 + 状态刷新
const { cards, stateHits } = await homepageResponsiveness(driver);
// 响应度:有状态关键词数 ≥ 卡数的一半,视为大部分设备已连上
const ok = cards > 0 && stateHits >= Math.max(2, Math.ceil(cards * 0.5));
console.log(`[BLE校验] 第${attempt}/${maxRetries}次:卡片${cards}、状态命中${stateHits}${ok ? '已连上' : '大部分无响应,重试切换'}`);
if (ok) return true;
}
console.log('[BLE校验] 多次重试后仍大部分无响应(后续 BLE 控制可能失败,日志已标记)');
return false;
}