151 lines
5.9 KiB
Markdown
151 lines
5.9 KiB
Markdown
# Agent contract — `UpsertFacePhoto`
|
||
|
||
**Status: implemented on both sides.** The agent side lives in `Access-device-agent`
|
||
(`Devices/HikvisionIsapiClient.SetUserFaceAsync` and the `UpsertFacePhoto` case in
|
||
`Services/CommandWorker.DispatchAsync`). This document remains the reference for the wire
|
||
format and the device call.
|
||
|
||
Not yet verified against a physical terminal — see "Untested" at the end.
|
||
|
||
## Flow
|
||
|
||
```
|
||
Web ──POST client/customers/{id}/face-photo──▶ Server
|
||
│ validates JPEG, queues DeviceCommand
|
||
▼
|
||
Agent ──GET api/agent/commands ─────────────────▶ receives UpsertFacePhoto
|
||
──ISAPI POST────────────────────▶ Hikvision terminal (gym LAN)
|
||
──POST api/agent/commands/{id}/result ────▶ Server
|
||
│ purges payload, updates profile
|
||
▼
|
||
SignalR "FacePhotoUpdated" ──▶ browser
|
||
```
|
||
|
||
## Command the agent receives
|
||
|
||
`GET api/agent/commands` returns the usual envelope; `CommandType` is `UpsertFacePhoto`.
|
||
|
||
`PayloadJson`:
|
||
|
||
```json
|
||
{
|
||
"bindingId": "8f1c...", // guid, AccessDeviceBinding
|
||
"accessProfileId": 6, // long
|
||
"employeeNo": "HF6", // the person id already used by UpsertPerson
|
||
"name": "Prabodha",
|
||
"faceJpegBase64": "/9j/4AAQ...", // bare base64, no data: prefix
|
||
"desiredVersion": 4
|
||
}
|
||
```
|
||
|
||
The image is already normalised by the browser and re-checked server-side:
|
||
|
||
| Property | Guaranteed |
|
||
|---|---|
|
||
| Format | JPEG (magic bytes verified) |
|
||
| Dimensions | 480×640 from the web client; ≤ 1920×1920 enforced |
|
||
| Size | ≤ 200 KB enforced, typically 60–150 KB |
|
||
|
||
So the agent does **not** need to resize or convert. Decode the base64 and POST it.
|
||
|
||
**Ordering:** the person record must exist on the terminal before a face can attach to it.
|
||
`employeeNo` is the same value `UpsertPerson` uses. If a face command arrives for a person
|
||
the terminal doesn't have, run the person upsert first rather than failing.
|
||
|
||
## ISAPI call
|
||
|
||
Face data is attached to an existing person in a face library. Typical shape for the
|
||
DS-K1T series (confirm against your firmware — this varies more than Hikvision's docs
|
||
suggest):
|
||
|
||
```
|
||
POST /ISAPI/Intelligent/FDLib/FDSetUp?format=json
|
||
Content-Type: multipart/form-data; boundary=---boundary
|
||
Authorization: Digest ...
|
||
|
||
---boundary
|
||
Content-Disposition: form-data; name="FaceDataRecord"
|
||
Content-Type: application/json
|
||
|
||
{ "faceLibType": "blackFD", "FDID": "1", "FPID": "HF6", "name": "Prabodha" }
|
||
---boundary
|
||
Content-Disposition: form-data; name="FaceImage"; filename="face.jpg"
|
||
Content-Type: image/jpeg
|
||
|
||
<binary jpeg>
|
||
---boundary--
|
||
```
|
||
|
||
- `FPID` should be the `employeeNo`, so the face ties to the same person record.
|
||
- Use **Digest** auth, as with the other endpoints.
|
||
- Replacing a photo is the same call — the device overwrites the FPID's face.
|
||
|
||
## Result the agent posts back
|
||
|
||
`POST api/agent/commands/{id}/result`
|
||
|
||
**Success:**
|
||
```json
|
||
{ "success": true, "resultJson": "{\"fpid\":\"HF6\"}" }
|
||
```
|
||
|
||
The server reads `fpid` (also accepts `FPID`, `faceId`, `faceURL`) into
|
||
`AccessProfile.FacePhotoFPID` — the handle needed to replace or delete the face later.
|
||
Anything longer than 64 chars is truncated.
|
||
|
||
**Failure:**
|
||
```json
|
||
{ "success": false, "resultJson": "{\"detail\":\"No frontal face detected in the image\"}" }
|
||
```
|
||
|
||
Put a human-readable reason in `detail` — it is surfaced to gym staff verbatim, so it
|
||
should say what to do about it. Real Hikvision rejections worth mapping:
|
||
|
||
| Device response | Suggested `detail` |
|
||
|---|---|
|
||
| `lowFaceQuality` / no face found | `No clear frontal face was detected. Retake with the face centred and well lit.` |
|
||
| face already bound to another FPID | `This face is already enrolled for another member.` |
|
||
| library full | `The terminal's face library is full.` |
|
||
| image format rejected | `The terminal rejected the image format.` |
|
||
|
||
## Retry behaviour
|
||
|
||
`MaxAttempts` is **3** for face commands (lower than the 5 used for person/state commands):
|
||
a face rejected for quality will be rejected identically on every retry, so failing fast
|
||
gets the real message in front of staff sooner.
|
||
|
||
- Retries use the existing exponential backoff.
|
||
- On success **or** final failure the server clears `PayloadJson`, so the image is not
|
||
retained. A retry between those points still has the payload available.
|
||
- A newer photo supersedes an older queued one — the stale command is marked `Superseded`
|
||
and its payload cleared. The agent should treat `Superseded` as "stop, discard".
|
||
|
||
## Deleting a face
|
||
|
||
The **agent** side is implemented (`DeleteUserFaceAsync`, plus a `DeleteFacePhoto` dispatch
|
||
case), calling:
|
||
|
||
```
|
||
PUT /ISAPI/Intelligent/FDLib/FDSearch/Delete?format=json&FDID=1&faceLibType=blackFD
|
||
{ "FPID": [ { "value": "HF6" } ] }
|
||
```
|
||
|
||
The **server** does not queue this command yet — nothing calls it when a member is removed.
|
||
Adding that is a small change: queue `DeleteFacePhoto` with the stored `FacePhotoFPID`
|
||
wherever a member is deactivated, then clear the face columns on success.
|
||
|
||
## Untested
|
||
|
||
Everything here compiles and the wire format matches on both sides, but **no part of this
|
||
has run against a physical Hikvision terminal.** Expect to verify:
|
||
|
||
- `FDID = "1"` and `faceLibType = "blackFD"` are the right library on your model. Terminals
|
||
with multiple face libraries may need a different FDID.
|
||
- The multipart part names (`FaceDataRecord`, `FaceImage`) — these vary between firmware
|
||
generations more than Hikvision's documentation suggests.
|
||
- Whether your model accepts 480×640 at ~150 KB, or wants smaller.
|
||
|
||
The agent logs the full device response for both success and failure
|
||
(`SetUserFace {EmployeeNo} ... : {Response}`), which is the fastest way to see what the
|
||
terminal actually objects to on the first real run.
|