Add Olimex ESP32-C6-EVB + PN532 NFC driver and web UI

- New driver: app/drivers/olimex_esp32_c6_evb_pn532/
  - Full relay/input control (inherits base behaviour)
  - get_nfc_status(), get_nfc_config(), set_nfc_config() methods
  - manifest.json with NFC hardware metadata (UEXT1 pins, card standard)

- NFC management routes (boards.py):
  - GET  /boards/<id>/nfc            — management page
  - GET  /boards/<id>/nfc/status_json — live JSON (polled every 3 s)
  - POST /boards/<id>/nfc/config     — save auth UID / relay / timeout
  - POST /boards/<id>/nfc/enroll     — enrol last-seen card with one click

- New template: templates/boards/nfc.html
  - Live reader status (PN532 ready, access state, last UID)
  - Quick Enroll: present card → Refresh → Enrol in one click
  - Manual Settings: type/paste UID, pick relay, set absence timeout

- detail.html: NFC Access Control button shown for pn532 board type
This commit is contained in:
2026-03-15 09:41:01 +02:00
parent 4766faa682
commit 1152f93a00
6 changed files with 594 additions and 0 deletions

View File

@@ -9,6 +9,8 @@ from app.services.board_service import poll_board, register_webhook, set_relay
from app.drivers.registry import registry
from flask import current_app
_NFC_DRIVER_ID = "olimex_esp32_c6_evb_pn532"
boards_bp = Blueprint("boards", __name__)
@@ -276,3 +278,101 @@ def edit_entities(board_id: int):
@login_required
def edit_labels(board_id: int):
return redirect(url_for("boards.edit_entities", board_id=board_id))
# ── NFC management (PN532 boards only) ────────────────────────────────────────
@boards_bp.route("/<int:board_id>/nfc")
@login_required
def nfc_management(board_id: int):
board = db.get_or_404(Board, board_id)
if board.board_type != _NFC_DRIVER_ID:
abort(404)
driver = registry.get(_NFC_DRIVER_ID)
nfc_status = driver.get_nfc_status(board) if driver else None
return render_template(
"boards/nfc.html",
board=board,
nfc=nfc_status or {},
)
@boards_bp.route("/<int:board_id>/nfc/status_json")
@login_required
def nfc_status_json(board_id: int):
board = db.get_or_404(Board, board_id)
if board.board_type != _NFC_DRIVER_ID:
abort(404)
driver = registry.get(_NFC_DRIVER_ID)
data = driver.get_nfc_status(board) if driver else None
if data is None:
return jsonify({"error": "Board unreachable"}), 502
return jsonify(data)
@boards_bp.route("/<int:board_id>/nfc/config", methods=["POST"])
@login_required
def nfc_config_save(board_id: int):
if not current_user.is_admin():
abort(403)
board = db.get_or_404(Board, board_id)
if board.board_type != _NFC_DRIVER_ID:
abort(404)
driver = registry.get(_NFC_DRIVER_ID)
if not driver:
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", 3000))
if relay_num < 1 or relay_num > 4:
flash("Relay number must be 14.", "danger")
return redirect(url_for("boards.nfc_management", board_id=board_id))
if pulse_ms < 100 or pulse_ms > 60000:
flash("Absence timeout must be between 100 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)
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")
else:
flash("Failed to push NFC config — board unreachable.", "danger")
return redirect(url_for("boards.nfc_management", board_id=board_id))
@boards_bp.route("/<int:board_id>/nfc/enroll", methods=["POST"])
@login_required
def nfc_enroll(board_id: int):
"""Read the last-seen UID from the board and immediately authorize it."""
if not current_user.is_admin():
abort(403)
board = db.get_or_404(Board, board_id)
if board.board_type != _NFC_DRIVER_ID:
abort(404)
driver = registry.get(_NFC_DRIVER_ID)
if not driver:
flash("NFC driver not available.", "danger")
return redirect(url_for("boards.nfc_management", board_id=board_id))
status = driver.get_nfc_status(board)
if not status:
flash("Board unreachable — could not read card UID.", "danger")
return redirect(url_for("boards.nfc_management", board_id=board_id))
uid = (status.get("last_uid") or "").strip().upper()
if not uid:
flash("No card has been presented to the reader yet.", "warning")
return redirect(url_for("boards.nfc_management", board_id=board_id))
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", 3000)))
ok = driver.set_nfc_config(board, auth_uid=uid, relay_num=relay_num, pulse_ms=pulse_ms)
if ok:
flash(f"Card enrolled — UID: {uid}, 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))