feat: setup iniziale sistema controllo accessi Focolari
Struttura progetto: - Backend mock Python (FastAPI) con API per gestione varchi - Frontend React + TypeScript + Vite + Tailwind CSS - Documentazione e piani di sviluppo Backend (backend-mock/): - API REST: /info-room, /login-validate, /anagrafica, /entry-request - Dati mock: 7 utenti, validatore (999999/focolari) - CORS abilitato, docs OpenAPI automatiche - Configurazione pipenv per ambiente virtuale Frontend (frontend/): - State machine completa per flusso accesso varco - Hook useRFIDScanner per lettura badge (pattern singolo) - Componenti UI: Logo, Button, Input, Modal, UserCard, Timer - Schermate: Loading, Login, ActiveGate, Success/Error Modal - Design system con colori Focolari - Ottimizzato per tablet touch Documentazione (ai-prompts/): - Welcome guide per futuri agenti - Piano sviluppo backend e frontend con checklist DA COMPLETARE: - Hook RFID multi-pattern (US/IT/altri layout tastiera) - Pagina /debug per diagnostica in loco - Logging console strutturato
This commit is contained in:
219
frontend/src/hooks/useRFIDScanner.ts
Normal file
219
frontend/src/hooks/useRFIDScanner.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* Focolari Voting System - RFID Scanner Hook
|
||||
*
|
||||
* Questo hook gestisce la lettura di badge RFID tramite lettori USB
|
||||
* che emulano una tastiera. Il protocollo prevede:
|
||||
* - Carattere di inizio: `;`
|
||||
* - Carattere di fine: `?`
|
||||
* - Esempio: `;00012345?`
|
||||
*
|
||||
* L'hook funziona indipendentemente dal focus dell'applicazione.
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import type { RFIDScannerState, RFIDScanResult } from '../types';
|
||||
|
||||
// Costanti
|
||||
const START_SENTINEL = ';';
|
||||
const END_SENTINEL = '?';
|
||||
const TIMEOUT_MS = 3000; // 3 secondi di timeout
|
||||
|
||||
interface UseRFIDScannerOptions {
|
||||
/** Callback chiamato quando un badge viene letto con successo */
|
||||
onScan: (code: string) => void;
|
||||
/** Callback opzionale chiamato in caso di timeout */
|
||||
onTimeout?: () => void;
|
||||
/** Callback opzionale chiamato quando inizia la scansione */
|
||||
onScanStart?: () => void;
|
||||
/** Se true, previene l'input nei campi di testo durante la scansione */
|
||||
preventDefaultOnScan?: boolean;
|
||||
/** Se true, l'hook è disabilitato */
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface UseRFIDScannerReturn {
|
||||
/** Stato corrente dello scanner */
|
||||
state: RFIDScannerState;
|
||||
/** Buffer corrente (solo per debug) */
|
||||
buffer: string;
|
||||
/** Ultimo codice scansionato */
|
||||
lastScan: RFIDScanResult | null;
|
||||
/** Reset manuale dello scanner */
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export function useRFIDScanner({
|
||||
onScan,
|
||||
onTimeout,
|
||||
onScanStart,
|
||||
preventDefaultOnScan = true,
|
||||
disabled = false,
|
||||
}: UseRFIDScannerOptions): UseRFIDScannerReturn {
|
||||
const [state, setState] = useState<RFIDScannerState>('idle');
|
||||
const [buffer, setBuffer] = useState<string>('');
|
||||
const [lastScan, setLastScan] = useState<RFIDScanResult | null>(null);
|
||||
|
||||
// Refs per mantenere i valori aggiornati nei callback
|
||||
const bufferRef = useRef<string>('');
|
||||
const stateRef = useRef<RFIDScannerState>('idle');
|
||||
const timeoutRef = useRef<number | null>(null);
|
||||
|
||||
// Sync refs con state
|
||||
useEffect(() => {
|
||||
bufferRef.current = buffer;
|
||||
}, [buffer]);
|
||||
|
||||
useEffect(() => {
|
||||
stateRef.current = state;
|
||||
}, [state]);
|
||||
|
||||
/**
|
||||
* Pulisce il timeout attivo
|
||||
*/
|
||||
const clearScanTimeout = useCallback(() => {
|
||||
if (timeoutRef.current !== null) {
|
||||
window.clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Resetta lo scanner allo stato idle
|
||||
*/
|
||||
const reset = useCallback(() => {
|
||||
clearScanTimeout();
|
||||
setState('idle');
|
||||
setBuffer('');
|
||||
bufferRef.current = '';
|
||||
stateRef.current = 'idle';
|
||||
}, [clearScanTimeout]);
|
||||
|
||||
/**
|
||||
* Avvia il timeout di sicurezza
|
||||
*/
|
||||
const startTimeout = useCallback(() => {
|
||||
clearScanTimeout();
|
||||
timeoutRef.current = window.setTimeout(() => {
|
||||
console.warn('[RFID Scanner] Timeout - lettura incompleta scartata');
|
||||
onTimeout?.();
|
||||
reset();
|
||||
}, TIMEOUT_MS);
|
||||
}, [clearScanTimeout, onTimeout, reset]);
|
||||
|
||||
/**
|
||||
* Handler principale per gli eventi keydown
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
const key = event.key;
|
||||
|
||||
// Ignora tasti speciali (frecce, funzione, ecc.)
|
||||
if (key.length > 1 && key !== START_SENTINEL && key !== END_SENTINEL) {
|
||||
// Eccezione per Backspace in stato scanning: ignora ma non resetta
|
||||
if (key === 'Backspace' && stateRef.current === 'scanning') {
|
||||
if (preventDefaultOnScan) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// STATO IDLE: attende il carattere di inizio
|
||||
if (stateRef.current === 'idle') {
|
||||
if (key === START_SENTINEL) {
|
||||
console.log('[RFID Scanner] Start sentinel rilevato - inizio scansione');
|
||||
|
||||
if (preventDefaultOnScan) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
setState('scanning');
|
||||
setBuffer('');
|
||||
bufferRef.current = '';
|
||||
startTimeout();
|
||||
onScanStart?.();
|
||||
}
|
||||
// Altrimenti ignora il tasto (comportamento normale)
|
||||
return;
|
||||
}
|
||||
|
||||
// STATO SCANNING: accumula i caratteri o termina
|
||||
if (stateRef.current === 'scanning') {
|
||||
if (preventDefaultOnScan) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (key === END_SENTINEL) {
|
||||
// Fine della scansione
|
||||
clearScanTimeout();
|
||||
|
||||
const scannedCode = bufferRef.current.trim();
|
||||
|
||||
if (scannedCode.length > 0) {
|
||||
console.log('[RFID Scanner] Codice scansionato:', scannedCode);
|
||||
|
||||
const result: RFIDScanResult = {
|
||||
code: scannedCode,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
setLastScan(result);
|
||||
onScan(scannedCode);
|
||||
} else {
|
||||
console.warn('[RFID Scanner] Codice vuoto scartato');
|
||||
}
|
||||
|
||||
reset();
|
||||
} else if (key === START_SENTINEL) {
|
||||
// Nuovo start sentinel durante scansione: resetta e ricomincia
|
||||
console.log('[RFID Scanner] Nuovo start sentinel - reset buffer');
|
||||
setBuffer('');
|
||||
bufferRef.current = '';
|
||||
startTimeout();
|
||||
} else {
|
||||
// Accumula il carattere nel buffer
|
||||
const newBuffer = bufferRef.current + key;
|
||||
setBuffer(newBuffer);
|
||||
bufferRef.current = newBuffer;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Aggiungi listener globale
|
||||
window.addEventListener('keydown', handleKeyDown, { capture: true });
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown, { capture: true });
|
||||
clearScanTimeout();
|
||||
};
|
||||
}, [
|
||||
disabled,
|
||||
onScan,
|
||||
onScanStart,
|
||||
preventDefaultOnScan,
|
||||
clearScanTimeout,
|
||||
reset,
|
||||
startTimeout,
|
||||
]);
|
||||
|
||||
// Cleanup al unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearScanTimeout();
|
||||
};
|
||||
}, [clearScanTimeout]);
|
||||
|
||||
return {
|
||||
state,
|
||||
buffer,
|
||||
lastScan,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
|
||||
export default useRFIDScanner;
|
||||
Reference in New Issue
Block a user