141 lines
9.0 KiB
TypeScript
141 lines
9.0 KiB
TypeScript
/**
|
||
* 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);
|
||
}
|
||
|