Skip to content

Commit b868795

Browse files
authored
Merge pull request #6 from sysdevrun/claude/nfc-usb-reader-app-CccoR
Claude/nfc usb reader app ccco r
2 parents ba79e5d + c7d2e20 commit b868795

3 files changed

Lines changed: 167 additions & 77 deletions

File tree

src/lib/nfc-device.ts

Lines changed: 68 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -73,57 +73,94 @@ export async function sendCommand(
7373
}
7474

7575
/**
76-
* Get card UID using RDR-518 proprietary command
76+
* Get card UID using ASK RDR-518 proprietary command
7777
* Command: 80 0a 01 03 00 00 02 11 03 01 01 14 00 9c f8
78-
* Response format: [LEN] 01 03 00 [ATQA 2 bytes] 00 [SAK] [UID 4 bytes] [STATUS] [CRC-16 2 bytes]
79-
* Example: 0b 01 03 00 05 06 00 08 8d 69 5d f1 00 e2 4e
78+
*
79+
* Response format (card found - 15 bytes):
80+
* [LEN=0x0b] 01 03 00 [ATQA 2B] 00 [SAK] [UID 4B] [STATUS=0x00] [CRC-16]
81+
* Example: 0b 01 03 00 05 06 00 08 8d 69 5d f1 00 e2 4e
82+
*
83+
* Response format (no card - 8 bytes):
84+
* [LEN=0x04] 01 03 00 [ERROR=0x6f] 00 [CRC-16]
85+
* Example: 04 01 03 00 6f 00 1d cb
8086
*/
8187
export async function getCardUid(device: NfcDevice): Promise<NfcCommandResult> {
8288
const command = fromHex('80 0a 01 03 00 00 02 11 03 01 01 14 00 9c f8');
8389
const result = await sendCommand(device, command);
8490

85-
if (result.success && result.data && result.data.length >= 13) {
86-
// Parse RDR-518 response format
87-
// [0] = LEN (0x0b = 11)
88-
// [1-3] = Header (01 03 00)
89-
// [4-5] = ATQA
90-
// [6] = 00
91-
// [7] = SAK
92-
// [8-11] = UID (4 bytes)
93-
// [12] = Status
94-
// [13-14] = CRC-16
95-
91+
if (result.success && result.data && result.data.length >= 8) {
9692
const len = result.data[0];
97-
const atqa = result.data.subarray(4, 6);
98-
const sak = result.data[7];
99-
const uid = result.data.subarray(8, 12);
100-
const status = result.data[12];
10193

102-
if (status === 0x00 && len === 0x0b) {
103-
return {
104-
success: true,
105-
data: uid,
106-
hexData: toHex(uid),
107-
atqa: toHex(atqa),
108-
sak: sak.toString(16).padStart(2, '0').toUpperCase(),
109-
message: `UID: ${toHex(uid)}`,
110-
};
111-
} else {
94+
// Check for "no card" response: LEN=0x04, error code at [4]=0x6f
95+
if (len === 0x04 && result.data.length >= 8) {
96+
const errorCode = result.data[4];
97+
if (errorCode === 0x6f) {
98+
return {
99+
success: false,
100+
data: result.data,
101+
hexData: result.hexData,
102+
message: 'No card in field',
103+
};
104+
}
112105
return {
113106
success: false,
114107
data: result.data,
115108
hexData: result.hexData,
116-
message: 'No card detected or read error',
109+
message: `Reader error: 0x${errorCode.toString(16).padStart(2, '0')}`,
117110
};
118111
}
112+
113+
// Check for "card found" response: LEN=0x0b (15 bytes total)
114+
if (len === 0x0b && result.data.length >= 15) {
115+
// Parse card data
116+
// [0] = LEN (0x0b = 11)
117+
// [1-3] = Header (01 03 00)
118+
// [4-5] = ATQA
119+
// [6] = 00
120+
// [7] = SAK
121+
// [8-11] = UID (4 bytes)
122+
// [12] = Status
123+
// [13-14] = CRC-16
124+
125+
const atqa = result.data.subarray(4, 6);
126+
const sak = result.data[7];
127+
const uid = result.data.subarray(8, 12);
128+
const status = result.data[12];
129+
130+
if (status === 0x00) {
131+
return {
132+
success: true,
133+
data: uid,
134+
hexData: toHex(uid),
135+
atqa: toHex(atqa),
136+
sak: sak.toString(16).padStart(2, '0').toUpperCase(),
137+
message: `UID: ${toHex(uid)}`,
138+
};
139+
} else {
140+
return {
141+
success: false,
142+
data: result.data,
143+
hexData: result.hexData,
144+
message: `Card error: status 0x${status.toString(16).padStart(2, '0')}`,
145+
};
146+
}
147+
}
148+
149+
// Unknown response format
150+
return {
151+
success: false,
152+
data: result.data,
153+
hexData: result.hexData,
154+
message: `Unknown response (LEN=0x${len.toString(16)})`,
155+
};
119156
}
120157

121-
if (result.success && result.data) {
158+
if (result.success && result.data && result.data.length > 0) {
122159
return {
123160
success: false,
124161
data: result.data,
125162
hexData: result.hexData,
126-
message: 'No card detected',
163+
message: `Incomplete response (${result.data.length} bytes)`,
127164
};
128165
}
129166

src/lib/usb-cdc.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export interface CdcConnectionResult {
2020

2121
// Known NFC reader USB vendor/product IDs
2222
export const KNOWN_NFC_READERS = [
23-
{ vendorId: 0x1fd3, productId: 0x0108, name: 'RDR-518 NFC Reader' },
23+
{ vendorId: 0x1fd3, productId: 0x0108, name: 'ASK RDR-518' },
2424
{ vendorId: 0x072f, productId: 0x2200, name: 'ACS ACR122U' },
2525
{ vendorId: 0x072f, productId: 0x223b, name: 'ACS ACR1252U' },
2626
{ vendorId: 0x072f, productId: 0x8911, name: 'ACS ACR1281S-C1 (ACR518)' },

src/lib/web-serial.ts

Lines changed: 98 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export interface SerialConnectionResult {
1919

2020
// Known NFC reader USB vendor/product IDs for serial filter
2121
export const KNOWN_SERIAL_NFC_READERS = [
22-
{ usbVendorId: 0x1fd3, usbProductId: 0x0108 }, // RDR-518 NFC Reader
22+
{ usbVendorId: 0x1fd3, usbProductId: 0x0108 }, // ASK RDR-518
2323
{ usbVendorId: 0x072f }, // ACS readers
2424
{ usbVendorId: 0x1a86 }, // CH340
2525
{ usbVendorId: 0x0403 }, // FTDI
@@ -127,82 +127,135 @@ export async function sendSerialData(device: SerialDevice, data: Uint8Array): Pr
127127
}
128128

129129
/**
130-
* Receive data from serial port with timeout
130+
* Send command and receive response
131+
* For ASK RDR-518: response format is [LEN] [DATA...] [CRC-16]
131132
*/
132-
export async function receiveSerialData(
133+
export async function transceiveSerial(
133134
device: SerialDevice,
135+
command: Uint8Array,
134136
timeout: number = 2000
137+
): Promise<Uint8Array> {
138+
if (!device.reader || !device.writer) {
139+
throw new Error('Serial port not ready');
140+
}
141+
142+
// Clear any stale data in the buffer first
143+
await clearSerialBuffer(device);
144+
145+
// Send the command
146+
await sendSerialData(device, command);
147+
148+
// Wait for device to process command and perform RF operation
149+
await new Promise(resolve => setTimeout(resolve, 150));
150+
151+
// Read response, looking for a complete frame
152+
return await receiveSerialFrame(device, timeout);
153+
}
154+
155+
/**
156+
* Clear any stale data from the serial buffer
157+
*/
158+
async function clearSerialBuffer(device: SerialDevice): Promise<void> {
159+
if (!device.reader) return;
160+
161+
const startTime = Date.now();
162+
while (Date.now() - startTime < 100) {
163+
try {
164+
const readPromise = device.reader.read();
165+
const timeoutPromise = new Promise<{ value: undefined; done: true }>((resolve) =>
166+
setTimeout(() => resolve({ value: undefined, done: true }), 20)
167+
);
168+
const result = await Promise.race([readPromise, timeoutPromise]);
169+
if (result.done || !result.value || result.value.length === 0) {
170+
break; // Buffer is empty
171+
}
172+
// Discard stale data and continue clearing
173+
} catch {
174+
break;
175+
}
176+
}
177+
}
178+
179+
/**
180+
* Receive a complete frame from serial port
181+
* Frame format: [LEN] [DATA...] [CRC-16]
182+
* LEN=0x04 for no-card (8 bytes total), LEN=0x0b for card-found (15 bytes total)
183+
*/
184+
async function receiveSerialFrame(
185+
device: SerialDevice,
186+
timeout: number
135187
): Promise<Uint8Array> {
136188
if (!device.reader) {
137189
throw new Error('Serial port reader not available');
138190
}
139191

140192
const chunks: Uint8Array[] = [];
141193
const startTime = Date.now();
194+
let totalBytes = 0;
142195

143-
try {
144-
while (Date.now() - startTime < timeout) {
145-
const readPromise = device.reader.read();
146-
const timeoutPromise = new Promise<{ value: undefined; done: true }>((resolve) =>
147-
setTimeout(() => resolve({ value: undefined, done: true }), Math.max(100, timeout - (Date.now() - startTime)))
148-
);
196+
while (Date.now() - startTime < timeout) {
197+
const remainingTime = Math.max(50, timeout - (Date.now() - startTime));
149198

150-
const result = await Promise.race([readPromise, timeoutPromise]);
199+
const readPromise = device.reader.read();
200+
const timeoutPromise = new Promise<{ value: undefined; done: true }>((resolve) =>
201+
setTimeout(() => resolve({ value: undefined, done: true }), Math.min(300, remainingTime))
202+
);
151203

152-
if (result.done) {
153-
break;
154-
}
204+
const result = await Promise.race([readPromise, timeoutPromise]);
155205

156-
if (result.value && result.value.length > 0) {
157-
chunks.push(result.value);
158-
// If we received data, wait a bit more for additional data
206+
if (result.value && result.value.length > 0) {
207+
chunks.push(result.value);
208+
totalBytes += result.value.length;
209+
210+
// Check if we have a complete frame
211+
const combined = combineChunks(chunks, totalBytes);
212+
if (isCompleteFrame(combined)) {
213+
return combined;
214+
}
215+
} else if (result.done) {
216+
// Timeout on this read, but keep trying if we don't have data yet
217+
if (totalBytes > 0) {
218+
// We have some data, wait a bit more then check
159219
await new Promise(resolve => setTimeout(resolve, 50));
160220
}
161221
}
162-
} catch (error) {
163-
console.error('Read error:', error);
164222
}
165223

166-
// Combine all chunks
167-
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
224+
// Return whatever we got
225+
return combineChunks(chunks, totalBytes);
226+
}
227+
228+
/**
229+
* Helper to combine chunks into a single Uint8Array
230+
*/
231+
function combineChunks(chunks: Uint8Array[], totalLength: number): Uint8Array {
168232
const result = new Uint8Array(totalLength);
169233
let offset = 0;
170234
for (const chunk of chunks) {
171235
result.set(chunk, offset);
172236
offset += chunk.length;
173237
}
174-
175238
return result;
176239
}
177240

178241
/**
179-
* Send command and receive response
242+
* Check if we have a complete frame based on LEN byte
180243
*/
181-
export async function transceiveSerial(
182-
device: SerialDevice,
183-
command: Uint8Array,
184-
timeout: number = 2000
185-
): Promise<Uint8Array> {
186-
// Clear any pending data first
187-
if (device.reader) {
188-
try {
189-
// Quick read to clear buffer
190-
const clearPromise = device.reader.read();
191-
const quickTimeout = new Promise<{ done: true }>((resolve) =>
192-
setTimeout(() => resolve({ done: true }), 50)
193-
);
194-
await Promise.race([clearPromise, quickTimeout]);
195-
} catch {
196-
// Ignore clear errors
197-
}
198-
}
244+
function isCompleteFrame(data: Uint8Array): boolean {
245+
if (data.length < 4) return false;
199246

200-
await sendSerialData(device, command);
247+
const len = data[0];
248+
249+
// Known frame sizes for ASK RDR-518:
250+
// LEN=0x04: no-card response, total 8 bytes (4 + 2 data + 2 CRC)
251+
// LEN=0x0b: card-found response, total 15 bytes
252+
if (len === 0x04 && data.length >= 8) return true;
253+
if (len === 0x0b && data.length >= 15) return true;
201254

202-
// Small delay to allow device to process
203-
await new Promise(resolve => setTimeout(resolve, 50));
255+
// Generic check: LEN + 4 bytes (header estimate)
256+
if (data.length >= len + 4) return true;
204257

205-
return await receiveSerialData(device, timeout);
258+
return false;
206259
}
207260

208261
/**
@@ -212,7 +265,7 @@ export function getSerialPortInfo(port: SerialPort): string {
212265
const info = port.getInfo();
213266

214267
if (info.usbVendorId === 0x1fd3 && info.usbProductId === 0x0108) {
215-
return 'RDR-518 NFC Reader';
268+
return 'ASK RDR-518';
216269
}
217270
if (info.usbVendorId === 0x072f) {
218271
return 'ACS NFC Reader';

0 commit comments

Comments
 (0)