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',
|
'name': 'Fusion Clock',
|
||||||
'version': '19.0.3.1.1',
|
'version': '19.0.3.2.0',
|
||||||
'category': 'Human Resources/Attendances',
|
'category': 'Human Resources/Attendances',
|
||||||
'summary': 'Complete Employee T&A with Geofencing, Shifts, Penalties, Overtime, Kiosk, Dashboard & Payroll Export',
|
'summary': 'Complete Employee T&A with Geofencing, Shifts, Penalties, Overtime, Kiosk, Dashboard & Payroll Export',
|
||||||
'description': """
|
'description': """
|
||||||
|
|||||||
@@ -414,6 +414,74 @@
|
|||||||
debugLog("startNfcReader: listeners attached, nfcReady=true");
|
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) {
|
function onNfcReading(event) {
|
||||||
// event.serialNumber is the card UID — works for raw MIFARE access cards
|
// event.serialNumber is the card UID — works for raw MIFARE access cards
|
||||||
const rawSerial = event.serialNumber || "";
|
const rawSerial = event.serialNumber || "";
|
||||||
@@ -547,27 +615,42 @@
|
|||||||
if (setupBtn) {
|
if (setupBtn) {
|
||||||
setupBtn.addEventListener("click", async () => {
|
setupBtn.addEventListener("click", async () => {
|
||||||
debugLog("setup button clicked");
|
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 {
|
try {
|
||||||
await startNfcReader();
|
await startNfcReader();
|
||||||
debugLog("setup: NFC ready, starting camera...");
|
webNfcOk = true;
|
||||||
try {
|
debugLog("setup: Web NFC ready ✓");
|
||||||
await startCamera();
|
} catch (webNfcErr) {
|
||||||
debugLog("setup: camera ready ✓");
|
debugLog("setup: Web NFC unavailable, continuing with USB HID — " + webNfcErr.message);
|
||||||
} 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>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
// 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