feat(fusion_clock): USB HID reader support + desktop-tolerant kiosk setup
The NFC kiosk previously required Web NFC, which is Android-Chrome-only. This blocked desktop testing and locked us to a single hardware path. Add a keyboard-wedge listener that captures keystrokes from USB HID NFC readers (the standard Sycreader/Yanzeo class). The listener buffers hex chars + separators, flushes on Enter (or 600ms idle as fallback for readers without a terminator), and routes the UID through the same handleTap()/_onEnrollTap() codepath as Web NFC. Photo verification, penalty calc, and activity logging all fire identically. Make the setup button tolerant: try Web NFC, but treat its absence as non-fatal. USB HID always activates. Only hard-fail when photoRequired is True AND the camera is unavailable. Result: same kiosk page now works on Android Chrome (Web NFC), desktop Chrome with a USB reader, or both at once. Bump manifest to 19.0.3.2.0.
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Clock',
|
||||
'version': '19.0.3.1.1',
|
||||
'version': '19.0.3.2.0',
|
||||
'category': 'Human Resources/Attendances',
|
||||
'summary': 'Complete Employee T&A with Geofencing, Shifts, Penalties, Overtime, Kiosk, Dashboard & Payroll Export',
|
||||
'description': """
|
||||
|
||||
@@ -414,6 +414,74 @@
|
||||
debugLog("startNfcReader: listeners attached, nfcReady=true");
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// USB HID keyboard-wedge listener (works alongside Web NFC).
|
||||
// Most USB NFC readers in HID mode type the UID as keystrokes and
|
||||
// end with Enter. We buffer chars until Enter arrives (or 500ms
|
||||
// pause), then route the UID through the same flow Web NFC uses.
|
||||
//
|
||||
// Critical: this listener fires the same handleTap()/_onEnrollTap()
|
||||
// codepath as Web NFC, so penalty + photo + activity log all work
|
||||
// identically regardless of which reader produced the UID.
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
let _hidBuffer = "";
|
||||
let _hidLastKeyAt = 0;
|
||||
let _hidFlushTimer = null;
|
||||
const HID_RESET_MS = 500; // pause longer than this resets the buffer
|
||||
const HID_FLUSH_MS = 600; // if no Enter arrives, flush this long after last char
|
||||
const HID_MIN_LEN = 4; // shortest plausible UID
|
||||
const HID_CHAR_RE = /^[0-9A-Fa-f:\-]$/; // hex digits + common separators
|
||||
|
||||
function _flushHidBuffer() {
|
||||
const uid = _hidBuffer.trim().toUpperCase();
|
||||
_hidBuffer = "";
|
||||
if (_hidFlushTimer) { clearTimeout(_hidFlushTimer); _hidFlushTimer = null; }
|
||||
if (uid.length < HID_MIN_LEN) {
|
||||
debugLog("HID flush: too short, ignored (" + JSON.stringify(uid) + ")");
|
||||
return;
|
||||
}
|
||||
debugLog("HID flush: uid=" + uid + " state=" + currentState);
|
||||
if (currentState === STATE.ENROLL) {
|
||||
window.__nfcKiosk && window.__nfcKiosk._onEnrollTap && window.__nfcKiosk._onEnrollTap(uid);
|
||||
} else if (currentState === STATE.IDLE) {
|
||||
handleTap(uid);
|
||||
} else {
|
||||
debugLog(" → IGNORED: state=" + currentState);
|
||||
}
|
||||
}
|
||||
|
||||
function startUsbHidListener() {
|
||||
document.addEventListener("keydown", (e) => {
|
||||
// Don't capture keystrokes inside form inputs — preserves
|
||||
// typing in enroll-mode search box, etc.
|
||||
const t = e.target;
|
||||
if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) {
|
||||
return;
|
||||
}
|
||||
// Don't fight the existing Ctrl+Shift+T mock-tap shortcut.
|
||||
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
||||
|
||||
const now = Date.now();
|
||||
if (now - _hidLastKeyAt > HID_RESET_MS) {
|
||||
_hidBuffer = "";
|
||||
}
|
||||
_hidLastKeyAt = now;
|
||||
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
_flushHidBuffer();
|
||||
return;
|
||||
}
|
||||
if (HID_CHAR_RE.test(e.key)) {
|
||||
_hidBuffer += e.key;
|
||||
// Fallback flush if the reader doesn't emit Enter
|
||||
if (_hidFlushTimer) clearTimeout(_hidFlushTimer);
|
||||
_hidFlushTimer = setTimeout(_flushHidBuffer, HID_FLUSH_MS);
|
||||
}
|
||||
});
|
||||
debugLog("startUsbHidListener: listening for HID keystrokes ✓");
|
||||
}
|
||||
|
||||
function onNfcReading(event) {
|
||||
// event.serialNumber is the card UID — works for raw MIFARE access cards
|
||||
const rawSerial = event.serialNumber || "";
|
||||
@@ -547,27 +615,42 @@
|
||||
if (setupBtn) {
|
||||
setupBtn.addEventListener("click", async () => {
|
||||
debugLog("setup button clicked");
|
||||
// Try Web NFC, but don't fail if absent — USB HID reader is a
|
||||
// first-class alternative (works on desktops/iOS too).
|
||||
let webNfcOk = false;
|
||||
try {
|
||||
await startNfcReader();
|
||||
debugLog("setup: NFC ready, starting camera...");
|
||||
try {
|
||||
await startCamera();
|
||||
debugLog("setup: camera ready ✓");
|
||||
} catch (camErr) {
|
||||
debugLog("setup: camera failed: " + camErr.message);
|
||||
if (photoRequired) throw camErr;
|
||||
console.warn("[nfc-kiosk] camera unavailable, continuing (photo not required)", camErr);
|
||||
}
|
||||
await acquireWakeLock();
|
||||
setState(STATE.IDLE);
|
||||
} catch (e) {
|
||||
stateContainer.innerHTML = `
|
||||
<div class="nfc-kiosk__setup">
|
||||
<h2 style="color:#d9374e">Setup failed</h2>
|
||||
<p>${escapeHtml(e.message)}</p>
|
||||
</div>
|
||||
`;
|
||||
webNfcOk = true;
|
||||
debugLog("setup: Web NFC ready ✓");
|
||||
} catch (webNfcErr) {
|
||||
debugLog("setup: Web NFC unavailable, continuing with USB HID — " + webNfcErr.message);
|
||||
}
|
||||
// USB HID listener: no permission needed, works on any platform.
|
||||
startUsbHidListener();
|
||||
// Camera: best-effort unless photoRequired forces it.
|
||||
try {
|
||||
await startCamera();
|
||||
debugLog("setup: camera ready ✓");
|
||||
} catch (camErr) {
|
||||
debugLog("setup: camera failed: " + camErr.message);
|
||||
if (photoRequired) {
|
||||
// Only THIS path is a hard fail. Use the existing error
|
||||
// render to keep DOM patterns consistent with the rest
|
||||
// of this file.
|
||||
stateContainer.innerHTML = `
|
||||
<div class="nfc-kiosk__setup">
|
||||
<h2 style="color:#d9374e">Setup failed</h2>
|
||||
<p>${escapeHtml(camErr.message)}</p>
|
||||
<p style="opacity:.7;font-size:.9em">Camera is required but unavailable. Either plug in a webcam, or disable "Photo Required" in Fusion Clock settings.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
console.warn("[nfc-kiosk] camera unavailable, continuing (photo not required)", camErr);
|
||||
}
|
||||
await acquireWakeLock();
|
||||
setState(STATE.IDLE);
|
||||
debugLog("setup: IDLE — Web NFC: " + (webNfcOk ? "✓" : "✗") + " · USB HID: ✓");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user