Files
sasa-membership/ESP_RFID_API.md
T

464 lines
10 KiB
Markdown

# ESP RFID Integration Contract
The ESP32 reader is expected to host its own local setup/dashboard UI. The reader registers itself with the SASA server, waits for admin approval, receives an API key once, then uses that key for heartbeat, time sync, taps, dashboard login checks, and queued card-write jobs.
All timestamps are UTC ISO-8601 unless otherwise noted.
## 1. Reader Setup and Approval
### Register from the ESP dashboard
Unauthenticated endpoint. Use this when the reader first boots or when it is reset/reconfigured.
```http
POST /api/v1/esp/device/register
Content-Type: application/json
```
Request:
```json
{
"device_id": "front-desk-01",
"name": "Front Desk Reader",
"location": "Reception",
"reader_type": "checkin_checkout",
"can_write_cards": true,
"firmware_version": "esp32-rfid-0.1.0",
"notes": "PN532 over I2C"
}
```
Response:
```json
{
"device_id": "front-desk-01",
"provisioning_status": "pending",
"registration_token": "one-time-registration-token",
"message": "Registration received. Approve this reader in the admin panel.",
"poll_interval_seconds": 5
}
```
Firmware must store:
- `device_id`
- `registration_token`
### Admin approval
In the web portal:
- Dashboard -> Admin -> ESP RFID -> Setup
- Approve or reject pending readers.
When approved, the server does not show the key in the admin panel. The reader gets it through the provisioning poll below.
### Poll provisioning status
Use this while waiting for approval.
```http
GET /api/v1/esp/device/provisioning-status
X-ESP-Device-ID: front-desk-01
X-ESP-Registration-Token: one-time-registration-token
```
Pending response:
```json
{
"device_id": "front-desk-01",
"provisioning_status": "pending",
"message": "Waiting for admin approval.",
"api_key": null,
"poll_interval_seconds": 5
}
```
Approved response:
```json
{
"device_id": "front-desk-01",
"provisioning_status": "provisioned",
"message": "Reader approved. Store this API key; it will not be returned again.",
"api_key": "generated-api-key",
"poll_interval_seconds": 5
}
```
Firmware must store `api_key` permanently. The server only returns it once. If it is lost, either rotate the key in admin and manually enter it on the ESP, or re-register the reader.
Rejected response:
```json
{
"device_id": "front-desk-01",
"provisioning_status": "rejected",
"message": "Reader registration rejected.",
"api_key": null,
"poll_interval_seconds": 5
}
```
## 2. Authenticated Reader Requests
After provisioning, every reader endpoint uses:
```http
X-ESP-Device-ID: front-desk-01
X-ESP-API-Key: generated-api-key
Content-Type: application/json
```
Invalid, inactive, unapproved, or rejected readers receive `401`.
## 3. Time Sync
The backend returns the server clock in UTC/Zulu. Firmware should treat this as the source of truth and display it locally as needed.
## 3. Local ESP Dashboard
The firmware in `ESP/main.cpp` serves the local dashboard on port `80`.
Access options:
- If WiFi connects, open `http://<reader-lan-ip>/`.
- If WiFi does not connect, the reader starts a setup AP named `RFID-Setup-XXXXXX`; connect to it and open `http://192.168.4.1/`.
Default local dashboard credentials are configured in `ESP/secrets.h`:
- Username: `admin`
- Password: `admin`
The dashboard shows:
- WiFi status and RSSI
- Provisioning status
- Current operating mode
- Server-synced clock
- Last heartbeat result
- Current write-job state
- Last card UID/action/result
The local UI also exposes controls for:
- Saving server/WiFi/reader configuration
- Registering or re-registering the reader
- Manually syncing time
- Manually polling for queued write jobs
- Clearing provisioning
- Rebooting
Local dashboard JSON endpoints:
- `GET /api/status`
- `POST /api/config`
- `POST /api/register`
- `POST /api/sync-time`
- `POST /api/poll-job`
- `POST /api/clear-provisioning`
- `POST /api/cancel-local-job`
- `POST /api/reboot`
These are local ESP endpoints, not SASA backend endpoints.
## 4. Heartbeat
The reader sends a heartbeat after provisioning so the server knows it is alive and so the ESP can show heartbeat status on its dashboard.
```http
POST /api/v1/esp/device/heartbeat
X-ESP-Device-ID: front-desk-01
X-ESP-API-Key: generated-api-key
Content-Type: application/json
```
Request:
```json
{
"mode": "idle",
"message": "Heartbeat OK",
"wifi_rssi": -54,
"free_heap": 184320,
"firmware_version": "esp32-rfid-0.2.0",
"active_write_job_id": null
}
```
Response:
```json
{
"ok": true,
"server_time_utc": "2026-05-05T10:15:30.123456",
"unix_ms": 1777976130123,
"heartbeat_interval_seconds": 10,
"time_poll_interval_seconds": 3,
"write_job_poll_interval_seconds": 3
}
```
Firmware modes currently used:
- `setup`
- `waiting approval`
- `idle`
- `syncing time`
- `heartbeat`
- `checking jobs`
- `ready to write`
- `writing card`
- `reporting tap`
- `error`
The backend updates `last_seen_at` when heartbeat succeeds.
## 5. Time Sync
The reader should poll every 3 seconds.
```http
GET /api/v1/esp/device/time
```
Response:
```json
{
"server_time_utc": "2026-05-05T10:15:30.123456",
"unix_ms": 1777976130123,
"poll_interval_seconds": 3
}
```
Use `unix_ms` to correct the ESP local clock. Do not convert the value to a local timezone before applying it on the device.
## 6. Check-In and Check-Out Taps
```http
POST /api/v1/esp/device/taps
```
Request:
```json
{
"card_uid": "04A1B2C3D4",
"tapped_at": "2026-05-05T10:15:30.123456",
"reader_type": "checkin_checkout"
}
```
`tapped_at` is optional. If omitted, server time is used. If provided, it is normalized to UTC before persistence.
Accepted check-in:
```json
{
"accepted": true,
"action": "check_in",
"message": "Checked in",
"server_time_utc": "2026-05-05T10:15:30.123456",
"tap_id": 123,
"session_id": 45,
"user_id": 7,
"user_name": "Jane Smith",
"checked_in_at": "2026-05-05T10:15:30.123456",
"checked_out_at": null,
"duration_seconds": null
}
```
Accepted check-out:
```json
{
"accepted": true,
"action": "check_out",
"message": "Checked out",
"server_time_utc": "2026-05-05T12:20:30.123456",
"tap_id": 124,
"session_id": 45,
"user_id": 7,
"user_name": "Jane Smith",
"checked_in_at": "2026-05-05T10:15:30.123456",
"checked_out_at": "2026-05-05T12:20:30.123456",
"duration_seconds": 7500
}
```
Denied:
```json
{
"accepted": false,
"action": "denied",
"message": "Unknown RFID card",
"server_time_utc": "2026-05-05T10:15:30.123456",
"tap_id": 125,
"session_id": null,
"user_id": null,
"user_name": null
}
```
The server logs denied taps so unknown cards can still be audited.
The stored tap, check-in, check-out, approval, heartbeat, and write-job timestamps are all UTC. The frontend renders those values in Europe/London, but the backend and ESP contract stay on UTC/Zulu.
## 7. Queued RFID Card Writes
Admins queue card writes in:
- Dashboard -> Admin -> ESP RFID -> Setup -> Queue Card Write
They select:
- User
- Writing reader
- Card label
The reader does not need an inbound network port. It polls for jobs.
### Poll next write job
Only readers with `can_write_cards=true` may use this endpoint.
```http
GET /api/v1/esp/device/write-jobs/next
```
No job response:
```json
null
```
Job response:
```json
{
"id": 20,
"reader_id": 3,
"user_id": 7,
"card_id": null,
"label": "Jane main card",
"status": "claimed",
"requested_by_user_id": 1,
"card_uid": null,
"write_payload": "{\"job_id\":20,\"user_id\":7,\"user_name\":\"Jane Smith\",\"label\":\"Jane main card\",\"issued_at\":\"2026-05-05T10:15:30.123456\"}",
"claimed_at": "2026-05-05T10:15:30.123456",
"completed_at": null,
"error_message": null,
"created_at": "2026-05-05T10:14:00.000000",
"updated_at": "2026-05-05T10:15:30.123456"
}
```
Firmware behavior:
1. Poll for a job.
2. If a job is returned, enter `ready to write` mode and show the target user/label on the ESP dashboard.
3. On the next card tap, write `write_payload` to the card if supported by the chosen RFID card type.
4. Report success/failure.
The included ESP firmware writes MIFARE Classic 1K/4K cards using the default key `FF FF FF FF FF FF`, starting at block 4 and skipping trailer blocks. Unsupported cards are reported as failed write jobs.
### Complete write job
```http
POST /api/v1/esp/device/write-jobs/20/complete
```
Success request:
```json
{
"success": true,
"card_uid": "04A1B2C3D4"
}
```
Failure request:
```json
{
"success": false,
"error_message": "Card write failed verification"
}
```
On success, the server creates or updates the RFID card record and assigns it to the selected user with the queued label.
## 8. ESP Local Dashboard Login
Use this when someone logs into the ESP-hosted dashboard.
```http
POST /api/v1/esp/device/dashboard-login
```
Request:
```json
{
"email": "admin@example.com",
"password": "password"
}
```
Response:
```json
{
"valid": true,
"user_id": 1,
"role": "admin",
"user_name": "Admin User"
}
```
Only `admin` and `super_admin` users return `valid: true`.
## 9. Admin API Summary
These endpoints use the normal portal JWT.
- `GET /api/v1/esp/admin/readers`
- `POST /api/v1/esp/admin/readers` manual reader/key creation fallback
- `PUT /api/v1/esp/admin/readers/{reader_id}`
- `POST /api/v1/esp/admin/readers/{reader_id}/approve`
- `POST /api/v1/esp/admin/readers/{reader_id}/reject`
- `DELETE /api/v1/esp/admin/readers/{reader_id}`
- `POST /api/v1/esp/device/heartbeat`
- `GET /api/v1/esp/admin/cards`
- `POST /api/v1/esp/admin/cards`
- `PUT /api/v1/esp/admin/cards/{card_id}`
- `GET /api/v1/esp/admin/write-jobs?limit=100`
- `POST /api/v1/esp/admin/write-jobs`
- `POST /api/v1/esp/admin/write-jobs/{job_id}/cancel`
- `GET /api/v1/esp/admin/taps?limit=100`
- `GET /api/v1/esp/admin/attendance?open_only=false&limit=100`
- `POST /api/v1/esp/admin/attendance/close-stale`
Stale close request:
```json
{
"checkout_hour": 17
}
```
The backend also runs stale closing automatically on startup and hourly. It closes sessions checked in before today's midnight, sets checkout to 17:00 on the check-in date, and marks `checkout_source` as `system`.
## 10. Time Contract Summary
- API responses use UTC/Zulu datetimes.
- ESP readers sync against `/api/v1/esp/device/time`.
- Reader-supplied timestamps are normalized to UTC before storage.
- The browser frontend displays these values in Europe/London for operators and members.