@samlab-corp/sfm-node 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/data.d.ts +4 -4
- package/dist/commands/data.js +12 -12
- package/dist/commands/data.js.map +1 -1
- package/dist/commands/fingerprint.d.ts +23 -23
- package/dist/commands/fingerprint.d.ts.map +1 -1
- package/dist/commands/fingerprint.js +114 -114
- package/dist/commands/fingerprint.js.map +1 -1
- package/dist/commands/firmware.d.ts +5 -5
- package/dist/commands/firmware.d.ts.map +1 -1
- package/dist/commands/firmware.js +32 -32
- package/dist/commands/firmware.js.map +1 -1
- package/dist/commands/freescan.d.ts +16 -16
- package/dist/commands/freescan.d.ts.map +1 -1
- package/dist/commands/freescan.js +41 -41
- package/dist/commands/freescan.js.map +1 -1
- package/dist/commands/index.d.ts +12 -12
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +14 -14
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/system.d.ts +12 -12
- package/dist/commands/system.d.ts.map +1 -1
- package/dist/commands/system.js +14 -14
- package/dist/commands/system.js.map +1 -1
- package/dist/constants/protocol.d.ts +25 -0
- package/dist/constants/protocol.d.ts.map +1 -1
- package/dist/constants/protocol.js +26 -1
- package/dist/constants/protocol.js.map +1 -1
- package/dist/core/client.d.ts +33 -21
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +72 -52
- package/dist/core/client.js.map +1 -1
- package/dist/core/crypto.d.ts +19 -19
- package/dist/core/crypto.js +26 -26
- package/dist/core/crypto.js.map +1 -1
- package/dist/core/errors.d.ts +6 -6
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +46 -32
- package/dist/core/errors.js.map +1 -1
- package/dist/core/index.d.ts +3 -3
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +5 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/packet.d.ts +81 -41
- package/dist/core/packet.d.ts.map +1 -1
- package/dist/core/packet.js +256 -60
- package/dist/core/packet.js.map +1 -1
- package/dist/core/serial.d.ts +37 -27
- package/dist/core/serial.d.ts.map +1 -1
- package/dist/core/serial.js +91 -62
- package/dist/core/serial.js.map +1 -1
- package/dist/core/tcp.d.ts +14 -14
- package/dist/core/tcp.js +17 -17
- package/dist/core/tcp.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* 지문 등록/검증/식별/캡처 명령
|
|
4
4
|
*/
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.FingerprintCommands = void 0;
|
|
@@ -11,12 +11,12 @@ class FingerprintCommands {
|
|
|
11
11
|
this.client = client;
|
|
12
12
|
this.system = null;
|
|
13
13
|
}
|
|
14
|
-
/** @internal
|
|
14
|
+
/** @internal SystemCommands 참조 설정 */
|
|
15
15
|
_setSystemCommands(system) {
|
|
16
16
|
this.system = system;
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
19
|
+
* 등록 스캔 1 - ES 명령 전송 + 0x62 (스캔 성공) + 0x61 (템플릿 1 저장) 수신
|
|
20
20
|
*/
|
|
21
21
|
async enrollScan1(userId, flag = index_js_1.SFM_FLAG.NONE, { onLog } = {}) {
|
|
22
22
|
if (userId == null) {
|
|
@@ -25,9 +25,9 @@ class FingerprintCommands {
|
|
|
25
25
|
const log = (msg) => {
|
|
26
26
|
onLog?.(msg);
|
|
27
27
|
};
|
|
28
|
-
log(`[ENROLL]
|
|
28
|
+
log(`[ENROLL] 스캔 1 시작 - userId: ${userId}`);
|
|
29
29
|
const response = await this.client.sendCommand(index_js_1.SFM_CMD.ES, userId, 0, flag);
|
|
30
|
-
log(`[ENROLL]
|
|
30
|
+
log(`[ENROLL] 응답: flag=0x${response.flag?.toString(16)}, param=${response.param}, valid=${response.valid}`);
|
|
31
31
|
if (!response.valid) {
|
|
32
32
|
return { success: false, step: 1, error: response.error || "unknown error" };
|
|
33
33
|
}
|
|
@@ -35,33 +35,33 @@ class FingerprintCommands {
|
|
|
35
35
|
return {
|
|
36
36
|
success: false,
|
|
37
37
|
step: 1,
|
|
38
|
-
error:
|
|
38
|
+
error: `스캔 1 실패: 0x${response.flag?.toString(16) ?? "??"}`,
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
|
-
log(`[ENROLL]
|
|
41
|
+
log(`[ENROLL] 스캔 1 성공 (0x62), 템플릿 저장 대기 중...`);
|
|
42
42
|
const saveResponse = await this.client.receiveResponse(this.client.commandTimeout);
|
|
43
|
-
log(`[ENROLL]
|
|
43
|
+
log(`[ENROLL] 저장 1 응답: flag=0x${saveResponse.flag?.toString(16)}, valid=${saveResponse.valid}`);
|
|
44
44
|
if (!saveResponse.valid || saveResponse.flag !== index_js_1.SFM_ERROR.SUCCESS) {
|
|
45
45
|
return {
|
|
46
46
|
success: false,
|
|
47
47
|
step: 1,
|
|
48
48
|
error: saveResponse.error ||
|
|
49
|
-
|
|
49
|
+
`저장 1 실패: 0x${saveResponse.flag?.toString(16)}`,
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
|
-
log(`[ENROLL]
|
|
52
|
+
log(`[ENROLL] 템플릿 1 저장 완료`);
|
|
53
53
|
return { success: true, step: 1, userId: saveResponse.param ?? 0 };
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
|
-
*
|
|
56
|
+
* 등록 스캔 2 - 0x62 (스캔 성공) + 0x61 (템플릿 2 저장) 수신
|
|
57
57
|
*/
|
|
58
58
|
async enrollScan2({ onLog } = {}) {
|
|
59
59
|
const log = (msg) => {
|
|
60
60
|
onLog?.(msg);
|
|
61
61
|
};
|
|
62
|
-
log(`[ENROLL]
|
|
62
|
+
log(`[ENROLL] 스캔 2 대기 중...`);
|
|
63
63
|
const scanResponse = await this.client.receiveResponse(this.client.commandTimeout);
|
|
64
|
-
log(`[ENROLL]
|
|
64
|
+
log(`[ENROLL] 스캔 2 응답: flag=0x${scanResponse.flag?.toString(16)}, valid=${scanResponse.valid}`);
|
|
65
65
|
if (!scanResponse.valid) {
|
|
66
66
|
return { success: false, step: 2, error: scanResponse.error || "unknown error" };
|
|
67
67
|
}
|
|
@@ -69,35 +69,35 @@ class FingerprintCommands {
|
|
|
69
69
|
return {
|
|
70
70
|
success: false,
|
|
71
71
|
step: 2,
|
|
72
|
-
error:
|
|
72
|
+
error: `스캔 2 실패: 0x${scanResponse.flag?.toString(16) ?? "??"}`,
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
|
-
log(`[ENROLL]
|
|
75
|
+
log(`[ENROLL] 스캔 2 성공 (0x62), 템플릿 저장 대기 중...`);
|
|
76
76
|
const saveResponse = await this.client.receiveResponse(this.client.commandTimeout);
|
|
77
|
-
log(`[ENROLL]
|
|
77
|
+
log(`[ENROLL] 저장 2 응답: flag=0x${saveResponse.flag?.toString(16)}, valid=${saveResponse.valid}`);
|
|
78
78
|
if (!saveResponse.valid || saveResponse.flag !== index_js_1.SFM_ERROR.SUCCESS) {
|
|
79
79
|
return {
|
|
80
80
|
success: false,
|
|
81
81
|
step: 2,
|
|
82
82
|
error: saveResponse.error ||
|
|
83
|
-
|
|
83
|
+
`저장 2 실패: 0x${saveResponse.flag?.toString(16)}`,
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
|
-
log(`[ENROLL]
|
|
86
|
+
log(`[ENROLL] 템플릿 2 저장 완료 - 등록 완료`);
|
|
87
87
|
return { success: true, step: 2, userId: saveResponse.param ?? 0 };
|
|
88
88
|
}
|
|
89
89
|
/**
|
|
90
|
-
*
|
|
90
|
+
* 등록 (현재 ENROLL_MODE 설정에 자동 적응)
|
|
91
91
|
*
|
|
92
|
-
* - SCAN1 (0x30): 1
|
|
93
|
-
* - SCAN2_MERGE (0x31) / SCAN2_DUAL (0x41):
|
|
92
|
+
* - SCAN1 (0x30): 1회 스캔만
|
|
93
|
+
* - SCAN2_MERGE (0x31) / SCAN2_DUAL (0x41): 스캔 1 + 스캔 2
|
|
94
94
|
*/
|
|
95
95
|
async enroll(userId, flag = index_js_1.SFM_FLAG.NONE, { onLog } = {}) {
|
|
96
96
|
const log = (msg) => {
|
|
97
97
|
onLog?.(msg);
|
|
98
98
|
};
|
|
99
|
-
//
|
|
100
|
-
let enrollMode = 0x31; //
|
|
99
|
+
// 디바이스에서 현재 ENROLL_MODE 읽기
|
|
100
|
+
let enrollMode = 0x31; // 기본값: SCAN2_MERGE
|
|
101
101
|
if (this.system) {
|
|
102
102
|
const param = await this.system.getParam("ENROLL_MODE");
|
|
103
103
|
if (!param.error && param.value != null) {
|
|
@@ -108,12 +108,12 @@ class FingerprintCommands {
|
|
|
108
108
|
const result1 = await this.enrollScan1(userId, flag, { onLog });
|
|
109
109
|
if (!result1.success)
|
|
110
110
|
return result1;
|
|
111
|
-
// SCAN1
|
|
111
|
+
// SCAN1 모드: 1회 스캔으로 충분
|
|
112
112
|
if (enrollMode === 0x30) {
|
|
113
|
-
log("[ENROLL] SCAN1
|
|
113
|
+
log("[ENROLL] SCAN1 모드 — 등록 완료");
|
|
114
114
|
return result1;
|
|
115
115
|
}
|
|
116
|
-
// SCAN2_MERGE / SCAN2_DUAL:
|
|
116
|
+
// SCAN2_MERGE / SCAN2_DUAL: 2차 스캔 필요
|
|
117
117
|
return this.enrollScan2({ onLog });
|
|
118
118
|
}
|
|
119
119
|
/**
|
|
@@ -126,19 +126,19 @@ class FingerprintCommands {
|
|
|
126
126
|
const log = (msg) => {
|
|
127
127
|
onLog?.(msg);
|
|
128
128
|
};
|
|
129
|
-
log(`[VERIFY] 1:1
|
|
129
|
+
log(`[VERIFY] 1:1 검증 시작 - userId: ${userId}`);
|
|
130
130
|
const response = await this.client.sendCommand(index_js_1.SFM_CMD.VS, userId, 0, 0);
|
|
131
|
-
log(`[VERIFY]
|
|
131
|
+
log(`[VERIFY] 응답: flag=0x${response.flag?.toString(16)}, param=${response.param}, valid=${response.valid}`);
|
|
132
132
|
if (!response.valid) {
|
|
133
133
|
return { success: false, error: response.error || "unknown error" };
|
|
134
134
|
}
|
|
135
135
|
let lastResponse = response;
|
|
136
136
|
let scanCount = 1;
|
|
137
137
|
while (lastResponse.flag === index_js_1.SFM_ERROR.SCAN_SUCCESS) {
|
|
138
|
-
log(`[VERIFY]
|
|
138
|
+
log(`[VERIFY] 스캔 #${scanCount} 성공 (0x62), 매칭 대기 중...`);
|
|
139
139
|
lastResponse = await this.client.receiveResponse(this.client.commandTimeout);
|
|
140
140
|
scanCount++;
|
|
141
|
-
log(`[VERIFY]
|
|
141
|
+
log(`[VERIFY] 응답 #${scanCount}: flag=0x${lastResponse.flag?.toString(16)}, param=${lastResponse.param}, valid=${lastResponse.valid}`);
|
|
142
142
|
if (!lastResponse.valid) {
|
|
143
143
|
return { success: false, error: lastResponse.error || "unknown error" };
|
|
144
144
|
}
|
|
@@ -147,7 +147,7 @@ class FingerprintCommands {
|
|
|
147
147
|
success: lastResponse.flag === index_js_1.SFM_ERROR.SUCCESS,
|
|
148
148
|
userId: lastResponse.param,
|
|
149
149
|
error: lastResponse.flag !== index_js_1.SFM_ERROR.SUCCESS
|
|
150
|
-
?
|
|
150
|
+
? `검증 실패: 0x${lastResponse.flag?.toString(16) ?? "??"}`
|
|
151
151
|
: null,
|
|
152
152
|
};
|
|
153
153
|
}
|
|
@@ -158,19 +158,19 @@ class FingerprintCommands {
|
|
|
158
158
|
const log = (msg) => {
|
|
159
159
|
onLog?.(msg);
|
|
160
160
|
};
|
|
161
|
-
log(`[IDENTIFY] 1:N
|
|
161
|
+
log(`[IDENTIFY] 1:N 식별 시작`);
|
|
162
162
|
const response = await this.client.sendCommand(index_js_1.SFM_CMD.IS, 0, 0, 0);
|
|
163
|
-
log(`[IDENTIFY]
|
|
163
|
+
log(`[IDENTIFY] 응답: flag=0x${response.flag?.toString(16)}, param=${response.param}, valid=${response.valid}`);
|
|
164
164
|
if (!response.valid) {
|
|
165
165
|
return { success: false, userId: 0, error: response.error || "unknown error" };
|
|
166
166
|
}
|
|
167
167
|
let lastResponse = response;
|
|
168
168
|
let scanCount = 1;
|
|
169
169
|
while (lastResponse.flag === index_js_1.SFM_ERROR.SCAN_SUCCESS) {
|
|
170
|
-
log(`[IDENTIFY]
|
|
170
|
+
log(`[IDENTIFY] 스캔 #${scanCount} 성공 (0x62), 매칭 대기 중...`);
|
|
171
171
|
lastResponse = await this.client.receiveResponse(this.client.commandTimeout);
|
|
172
172
|
scanCount++;
|
|
173
|
-
log(`[IDENTIFY]
|
|
173
|
+
log(`[IDENTIFY] 응답 #${scanCount}: flag=0x${lastResponse.flag?.toString(16)}, param=${lastResponse.param}, valid=${lastResponse.valid}`);
|
|
174
174
|
if (!lastResponse.valid) {
|
|
175
175
|
return { success: false, userId: 0, error: lastResponse.error || "unknown error" };
|
|
176
176
|
}
|
|
@@ -179,12 +179,12 @@ class FingerprintCommands {
|
|
|
179
179
|
success: lastResponse.flag === index_js_1.SFM_ERROR.SUCCESS,
|
|
180
180
|
userId: lastResponse.param ?? 0,
|
|
181
181
|
error: lastResponse.flag !== index_js_1.SFM_ERROR.SUCCESS
|
|
182
|
-
?
|
|
182
|
+
? `식별 실패: 0x${lastResponse.flag?.toString(16) ?? "??"}`
|
|
183
183
|
: null,
|
|
184
184
|
};
|
|
185
185
|
}
|
|
186
186
|
/**
|
|
187
|
-
*
|
|
187
|
+
* 특정 템플릿 삭제 (DT: 0x16)
|
|
188
188
|
*/
|
|
189
189
|
async deleteTemplate(userId) {
|
|
190
190
|
if (userId == null) {
|
|
@@ -197,12 +197,12 @@ class FingerprintCommands {
|
|
|
197
197
|
return {
|
|
198
198
|
success: response.flag === index_js_1.SFM_ERROR.SUCCESS,
|
|
199
199
|
error: response.flag !== index_js_1.SFM_ERROR.SUCCESS
|
|
200
|
-
?
|
|
200
|
+
? `삭제 실패: 0x${response.flag?.toString(16) ?? "??"}`
|
|
201
201
|
: null,
|
|
202
202
|
};
|
|
203
203
|
}
|
|
204
204
|
/**
|
|
205
|
-
*
|
|
205
|
+
* 전체 템플릿 삭제 (DA: 0x17)
|
|
206
206
|
*/
|
|
207
207
|
async deleteAllTemplates() {
|
|
208
208
|
const response = await this.client.sendCommand(index_js_1.SFM_CMD.DA, 0, 0, 0);
|
|
@@ -211,17 +211,17 @@ class FingerprintCommands {
|
|
|
211
211
|
}
|
|
212
212
|
return {
|
|
213
213
|
success: response.flag === index_js_1.SFM_ERROR.SUCCESS,
|
|
214
|
-
error: response.flag !== index_js_1.SFM_ERROR.SUCCESS ? "
|
|
214
|
+
error: response.flag !== index_js_1.SFM_ERROR.SUCCESS ? "전체 삭제 실패" : null,
|
|
215
215
|
};
|
|
216
216
|
}
|
|
217
217
|
/**
|
|
218
|
-
*
|
|
218
|
+
* 등록된 사용자 ID 목록 조회 (LT: 0x18)
|
|
219
219
|
*/
|
|
220
220
|
async listUserIds({ onLog } = {}) {
|
|
221
221
|
const log = (msg) => {
|
|
222
222
|
onLog?.(msg);
|
|
223
223
|
};
|
|
224
|
-
log("[LIST]
|
|
224
|
+
log("[LIST] 사용자 ID 목록 조회 중");
|
|
225
225
|
const response = await this.client.sendCommand(index_js_1.SFM_CMD.LT, 0, 0, 0);
|
|
226
226
|
if (!response.valid) {
|
|
227
227
|
return { success: false, userIds: [], count: 0, error: response.error || "unknown error" };
|
|
@@ -231,36 +231,36 @@ class FingerprintCommands {
|
|
|
231
231
|
success: false,
|
|
232
232
|
userIds: [],
|
|
233
233
|
count: 0,
|
|
234
|
-
error:
|
|
234
|
+
error: `목록 조회 실패: 0x${response.flag?.toString(16) ?? "??"}`,
|
|
235
235
|
};
|
|
236
236
|
}
|
|
237
237
|
const count = response.param ?? 0;
|
|
238
238
|
const dataSize = response.size ?? 0;
|
|
239
|
-
log(`[LIST]
|
|
239
|
+
log(`[LIST] 등록 사용자: ${count}, 데이터 크기: ${dataSize}`);
|
|
240
240
|
if (count === 0 || dataSize === 0) {
|
|
241
241
|
return { success: true, userIds: [], count: 0, error: null };
|
|
242
242
|
}
|
|
243
243
|
const rawData = await this.client.serial.read(dataSize, this.client.commandTimeout);
|
|
244
244
|
if (rawData.length < dataSize) {
|
|
245
|
-
return { success: false, userIds: [], count: 0, error:
|
|
245
|
+
return { success: false, userIds: [], count: 0, error: `불완전한 데이터: ${rawData.length}/${dataSize} 바이트 수신` };
|
|
246
246
|
}
|
|
247
|
-
log(`[LIST]
|
|
247
|
+
log(`[LIST] 원시 데이터 (hex): ${Buffer.from(rawData).toString("hex").match(/../g)?.join(" ") ?? ""}`);
|
|
248
248
|
const allIds = [];
|
|
249
249
|
for (let i = 0; i + 4 <= rawData.length; i += 4) {
|
|
250
250
|
allIds.push(rawData.readUInt32LE(i));
|
|
251
251
|
}
|
|
252
252
|
const userIds = [...new Set(allIds)];
|
|
253
|
-
log(`[LIST]
|
|
253
|
+
log(`[LIST] 템플릿: ${count}, 고유 사용자: ${userIds.length}, ID: [${userIds.join(", ")}]`);
|
|
254
254
|
return { success: true, userIds, count, error: null };
|
|
255
255
|
}
|
|
256
256
|
/**
|
|
257
|
-
*
|
|
257
|
+
* 디바이스에서 템플릿 읽기 (RTX: 0x89, 핑퐁 프로토콜)
|
|
258
258
|
*
|
|
259
|
-
* RTX
|
|
260
|
-
*
|
|
259
|
+
* RTX 응답 Size 필드: (templateCount << 16) | templateSize
|
|
260
|
+
* 예: 131456 = 0x00020180 → count=2, size=384
|
|
261
261
|
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
262
|
+
* 흐름: RTX 전송 → 384B 데이터 수신 → ACK (RTX) 전송 → 384B 데이터 수신 → ...
|
|
263
|
+
* 디바이스 flag: 0x74 (CONTINUE) = 추가 청크 있음, 0x61 (SUCCESS) = 마지막 청크.
|
|
264
264
|
*/
|
|
265
265
|
async readTemplate(userId, { onLog } = {}) {
|
|
266
266
|
if (userId == null) {
|
|
@@ -269,10 +269,10 @@ class FingerprintCommands {
|
|
|
269
269
|
const log = (msg) => {
|
|
270
270
|
onLog?.(msg);
|
|
271
271
|
};
|
|
272
|
-
// Flag=0:
|
|
273
|
-
log(`[RTX]
|
|
272
|
+
// Flag=0: 해당 userId의 모든 템플릿 읽기
|
|
273
|
+
log(`[RTX] userId: ${userId} 템플릿 읽기`);
|
|
274
274
|
const response = await this.client.sendCommand(index_js_1.SFM_CMD.RTX, userId, 384, 0);
|
|
275
|
-
log(`[RTX]
|
|
275
|
+
log(`[RTX] 응답: flag=0x${response.flag?.toString(16)}, param=${response.param}, size=${response.size}, valid=${response.valid}`);
|
|
276
276
|
if (!response.valid) {
|
|
277
277
|
return { success: false, templateData: null, error: response.error || "unknown error" };
|
|
278
278
|
}
|
|
@@ -280,39 +280,39 @@ class FingerprintCommands {
|
|
|
280
280
|
return {
|
|
281
281
|
success: false,
|
|
282
282
|
templateData: null,
|
|
283
|
-
error:
|
|
283
|
+
error: `템플릿 읽기 실패: 0x${response.flag?.toString(16) ?? "??"}`,
|
|
284
284
|
};
|
|
285
285
|
}
|
|
286
|
-
//
|
|
286
|
+
// RTX 응답 size 파싱: (templateCount << 16) | templateSize
|
|
287
287
|
const rawSize = response.size ?? 0;
|
|
288
288
|
const templateCount = (rawSize >>> 16) & 0xffff;
|
|
289
289
|
const templateSize = rawSize & 0xffff;
|
|
290
290
|
log(`[RTX] templateCount=${templateCount}, templateSize=${templateSize}B`);
|
|
291
291
|
if (templateSize === 0) {
|
|
292
|
-
return { success: false, templateData: null, error: "
|
|
292
|
+
return { success: false, templateData: null, error: "템플릿 데이터 없음 (size=0)" };
|
|
293
293
|
}
|
|
294
294
|
const chunks = [];
|
|
295
295
|
const MAX_TEMPLATE_CHUNKS = 100;
|
|
296
296
|
const totalChunks = templateCount || 1;
|
|
297
297
|
if (totalChunks > MAX_TEMPLATE_CHUNKS) {
|
|
298
|
-
return { success: false, templateData: null, error:
|
|
298
|
+
return { success: false, templateData: null, error: `비정상적인 청크 수: ${totalChunks} (최대 ${MAX_TEMPLATE_CHUNKS})` };
|
|
299
299
|
}
|
|
300
300
|
for (let i = 0; i < totalChunks; i++) {
|
|
301
|
-
log(`[RTX]
|
|
301
|
+
log(`[RTX] 청크 ${i + 1}/${totalChunks} 수신 중...`);
|
|
302
302
|
const readTimeout = Math.max(this.client.commandTimeout, Math.ceil(templateSize / 100) + 5000);
|
|
303
|
-
//
|
|
304
|
-
//
|
|
303
|
+
// 확장 데이터 전송 프로토콜:
|
|
304
|
+
// 데이터 패킷 헤더(13B) + 데이터 본문(templateSize) + 데이터 체크섬(4B)
|
|
305
305
|
const dataPacketHeader = await this.client.serial.read(13, readTimeout);
|
|
306
|
-
log(`[RTX]
|
|
306
|
+
log(`[RTX] 데이터 패킷 헤더: ${Buffer.from(dataPacketHeader).toString("hex")}`);
|
|
307
307
|
const chunkData = await this.client.serial.read(templateSize, readTimeout);
|
|
308
308
|
if (!chunkData || chunkData.length < templateSize) {
|
|
309
309
|
return {
|
|
310
310
|
success: false,
|
|
311
311
|
templateData: null,
|
|
312
|
-
error:
|
|
312
|
+
error: `불완전한 읽기: ${chunkData?.length ?? 0}/${templateSize}B`,
|
|
313
313
|
};
|
|
314
314
|
}
|
|
315
|
-
//
|
|
315
|
+
// 4바이트 체크섬 읽기 및 검증
|
|
316
316
|
const checksumData = await this.client.serial.read(4, readTimeout);
|
|
317
317
|
const receivedChecksum = checksumData.readUInt32LE(0);
|
|
318
318
|
let calculatedChecksum = 0;
|
|
@@ -320,30 +320,30 @@ class FingerprintCommands {
|
|
|
320
320
|
calculatedChecksum += chunkData[j];
|
|
321
321
|
}
|
|
322
322
|
calculatedChecksum = calculatedChecksum >>> 0;
|
|
323
|
-
log(`[RTX]
|
|
323
|
+
log(`[RTX] 청크 ${i + 1} 수신: ${chunkData.length}B, 체크섬: received=0x${receivedChecksum.toString(16)}, calculated=0x${calculatedChecksum.toString(16)}`);
|
|
324
324
|
if (receivedChecksum !== calculatedChecksum) {
|
|
325
325
|
return {
|
|
326
326
|
success: false,
|
|
327
327
|
templateData: null,
|
|
328
|
-
error:
|
|
328
|
+
error: `청크 ${i + 1} 체크섬 불일치: received=0x${receivedChecksum.toString(16)}, calculated=0x${calculatedChecksum.toString(16)}`,
|
|
329
329
|
};
|
|
330
330
|
}
|
|
331
331
|
chunks.push(chunkData.subarray(0, templateSize));
|
|
332
|
-
//
|
|
332
|
+
// DATA_OK(0x83) ACK 전송하여 다음 데이터 패킷 트리거
|
|
333
333
|
const DATA_OK = 0x83;
|
|
334
|
-
log(`[RTX]
|
|
334
|
+
log(`[RTX] DATA_OK ACK 전송 중...`);
|
|
335
335
|
const ackPacket = (0, packet_js_1.buildNormalPacket)(index_js_1.SFM_CMD.RTX, 0, 0, DATA_OK);
|
|
336
336
|
await this.client._writePacket(ackPacket);
|
|
337
|
-
log(`[RTX] ACK
|
|
337
|
+
log(`[RTX] ACK 전송: ${Buffer.from(ackPacket).toString("hex")}`);
|
|
338
338
|
}
|
|
339
339
|
const templateData = Buffer.concat(chunks);
|
|
340
|
-
log(`[RTX]
|
|
340
|
+
log(`[RTX] 템플릿 수신 완료: ${templateData.length}B (${chunks.length}개 청크)`);
|
|
341
341
|
return { success: true, templateData, error: null };
|
|
342
342
|
}
|
|
343
343
|
/**
|
|
344
|
-
*
|
|
344
|
+
* 디바이스에 템플릿 쓰기 (ET: 0x07 — 단순 템플릿 등록)
|
|
345
345
|
*
|
|
346
|
-
*
|
|
346
|
+
* 키오스크 호환 프로토콜 (rs232/fingerPrinter.js enrollByTemplate):
|
|
347
347
|
* 1. [Command Packet 13B] + [Template Data] + [END 0x0A] 를 한 번에 전송
|
|
348
348
|
* 2. 디바이스로부터 2개 응답 수신 (스캔 처리 + 최종 결과)
|
|
349
349
|
*
|
|
@@ -360,55 +360,55 @@ class FingerprintCommands {
|
|
|
360
360
|
const CHUNK = 384;
|
|
361
361
|
const totalSize = templateData.length;
|
|
362
362
|
const chunkCount = Math.ceil(totalSize / CHUNK);
|
|
363
|
-
log(`[ET]
|
|
363
|
+
log(`[ET] userId: ${userId} 템플릿 쓰기, totalSize: ${totalSize}B, chunks: ${chunkCount}`);
|
|
364
364
|
let lastResponseParam = userId;
|
|
365
365
|
for (let i = 0; i < chunkCount; i++) {
|
|
366
366
|
const offset = i * CHUNK;
|
|
367
367
|
const currentChunkSize = Math.min(CHUNK, totalSize - offset);
|
|
368
368
|
const chunkData = templateData.subarray(offset, offset + currentChunkSize);
|
|
369
|
-
log(`[ET]
|
|
370
|
-
//
|
|
369
|
+
log(`[ET] 청크 ${i + 1}/${chunkCount} 전송: ${currentChunkSize}B`);
|
|
370
|
+
// 명령 패킷 생성 (13B)
|
|
371
371
|
const cmdPacket = (0, packet_js_1.buildNormalPacket)(index_js_1.SFM_CMD.ET, userId, currentChunkSize, flag);
|
|
372
372
|
// ET 프로토콜: [Command 13B] + [Template Data] + [END 0x0A]
|
|
373
373
|
const endByte = Buffer.from([0x0a]);
|
|
374
374
|
const combined = Buffer.concat([cmdPacket, chunkData, endByte]);
|
|
375
|
-
log(`[ET]
|
|
375
|
+
log(`[ET] 패킷: ${Buffer.from(combined).toString("hex")}`);
|
|
376
376
|
await this.client.serial.write(combined);
|
|
377
377
|
// ET는 2개 응답을 수신
|
|
378
378
|
// 응답 1: 처리 중간 응답
|
|
379
379
|
const response1 = await this.client.receiveResponse(this.client.commandTimeout);
|
|
380
|
-
log(`[ET]
|
|
380
|
+
log(`[ET] 응답 1: flag=0x${response1.flag?.toString(16)}, param=${response1.param}, valid=${response1.valid}`);
|
|
381
381
|
if (!response1.valid) {
|
|
382
|
-
return { success: false, error: `ET
|
|
382
|
+
return { success: false, error: `ET 청크 ${i + 1} 응답 1 무효: ${response1.error}` };
|
|
383
383
|
}
|
|
384
384
|
// 응답 2: 최종 결과
|
|
385
385
|
const response2 = await this.client.receiveResponse(this.client.commandTimeout);
|
|
386
|
-
log(`[ET]
|
|
386
|
+
log(`[ET] 응답 2: flag=0x${response2.flag?.toString(16)}, param=${response2.param}, valid=${response2.valid}`);
|
|
387
387
|
if (!response2.valid) {
|
|
388
|
-
return { success: false, error: `ET
|
|
388
|
+
return { success: false, error: `ET 청크 ${i + 1} 응답 2 무효: ${response2.error}` };
|
|
389
389
|
}
|
|
390
390
|
if (response2.flag !== index_js_1.SFM_ERROR.SUCCESS) {
|
|
391
391
|
return {
|
|
392
392
|
success: false,
|
|
393
|
-
error: `ET
|
|
393
|
+
error: `ET 청크 ${i + 1} 실패: 0x${response2.flag?.toString(16) ?? "??"}`,
|
|
394
394
|
};
|
|
395
395
|
}
|
|
396
396
|
lastResponseParam = response2.param ?? userId;
|
|
397
|
-
log(`[ET]
|
|
397
|
+
log(`[ET] 청크 ${i + 1}/${chunkCount} 등록 성공`);
|
|
398
398
|
}
|
|
399
|
-
log(`[ET]
|
|
399
|
+
log(`[ET] 템플릿 쓰기 성공 (${chunkCount}개 청크, ${totalSize}B)`);
|
|
400
400
|
return { success: true, userId: lastResponseParam, error: null };
|
|
401
401
|
}
|
|
402
402
|
/**
|
|
403
|
-
*
|
|
403
|
+
* 디바이스에 템플릿 쓰기 (ETX: 0x87 — 확장 템플릿 등록)
|
|
404
404
|
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
405
|
+
* 확장 데이터 전송 프로토콜 (Appendix B):
|
|
406
|
+
* 멀티 청크 지원: 768B → 384B 2개 청크를 하나의 ETX 세션에서 전송.
|
|
407
407
|
*
|
|
408
408
|
* 1. sendCommand(ETX, userId, sizeField, flag) → SUCCESS(0x61)
|
|
409
409
|
* sizeField: (templateCount << 24) | (enrollCount << 16) | chunkSize
|
|
410
|
-
* 2.
|
|
411
|
-
*
|
|
410
|
+
* 2. 각 청크별:
|
|
411
|
+
* 데이터 패킷 헤더(13B) + 데이터 본문(384B) + 데이터 체크섬(4B)
|
|
412
412
|
* → DATA_OK(0x83) 응답 수신
|
|
413
413
|
*/
|
|
414
414
|
async writeTemplateMulti(userId, templateData, flag = index_js_1.SFM_FLAG.NONE, { onLog } = {}) {
|
|
@@ -423,34 +423,34 @@ class FingerprintCommands {
|
|
|
423
423
|
const templateCount = Math.ceil(totalSize / CHUNK);
|
|
424
424
|
const chunkSize = Math.min(totalSize, CHUNK);
|
|
425
425
|
if (templateCount > 255) {
|
|
426
|
-
return { success: false, error:
|
|
426
|
+
return { success: false, error: `템플릿이 너무 큼: ${templateCount}개 청크가 최대 255 초과` };
|
|
427
427
|
}
|
|
428
428
|
// ETX Size: (templateCount << 24) | (enrollCount << 16) | chunkSize
|
|
429
429
|
// enrollCount=1: 각 템플릿을 1번씩만 저장
|
|
430
430
|
const sizeField = (templateCount << 24) | (1 << 16) | chunkSize;
|
|
431
|
-
log(`[ETX]
|
|
432
|
-
//
|
|
431
|
+
log(`[ETX] userId: ${userId} 템플릿 쓰기, totalSize: ${totalSize}B, chunks: ${templateCount}, sizeField=0x${sizeField.toString(16)}`);
|
|
432
|
+
// 1단계: ETX 명령 패킷 전송 + SUCCESS(0x61) 응답 대기
|
|
433
433
|
const cmdResponse = await this.client.sendCommand(index_js_1.SFM_CMD.ETX, userId, sizeField, flag);
|
|
434
|
-
log(`[ETX]
|
|
434
|
+
log(`[ETX] 명령 응답: flag=0x${cmdResponse.flag?.toString(16)}, param=${cmdResponse.param}, valid=${cmdResponse.valid}`);
|
|
435
435
|
if (!cmdResponse.valid) {
|
|
436
436
|
return { success: false, error: cmdResponse.error || "unknown error" };
|
|
437
437
|
}
|
|
438
438
|
if (cmdResponse.flag !== index_js_1.SFM_ERROR.SUCCESS) {
|
|
439
439
|
return {
|
|
440
440
|
success: false,
|
|
441
|
-
error: `ETX
|
|
441
|
+
error: `ETX 명령 실패: 0x${cmdResponse.flag?.toString(16) ?? "??"}`,
|
|
442
442
|
};
|
|
443
443
|
}
|
|
444
|
-
//
|
|
444
|
+
// 2단계: 확장 데이터 전송 프로토콜로 각 청크 전송
|
|
445
445
|
const DATA_OK = 0x83;
|
|
446
446
|
for (let i = 0; i < templateCount; i++) {
|
|
447
447
|
const offset = i * CHUNK;
|
|
448
448
|
const currentChunkSize = Math.min(CHUNK, totalSize - offset);
|
|
449
449
|
const chunkData = templateData.subarray(offset, offset + currentChunkSize);
|
|
450
|
-
//
|
|
450
|
+
// 데이터 패킷 헤더 param: (totalPackets & 0xFFFF) | ((packetIndex & 0xFFFF) << 16)
|
|
451
451
|
const dataPacketParam = (templateCount & 0xFFFF) | ((i & 0xFFFF) << 16);
|
|
452
452
|
const dataHeader = (0, packet_js_1.buildNormalPacket)(index_js_1.SFM_CMD.ETX, dataPacketParam, currentChunkSize, 0);
|
|
453
|
-
//
|
|
453
|
+
// 데이터 체크섬: 전체 바이트 합 (& 0xFF 아님)
|
|
454
454
|
let checksum = 0;
|
|
455
455
|
for (let j = 0; j < currentChunkSize; j++) {
|
|
456
456
|
checksum += chunkData[j];
|
|
@@ -458,35 +458,35 @@ class FingerprintCommands {
|
|
|
458
458
|
const checksumBuf = Buffer.alloc(4);
|
|
459
459
|
checksumBuf.writeUInt32LE(checksum >>> 0, 0);
|
|
460
460
|
const combined = Buffer.concat([dataHeader, chunkData, checksumBuf]);
|
|
461
|
-
log(`[ETX]
|
|
461
|
+
log(`[ETX] 청크 ${i + 1}/${templateCount} 전송: ${combined.length}B (header:13 + data:${currentChunkSize} + checksum:4, checksum=${checksum})`);
|
|
462
462
|
await this.client.serial.write(combined);
|
|
463
|
-
//
|
|
463
|
+
// DATA_OK(0x83) 응답 대기
|
|
464
464
|
const ackResponse = await this.client.receiveResponse(this.client.commandTimeout);
|
|
465
|
-
log(`[ETX]
|
|
465
|
+
log(`[ETX] 청크 ${i + 1} ACK: flag=0x${ackResponse.flag?.toString(16)}, param=${ackResponse.param}, valid=${ackResponse.valid}`);
|
|
466
466
|
if (!ackResponse.valid) {
|
|
467
|
-
return { success: false, error:
|
|
467
|
+
return { success: false, error: `청크 ${i + 1} ACK 무효: ${ackResponse.error}` };
|
|
468
468
|
}
|
|
469
469
|
if (ackResponse.flag !== index_js_1.SFM_ERROR.SUCCESS && ackResponse.flag !== DATA_OK) {
|
|
470
470
|
return {
|
|
471
471
|
success: false,
|
|
472
|
-
error: `ETX
|
|
472
|
+
error: `ETX 청크 ${i + 1} 실패: 0x${ackResponse.flag?.toString(16) ?? "??"}`,
|
|
473
473
|
};
|
|
474
474
|
}
|
|
475
475
|
}
|
|
476
|
-
log(`[ETX]
|
|
477
|
-
//
|
|
476
|
+
log(`[ETX] 템플릿 쓰기 성공 (${templateCount}개 청크, ${totalSize}B)`);
|
|
477
|
+
// 디바이스의 잔여 응답 소진 (예: 쓰기 후 SCAN_SUCCESS)
|
|
478
478
|
await new Promise((r) => setTimeout(r, 100));
|
|
479
479
|
await this.client.serial.purgeRX();
|
|
480
480
|
return { success: true, userId: cmdResponse.param, error: null };
|
|
481
481
|
}
|
|
482
482
|
/**
|
|
483
|
-
*
|
|
483
|
+
* 지문 이미지 캡처 (SI: 0x15)
|
|
484
484
|
*/
|
|
485
485
|
async captureImage({ onLog } = {}) {
|
|
486
486
|
const log = (msg) => {
|
|
487
487
|
onLog?.(msg);
|
|
488
488
|
};
|
|
489
|
-
log("[CAPTURE]
|
|
489
|
+
log("[CAPTURE] 지문 스캔 시작 (SI: 0x15)");
|
|
490
490
|
const siResponse = await this.client.sendCommand(index_js_1.SFM_CMD.SI, 0, 0, 0);
|
|
491
491
|
log(`[CAPTURE] SI 응답: flag=0x${siResponse.flag?.toString(16)}, param=${siResponse.param}, size=${siResponse.size}, valid=${siResponse.valid}`);
|
|
492
492
|
if (!siResponse.valid) {
|
|
@@ -500,9 +500,9 @@ class FingerprintCommands {
|
|
|
500
500
|
}
|
|
501
501
|
let lastResponse = siResponse;
|
|
502
502
|
while (lastResponse.flag === index_js_1.SFM_ERROR.SCAN_SUCCESS) {
|
|
503
|
-
log("[CAPTURE]
|
|
503
|
+
log("[CAPTURE] 스캔 성공 (0x62), 다음 응답 대기 중...");
|
|
504
504
|
lastResponse = await this.client.receiveResponse(this.client.commandTimeout);
|
|
505
|
-
log(`[CAPTURE]
|
|
505
|
+
log(`[CAPTURE] 응답: flag=0x${lastResponse.flag?.toString(16)}, param=${lastResponse.param}, size=${lastResponse.size}, valid=${lastResponse.valid}`);
|
|
506
506
|
if (!lastResponse.valid) {
|
|
507
507
|
return {
|
|
508
508
|
success: false,
|
|
@@ -519,7 +519,7 @@ class FingerprintCommands {
|
|
|
519
519
|
imageData: null,
|
|
520
520
|
width: 0,
|
|
521
521
|
height: 0,
|
|
522
|
-
error:
|
|
522
|
+
error: `스캔 실패: 0x${lastResponse.flag?.toString(16) ?? "??"}`,
|
|
523
523
|
};
|
|
524
524
|
}
|
|
525
525
|
const imageSize = lastResponse.size ?? 0;
|
|
@@ -530,10 +530,10 @@ class FingerprintCommands {
|
|
|
530
530
|
imageData: null,
|
|
531
531
|
width: 0,
|
|
532
532
|
height: 0,
|
|
533
|
-
error: "
|
|
533
|
+
error: "이미지 데이터 없음 (size=0)",
|
|
534
534
|
};
|
|
535
535
|
}
|
|
536
|
-
log(`[CAPTURE]
|
|
536
|
+
log(`[CAPTURE] 이미지 데이터 수신: ${imageSize}B`);
|
|
537
537
|
const readTimeout = Math.max(this.client.commandTimeout, Math.ceil(imageSize / 100) + 5000);
|
|
538
538
|
const imageData = await this.client.serial.read(imageSize, readTimeout);
|
|
539
539
|
if (!imageData || imageData.length < imageSize) {
|
|
@@ -542,10 +542,10 @@ class FingerprintCommands {
|
|
|
542
542
|
imageData: null,
|
|
543
543
|
width: 0,
|
|
544
544
|
height: 0,
|
|
545
|
-
error:
|
|
545
|
+
error: `불완전한 읽기: ${imageData?.length ?? 0}/${imageSize}B`,
|
|
546
546
|
};
|
|
547
547
|
}
|
|
548
|
-
log(`[CAPTURE]
|
|
548
|
+
log(`[CAPTURE] 이미지 수신 완료: ${imageData.length}B`);
|
|
549
549
|
const sqrt = Math.sqrt(imageSize);
|
|
550
550
|
let width, height;
|
|
551
551
|
if (Number.isInteger(sqrt)) {
|
|
@@ -576,7 +576,7 @@ class FingerprintCommands {
|
|
|
576
576
|
}
|
|
577
577
|
}
|
|
578
578
|
}
|
|
579
|
-
log(`[CAPTURE]
|
|
579
|
+
log(`[CAPTURE] 이미지 해상도: ${width}x${height} (${width * height} vs ${imageSize})`);
|
|
580
580
|
return { success: true, imageData, width, height, error: null };
|
|
581
581
|
}
|
|
582
582
|
}
|