A Hikvision DS-2CD1047G3H-LIU returned HTTP 503 (statusCode 2 / deviceBusy)
on EVERY main-stream snapshot — its main encoder is persistently saturated.
Probed on hardware: channels/101/picture → 503 on 5 consecutive tries, while
channels/102/picture (sub stream) → 200 clean JPEG every time. A retry loop
can't fix a persistent busy; the real fix is stream selection.
- Add a `stream` config field to the Hikvision driver (1=main, default for
back-compat; 2=sub). ISAPI channel id is <channel><stream> (101 main, 102 sub).
Verified live: setting the G3H to Sub flips its status degraded→ready (14.7KB
JPEG in ~87ms).
- captureSnapshot also retries the TRANSIENT case (503/500, linear backoff
250/500/750ms ×4) then fails naming it "(device busy)"; does NOT retry 401/404
(config errors won't self-heal). Complements captureSnapshotShared (concurrent
de-dup). healthCheck still reports a live 503 as degraded (surfaces a saturated
main stream rather than hiding it).
Tests: camera.test.ts (10) — retry behaviour + main/sub path selection.
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
Diagnosed why no camera push ever landed: (1) the route only registered
POST/GET, so a probe with another method got a generic 404 the camera
reads as "service available" while our handler never ran; (2) more
fundamentally, WSL mirrored mode REWRITES the inbound source IP to the
host's own address (10.0.10.203), so the camera's real IP (10.0.10.12)
never survives and the source-IP guard rejected every push as a mismatch.
- Register the event route on POST/GET/PUT/PATCH/DELETE/OPTIONS (HEAD comes
with GET) so ANYTHING hitting the path reaches the handler and is recorded.
- Log + store the HTTP method of each hit; log every hit on arrival, before
any guard, so even a rejected probe is visible immediately.
- Add per-device skipSourceIpCheck (a Setup checkbox) to bypass the
source-IP guard where the network rewrites the source (WSL). Digest auth +
the signed ledger remain the real guards.
Tests: hik-alarm 10 (skip-IP accept + method capture). server green;
web build green (new checkbox renderer + this field).
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
Newer Hik firmware can PUSH events to us: Event -> Smart/VCA with
"Detection Target: Human/Vehicle" + Notify Surveillance Center + Alarm
Settings -> Alarm Server makes the camera HTTP-POST an
EventNotificationAlert on each detection.
- New POST /api/devices/hikvision/:deviceId/event (routes/hikvision-alarm.ts):
same machine-push pattern as the Dingtian Input Link — source-IP guarded
+ optional HTTP Digest, not behind the SPA cookie/CSRF.
- Discovery-first / permissive: a wildcard content-type parser accepts ANY
body as raw bytes (event XML, multipart+JPEG, or JSON — Hik varies by
firmware), records it verbatim as a kind:"alarm" device_event, and
best-effort extracts eventType/target/plate/dateTime/channelID for the
summary + a loud log line. The point is to SEE exactly what a camera
sends before wiring it further.
- hikvision driver gains alarmPushEnabled + pushUser/pushPassword config and
pushesToBackend:true (setup offers the backend push IP).
- NOT yet a barrier trigger / DeviceReadEvent — records only. A plate read
is advisory, never the sole reason a barrier opens; the read-bus/ANPR
wiring is a deliberate next step once the real payload is known.
Tests: hikvision-alarm.test.ts (6: vehicle XML summary, ANPR plate, raw
JSON, wrong-IP 404, disabled 404, unknown-device 404). server 109/109;
build+lint 14/14. Wiki: lpr-camera.md + log.
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
A parking lot is one pool of spaces with a flexible set of entry/exit
points — no "lane". Direction is a property of each RELAY inside an access
controller; readers/cameras bind to a controller relay and inherit it.
Schema:
- drop `lane` from ledger_events, device_events, sessions
- rename lane_devices -> devices (no lane/direction columns)
- access config.relays=[{relay,direction,button?}]; reader/camera
config.controllerId+relay binding
- fresh 0000_baseline migration (history reset; dev data was throwaway)
Signed ledger:
- remove `lane` from canonicalize(); bump signer keyId sw-hmac-v1 -> v2
(v1 events won't verify under v2 — intentional, gated per-event by keyId)
Server:
- new device-resolve.ts (replaces lane-map.ts): relayForButton,
relayForDevice, firstRelayByDirection, devicesByDirection
- entry-flow: button terminal -> its relay; exit/permit: reader's bound
relay; dispatcher resolves the bound relay + inherited direction
- camera snapshots fire by direction site-wide, async, never block open
- DeviceConfig widened to nested JSON for relays[]
Web:
- wizard: no lane selector; add controllers (relay map + entry-button
terminal) first, then bind readers/cameras/printers to a controller relay
Wiki: new entry-exit-points.md (replaces lane-direction); reworked
entry-exit-readers, parking-session, first-run-setup, device-registry,
append-only-event-chain, device-events; removed stale lane/LaneMap mentions.
Replace the camera stub with HttpCamera: Hikvision ISAPI and Dahua CGI
snapshots over client-side HTTP Digest (new drivers/http-digest.ts).
healthCheck() now pulls a real frame instead of returning ready/stub.
Snapshot carries bytes (driver fetches); storage/imageRef is the caller's
job, keeping the adapter free of storage deps.
Fix the cosmetic Backend-push-IP field: add pushesToBackend to DeviceDriver
(only Dingtian sets it), expose as pushCapable in the catalog, and gate the
wizard's backend-IP fetch + field on it so pull-only devices hide it.
Verified on hardware (Hikvision 10.0.10.121): healthCheck ready,
captureSnapshot returns a valid JPEG.
Make the device-adapter pattern selectable so the admin chooses hardware at
install — per lane, from a catalog of supported drivers. Adding a device =
registering one more driver; no business-logic change.
packages/devices:
- interfaces.ts: AccessControlDevice / ReaderDevice / CameraDevice / PrinterDevice
(adds CameraDevice for entry/exit snapshot-on-event; access relay stays
intent-only per "a barrier is not a door").
- registry.ts: driver catalog with per-driver config fields + factory, config
validation, and a catalog payload for the setup UI.
- drivers/: stub adapters — access (zkteco, esp32-relay), reader (wiegand,
tcp-ip), camera (hikvision, dahua). Real vendor protocols TBD.
packages/db:
- lane_devices + setup_state tables (migration 0001); re-export query helpers.
apps/server:
- routes/setup.ts: GET /api/setup/catalog (public schema), and admin-only
/assign, /state, /complete with registry validation before persisting.
- extract auth.ts (requireJwtSecret, requireRole, JWT type aug).
apps/web:
- SetupWizard scaffold + api client: pick a driver per category for a lane,
render its config fields.
wiki: device-registry + first-run-setup concept pages; cross-link from
device-adapter-pattern; index + log updated.
Verified: full turbo build (5/5); catalog lists all drivers; admin assign
persists; missing-config and no-token requests are rejected.