86 lines
3.5 KiB
TypeScript
86 lines
3.5 KiB
TypeScript
/**
|
|
* 二维码解码 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<string | null> {
|
|
const b64 = await driver.screenshot();
|
|
return decodeQRFromBase64(b64);
|
|
}
|
|
|
|
/**
|
|
* 解析 QR 内容里的 WiFi 字段。
|
|
* SwitchBot 摄像头配网码实测为 JSON:{"s":"<ssid>","p":"<password>","t":"<token>","r":"<region>"}。
|
|
* 也兼容标准 WiFi 码 `WIFI:S:<ssid>;T:WPA;P:<pass>;;`,及常见 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 };
|
|
}
|
|
|