/** * 二维码解码 helper —— 截屏 → 解码,用于摄像头 WiFi 配网 QR 的**凭据校验**。 * (摄像头联网那步是镜头光学扫描,需物理治具;本 helper 负责解码+断言 QR 内容正确。) * * 依赖:pngjs(解 PNG)+ jsqr(解码)。截图是 PNG。 */ import { PNG } from 'pngjs'; import jsQR from 'jsqr'; import { DeviceDriver } from '../../drivers/types'; /** 从 PNG buffer 解码二维码;失败返回 null。可选裁剪中心区域提高小码命中率。 */ export function decodeQRFromPngBuffer(buf: Buffer, cropCenter = false): string | null { const png = PNG.sync.read(buf); let data = png.data, width = png.width, height = png.height; if (cropCenter) { // 取中心 60% 区域(二维码通常居中)重采样,提高识别率 const cw = Math.floor(width * 0.6), ch = Math.floor(height * 0.6); const ox = Math.floor((width - cw) / 2), oy = Math.floor((height - ch) / 2); const cropped = Buffer.alloc(cw * ch * 4); for (let y = 0; y < ch; y++) { png.data.copy(cropped, y * cw * 4, ((oy + y) * width + ox) * 4, ((oy + y) * width + ox + cw) * 4); } data = cropped; width = cw; height = ch; } const code = jsQR(new Uint8ClampedArray(data), width, height); return code ? code.data : null; } /** 从 base64 PNG 解码。先整图,失败再试中心裁剪。 */ export function decodeQRFromBase64(b64: string): string | null { const buf = Buffer.from(b64, 'base64'); return decodeQRFromPngBuffer(buf, false) || decodeQRFromPngBuffer(buf, true); } /** 截当前屏并解码(Appium 截图)。 */ export async function decodeQRFromScreen(driver: DeviceDriver): Promise { const b64 = await driver.screenshot(); return decodeQRFromBase64(b64); } /** * 解析 QR 内容里的 WiFi 字段。 * SwitchBot 摄像头配网码实测为 JSON:{"s":"","p":"","t":"","r":""}。 * 也兼容标准 WiFi 码 `WIFI:S:;T:WPA;P:;;`,及常见 key=value 启发式。 */ export interface WifiQR { ssid?: string; password?: string; token?: string; region?: string; raw: string; } export function parseWifiQR(text: string): WifiQR { const out: WifiQR = { raw: text }; // 1) SwitchBot JSON 码 try { const j = JSON.parse(text); if (j && typeof j === 'object') { out.ssid = j.s ?? j.ssid; out.password = j.p ?? j.password ?? j.pwd; out.token = j.t ?? j.token; out.region = j.r ?? j.region; if (out.ssid || out.password) return out; } } catch { /* 非 JSON,继续 */ } // 2) 标准 WiFi 码 const std = text.match(/WIFI:(?:[^;]*;)*S:([^;]*);(?:[^;]*;)*P:([^;]*);/i); if (std) { out.ssid = std[1]; out.password = std[2]; return out; } // 3) 启发式 const ssid = text.match(/(?:ssid|wifi_?name)["':=\s]+([^"',;&}\s]+)/i); const pwd = text.match(/(?:password|pass|pwd|psk)["':=\s]+([^"',;&}\s]+)/i); if (ssid) out.ssid = ssid[1]; if (pwd) out.password = pwd[1]; return out; } /** 校验 QR 内 WiFi 凭据是否与期望一致(用于摄像头配网断言)。 */ export function verifyWifiQR(text: string, expect: { ssid?: string; password?: string }): { ok: boolean; parsed: WifiQR; reason?: string } { const parsed = parseWifiQR(text); if (expect.ssid && parsed.ssid !== expect.ssid) return { ok: false, parsed, reason: `SSID 不符: 期望 ${expect.ssid} 实际 ${parsed.ssid}` }; if (expect.password && parsed.password !== expect.password) return { ok: false, parsed, reason: `密码不符: 期望 ${expect.password} 实际 ${parsed.password}` }; return { ok: true, parsed }; }