/** * 双协议网络前置:按 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 { adbShell(`svc wifi ${on ? 'enable' : 'disable'}`); await sleep(2000); } async function androidSetBluetooth(on: boolean): Promise { 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 { 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 { 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 { 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 { 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 { 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; }