feat(devices): radar presence input + button-light output on the controller
Model the entry button (I1) and a Hikvision radar (I2) as named children of the access controller, and drive the button's 12V lamp on a spare relay. - Radar = the existing relays[].presenceInput one-car-one-ticket gate, now labelled presenceKind: loop|radar. A radar may idle opposite the button, so add a per-input active-level override: relays[].presenceActiveLow -> driver inputActiveLow set, inverting just that terminal (pure helper inputActive()). The Dingtian has one board-wide resting level otherwise. - AuxOutputDevice.setAux(channel,on) capability on the device interface (Dingtian latch) so business logic drives a NON-barrier lamp through the interface. Barriers still only pulseOpen — barrier-not-a-door preserved. - ButtonLightController: subscribes to the radar input edge + the camera lane status and drives a 3-state lamp — radar+car=solid, radar-only=blink (~1Hz), else off. Fails OFF on host loss/error; de-duped. A radar detection never opens a barrier on its own (advisory; threat model). - SetupWizard: presence kind + active-low + a button-light relay picker; sq+en i18n. Tests: button-light.test.ts (truth table + blink + fail-OFF + de-dupe), access-dingtian.test.ts (active-level inversion). Workspace build+lint+test green (158 server tests). Wiki: hikvision-radar, button-light-indicator + updates. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -33,12 +33,32 @@ export interface RelaySpec {
|
||||
* Both absent = no guard (legacy behaviour). See wiki/concepts/entry-double-press.md.
|
||||
*/
|
||||
readonly presenceInput?: number;
|
||||
/** What kind of sensor is on `presenceInput` — an induction LOOP or a RADAR. Label
|
||||
* only (the gate behaviour is identical); drives UI copy + telemetry. Default loop. */
|
||||
readonly presenceKind?: "loop" | "radar";
|
||||
/** The presence terminal's ACTIVE level is LOW (idles HIGH). Maps to the driver's
|
||||
* per-input `inputActiveLow` override so a radar wired opposite the button reads
|
||||
* right. See wiki/entities/hikvision-radar.md. */
|
||||
readonly presenceActiveLow?: boolean;
|
||||
readonly entryCooldownSec?: number;
|
||||
}
|
||||
|
||||
/** A non-barrier indicator lamp wired to a spare relay (e.g. the entry button's
|
||||
* 12 V light). Driven by the server LightController off the radar + lane status —
|
||||
* NOT a barrier. See wiki/concepts/button-light-indicator.md. */
|
||||
export interface ButtonLightSpec {
|
||||
/** 1-based spare relay channel the lamp is wired to. */
|
||||
readonly relay: number;
|
||||
/** Blink cadence (ms on / ms off) for the radar-only state. Default 500/500. */
|
||||
readonly blinkOnMs?: number;
|
||||
readonly blinkOffMs?: number;
|
||||
}
|
||||
|
||||
/** Access controller config (the `relays[]` map + connection fields). */
|
||||
interface AccessConfig {
|
||||
readonly relays?: RelaySpec[];
|
||||
/** Optional button-lamp output on a spare relay. */
|
||||
readonly buttonLight?: ButtonLightSpec;
|
||||
readonly [k: string]: unknown;
|
||||
}
|
||||
|
||||
@@ -60,9 +80,11 @@ export interface ResolvedRelay {
|
||||
readonly controller: DeviceRow;
|
||||
readonly relay: number;
|
||||
readonly direction: Direction;
|
||||
/** 1-based presence-loop input gating this relay's entry (when wired). */
|
||||
/** 1-based presence input gating this relay's entry (loop or radar, when wired). */
|
||||
readonly presenceInput?: number;
|
||||
/** Cooldown seconds suppressing repeat presses (fallback when no presence loop). */
|
||||
/** Sensor kind on the presence input (loop|radar) — telemetry/label only. */
|
||||
readonly presenceKind?: "loop" | "radar";
|
||||
/** Cooldown seconds suppressing repeat presses (fallback when no presence input). */
|
||||
readonly entryCooldownSec?: number;
|
||||
}
|
||||
|
||||
@@ -102,6 +124,7 @@ export function relayForButton(db: Db, controllerId: string, terminal: number):
|
||||
relay: spec.relay,
|
||||
direction: spec.direction,
|
||||
presenceInput: spec.presenceInput,
|
||||
presenceKind: spec.presenceKind ?? "loop",
|
||||
entryCooldownSec: spec.entryCooldownSec,
|
||||
};
|
||||
}
|
||||
@@ -122,7 +145,20 @@ export function relayForPresence(db: Db, controllerId: string, terminal: number)
|
||||
const spec = relaysOf(row).find((r) => r.presenceInput === terminal);
|
||||
if (!spec) return null;
|
||||
if (spec.direction !== "entry" && spec.direction !== "both") return null;
|
||||
return { controller: row, relay: spec.relay, direction: spec.direction };
|
||||
return {
|
||||
controller: row,
|
||||
relay: spec.relay,
|
||||
direction: spec.direction,
|
||||
presenceInput: spec.presenceInput,
|
||||
presenceKind: spec.presenceKind ?? "loop",
|
||||
};
|
||||
}
|
||||
|
||||
/** The button-lamp output declared on an access controller, or null. */
|
||||
export function buttonLightOf(row: DeviceRow): ButtonLightSpec | null {
|
||||
const cfg = row.config as AccessConfig;
|
||||
const bl = cfg.buttonLight;
|
||||
return bl && typeof bl.relay === "number" ? bl : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user