248 lines
7.1 KiB
JavaScript
248 lines
7.1 KiB
JavaScript
const { parentPort } = require("worker_threads");
|
||
const koffi = require("koffi");
|
||
const path = require("path");
|
||
const iconv = require("iconv-lite");
|
||
|
||
// 定义结构体
|
||
const IDCardData = koffi.struct("IDCardData", {
|
||
Name: koffi.array("uint8", 32),
|
||
Sex: koffi.array("uint8", 6),
|
||
Nation: koffi.array("uint8", 20),
|
||
Born: koffi.array("uint8", 18),
|
||
Address: koffi.array("uint8", 72),
|
||
IDCardNo: koffi.array("uint8", 38),
|
||
GrantDept: koffi.array("uint8", 32),
|
||
UserLifeBegin: koffi.array("uint8", 18),
|
||
UserLifeEnd: koffi.array("uint8", 18),
|
||
reserved: koffi.array("uint8", 38),
|
||
PhotoFileName: koffi.array("uint8", 255),
|
||
});
|
||
|
||
let lib = null;
|
||
let running = false;
|
||
|
||
// 解码字符串 (尝试 UCS-2/UTF-16LE)
|
||
function decodeString(buffer) {
|
||
// 这里的 buffer 是 uint8 数组,先转为 Buffer
|
||
const buf = Buffer.from(buffer);
|
||
|
||
// 很多读卡器 DLL 返回的是 UCS-2 (UTF-16LE)
|
||
// 也有可能是 GBK。我们可以通过检查字节特征来判断,或者优先尝试 UCS-2
|
||
// UCS-2 的特征是每两个字节一个字符,对于 ASCII 字符,高字节通常是 0x00
|
||
|
||
// 尝试作为 UTF-16LE 解码
|
||
// 找到双 null 结尾 (0x00 0x00)
|
||
let nullIndex = -1;
|
||
for (let i = 0; i < buf.length - 1; i += 2) {
|
||
if (buf[i] === 0 && buf[i + 1] === 0) {
|
||
nullIndex = i;
|
||
break;
|
||
}
|
||
}
|
||
const validBuf16 = nullIndex !== -1 ? buf.slice(0, nullIndex) : buf;
|
||
const str16 = iconv.decode(validBuf16, "utf16le").trim();
|
||
|
||
// 如果解码出来的字符串看起来是乱码(包含很多 )或者为空但 buffer 不为空,可能需要尝试 GBK
|
||
// 但通常 Syn_IDCardRead.dll 返回的是 UCS-2
|
||
// 让我们先打印一下原始 hex 以便调试
|
||
// parentPort.postMessage({ type: 'log', payload: `Hex: ${buf.slice(0, 10).toString('hex')}` });
|
||
|
||
return str16;
|
||
}
|
||
|
||
// 加载 DLL
|
||
function loadDll() {
|
||
if (lib) return true;
|
||
try {
|
||
const fs = require("fs");
|
||
const dllNames = ["Syn_IDCardRead.dll", "SynIDCardRead.dll"];
|
||
|
||
// 确定 DLL 目录:优先检查生产环境 resources/xzx,否则使用开发环境 resources/xzx
|
||
let dllDir = path.join(process.cwd(), "resources", "xzx");
|
||
if (process.resourcesPath) {
|
||
const prodDir = path.join(process.resourcesPath, "xzx");
|
||
if (fs.existsSync(prodDir)) {
|
||
dllDir = prodDir;
|
||
}
|
||
}
|
||
|
||
let dllPath = null;
|
||
for (const name of dllNames) {
|
||
const p = path.join(dllDir, name);
|
||
if (fs.existsSync(p)) {
|
||
dllPath = p;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!dllPath) {
|
||
throw new Error(`DLL not found in ${dllDir}`);
|
||
}
|
||
|
||
parentPort.postMessage({
|
||
type: "log",
|
||
payload: `Loading DLL from: ${dllPath}`,
|
||
});
|
||
lib = koffi.load(dllPath);
|
||
|
||
// 尝试使用 stdcall 约定,这对于 Windows 32位 DLL 很常见
|
||
// koffi 2.x 版本中,stdcall 约定需要在函数名前加 __stdcall
|
||
// 如果字符串解析失败,可以尝试使用 lib.stdcall() 方法
|
||
|
||
// 辅助函数:尝试加载函数,支持原名和修饰名
|
||
const loadFunc = (name, alias, ret, args) => {
|
||
try {
|
||
return lib.stdcall(name, ret, args);
|
||
} catch (e) {
|
||
try {
|
||
return lib.stdcall(alias, ret, args);
|
||
} catch (e2) {
|
||
throw new Error(
|
||
`Cannot find function '${name}' or '${alias}' in DLL`
|
||
);
|
||
}
|
||
}
|
||
};
|
||
|
||
try {
|
||
return {
|
||
Syn_OpenPort: loadFunc("Syn_OpenPort", "_Syn_OpenPort@4", "int", [
|
||
"int",
|
||
]),
|
||
Syn_ClosePort: loadFunc("Syn_ClosePort", "_Syn_ClosePort@4", "int", [
|
||
"int",
|
||
]),
|
||
Syn_StartFindIDCard: loadFunc(
|
||
"Syn_StartFindIDCard",
|
||
"_Syn_StartFindIDCard@12",
|
||
"int",
|
||
["int", "uint8*", "int"]
|
||
),
|
||
Syn_SelectIDCard: loadFunc(
|
||
"Syn_SelectIDCard",
|
||
"_Syn_SelectIDCard@12",
|
||
"int",
|
||
["int", "uint8*", "int"]
|
||
),
|
||
Syn_ReadMsg: loadFunc("Syn_ReadMsg", "_Syn_ReadMsg@12", "int", [
|
||
"int",
|
||
"int",
|
||
koffi.out(koffi.pointer(IDCardData)),
|
||
]),
|
||
};
|
||
} catch (e) {
|
||
// 如果 lib.stdcall 也不行,尝试回退到 func 但不带 __stdcall (可能不是 stdcall 或者 koffi 版本差异)
|
||
// 但根据之前的错误,不带 __stdcall 找不到函数,带了又报类型错误,说明很可能是 stdcall 但字符串解析有问题
|
||
// 这里我们坚持用 lib.stdcall 这种显式 API 调用,它比字符串解析更稳定
|
||
throw e;
|
||
}
|
||
} catch (err) {
|
||
parentPort.postMessage({
|
||
type: "error",
|
||
payload: `Failed to load DLL: ${err.message}`,
|
||
});
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async function startListen() {
|
||
if (running) return;
|
||
running = true;
|
||
|
||
const api = loadDll();
|
||
if (!api) {
|
||
running = false;
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 尝试打开端口,优先尝试 1001 (USB)
|
||
let port = 1001;
|
||
let openRes = api.Syn_OpenPort(port);
|
||
|
||
if (openRes !== 0) {
|
||
// 如果 1001 失败,尝试 1001-1016
|
||
for (let p = 1002; p <= 1016; p++) {
|
||
if (api.Syn_OpenPort(p) === 0) {
|
||
port = p;
|
||
openRes = 0;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (openRes !== 0) {
|
||
parentPort.postMessage({
|
||
type: "error",
|
||
payload: `Open port failed (tried 1001-1016)`,
|
||
});
|
||
running = false;
|
||
return;
|
||
}
|
||
|
||
parentPort.postMessage({
|
||
type: "log",
|
||
payload: `Port ${port} opened successfully`,
|
||
});
|
||
|
||
const iin = Buffer.alloc(4);
|
||
const sn = Buffer.alloc(8);
|
||
// IDCardData 结构体大小计算: 32+6+20+18+72+38+32+18+18+38+255 = 547 字节
|
||
// koffi 会自动处理结构体内存分配
|
||
|
||
while (running) {
|
||
// 寻卡
|
||
api.Syn_StartFindIDCard(port, iin, 0);
|
||
// 选卡
|
||
api.Syn_SelectIDCard(port, sn, 0);
|
||
|
||
// 读卡
|
||
const data = {}; // koffi 输出对象
|
||
const ret = api.Syn_ReadMsg(port, 0, data);
|
||
|
||
if (ret === 0 && data) {
|
||
const payload = {
|
||
name: decodeString(data.Name),
|
||
sex: decodeString(data.Sex),
|
||
nation: decodeString(data.Nation),
|
||
born: decodeString(data.Born),
|
||
address: decodeString(data.Address),
|
||
id_card_no: decodeString(data.IDCardNo),
|
||
grant_dept: decodeString(data.GrantDept),
|
||
life_begin: decodeString(data.UserLifeBegin),
|
||
life_end: decodeString(data.UserLifeEnd),
|
||
photo_path: decodeString(data.PhotoFileName),
|
||
};
|
||
|
||
parentPort.postMessage({
|
||
type: "log",
|
||
payload: `Read IDCard success: ${payload.name} ${payload.id_card_no}`,
|
||
});
|
||
parentPort.postMessage({ type: "data", payload });
|
||
|
||
// 读到卡后暂停一下,避免重复读取太快
|
||
await new Promise((r) => setTimeout(r, 500));
|
||
} else {
|
||
// 没读到卡,稍微等待
|
||
await new Promise((r) => setTimeout(r, 150));
|
||
}
|
||
}
|
||
|
||
api.Syn_ClosePort(port);
|
||
} catch (err) {
|
||
parentPort.postMessage({
|
||
type: "error",
|
||
payload: `Worker error: ${err.message}`,
|
||
});
|
||
running = false;
|
||
}
|
||
}
|
||
|
||
parentPort.on("message", (msg) => {
|
||
if (msg === "start") {
|
||
startListen();
|
||
} else if (msg === "stop") {
|
||
running = false;
|
||
}
|
||
});
|