app.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. const reader = new ReaderClient();
  2. const MAX_LOG_LINES = 300;
  3. const logLines = [];
  4. // 状态
  5. let isPolling = false;
  6. let pollingTimer = null;
  7. let readCount = 0;
  8. // 去重相关
  9. let seenUids = new Map(); // uid -> { protocol, time }
  10. const elements = {
  11. statusTag: document.getElementById("statusTag"),
  12. wsUrlText: document.getElementById("wsUrlText"),
  13. uidText: document.getElementById("nfch"), // ("uidText")。Lin
  14. readCountText: document.getElementById("readCountText"),
  15. lastProtocolText: document.getElementById("lastProtocolText"),
  16. // ISO14443A
  17. keyTypeInput: document.getElementById("keyTypeInput"),
  18. keyInput: document.getElementById("keyInput"),
  19. blockAddressInput: document.getElementById("blockAddressInput"),
  20. numberOfBlocksReadInput: document.getElementById("numberOfBlocksReadInput"),
  21. // ISO15693
  22. readDataTypeInput: document.getElementById("readDataTypeInput"),
  23. readSecurityStatusInput: document.getElementById("readSecurityStatusInput"),
  24. iso15693BlockAddressInput: document.getElementById("iso15693BlockAddressInput"),
  25. iso15693NumberOfBlocksInput: document.getElementById("iso15693NumberOfBlocksInput"),
  26. // 轮询
  27. pollingInterval: document.getElementById("pollingInterval"),
  28. dedupeMode: document.getElementById("dedupeMode"),
  29. dedupeTime: document.getElementById("dedupeTime"),
  30. dedupeTimeItem: document.getElementById("dedupeTimeItem"),
  31. // 按钮
  32. btnConnect: document.getElementById("btnConnect"),
  33. btnReadUid: document.getElementById("btnReadUid"),
  34. btnStartPolling: document.getElementById("btnStartPolling"),
  35. btnStopPolling: document.getElementById("btnStopPolling"),
  36. btnClearLog: document.getElementById("btnClearLog"),
  37. loadingHint: document.getElementById("loadingHint"),
  38. logArea: document.getElementById("logArea"),
  39. };
  40. function log(message, type = "info") {
  41. const time = new Date().toLocaleTimeString();
  42. const prefix = type === "success" ? "✓" : type === "error" ? "✗" : type === "warn" ? "⚠" : "•";
  43. const line = `[${time}] ${prefix} ${message}`;
  44. console.log(line);
  45. logLines.unshift(line);
  46. if (logLines.length > MAX_LOG_LINES) {
  47. logLines.length = MAX_LOG_LINES;
  48. }
  49. elements.logArea.value = logLines.join("\n");
  50. }
  51. function getErrorMessage(err) {
  52. if (err instanceof Error && err.message) {
  53. return err.message;
  54. }
  55. if (typeof err === "string") {
  56. return err;
  57. }
  58. try {
  59. return JSON.stringify(err);
  60. } catch {
  61. return "未知错误";
  62. }
  63. }
  64. function setLoading(text = "") {
  65. elements.loadingHint.textContent = text;
  66. }
  67. function setStatus(text, ok) {
  68. elements.statusTag.textContent = text;
  69. elements.statusTag.classList.toggle("ok", Boolean(ok));
  70. elements.statusTag.classList.toggle("bad", !ok);
  71. }
  72. function syncButtons() {
  73. const connected = reader.connected;
  74. elements.btnReadUid.disabled = !connected;
  75. elements.btnStartPolling.disabled = !connected || isPolling;
  76. elements.btnStopPolling.disabled = !connected || !isPolling;
  77. }
  78. function updateReadCount() {
  79. readCount++;
  80. elements.readCountText.textContent = readCount;
  81. }
  82. function normalizeHexKey(value) {
  83. return String(value || "")
  84. .trim()
  85. .toUpperCase()
  86. .replace(/[^0-9A-F]/g, "");
  87. }
  88. function applyReaderConfig() {
  89. // ISO14443A 参数
  90. const key = normalizeHexKey(elements.keyInput.value);
  91. const keyType = String(elements.keyTypeInput.value || "0");
  92. const blockAddress = String(elements.blockAddressInput.value || "1").trim();
  93. const numberOfBlocksRead = String(
  94. elements.numberOfBlocksReadInput.value || "1"
  95. ).trim();
  96. if (!/^[0-9A-F]{12}$/.test(key)) {
  97. throw new Error("密钥必须是 12 位十六进制字符");
  98. }
  99. if (!["0", "1"].includes(keyType)) {
  100. throw new Error("密钥类型必须是 Key A 或 Key B");
  101. }
  102. if (!/^\d+$/.test(blockAddress)) {
  103. throw new Error("块地址必须是非负整数");
  104. }
  105. if (!/^\d+$/.test(numberOfBlocksRead) || Number(numberOfBlocksRead) <= 0) {
  106. throw new Error("读取块数必须是大于 0 的整数");
  107. }
  108. reader.keyType = keyType;
  109. reader.key = key;
  110. reader.blockAddress = blockAddress;
  111. reader.numberOfBlocksRead = numberOfBlocksRead;
  112. elements.keyInput.value = key;
  113. // ISO15693 参数
  114. reader.readDataType = elements.readDataTypeInput.value;
  115. reader.readSecurityStatus = elements.readSecurityStatusInput.value;
  116. reader.iso15693BlockAddress = elements.iso15693BlockAddressInput.value;
  117. reader.iso15693NumberOfBlocks = elements.iso15693NumberOfBlocksInput.value;
  118. }
  119. async function connectReader() {
  120. applyReaderConfig();
  121. setLoading("连接中...");
  122. log("开始连接读卡器(双协议检测)");
  123. await reader.selectDevice();
  124. setStatus("已连接", true);
  125. setLoading("");
  126. syncButtons();
  127. log("读卡器连接成功");
  128. }
  129. // 检测 UID 是否已存在(去重)
  130. function checkDuplicate(uid) {
  131. const dedupeMode = elements.dedupeMode.value;
  132. const dedupeTimeMs = parseInt(elements.dedupeTime.value, 10);
  133. if (dedupeMode === "none") {
  134. return { isDup: false };
  135. }
  136. if (dedupeMode === "session") {
  137. if (seenUids.has(uid)) {
  138. const info = seenUids.get(uid);
  139. return { isDup: true, reason: `已在会话中读取过(${info.protocol})` };
  140. }
  141. return { isDup: false };
  142. }
  143. if (dedupeMode === "time") {
  144. const lastTime = seenUids.has(uid) ? seenUids.get(uid).time : null;
  145. if (lastTime && Date.now() - lastTime < dedupeTimeMs) {
  146. const info = seenUids.get(uid);
  147. const remain = Math.ceil((dedupeTimeMs - (Date.now() - lastTime)) / 1000);
  148. return { isDup: true, reason: `时间窗口内已读取(${info.protocol}),${remain}秒后可再次读取` };
  149. }
  150. return { isDup: false };
  151. }
  152. return { isDup: false };
  153. }
  154. function recordUid(uid, protocol) {
  155. seenUids.set(uid, { protocol, time: Date.now() });
  156. }
  157. function clearDedupeHistory() {
  158. seenUids.clear();
  159. log("去重历史已清空");
  160. }
  161. // 同步读取两种协议,返回成功读取的结果
  162. async function readUidBothProtocols() {
  163. if (!reader.connected) {
  164. throw new Error("读卡器未连接,请先连接读卡器");
  165. }
  166. applyReaderConfig();
  167. const results = [];
  168. // 先尝试 ISO14443A
  169. try {
  170. log("尝试读取 ISO14443A...");
  171. const uid14443 = await reader.readUid("ISO14443A");
  172. results.push({ protocol: "ISO14443A", uid: uid14443 });
  173. } catch (err) {
  174. if (!err.message?.includes("未读取到 UID")) {
  175. log(`ISO14443A 读取异常: ${getErrorMessage(err)}`, "warn");
  176. }
  177. }
  178. // 再尝试 ISO15693
  179. try {
  180. log("尝试读取 ISO15693...");
  181. const uid15693 = await reader.readUid("ISO15693");
  182. results.push({ protocol: "ISO15693", uid: uid15693 });
  183. } catch (err) {
  184. if (!err.message?.includes("未读取到 UID")) {
  185. log(`ISO15693 读取异常: ${getErrorMessage(err)}`, "warn");
  186. }
  187. }
  188. if (results.length === 0) {
  189. throw new Error("未读取到 UID,请确认卡片/标签已到读卡位");
  190. }
  191. // 处理读取结果(去重逻辑)
  192. const validResults = [];
  193. for (const result of results) {
  194. const dupCheck = checkDuplicate(result.uid);
  195. if (dupCheck.isDup) {
  196. log(`${result.protocol} 读取到 UID: ${result.uid}(${dupCheck.reason})`, "warn");
  197. } else {
  198. validResults.push(result);
  199. }
  200. }
  201. if (validResults.length === 0) {
  202. // 全都是重复的,取第一个显示但不记录
  203. return results[0];
  204. }
  205. // 返回第一个有效结果
  206. const firstResult = validResults[0];
  207. recordUid(firstResult.uid, firstResult.protocol);
  208. return firstResult;
  209. }
  210. async function readUid(showError = true) {
  211. setLoading("读取 UID 中...");
  212. const result = await readUidBothProtocols();
  213. elements.uidText.textContent = result.uid;
  214. elements.lastProtocolText.textContent = result.protocol;
  215. updateReadCount();
  216. setLoading("");
  217. log(`读取成功 [${result.protocol}] UID=${result.uid}`, "success");
  218. // Console 输出 UID
  219. console.log("%c[RFID] 读取到 UID:", "color: #10b981; font-weight: bold; font-size: 14px;", result.uid);
  220. console.log("%c[RFID] 协议:", "color: #64748b;", result.protocol);
  221. console.log("%c[RFID] 时间:", "color: #64748b;", new Date().toISOString());
  222. return result;
  223. }
  224. async function startPolling() {
  225. if (isPolling) return;
  226. if (!reader.connected) {
  227. log("读卡器未连接,无法启动轮询", "error");
  228. alert("请先连接读卡器");
  229. return;
  230. }
  231. isPolling = true;
  232. syncButtons();
  233. const interval = parseInt(elements.pollingInterval.value, 10);
  234. log(`开始定时轮询(双协议),间隔 ${interval}ms`);
  235. // 立即执行一次
  236. await doPoll();
  237. // 定时轮询
  238. pollingTimer = setInterval(async () => {
  239. if (!isPolling) return;
  240. await doPoll();
  241. }, interval);
  242. }
  243. async function doPoll() {
  244. try {
  245. await readUid(false);
  246. } catch (err) {
  247. // 轮询模式下,读不到卡不报错
  248. const msg = getErrorMessage(err);
  249. if (!msg.includes("未读取到 UID")) {
  250. log(`轮询错误: ${msg}`, "error");
  251. }
  252. }
  253. }
  254. function stopPolling() {
  255. if (!isPolling) return;
  256. isPolling = false;
  257. if (pollingTimer) {
  258. clearInterval(pollingTimer);
  259. pollingTimer = null;
  260. }
  261. syncButtons();
  262. setLoading("");
  263. log("已停止定时轮询");
  264. }
  265. function clearLog() {
  266. logLines.length = 0;
  267. elements.logArea.value = "";
  268. log("日志已清空");
  269. }
  270. function initView() {
  271. elements.wsUrlText.textContent = reader.wsUrl;
  272. elements.uidText.textContent = "-";
  273. elements.readCountText.textContent = "0";
  274. elements.lastProtocolText.textContent = "-";
  275. // ISO14443A 默认值
  276. elements.keyTypeInput.value = reader.keyType;
  277. elements.keyInput.value = reader.key;
  278. elements.blockAddressInput.value = reader.blockAddress;
  279. elements.numberOfBlocksReadInput.value = reader.numberOfBlocksRead;
  280. // ISO15693 默认值
  281. elements.readDataTypeInput.value = reader.readDataType;
  282. elements.readSecurityStatusInput.value = reader.readSecurityStatus;
  283. elements.iso15693BlockAddressInput.value = reader.iso15693BlockAddress;
  284. elements.iso15693NumberOfBlocksInput.value = reader.iso15693NumberOfBlocks;
  285. setStatus("未连接", false);
  286. syncButtons();
  287. log("页面已初始化,支持双协议自动检测(ISO14443A + ISO15693)");
  288. }
  289. function bindEvents() {
  290. // 连接读卡器
  291. elements.btnConnect.addEventListener("click", async () => {
  292. try {
  293. await connectReader();
  294. } catch (err) {
  295. const msg = getErrorMessage(err);
  296. setStatus("连接失败", false);
  297. setLoading("");
  298. syncButtons();
  299. log(`连接失败: ${msg}`, "error");
  300. alert(msg);
  301. }
  302. });
  303. // 读取 UID
  304. elements.btnReadUid.addEventListener("click", async () => {
  305. try {
  306. await readUid(true);
  307. } catch (err) {
  308. const msg = getErrorMessage(err);
  309. setLoading("");
  310. log(`读取失败: ${msg}`, "error");
  311. alert(msg);
  312. }
  313. });
  314. // 开始轮询
  315. elements.btnStartPolling.addEventListener("click", () => {
  316. startPolling();
  317. });
  318. // 停止轮询
  319. elements.btnStopPolling.addEventListener("click", () => {
  320. stopPolling();
  321. });
  322. // 清空日志
  323. elements.btnClearLog.addEventListener("click", () => {
  324. clearLog();
  325. });
  326. // 去重模式切换
  327. elements.dedupeMode.addEventListener("change", (e) => {
  328. if (e.target.value === "time") {
  329. elements.dedupeTimeItem.style.display = "block";
  330. } else {
  331. elements.dedupeTimeItem.style.display = "none";
  332. }
  333. // 切换去重模式时清空历史
  334. clearDedupeHistory();
  335. });
  336. }
  337. bindEvents();
  338. initView();