feat(devices): camera clock sync via ISAPI — heal the 1970 power-cut reset
Build desktop / desktop (push) Successful in 4m18s
CI / check (push) Successful in 44s
Build & push images / images (push) Successful in 2m51s

park-buzi field observation: after a power cut the Hikvision cameras
reboot at the 1970 epoch (no/dead RTC battery, no NTP) and stay there
until a human logs into the web UI (which silently pushes the browser
clock) — corrupting the snapshot OSD timestamps (the evidence trail) and
ANPR push times meanwhile.

The host is the site's time authority (offline-first, no NTP infra):

- Device monitor triggers a sync at each camera's offline→ready edge —
  exactly the power-restored moment — plus a 24h backstop; the attempt
  is stamped before the async call so a failing camera retries at
  backstop cadence, never every poll.
- HikvisionCamera.syncClock: GET /ISAPI/System/time; drift ≤60s → leave
  alone; beyond (or unparseable = infinite drift) → PUT timeMode=manual
  with the site wall-clock now WITH explicit utc offset
  (localIsoWithOffset), echoing the camera's timeZone verbatim — correct
  the clock, never fight its tz/DST config.
- Jumps >1h (the power-cut signature) log warn (persisted to app_logs);
  small corrections info. Capability-guarded (isClockSyncable) —
  hikvision only; dahua's CGI has no such endpoint.
- http-digest generalised to digestRequest (GET/PUT/POST + body); the
  handshake was already method-aware. digestGet delegates unchanged.

8 new tests: in-sync no-op, 1970 PUT shape (manual + host instant +
echoed tz), unparseable→sync, failed-set surfaces, dahua non-capability,
DST-both-sides pins on the offset formatter.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
2026-07-07 12:56:51 +02:00
parent 7f42805e8d
commit 6ceaadfbf2
8 changed files with 324 additions and 16 deletions
+29 -1
View File
@@ -2,7 +2,7 @@
type: entity
tags: [parking, hardware, readers, offline-first]
sources: [parking-system-architecture]
updated: 2026-06-27
updated: 2026-07-07
---
# LPR Camera
@@ -110,6 +110,34 @@ Covered by `packages/devices/src/drivers/camera.test.ts` (retry behaviour + the
selection). `healthCheck()` deliberately reports a live 503 as `degraded` (it surfaces a genuinely
saturated main stream rather than hiding it behind a retry).
## Clock sync — the 1970 power-cut reset (built 2026-07-07)
Field observation (park-buzi): after a power cut these cameras come back with their clock at the
**1970 epoch** (no/dead RTC battery, no NTP) and stay there until a human logs into the web UI —
Hikvision's web login silently pushes the browser clock. A wrong camera clock corrupts the OSD
timestamp burned into every snapshot (the evidence trail) and the times on ANPR pushes.
Built: the HOST is the site's time authority (offline-first — no NTP infra dependency), and the
device monitor re-syncs each Hikvision camera over the same Digest-auth ISAPI used for snapshots:
- **Trigger:** the camera's **offline → ready transition** (exactly the power-restored moment) +
a **24h backstop**; the attempt timestamp is stamped BEFORE the async call, so a failing camera
retries at backstop cadence, never every 8s poll.
- **Mechanics** (`HikvisionCamera.syncClock`): `GET /ISAPI/System/time`, parse `localTime`; drift ≤
60s → leave alone. Beyond that → `PUT /ISAPI/System/time` with `timeMode=manual`, the site's
wall-clock now WITH explicit utc offset (`localIsoWithOffset(siteTz)`, e.g.
`2026-07-07T15:30:22+02:00` — the offset makes the instant unambiguous), and the camera's own
`timeZone` string **echoed back verbatim** (we correct the clock, never fight its tz/DST config).
An unparseable camera reply counts as infinite drift → sync.
- **Visibility:** a sync after a big jump (>1h — the power-cut signature) logs at **warn**
(persisted to app_logs); small corrections log info. Failures log warn.
- **Scope:** Hikvision only (`isClockSyncable` capability guard); the Dahua driver's CGI has no
ISAPI time endpoint — a Dahua clock sync would be its own driver work.
- Rejected alternative: camera-side **NTP against the booth** (chrony on the appliance). More
standard, but adds a provisioning dependency per booth and the camera polls NTP on ITS schedule
— a freshly power-cycled camera could still sit at 1970 for a while, which is precisely the
moment that matters.
## Camera PUSH — "Alarm Server" event notifications (2026-06-22)
Separate from the **pull** snapshot path above: newer Hikvision firmware can **push** an event to
+11
View File
@@ -2505,3 +2505,14 @@ GET /api/setup/usb-printers enumerates /dev/usb/lpN + sysfs ieee1284_id make/mod
devicePath is now a select of PRESENT printers (fresh form preselects the first; a saved-but-
absent path stays selectable, flagged; none found → free-text + hint). Transport option label no
longer hardcodes lp0. Details on [[printer-usb-transport]].
## [2026-07-07] update | Camera clock sync via ISAPI — the 1970 power-cut reset healed
park-buzi observation: power-cut Hik cameras reboot at the 1970 epoch (no RTC battery, no NTP)
until a web-UI login pushes the browser clock — corrupting snapshot OSD timestamps (evidence) and
ANPR push times meanwhile. Built host-as-time-authority sync (details on [[lpr-camera]] §Clock
sync): device monitor triggers at the offline→ready edge + 24h backstop; HikvisionCamera.syncClock
GETs /ISAPI/System/time, and beyond 60s drift PUTs manual time with the site's wall-clock + explicit
offset, echoing the camera's timeZone verbatim; >1h jumps log warn (persisted). digest client
generalised GET→GET/PUT/POST with body (the handshake was already method-aware). Capability-guarded
(isClockSyncable — hikvision only). 8 new tests (5 devices, 3 tz-offset).