diff --git a/app/drivers/olimex_esp32_c6_evb_pn532/driver.py b/app/drivers/olimex_esp32_c6_evb_pn532/driver.py index 7c03440..55c477e 100644 --- a/app/drivers/olimex_esp32_c6_evb_pn532/driver.py +++ b/app/drivers/olimex_esp32_c6_evb_pn532/driver.py @@ -189,27 +189,34 @@ class OlimexESP32C6EVBPn532Driver(BoardDriver): def set_nfc_config( self, board: "Board", - auth_uid: str = "", + auth_uids: list[str] | None = None, + auth_uid: str = "", # legacy single-UID shim — ignored if auth_uids given relay_num: int = 1, - pulse_ms: int = 3000, + pulse_ms: int = 0, ) -> bool: """Push NFC access-control config to the board. - auth_uid: authorized card UID (e.g. "04:AB:CD:EF"); empty = no card authorized. + auth_uids: list of authorized card UIDs (up to 10); empty list = clear all. relay_num: which relay to open on a matching card (1-4). - pulse_ms: absence timeout — relay closes this many ms after card is removed (100-60000). + pulse_ms: absence timeout ms; 0 = use board default (5 s). """ + if auth_uids is None: + # backwards-compat: single uid string → list + auth_uids = [auth_uid.upper().strip()] if auth_uid.strip() else [] + + uids_clean = [u.upper().strip() for u in auth_uids if u.strip()][:10] + uids_param = urllib.parse.quote(",".join(uids_clean)) url = ( f"{board.base_url}/nfc/config" - f"?auth_uid={urllib.parse.quote(auth_uid.upper())}" + f"?auth_uids={uids_param}" f"&relay={relay_num}" f"&pulse_ms={pulse_ms}" ) result = _post(url, _auth(board, "POST", url)) if result: logger.info( - "NFC config pushed to board '%s': uid='%s' relay=%d pulse=%dms", - board.name, auth_uid, relay_num, pulse_ms, + "NFC config pushed to board '%s': uids=%s relay=%d pulse=%dms", + board.name, uids_clean, relay_num, pulse_ms, ) else: logger.warning("NFC config push failed for board '%s'", board.name) diff --git a/app/routes/boards.py b/app/routes/boards.py index 7d421ee..cb0528c 100644 --- a/app/routes/boards.py +++ b/app/routes/boards.py @@ -318,6 +318,7 @@ def nfc_status_json(board_id: int): @boards_bp.route("//nfc/config", methods=["POST"]) @login_required def nfc_config_save(board_id: int): + """Save relay/timeout settings and optionally add a new authorized UID.""" if not current_user.is_admin(): abort(403) board = db.get_or_404(Board, board_id) @@ -328,9 +329,9 @@ def nfc_config_save(board_id: int): flash("NFC driver not available.", "danger") return redirect(url_for("boards.nfc_management", board_id=board_id)) - auth_uid = request.form.get("auth_uid", "").strip().upper() relay_num = int(request.form.get("relay_num", 1)) pulse_ms = int(request.form.get("pulse_ms", 0)) + new_uid = request.form.get("new_uid", "").strip().upper() if relay_num < 1 or relay_num > board.num_relays: flash(f"Relay number must be 1–{board.num_relays}.", "danger") @@ -339,10 +340,21 @@ def nfc_config_save(board_id: int): flash("Release delay must be between 0 and 60 000 ms.", "danger") return redirect(url_for("boards.nfc_management", board_id=board_id)) - ok = driver.set_nfc_config(board, auth_uid=auth_uid, relay_num=relay_num, pulse_ms=pulse_ms) + # Fetch current list from board so we can append + current_cfg = driver.get_nfc_config(board) or {} + current_uids: list[str] = current_cfg.get("auth_uids") or ( + [current_cfg["auth_uid"]] if current_cfg.get("auth_uid") else [] + ) + if new_uid: + if new_uid not in current_uids: + if len(current_uids) >= 10: + flash("Maximum 10 authorized cards reached. Remove one before adding.", "danger") + return redirect(url_for("boards.nfc_management", board_id=board_id)) + current_uids.append(new_uid) + + ok = driver.set_nfc_config(board, auth_uids=current_uids, relay_num=relay_num, pulse_ms=pulse_ms) if ok: - uid_display = auth_uid if auth_uid else "(any card)" - flash(f"NFC config saved — authorized UID: {uid_display}, relay: {relay_num}, timeout: {pulse_ms} ms", "success") + flash(f"NFC config saved — {len(current_uids)} card(s) authorized, relay: {relay_num}, timeout: {pulse_ms} ms", "success") else: flash("Failed to push NFC config — board unreachable.", "danger") return redirect(url_for("boards.nfc_management", board_id=board_id)) @@ -351,7 +363,7 @@ def nfc_config_save(board_id: int): @boards_bp.route("//nfc/enroll", methods=["POST"]) @login_required def nfc_enroll(board_id: int): - """Read the last-seen UID from the board and immediately authorize it.""" + """Read the last-seen UID from the board and add it to the authorized list.""" if not current_user.is_admin(): abort(403) board = db.get_or_404(Board, board_id) @@ -375,14 +387,62 @@ def nfc_enroll(board_id: int): relay_num = int(request.form.get("relay_num", status.get("relay_num", 1))) pulse_ms = int(request.form.get("pulse_ms", status.get("pulse_ms", 0))) - ok = driver.set_nfc_config(board, auth_uid=uid, relay_num=relay_num, pulse_ms=pulse_ms) + # Fetch current list and append + current_cfg = driver.get_nfc_config(board) or {} + current_uids: list[str] = current_cfg.get("auth_uids") or ( + [current_cfg["auth_uid"]] if current_cfg.get("auth_uid") else [] + ) + if uid in current_uids: + flash(f"Card already enrolled — UID: {uid}", "info") + return redirect(url_for("boards.nfc_management", board_id=board_id)) + if len(current_uids) >= 10: + flash("Maximum 10 authorized cards reached. Remove one before enrolling.", "danger") + return redirect(url_for("boards.nfc_management", board_id=board_id)) + current_uids.append(uid) + + ok = driver.set_nfc_config(board, auth_uids=current_uids, relay_num=relay_num, pulse_ms=pulse_ms) if ok: - flash(f"Card enrolled — UID: {uid}, relay: {relay_num}, timeout: {pulse_ms} ms", "success") + flash(f"Card enrolled — UID: {uid} ({len(current_uids)} card(s) total), relay: {relay_num}, timeout: {pulse_ms} ms", "success") else: flash("Card read OK but failed to push config to board.", "danger") return redirect(url_for("boards.nfc_management", board_id=board_id)) +@boards_bp.route("//nfc/remove_card", methods=["POST"]) +@login_required +def nfc_remove_card(board_id: int): + """Remove one UID from the authorized list.""" + if not current_user.is_admin(): + abort(403) + board = db.get_or_404(Board, board_id) + if board.board_type not in _NFC_DRIVER_IDS: + abort(404) + driver = registry.get(board.board_type) + if not driver: + flash("NFC driver not available.", "danger") + return redirect(url_for("boards.nfc_management", board_id=board_id)) + + uid_to_remove = request.form.get("uid", "").strip().upper() + if not uid_to_remove: + flash("No UID specified.", "danger") + return redirect(url_for("boards.nfc_management", board_id=board_id)) + + current_cfg = driver.get_nfc_config(board) or {} + current_uids: list[str] = current_cfg.get("auth_uids") or ( + [current_cfg["auth_uid"]] if current_cfg.get("auth_uid") else [] + ) + relay_num = int(current_cfg.get("relay_num", 1)) + pulse_ms = int(current_cfg.get("pulse_ms", 0)) + + new_uids = [u for u in current_uids if u != uid_to_remove] + ok = driver.set_nfc_config(board, auth_uids=new_uids, relay_num=relay_num, pulse_ms=pulse_ms) + if ok: + flash(f"Card removed — UID: {uid_to_remove} ({len(new_uids)} card(s) remaining)", "success") + else: + flash("Failed to push updated config — board unreachable.", "danger") + return redirect(url_for("boards.nfc_management", board_id=board_id)) + + @boards_bp.route("//nfc/enable", methods=["POST"]) @login_required def nfc_enable(board_id: int): diff --git a/app/templates/boards/nfc.html b/app/templates/boards/nfc.html index 33f1e38..ed86373 100644 --- a/app/templates/boards/nfc.html +++ b/app/templates/boards/nfc.html @@ -75,14 +75,30 @@ - +
-
Authorized card UID
- {% if nfc.get('auth_uid') %} - {{ nfc.get('auth_uid') }} - {% else %} - None — no card enrolled yet - {% endif %} +
Authorized cards + {{ (nfc.get('auth_uids') or ([nfc['auth_uid']] if nfc.get('auth_uid') else [])) | length }}/10 +
+
+ {% set uids = nfc.get('auth_uids') or ([nfc['auth_uid']] if nfc.get('auth_uid') else []) %} + {% if uids %} + {% for uid in uids %} + + {{ uid }} +
+ + +
+
+ {% endfor %} + {% else %} + None — no card enrolled yet + {% endif %} +
@@ -187,19 +203,26 @@
- Manual Settings + Settings & Add Card
- - + +
+ + +
@@ -210,21 +233,15 @@
- + -
Relay turns off this many ms after the card is removed (0 = immediately).
+
Relay turns off after card absent this long (0 = use board default 5 s).
-
+
- {% if nfc.get('auth_uid') %} - - {% endif %}
@@ -272,15 +289,18 @@ function loadStatus() { if (useBtn) useBtn.disabled = !d.last_uid; const enrollBtn = document.getElementById('enroll-btn'); if (enrollBtn) enrollBtn.disabled = !d.last_uid; - // authorized UID display - const authEl = document.getElementById('auth-uid-display'); - if (authEl) { - if (d.auth_uid) { - authEl.className = 'fs-6 fw-bold text-success'; - authEl.textContent = d.auth_uid; + // authorized cards list + const authList = document.getElementById('auth-uid-list'); + if (authList) { + const uids = d.auth_uids || (d.auth_uid ? [d.auth_uid] : []); + const countBadge = document.getElementById('card-count-badge'); + if (countBadge) countBadge.textContent = uids.length + '/10'; + if (uids.length === 0) { + authList.innerHTML = 'None — no card enrolled yet'; } else { - authEl.className = 'text-danger small'; - authEl.innerHTML = 'None — no card enrolled yet'; + authList.innerHTML = uids.map(uid => + `${uid}` + ).join(''); } } // relay & timeout summary @@ -307,8 +327,9 @@ function useLastUID() { if (uid && uid !== '—') { const manualUid = document.getElementById('manual-uid'); if (manualUid) { - manualUid.value = uid; + manualUid.value = uid.toUpperCase(); manualUid.scrollIntoView({ behavior: 'smooth', block: 'center' }); + manualUid.focus(); } } }