reader-client.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. class ReaderClient {
  2. constructor(options = {}) {
  3. this.wsUrl = options.wsUrl || "ws://127.0.0.1:6689";
  4. this.driveType = options.driveType || "D5200";
  5. this.portName = options.portName || "USB";
  6. this.hidSN = options.hidSN || "";
  7. this.driverIp = options.driverIp || "";
  8. this.driverPort = options.driverPort || "6688";
  9. // ISO14443A 参数
  10. this.keyType = options.keyType || "0";
  11. this.key = options.key || "FFFFFFFFFFFF";
  12. this.blockAddress = options.blockAddress || "1";
  13. this.numberOfBlocksRead = options.numberOfBlocksRead || "1";
  14. // ISO15693 参数
  15. this.readDataType = options.readDataType || "0";
  16. this.readSecurityStatus = options.readSecurityStatus || "0";
  17. this.iso15693BlockAddress = options.iso15693BlockAddress || "0";
  18. this.iso15693NumberOfBlocks = options.iso15693NumberOfBlocks || "7";
  19. this.ws = null;
  20. this.connected = false;
  21. this.handle = null;
  22. this.requestId = 1;
  23. this.pending = new Map();
  24. }
  25. async init() {
  26. if (typeof WebSocket === "undefined") {
  27. throw new Error("当前浏览器不支持 WebSocket");
  28. }
  29. return true;
  30. }
  31. async ensureWsConnected() {
  32. if (this.ws && this.ws.readyState === WebSocket.OPEN) {
  33. return;
  34. }
  35. if (this.ws && this.ws.readyState === WebSocket.CONNECTING) {
  36. await new Promise((resolve, reject) => {
  37. const timeoutRef = setTimeout(
  38. () => reject(new Error("读卡器服务连接超时")),
  39. 5000
  40. );
  41. const onOpen = () => {
  42. clearTimeout(timeoutRef);
  43. this.ws.removeEventListener("open", onOpen);
  44. this.ws.removeEventListener("error", onError);
  45. resolve();
  46. };
  47. const onError = () => {
  48. clearTimeout(timeoutRef);
  49. this.ws.removeEventListener("open", onOpen);
  50. this.ws.removeEventListener("error", onError);
  51. reject(new Error("读卡器服务连接失败"));
  52. };
  53. this.ws.addEventListener("open", onOpen);
  54. this.ws.addEventListener("error", onError);
  55. });
  56. return;
  57. }
  58. this.ws = new WebSocket(this.wsUrl);
  59. this.ws.onmessage = (event) => {
  60. this.handleMessage(event.data);
  61. };
  62. this.ws.onclose = () => {
  63. this.connected = false;
  64. this.handle = null;
  65. for (const [, item] of this.pending) {
  66. clearTimeout(item.timeoutRef);
  67. item.reject(new Error("读卡器服务连接已断开"));
  68. }
  69. this.pending.clear();
  70. };
  71. this.ws.onerror = () => {
  72. // 具体错误在请求超时/close时抛出
  73. };
  74. await new Promise((resolve, reject) => {
  75. const timeoutRef = setTimeout(
  76. () => reject(new Error("读卡器服务连接超时")),
  77. 5000
  78. );
  79. this.ws.onopen = () => {
  80. clearTimeout(timeoutRef);
  81. resolve();
  82. };
  83. this.ws.onerror = () => {
  84. clearTimeout(timeoutRef);
  85. reject(new Error("读卡器服务连接失败,请先启动厂家读卡器服务程序"));
  86. };
  87. });
  88. }
  89. handleMessage(rawData) {
  90. let payload;
  91. try {
  92. payload = JSON.parse(rawData);
  93. } catch {
  94. for (const [, item] of this.pending) {
  95. clearTimeout(item.timeoutRef);
  96. item.resolve(rawData);
  97. }
  98. this.pending.clear();
  99. return;
  100. }
  101. const traceId = payload.traceId;
  102. if (traceId && this.pending.has(traceId)) {
  103. const item = this.pending.get(traceId);
  104. this.pending.delete(traceId);
  105. clearTimeout(item.timeoutRef);
  106. item.resolve(payload);
  107. return;
  108. }
  109. if (this.pending.size === 1) {
  110. const firstKey = this.pending.keys().next().value;
  111. const item = this.pending.get(firstKey);
  112. this.pending.delete(firstKey);
  113. clearTimeout(item.timeoutRef);
  114. item.resolve(payload);
  115. }
  116. }
  117. async sendCommand(command, param = {}, handleOverride, timeoutMs = 6000) {
  118. await this.ensureWsConnected();
  119. const traceId = `r${Date.now()}_${this.requestId++}`;
  120. const message = {
  121. command,
  122. param,
  123. traceId,
  124. };
  125. const finalHandle = handleOverride ?? this.handle;
  126. if (finalHandle !== null && finalHandle !== undefined) {
  127. message.handle = String(finalHandle);
  128. }
  129. const promise = new Promise((resolve, reject) => {
  130. const timeoutRef = setTimeout(() => {
  131. this.pending.delete(traceId);
  132. reject(new Error(`${command} 超时`));
  133. }, timeoutMs);
  134. this.pending.set(traceId, { resolve, reject, timeoutRef });
  135. });
  136. this.ws.send(JSON.stringify(message));
  137. return promise;
  138. }
  139. getPayloadCode(payload) {
  140. if (!payload || typeof payload !== "object") {
  141. return null;
  142. }
  143. if (payload.code != null) {
  144. return Number(payload.code);
  145. }
  146. if (payload.status != null) {
  147. return Number(payload.status);
  148. }
  149. if (payload.result?.code != null) {
  150. return Number(payload.result.code);
  151. }
  152. return null;
  153. }
  154. getPayloadMessage(payload) {
  155. if (!payload || typeof payload !== "object") {
  156. return "";
  157. }
  158. return String(
  159. payload.msg ??
  160. payload.message ??
  161. payload.error ??
  162. payload.result?.msg ??
  163. payload.result?.message ??
  164. ""
  165. );
  166. }
  167. extractHandle(payload) {
  168. if (!payload || typeof payload !== "object") {
  169. return null;
  170. }
  171. const h = payload.handle ?? payload.result?.handle ?? payload.data?.handle;
  172. if (h === null || h === undefined || h === "") {
  173. return null;
  174. }
  175. return String(h);
  176. }
  177. extractTagDataList(payload) {
  178. if (!payload || typeof payload !== "object") {
  179. return [];
  180. }
  181. if (Array.isArray(payload.tagDataList)) {
  182. return payload.tagDataList;
  183. }
  184. if (Array.isArray(payload.data)) {
  185. return payload.data;
  186. }
  187. if (payload.data && Array.isArray(payload.data.tagDataList)) {
  188. return payload.data.tagDataList;
  189. }
  190. if (payload.result && Array.isArray(payload.result.tagDataList)) {
  191. return payload.result.tagDataList;
  192. }
  193. if (payload.result && Array.isArray(payload.result.data)) {
  194. return payload.result.data;
  195. }
  196. return [];
  197. }
  198. buildISO14443AReadParam(readDataType) {
  199. return {
  200. antennaCollection: [1],
  201. readDataType: String(readDataType),
  202. // 厂家服务对 -1 的实现不稳定,仍带齐参数避免服务端空引用
  203. keyType: this.keyType,
  204. key: this.key,
  205. blockAddress: this.blockAddress,
  206. numberOfBlocksRead: this.numberOfBlocksRead,
  207. };
  208. }
  209. async sendISO14443ARead(readDataType) {
  210. return await this.sendCommand(
  211. "ISO14443A_BatchReadMultiBlocks",
  212. this.buildISO14443AReadParam(readDataType),
  213. this.handle,
  214. 6000
  215. );
  216. }
  217. extractUidFromReadResponse(readResp, emptyHint) {
  218. if (typeof readResp === "string") {
  219. throw new Error(readResp.trim() || "ISO14443A 读卡失败");
  220. }
  221. const code = this.getPayloadCode(readResp);
  222. if (code != null && code < 0) {
  223. const msg = this.getPayloadMessage(readResp) || "ISO14443A 读卡失败";
  224. throw new Error(msg);
  225. }
  226. const tagDataList = this.extractTagDataList(readResp);
  227. if (!Array.isArray(tagDataList) || tagDataList.length === 0) {
  228. throw new Error(emptyHint);
  229. }
  230. const firstRow = tagDataList[0];
  231. const uid = this.parseUidFromTagRow(firstRow);
  232. if (!uid) {
  233. throw new Error(emptyHint);
  234. }
  235. return uid.toUpperCase();
  236. }
  237. parseUidFromTagRow(row) {
  238. if (!row) {
  239. return "";
  240. }
  241. if (Array.isArray(row)) {
  242. return String(row[1] || "").trim();
  243. }
  244. if (typeof row === "object") {
  245. if (Array.isArray(row.value)) {
  246. return String(row.value[1] || "").trim();
  247. }
  248. if (Array.isArray(row.data)) {
  249. return String(row.data[1] || "").trim();
  250. }
  251. if (Array.isArray(row.tag)) {
  252. return String(row.tag[1] || "").trim();
  253. }
  254. for (const value of Object.values(row)) {
  255. if (Array.isArray(value)) {
  256. return String(value[1] || "").trim();
  257. }
  258. }
  259. }
  260. return "";
  261. }
  262. async selectDevice() {
  263. await this.init();
  264. try {
  265. await this.sendCommand("CloseDriver", {}, -1, 3000);
  266. } catch {
  267. // 忽略
  268. }
  269. const openResp = await this.sendCommand(
  270. "OpenDriver",
  271. {
  272. driveType: this.driveType,
  273. portName: this.portName,
  274. hidSN: this.hidSN,
  275. driverIp: this.driverIp,
  276. driverPort: this.driverPort,
  277. driverId: "",
  278. userName: "",
  279. passWord: "",
  280. },
  281. undefined,
  282. 8000
  283. );
  284. const code = this.getPayloadCode(openResp);
  285. if (code != null && code < 0) {
  286. const msg = this.getPayloadMessage(openResp) || "OpenDriver 失败";
  287. throw new Error(msg);
  288. }
  289. const openedHandle = this.extractHandle(openResp);
  290. if (!openedHandle) {
  291. throw new Error("OpenDriver 未返回 handle");
  292. }
  293. this.handle = openedHandle;
  294. this.connected = true;
  295. return true;
  296. }
  297. // ISO14443A 读取 UID
  298. async readUidISO14443A() {
  299. await this.init();
  300. if (!this.connected || !this.handle) {
  301. throw new Error("读卡器未连接,请先连接读卡器");
  302. }
  303. try {
  304. const inventoryResp = await this.sendISO14443ARead("-1");
  305. return this.extractUidFromReadResponse(
  306. inventoryResp,
  307. "未读取到 UID,请确认 ISO14443A 卡片已到读卡位"
  308. );
  309. } catch (inventoryErr) {
  310. const fallbackResp = await this.sendISO14443ARead("0");
  311. try {
  312. return this.extractUidFromReadResponse(
  313. fallbackResp,
  314. "未读取到 UID,请确认 ISO14443A 卡片已到读卡位"
  315. );
  316. } catch (fallbackErr) {
  317. const inventoryMsg =
  318. inventoryErr instanceof Error ? inventoryErr.message : String(inventoryErr || "");
  319. const fallbackMsg =
  320. fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr || "");
  321. throw new Error(
  322. `ISO14443A 读取失败;只盘点模式:${inventoryMsg || "未知错误"};读块回退模式:${fallbackMsg || "未知错误"}`
  323. );
  324. }
  325. }
  326. }
  327. // ISO15693 读取 UID
  328. async readUidISO15693() {
  329. await this.init();
  330. if (!this.connected || !this.handle) {
  331. throw new Error("读卡器未连接,请先连接读卡器");
  332. }
  333. const readResp = await this.sendCommand(
  334. "ISO15693_BatchReadMultiBlocks",
  335. {
  336. antennaCollection: [1],
  337. readDataType: this.readDataType,
  338. readSecurityStatus: this.readSecurityStatus,
  339. blockAddress: this.iso15693BlockAddress,
  340. numberOfBlocksRead: this.iso15693NumberOfBlocks,
  341. },
  342. this.handle,
  343. 6000
  344. );
  345. const code = this.getPayloadCode(readResp);
  346. if (code != null && code < 0) {
  347. const msg = this.getPayloadMessage(readResp) || "ISO15693 读卡失败";
  348. throw new Error(msg);
  349. }
  350. const tagDataList = this.extractTagDataList(readResp);
  351. if (!Array.isArray(tagDataList) || tagDataList.length === 0) {
  352. throw new Error("未读取到 UID,请确认 ISO15693 标签已到读卡位");
  353. }
  354. const firstRow = tagDataList[0];
  355. const uid = this.parseUidFromTagRow(firstRow);
  356. if (!uid) {
  357. throw new Error("未读取到 UID,请确认 ISO15693 标签已到读卡位");
  358. }
  359. return uid.toUpperCase();
  360. }
  361. // 根据协议类型读取 UID
  362. async readUid(protocol = "ISO14443A") {
  363. if (protocol === "ISO15693") {
  364. return await this.readUidISO15693();
  365. }
  366. return await this.readUidISO14443A();
  367. }
  368. }
  369. window.ReaderClient = ReaderClient;