AI_UIAutomation/utils/common/hub.helper.ts

141 lines
9.0 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.

/**
* Hub Mini WiFi 配网 —— 连上设备后进入 "Configure Wi-Fi" 页:
* - SSID 字段:默认预填手机当前 WiFi(如 "Deco"),需清空改填目标 SSID
* - 密码字段:hint "Enter Wi-Fi password",填密码
* - 点 Next 提交,等待联网完成
* 仅支持 2.4GHz WiFi(页面有提示)。凭据来自 config/wifi.config(env 可覆盖,默认 woanmarket/woankeji)。
*/
import { DeviceDriver } from '../../drivers/types';
import { sleep } from './element.helper';
import { getWifiCredentials, WifiCredentials } from '../../config/wifi.config';
import { navigateThroughWizard } from './navigation.helper';
const EDIT = 'new UiSelector().className("android.widget.EditText")';
/** 在 "Configure Wi-Fi" 页填 SSID + 密码并 Next;随后走完向导。creds 缺省取 config/wifi.config。 */
export async function configureHubWifi(driver: DeviceDriver, creds?: WifiCredentials): Promise<void> {
const { ssid, password } = creds || getWifiCredentials();
// 等配网页出现:标题(Configure Wi-Fi / Wi-Fi Settings 等)或 直接出现 ≥2 输入框(SSID+密码)即视为到达。
// BLE 连接较慢(relay 后约 24s 连上、27s 出页),窗口要够长(~37s)。
let onWifiPage = false;
for (let i = 0; i < 40; i++) {
const src = await driver.getSource();
const titleHit = /Configure Wi-?Fi|Wi-?Fi Settings|Wi-?Fi password|配置 ?Wi-?Fi/i.test(src);
const edCount = driver.platform === 'android'
? (await driver.findElementsRaw('-android uiautomator', EDIT)).length
: (await driver.findElementsRaw('predicate string', 'type == "XCUIElementTypeTextField" OR type == "XCUIElementTypeSecureTextField"')).length;
const fieldHit = edCount >= 2 && /Wi-?Fi|password|密码|2\.4\s?GHz/i.test(src);
if (titleHit || fieldHit) { onWifiPage = true; break; }
await driver.dismissPopupIfPresent();
await sleep(1500);
}
if (!onWifiPage) { console.log('WARN: 未到达 Configure Wi-Fi 页,跳过配网'); return; }
if (driver.platform === 'android') {
// Android:SSID/密码都是 EditText,直接定位元素 clear + type
const eds = await driver.findElementsRaw('-android uiautomator', EDIT);
if (eds.length < 2) { console.log('WARN: 未找到 SSID/密码输入框,跳过配网'); return; }
let ssidEl = eds[0], pwdEl = eds[1];
try { const t0 = (await driver.getElementAttribute(eds[0], 'text')) || ''; if (/password|密码/i.test(t0)) { pwdEl = eds[0]; ssidEl = eds[1]; } } catch { /* 默认顺序 */ }
await driver.clearText(ssidEl).catch(() => {});
await driver.typeText(ssidEl, ssid); await sleep(500);
// 输入 SSID 后键盘把页面顶起、遮住密码框 → 必须先收键盘才能输密码(goBack 在键盘弹起时只收键盘、不返回)
try { await driver.goBack(); } catch { /* 收键盘 */ }
await sleep(700);
// 收键盘后重新定位密码框(布局变化),密码框通常是最后一个 EditText
let pwd2 = pwdEl;
try {
const eds2 = await driver.findElementsRaw('-android uiautomator', EDIT);
if (eds2.length) pwd2 = eds2[eds2.length - 1];
} catch { /* 用原引用 */ }
await driver.typeText(pwd2, password); await sleep(500);
// 再收一次键盘,Next 才点得到
try { await driver.goBack(); } catch { /* 收键盘 */ }
await sleep(600);
} else {
// iOS:优先**元素法**(plug 等的 SSID=XCUIElementTypeTextField、密码=XCUIElementTypeSecureTextField,可定位)。
// 找不到标准输入框(如 hub 自绘 UI、无障碍不可见)再退**坐标法**(/wda/keys)。
const d = driver as any;
const ssidEl = await driver.findElementRaw('predicate string', 'type == "XCUIElementTypeTextField"').catch(() => null);
const pwdEl = await driver.findElementRaw('predicate string', 'type == "XCUIElementTypeSecureTextField"').catch(() => null);
if (ssidEl && pwdEl) {
// **关键:每个框即时重新 find 再 typeText**。填完 SSID 键盘弹出→界面树变→开头找的 pwd 元素 id 会 stale 失效,
// 导致密码 typeText 不生效(US 配网失败根因)。typeText 走 element/value=setValue(会自动聚焦,iOS 标准框可靠),框为空无需 clear。
const fill = async (predicate: string, text: string) => {
const el = await driver.findElementRaw('predicate string', predicate).catch(() => null);
if (!el) { console.log(`WARN: 未找到输入框(${predicate})`); return; }
await driver.tapElement(el).catch(() => {}); // 聚焦(已验证有效:tap 聚焦 → setValue)
await sleep(400);
await driver.typeText(el, text); // element/value setValue(已验证可正确填入)
await sleep(700);
};
await fill('type == "XCUIElementTypeTextField"', ssid);
await fill('type == "XCUIElementTypeSecureTextField"', password);
// 收键盘:iOS 键盘的 "Done"/"Return" 键(挡住下方 Next,必须先收;driver.tapElement 走 /wda/tap 真实点,有效)
for (const k of ['Done', 'Return', 'Go', 'Join', 'done', '完成']) {
const kb = await driver.findElementRaw('name', k).catch(() => null);
if (kb) { await driver.tapElement(kb); await sleep(900); break; }
}
} else {
// 坐标法(自绘 UI 的 hub,如旧机):坐标按 iPhone 14 Pro(393×852pt),SSID≈(118,358)、密码≈(118,443);换机型需调。
// 重要:SSID 预填手机当前 WiFi,聚焦不会全选,必须先清空——双击选中再键入;最后用 "Return" 收键盘。
await d.tap(118, 358); await sleep(800);
await d.doubleTap(118, 358); await sleep(700);
await d.sendKeys(['\b']); await sleep(300);
await d.sendKeys([ssid]); await sleep(800);
await d.tap(118, 443); await sleep(800);
await d.sendKeys([password]); await sleep(800);
const ret = await driver.findElementRaw('name', 'Return').catch(() => null);
if (ret) { await driver.tapElement(ret); await sleep(1000); }
}
}
console.log(`Hub WiFi 配网:SSID="${ssid}" 密码已填,提交...`);
// 点 Next(iOS 名为 Next;Android text=Next)
const next = driver.platform === 'android'
? await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Next")')
: await driver.findElementRaw('name', 'Next');
if (next) await driver.tapElement(next);
// 提交后是多步配网过程:连接 WiFi → 验证成功 → 设备配网成功 → 跳添加成功页,过程较久。
// 期间都是进度页,**不要乱点按钮**(误点会打断配网),只被动等"成功"强标志出现 + 关弹框。
const SUCCESS = [
'added successfully', 'Added successfully', 'Start Using', 'Use now', 'Pick a room',
'Set up complete', 'Setup complete', 'configured successfully', 'connected to',
'添加成功', '配网成功', '连接成功', '设置完成',
];
const FAIL = ['Failed', 'failed', 'incorrect', 'wrong', 'Unable', '失败', '密码错误', 'try again', 'Try Again'];
let reached = false;
let leftPage = 0; // 连续几次"已离开配网输入页"(提前结束用)
for (let t = 0; t < 30; t++) { // 最多 ~90s 等配网过程
const src = await driver.getSource();
if (SUCCESS.some((k) => src.includes(k))) { console.log(`Hub 配网成功(到结果页, +${t * 3}s)`); reached = true; break; }
if (FAIL.some((k) => src.includes(k))) {
const vis = (src.match(/text="([^"]+)"/g) || []).map((x) => x.replace(/text="|"/g, '')).filter(Boolean).slice(0, 12).join(' | ');
console.log(`WARN: Hub 配网疑似失败(+${t * 3}s)。页面:${vis}`);
break;
}
if (t % 3 === 0) { // 每 ~9s 记一次当前阶段,便于排查"连接/验证/配网"卡在哪
const vis = (src.match(/text="([^"]+)"/g) || []).map((x) => x.replace(/text="|"/g, '')).filter(Boolean).slice(0, 8).join(' | ');
console.log(` 配网进行中 +${t * 3}s: ${vis}`);
}
// 效率优化:提交后离开配网输入页(标题/密码框消失)= 已进入配网过程,提前结束(交后续首页校验),不死等 90s。
// 但"连接中/验证中"页(Connecting/Trying to connect)既非输入页也非成功页 → 不能算"已离开"(否则灯类慢连接被提前切断,设备没落账号),需继续等成功页。
const onInputPage = /Wi-?Fi Settings|Configure Wi-?Fi|Enter password|配置 ?Wi-?Fi/i.test(src);
const connecting = /Connecting|Trying to connect|connecting to your router|Verifying|连接中|正在连接|验证中/i.test(src);
if (!onInputPage && !connecting && t >= 1) {
if (++leftPage >= 2) { console.log(`配网页已离开(+${t * 3}s),提前结束等待`); reached = true; break; }
} else { leftPage = 0; }
await driver.dismissPopupIfPresent();
await sleep(3000);
}
if (!reached) console.log('WARN: 未等到 Hub 配网成功页(可能 WiFi 不可达/非2.4G/密码错,或过程超 90s)');
// 到结果页后走完结尾向导(Start Using / Done / 房间等)。
// 补充:部分 plug(如 US/JP)配网成功后多一步"Save/保存"页(EU 无此步)→ 加进按钮列表,补差异不改原流程。
await driver.dismissPopupIfPresent();
await navigateThroughWizard(driver, ['Save', '保存', 'Use now', 'Start Using', 'Done', 'Got it', 'OK', 'Skip', 'Next'], 8);
}