const reader = new ReaderClient(); const MAX_LOG_LINES = 300; const logLines = []; // 状态 let isPolling = false; let pollingTimer = null; let readCount = 0; // 去重相关 let seenUids = new Map(); // uid -> { protocol, time } const elements = { statusTag: document.getElementById("statusTag"), wsUrlText: document.getElementById("wsUrlText"), uidText: document.getElementById("nfch"), // ("uidText")。Lin readCountText: document.getElementById("readCountText"), lastProtocolText: document.getElementById("lastProtocolText"), // ISO14443A keyTypeInput: document.getElementById("keyTypeInput"), keyInput: document.getElementById("keyInput"), blockAddressInput: document.getElementById("blockAddressInput"), numberOfBlocksReadInput: document.getElementById("numberOfBlocksReadInput"), // ISO15693 readDataTypeInput: document.getElementById("readDataTypeInput"), readSecurityStatusInput: document.getElementById("readSecurityStatusInput"), iso15693BlockAddressInput: document.getElementById("iso15693BlockAddressInput"), iso15693NumberOfBlocksInput: document.getElementById("iso15693NumberOfBlocksInput"), // 轮询 pollingInterval: document.getElementById("pollingInterval"), dedupeMode: document.getElementById("dedupeMode"), dedupeTime: document.getElementById("dedupeTime"), dedupeTimeItem: document.getElementById("dedupeTimeItem"), // 按钮 btnConnect: document.getElementById("btnConnect"), btnReadUid: document.getElementById("btnReadUid"), btnStartPolling: document.getElementById("btnStartPolling"), btnStopPolling: document.getElementById("btnStopPolling"), btnClearLog: document.getElementById("btnClearLog"), loadingHint: document.getElementById("loadingHint"), logArea: document.getElementById("logArea"), }; function log(message, type = "info") { const time = new Date().toLocaleTimeString(); const prefix = type === "success" ? "✓" : type === "error" ? "✗" : type === "warn" ? "⚠" : "•"; const line = `[${time}] ${prefix} ${message}`; console.log(line); logLines.unshift(line); if (logLines.length > MAX_LOG_LINES) { logLines.length = MAX_LOG_LINES; } elements.logArea.value = logLines.join("\n"); } function getErrorMessage(err) { if (err instanceof Error && err.message) { return err.message; } if (typeof err === "string") { return err; } try { return JSON.stringify(err); } catch { return "未知错误"; } } function setLoading(text = "") { elements.loadingHint.textContent = text; } function setStatus(text, ok) { elements.statusTag.textContent = text; elements.statusTag.classList.toggle("ok", Boolean(ok)); elements.statusTag.classList.toggle("bad", !ok); } function syncButtons() { const connected = reader.connected; elements.btnReadUid.disabled = !connected; elements.btnStartPolling.disabled = !connected || isPolling; elements.btnStopPolling.disabled = !connected || !isPolling; } function updateReadCount() { readCount++; elements.readCountText.textContent = readCount; } function normalizeHexKey(value) { return String(value || "") .trim() .toUpperCase() .replace(/[^0-9A-F]/g, ""); } function applyReaderConfig() { // ISO14443A 参数 const key = normalizeHexKey(elements.keyInput.value); const keyType = String(elements.keyTypeInput.value || "0"); const blockAddress = String(elements.blockAddressInput.value || "1").trim(); const numberOfBlocksRead = String( elements.numberOfBlocksReadInput.value || "1" ).trim(); if (!/^[0-9A-F]{12}$/.test(key)) { throw new Error("密钥必须是 12 位十六进制字符"); } if (!["0", "1"].includes(keyType)) { throw new Error("密钥类型必须是 Key A 或 Key B"); } if (!/^\d+$/.test(blockAddress)) { throw new Error("块地址必须是非负整数"); } if (!/^\d+$/.test(numberOfBlocksRead) || Number(numberOfBlocksRead) <= 0) { throw new Error("读取块数必须是大于 0 的整数"); } reader.keyType = keyType; reader.key = key; reader.blockAddress = blockAddress; reader.numberOfBlocksRead = numberOfBlocksRead; elements.keyInput.value = key; // ISO15693 参数 reader.readDataType = elements.readDataTypeInput.value; reader.readSecurityStatus = elements.readSecurityStatusInput.value; reader.iso15693BlockAddress = elements.iso15693BlockAddressInput.value; reader.iso15693NumberOfBlocks = elements.iso15693NumberOfBlocksInput.value; } async function connectReader() { applyReaderConfig(); setLoading("连接中..."); log("开始连接读卡器(双协议检测)"); await reader.selectDevice(); setStatus("已连接", true); setLoading(""); syncButtons(); log("读卡器连接成功"); } // 检测 UID 是否已存在(去重) function checkDuplicate(uid) { const dedupeMode = elements.dedupeMode.value; const dedupeTimeMs = parseInt(elements.dedupeTime.value, 10); if (dedupeMode === "none") { return { isDup: false }; } if (dedupeMode === "session") { if (seenUids.has(uid)) { const info = seenUids.get(uid); return { isDup: true, reason: `已在会话中读取过(${info.protocol})` }; } return { isDup: false }; } if (dedupeMode === "time") { const lastTime = seenUids.has(uid) ? seenUids.get(uid).time : null; if (lastTime && Date.now() - lastTime < dedupeTimeMs) { const info = seenUids.get(uid); const remain = Math.ceil((dedupeTimeMs - (Date.now() - lastTime)) / 1000); return { isDup: true, reason: `时间窗口内已读取(${info.protocol}),${remain}秒后可再次读取` }; } return { isDup: false }; } return { isDup: false }; } function recordUid(uid, protocol) { seenUids.set(uid, { protocol, time: Date.now() }); } function clearDedupeHistory() { seenUids.clear(); log("去重历史已清空"); } // 同步读取两种协议,返回成功读取的结果 async function readUidBothProtocols() { if (!reader.connected) { throw new Error("读卡器未连接,请先连接读卡器"); } applyReaderConfig(); const results = []; // 先尝试 ISO14443A try { log("尝试读取 ISO14443A..."); const uid14443 = await reader.readUid("ISO14443A"); results.push({ protocol: "ISO14443A", uid: uid14443 }); } catch (err) { if (!err.message?.includes("未读取到 UID")) { log(`ISO14443A 读取异常: ${getErrorMessage(err)}`, "warn"); } } // 再尝试 ISO15693 try { log("尝试读取 ISO15693..."); const uid15693 = await reader.readUid("ISO15693"); results.push({ protocol: "ISO15693", uid: uid15693 }); } catch (err) { if (!err.message?.includes("未读取到 UID")) { log(`ISO15693 读取异常: ${getErrorMessage(err)}`, "warn"); } } if (results.length === 0) { throw new Error("未读取到 UID,请确认卡片/标签已到读卡位"); } // 处理读取结果(去重逻辑) const validResults = []; for (const result of results) { const dupCheck = checkDuplicate(result.uid); if (dupCheck.isDup) { log(`${result.protocol} 读取到 UID: ${result.uid}(${dupCheck.reason})`, "warn"); } else { validResults.push(result); } } if (validResults.length === 0) { // 全都是重复的,取第一个显示但不记录 return results[0]; } // 返回第一个有效结果 const firstResult = validResults[0]; recordUid(firstResult.uid, firstResult.protocol); return firstResult; } async function readUid(showError = true) { setLoading("读取 UID 中..."); const result = await readUidBothProtocols(); elements.uidText.textContent = result.uid; elements.lastProtocolText.textContent = result.protocol; updateReadCount(); setLoading(""); log(`读取成功 [${result.protocol}] UID=${result.uid}`, "success"); // Console 输出 UID console.log("%c[RFID] 读取到 UID:", "color: #10b981; font-weight: bold; font-size: 14px;", result.uid); console.log("%c[RFID] 协议:", "color: #64748b;", result.protocol); console.log("%c[RFID] 时间:", "color: #64748b;", new Date().toISOString()); return result; } async function startPolling() { if (isPolling) return; if (!reader.connected) { log("读卡器未连接,无法启动轮询", "error"); alert("请先连接读卡器"); return; } isPolling = true; syncButtons(); const interval = parseInt(elements.pollingInterval.value, 10); log(`开始定时轮询(双协议),间隔 ${interval}ms`); // 立即执行一次 await doPoll(); // 定时轮询 pollingTimer = setInterval(async () => { if (!isPolling) return; await doPoll(); }, interval); } async function doPoll() { try { await readUid(false); } catch (err) { // 轮询模式下,读不到卡不报错 const msg = getErrorMessage(err); if (!msg.includes("未读取到 UID")) { log(`轮询错误: ${msg}`, "error"); } } } function stopPolling() { if (!isPolling) return; isPolling = false; if (pollingTimer) { clearInterval(pollingTimer); pollingTimer = null; } syncButtons(); setLoading(""); log("已停止定时轮询"); } function clearLog() { logLines.length = 0; elements.logArea.value = ""; log("日志已清空"); } function initView() { elements.wsUrlText.textContent = reader.wsUrl; elements.uidText.textContent = "-"; elements.readCountText.textContent = "0"; elements.lastProtocolText.textContent = "-"; // ISO14443A 默认值 elements.keyTypeInput.value = reader.keyType; elements.keyInput.value = reader.key; elements.blockAddressInput.value = reader.blockAddress; elements.numberOfBlocksReadInput.value = reader.numberOfBlocksRead; // ISO15693 默认值 elements.readDataTypeInput.value = reader.readDataType; elements.readSecurityStatusInput.value = reader.readSecurityStatus; elements.iso15693BlockAddressInput.value = reader.iso15693BlockAddress; elements.iso15693NumberOfBlocksInput.value = reader.iso15693NumberOfBlocks; setStatus("未连接", false); syncButtons(); log("页面已初始化,支持双协议自动检测(ISO14443A + ISO15693)"); } function bindEvents() { // 连接读卡器 elements.btnConnect.addEventListener("click", async () => { try { await connectReader(); } catch (err) { const msg = getErrorMessage(err); setStatus("连接失败", false); setLoading(""); syncButtons(); log(`连接失败: ${msg}`, "error"); alert(msg); } }); // 读取 UID elements.btnReadUid.addEventListener("click", async () => { try { await readUid(true); } catch (err) { const msg = getErrorMessage(err); setLoading(""); log(`读取失败: ${msg}`, "error"); alert(msg); } }); // 开始轮询 elements.btnStartPolling.addEventListener("click", () => { startPolling(); }); // 停止轮询 elements.btnStopPolling.addEventListener("click", () => { stopPolling(); }); // 清空日志 elements.btnClearLog.addEventListener("click", () => { clearLog(); }); // 去重模式切换 elements.dedupeMode.addEventListener("change", (e) => { if (e.target.value === "time") { elements.dedupeTimeItem.style.display = "block"; } else { elements.dedupeTimeItem.style.display = "none"; } // 切换去重模式时清空历史 clearDedupeHistory(); }); } bindEvents(); initView();