202 lines
10 KiB
TypeScript
202 lines
10 KiB
TypeScript
/**
|
|
* Curtain / Curtain 3 连接后专属流程(选模式 + 行程校验)。
|
|
* **curtain 与 curtain3 校准流程不同**,分开实现:
|
|
* - curtain3:已实测全程跑通(Custom Calibration:朝向→已关闭→跳磁铁→Move left/right)。
|
|
* - curtain(base):校准流程与 curtain3 不同,待该设备可连接后补全;当前先走"稍后校准"。
|
|
*
|
|
* 作为 addDeviceWithSerialPairing 的 postConnectSteps 传入。所有点击用文字/desc 定位,不写死坐标。
|
|
*/
|
|
import { DeviceDriver } from '../../drivers/types';
|
|
import { sleep, waitForSource } from './element.helper';
|
|
|
|
export interface CurtainAddOptions {
|
|
/** true=做行程校验(需设备装在真实导轨上、电机能跑);false=Install and calibrate later 跳过校验。 */
|
|
calibrate?: boolean;
|
|
/** Determine 位置每个方向的行进等待(ms),默认 15000。 */
|
|
moveWaitMs?: number;
|
|
/** Install 页内联重命名的目标名(curtain 类首页都叫 "Curtain XX",靠改名区分型号)。不传则不改名。 */
|
|
renameTo?: string;
|
|
}
|
|
|
|
async function tapText(driver: DeviceDriver, txt: string): Promise<boolean> {
|
|
const el = driver.platform === 'android'
|
|
? await driver.findElementRaw('-android uiautomator', `new UiSelector().text("${txt}")`)
|
|
: await driver.findElementRaw('name', txt);
|
|
if (el) { await driver.tapElement(el); return true; }
|
|
return false;
|
|
}
|
|
|
|
async function tapDesc(driver: DeviceDriver, desc: string): Promise<boolean> {
|
|
const el = driver.platform === 'android'
|
|
? await driver.findElementRaw('-android uiautomator', `new UiSelector().descriptionContains("${desc}")`)
|
|
: await driver.findElementRaw('predicate string', `label CONTAINS "${desc}" OR name CONTAINS "${desc}"`);
|
|
if (el) { await driver.tapElement(el); return true; }
|
|
return false;
|
|
}
|
|
|
|
/** 按 resource-id 点击(Android)。base curtain 校准的 Move/Stop 是图标(ImageView),文字是不可点的标签,必须按 id 点图标。 */
|
|
async function tapId(driver: DeviceDriver, id: string): Promise<boolean> {
|
|
const el = await driver.findElementRaw('-android uiautomator', `new UiSelector().resourceId("com.theswitchbot.switchbot:id/${id}")`);
|
|
if (el) { await driver.tapElement(el); return true; }
|
|
return false;
|
|
}
|
|
|
|
|
|
async function selectModeOpenOneSide(driver: DeviceDriver): Promise<void> {
|
|
await waitForSource(driver, 'Select Mode', 60000).catch(() => {});
|
|
if (!(await tapDesc(driver, 'Open from one side'))) await tapText(driver, 'Open from one side');
|
|
await sleep(1000);
|
|
// 推进按钮:curtain3 用 "Confirm",base curtain 用 "Next" → 试 Confirm,无则 Next
|
|
if (!(await tapText(driver, 'Confirm'))) await tapText(driver, 'Next');
|
|
}
|
|
|
|
/** Curtain 3 连接后流程(含已验证的 Custom Calibration)。 */
|
|
export async function curtain3PostConnect(driver: DeviceDriver, opts: CurtainAddOptions = {}): Promise<void> {
|
|
const calibrate = opts.calibrate ?? false;
|
|
const moveWait = opts.moveWaitMs ?? 15000;
|
|
|
|
await selectModeOpenOneSide(driver);
|
|
|
|
await waitForSource(driver, 'Install', 30000).catch(() => {});
|
|
|
|
// Install 页含设备名(可编辑 EditText)+ Install / Install and calibrate later。
|
|
// 按需在**本页内联重命名**(curtain/curtain3/2025 首页都叫 "Curtain XX",靠改名区分)→ 再继续校准。
|
|
if (opts.renameTo) {
|
|
const nameEd = await driver.findElementRaw('-android uiautomator', 'new UiSelector().className("android.widget.EditText")').catch(() => null);
|
|
if (nameEd) {
|
|
await driver.tapElement(nameEd); await sleep(500);
|
|
await driver.clearText(nameEd).catch(() => {});
|
|
await driver.typeText(nameEd, opts.renameTo); await sleep(500);
|
|
try { await driver.goBack(); } catch { /* 收键盘 */ }
|
|
await sleep(700);
|
|
console.log(`curtain3: Install 页内联重命名为 "${opts.renameTo}"`);
|
|
} else {
|
|
console.log('curtain3: WARN Install 页未找到名称输入框,跳过重命名');
|
|
}
|
|
}
|
|
|
|
if (!calibrate) {
|
|
await tapText(driver, 'Install and calibrate later');
|
|
console.log('curtain3: 走 Install and calibrate later(跳过行程校验)');
|
|
return;
|
|
}
|
|
await tapText(driver, 'Install');
|
|
|
|
// Curtain Track 选轨道 → 底部 Skip
|
|
await waitForSource(driver, 'rail type', 15000).catch(() => {});
|
|
await tapText(driver, 'Skip');
|
|
|
|
// Calibrate → Custom Calibration
|
|
await waitForSource(driver, 'Custom Calibration', 15000).catch(() => {});
|
|
await tapText(driver, 'Custom Calibration');
|
|
|
|
// ① Confirm Orientation:勾选"logo 朝内"单选行 → Next
|
|
// 注意:用唯一子串 "logo is facing inwards"(单选行 "SwitchBot logo is facing inwards"),
|
|
// 不能用 "facing inwards" —— 会误匹配描述段 "...logo on your device is facing inwards",导致单选没勾上、Next 灰着。
|
|
await waitForSource(driver, 'Confirm Orientation', 15000).catch(() => {});
|
|
if (!(await tapDesc(driver, 'logo is facing inwards'))) await tapDesc(driver, 'facing inwards');
|
|
await sleep(800);
|
|
await tapText(driver, 'Next');
|
|
|
|
// ② Adjust Position:勾选"已关闭窗帘"单选行 → Start Calibration(同样用唯一子串避免误匹配描述)
|
|
await waitForSource(driver, 'fully closed', 15000).catch(() => {});
|
|
if (!(await tapDesc(driver, 'fully closed my curtains'))) await tapDesc(driver, 'fully closed');
|
|
await sleep(800);
|
|
await tapText(driver, 'Start Calibration');
|
|
|
|
// ③ Magnet Detection:Skip magnet detection → 弹框 Skip
|
|
await waitForSource(driver, 'Magnet Detection', 15000).catch(() => {});
|
|
await tapText(driver, 'Skip magnet detection');
|
|
await sleep(1500);
|
|
await tapText(driver, 'Skip');
|
|
|
|
// ④ Determine Fully open:Move left 行进 → 点"暂停"停在位 → Auto-Calibrate Close。
|
|
// Move left 点击后按钮会变成 "Pause"(窗帘行进中),必须先点 Pause 停下,否则位置没记录 → "Not calibrated"。
|
|
await waitForSource(driver, 'Move left', 20000).catch(() => {});
|
|
await tapText(driver, 'Move left');
|
|
await sleep(moveWait); // 让窗帘行进到目标全开位
|
|
// 行进中按钮变 Pause:点它停下(中英/可能为 "Pause"/"暂停")
|
|
if (!(await tapText(driver, 'Pause'))) await tapText(driver, '暂停');
|
|
await sleep(1500);
|
|
await tapText(driver, 'Auto-Calibrate Close');
|
|
await sleep(5000); // 等自动关闭校准结算(期间可能出现 "Something went wrong",仍可 Finish)
|
|
|
|
// ⑤ Finish:点一次可能不离开校验页(页面结算中),重试直到离开 Custom Calibration 页
|
|
for (let i = 0; i < 6; i++) {
|
|
const src = await driver.getSource();
|
|
if (!/Custom Calibration|Move left|Auto-Calibrate/i.test(src)) break; // 已离开校验页 = 完成
|
|
await tapText(driver, 'Finish');
|
|
await sleep(3000);
|
|
}
|
|
console.log('curtain3: Custom Calibration 完成');
|
|
}
|
|
|
|
/**
|
|
* Base Curtain 连接后流程(实测 2026-06-09 跑通)。流程与 curtain3 **不同**:
|
|
* Select Mode→Next → 命名页 Next → Installation Guide(选导轨)→ Skip Installation Guide
|
|
* → Calibrate → Manual Calibration → Confirm Orientation(勾 logo 朝向)→ Start Calibration
|
|
* → 2/3 **Move left**→Next → 3/3 **Move right**→Finish → 首页。
|
|
* 关键:左限位在 2/3、右限位在 3/3,分两页设;同页又左又右会"Route too short"。
|
|
*/
|
|
export async function curtainPostConnect(driver: DeviceDriver, opts: CurtainAddOptions = {}): Promise<void> {
|
|
const moveWait = opts.moveWaitMs ?? 5000; // Move 后等电机行进 ~5s 再 Stop 记录限位
|
|
|
|
// ① Select Mode:选"单边开合"(默认已勾)→ Next
|
|
await selectModeOpenOneSide(driver);
|
|
|
|
// ② 命名页(默认名如 "Curtain 4D"):按需**本页内联改名**(同 curtain3,首页都叫 "Curtain XX",靠改名区分型号)→ Next
|
|
await waitForSource(driver, 'Name', 30000).catch(() => {});
|
|
if (opts.renameTo) {
|
|
const nameEd = await driver.findElementRaw('-android uiautomator', 'new UiSelector().className("android.widget.EditText")').catch(() => null);
|
|
if (nameEd) {
|
|
await driver.tapElement(nameEd); await sleep(500);
|
|
await driver.clearText(nameEd).catch(() => {});
|
|
await driver.typeText(nameEd, opts.renameTo); await sleep(500);
|
|
try { await driver.goBack(); } catch { /* 收键盘 */ }
|
|
await sleep(700);
|
|
console.log(`curtain(base): 命名页内联重命名为 "${opts.renameTo}"`);
|
|
} else {
|
|
console.log('curtain(base): WARN 命名页未找到名称输入框,跳过重命名');
|
|
}
|
|
}
|
|
await tapText(driver, 'Next');
|
|
|
|
// ③ Installation Guide(选导轨类型 I/U/Rod)→ 底部 "Skip Installation Guide"
|
|
await waitForSource(driver, 'Installation Guide', 20000).catch(() => {});
|
|
await tapText(driver, 'Skip Installation Guide');
|
|
|
|
// ④ Calibrate 页 → Manual Calibration(base curtain 此页无"稍后校准",按需求走手动)
|
|
await waitForSource(driver, 'Calibrate', 15000).catch(() => {});
|
|
await tapText(driver, 'Manual Calibration');
|
|
|
|
// ⑤ Confirm Orientation:勾"logo 朝内" → Start Calibration
|
|
await waitForSource(driver, 'Confirm Orientation', 15000).catch(() => {});
|
|
if (!(await tapText(driver, 'I confirm device logo is facing indoors.')))
|
|
await tapDesc(driver, 'facing indoors');
|
|
await sleep(800);
|
|
await tapText(driver, 'Start Calibration');
|
|
|
|
// ⑥ 2/3 设左限位:点 ivToLeft 图标开始左移 → 等 ~5s → 再点 ivToLeft 停止 → Next。
|
|
// 注意:"Move left"/"Stop" 是不可点的文字标签,真正可点的是上方图标 ivToLeft(同一图标 toggle:点开始/再点停)。
|
|
await waitForSource(driver, 'Move left', 20000).catch(() => {});
|
|
await tapId(driver, 'ivToLeft');
|
|
await sleep(moveWait);
|
|
await tapId(driver, 'ivToLeft'); // 再点同一图标 = Stop
|
|
await sleep(1500);
|
|
await tapText(driver, 'Next');
|
|
|
|
// ⑦ 3/3 设右限位:点 ivToRight 图标右移 → 等 ~5s → 再点 ivToRight 停止 → Finish
|
|
await waitForSource(driver, '3/3', 15000).catch(() => {});
|
|
await tapId(driver, 'ivToRight');
|
|
await sleep(moveWait);
|
|
await tapId(driver, 'ivToRight'); // 再点同一图标 = Stop
|
|
await sleep(1500);
|
|
await tapText(driver, 'Finish');
|
|
|
|
// ⑧ Calibrated successfully → Done
|
|
await waitForSource(driver, 'Calibrated successfully', 15000).catch(() => {});
|
|
await tapText(driver, 'Done');
|
|
await sleep(3000);
|
|
console.log('curtain(base): Manual Calibration 完成(图标toggle: ivToLeft→stop→Next→ivToRight→stop→Finish→Done)');
|
|
}
|