diff --git a/apps/web/src/SetupWizard.tsx b/apps/web/src/SetupWizard.tsx index d92b6db..3eb600a 100644 --- a/apps/web/src/SetupWizard.tsx +++ b/apps/web/src/SetupWizard.tsx @@ -496,7 +496,7 @@ function DeviceForm({ setTestError(null); setTested(null); try { - setTested(await testDevice(selected.id, mergedScalarConfig())); + setTested(await testDevice(selected.id, mergedScalarConfig(), editing?.id)); } catch (e) { setTestError((e as Error).message); } finally { @@ -598,7 +598,12 @@ function DeviceForm({ )} - {selected.configFields.map((f) => + {selected.configFields + // pulseMs + inputRestingHigh are surfaced in the Outputs / Inputs model + // sections below (a relay setting and an input setting, respectively), so + // skip them here to avoid rendering them twice. See OutputEditor/InputEditor. + .filter((f) => !(isController && (f.key === "pulseMs" || f.key === "inputRestingHigh"))) + .map((f) => f.type === "boolean" ? ( // Boolean config field → a real checkbox (stores a true/false boolean, not // the string "true"). The label sits beside the box, with the help below. @@ -657,12 +662,33 @@ function DeviceForm({ ), )} - {/* CONTROLLER: the relay map — which relay opens which direction + entry button. */} - {isController && } - - {/* CONTROLLER: optional button-lamp output on a spare relay (radar + camera driven). */} + {/* CONTROLLER — OUTPUTS: the relays (barriers + the button lamp) + pulse time. */} {isController && ( - + { + setConfig((c) => ({ ...c, pulseMs: v })); + resetStatus(); + }} + /> + )} + + {/* CONTROLLER — INPUTS: the terminals (entry button, presence/radar), each bound + to the output relay it drives. Separated from the outputs above. */} + {isController && ( + { + setConfig((c) => ({ ...c, inputRestingHigh: v })); + resetStatus(); + }} + /> )} {/* BOUND device: which controller + relay it sits at. */} @@ -794,9 +820,28 @@ function DeviceForm({ ); } -/** Controller relay map editor: each row = a relay + its direction + (optional) - * the input terminal its entry button is wired to. */ -function RelayEditor({ relays, onChange }: { relays: RelaySpec[]; onChange: (r: RelaySpec[]) => void }) { +// ── Controller OUTPUTS (relays) ──────────────────────────────────────────── +// A relay is an OUTPUT: it opens a barrier (or drives the button lamp). This section +// owns relay number + direction, the pulse-open time (relay hold ms), and the lamp +// relay. The INPUT terminals wired to these relays live in InputEditor below — the two +// are deliberately separated (a controller's inputs and outputs are distinct things). + +/** Relays = outputs (barriers + lamp) + the pulse-open hold time. */ +function OutputEditor({ + relays, + onChange, + buttonLight, + onButtonLightChange, + pulseMs, + onPulseMsChange, +}: { + relays: RelaySpec[]; + onChange: (r: RelaySpec[]) => void; + buttonLight: ButtonLightSpec | null; + onButtonLightChange: (v: ButtonLightSpec | null) => void; + pulseMs: number | undefined; + onPulseMsChange: (v: number) => void; +}) { const { t } = useTranslation(); function update(i: number, patch: Partial) { onChange(relays.map((r, idx) => (idx === i ? { ...r, ...patch } : r))); @@ -808,11 +853,27 @@ function RelayEditor({ relays, onChange }: { relays: RelaySpec[]; onChange: (r: function remove(i: number) { onChange(relays.filter((_, idx) => idx !== i)); } + const barrierRelays = new Set(relays.map((r) => r.relay)); return (
- {t("setup.relaysTitle")} -

{t("setup.relaysHint")}

+ {t("setup.outputsTitle")} +

{t("setup.outputsHint")}

+ + {/* Pulse-open time applies to every barrier relay (how long it's held open). */} + + + {/* Barrier relays: number + direction. (Input terminals are in the Inputs section.) */} {relays.map((r, i) => (
+ ))} + + + {/* Button-lamp output (a spare relay) — an OUTPUT, so it lives here. Driven by the + radar + camera (blink = radar-only, solid = car confirmed, off otherwise). */} +
+ + {t("setup.buttonLight")} + + + {buttonLight?.relay != null && barrierRelays.has(buttonLight.relay) && ( + {t("setup.buttonLightBarrierWarn")} + )} + {buttonLight?.relay != null && ( + <> + + + + )} +
+
+ ); +} + +// ── Controller INPUTS (terminals) ────────────────────────────────────────── +// An input is a TERMINAL the host READS: the entry button, the presence/radar sensor. +// Each input belongs to an entry barrier (it triggers/gates that relay's entry), so we +// render one block per entry/both relay, labelled with the output relay it drives. The +// button never SETS a pulse — its electrical pulse is the device's to report — so no +// timing field lives here (pulse-open is an OUTPUT setting, in OutputEditor). + +/** Per-entry-relay input terminals: the entry button + the presence/radar sensor. */ +function InputEditor({ + relays, + onChange, + inputsIdleHigh, + onInputsIdleHighChange, +}: { + relays: RelaySpec[]; + onChange: (r: RelaySpec[]) => void; + inputsIdleHigh: boolean | undefined; + onInputsIdleHighChange: (v: boolean) => void; +}) { + const { t } = useTranslation(); + function update(i: number, patch: Partial) { + onChange(relays.map((r, idx) => (idx === i ? { ...r, ...patch } : r))); + } + // Inputs only matter for entry/both relays (transient entry). Keep each row's real + // index so updates target the right relay. + const entryRelays = relays + .map((r, i) => ({ r, i })) + .filter(({ r }) => r.direction === "entry" || r.direction === "both"); + + return ( +
+ {t("setup.inputsTitle")} +

{t("setup.inputsHint")}

+ + {/* Board-wide resting level (idle HIGH vs LOW) — an input property. */} + + + {entryRelays.length === 0 ? ( +

{t("setup.inputsNoEntryRelay")}

+ ) : ( + entryRelays.map(({ r, i }) => ( +
+ + {t("setup.inputsForRelay", { relay: r.relay })} + - )} - {(r.direction === "entry" || r.direction === "both") && ( - )} - {/* Presence sensor kind + active-level — only meaningful once a terminal is set. */} - {(r.direction === "entry" || r.direction === "both") && !!r.presenceInput && ( - <> - -
- ))} - -
- ); -} - -/** Button-lamp output: the entry button's 12 V light on a SPARE relay, driven by the - * radar + camera (blink = radar-only, solid = car confirmed, off otherwise). Optional. - * The relay picker offers every relay number on this controller; the operator picks a - * spare one (not a barrier relay). See wiki/concepts/button-light-indicator.md. */ -function ButtonLightEditor({ - relays, - value, - onChange, -}: { - relays: RelaySpec[]; - value: ButtonLightSpec | null; - onChange: (v: ButtonLightSpec | null) => void; -}) { - const { t } = useTranslation(); - // Relay numbers in use as barriers — shown as a hint so the operator avoids them. - const barrierRelays = new Set(relays.map((r) => r.relay)); - return ( -
- - {t("setup.buttonLight")} - - - {value?.relay != null && barrierRelays.has(value.relay) && ( - {t("setup.buttonLightBarrierWarn")} - )} - {value?.relay != null && ( - <> - - - + )} +
+ )) )} ); diff --git a/apps/web/src/lib/i18n/en.ts b/apps/web/src/lib/i18n/en.ts index b6d8d66..61e0516 100644 --- a/apps/web/src/lib/i18n/en.ts +++ b/apps/web/src/lib/i18n/en.ts @@ -359,6 +359,18 @@ export const en: Catalog = { relaysTitle: "Relays on this controller", relaysHint: "Each relay opens one barrier. Set its direction; for transient entry, set which input terminal the entry button is wired to.", + outputsTitle: "Outputs — relays (barriers + lamp)", + outputsHint: + "Relays are OUTPUTS: each opens a barrier (or drives the button lamp). Set the relay number and direction. The input terminals (button, sensor) are in the Inputs section below.", + pulseOpenMs: "Pulse open (ms)", + pulseOpenHint: "How long a barrier relay is held open (jog). Applies to all barrier relays.", + inputsTitle: "Inputs — terminals (button, sensor)", + inputsHint: + "Inputs are TERMINALS the host READS: the entry button and the presence/radar sensor. Each belongs to an entry barrier — it triggers or gates that relay.", + inputsIdleHigh: "Inputs idle HIGH", + inputsIdleHighHint: "This board idles inputs HIGH (status 1111); a press pulls LOW.", + inputsForRelay: "For relay {{relay}}", + inputsNoEntryRelay: "No entry relay — add an 'Entry' or 'Entry + exit' relay in Outputs to assign terminals.", relay: "Relay", entryButtonTerminal: "Entry button on terminal", presenceInput: "Presence sensor (terminal)", diff --git a/apps/web/src/lib/i18n/sq.ts b/apps/web/src/lib/i18n/sq.ts index f568dfb..075b06a 100644 --- a/apps/web/src/lib/i18n/sq.ts +++ b/apps/web/src/lib/i18n/sq.ts @@ -368,6 +368,18 @@ export const sq = { relaysTitle: "Relet në këtë kontrollues", relaysHint: "Çdo rele hap një barrierë. Cakto drejtimin e saj; për hyrje kalimtare, cakto në cilin terminal hyrës është lidhur butoni i hyrjes.", + outputsTitle: "Daljet — relet (barrierat + drita)", + outputsHint: + "Relet janë DALJE: secila hap një barrierë (ose ndez dritën e butonit). Cakto numrin e relesë dhe drejtimin. Terminalet hyrëse (butoni, sensori) janë te seksioni Hyrjet më poshtë.", + pulseOpenMs: "Kohëzgjatja e hapjes (ms)", + pulseOpenHint: "Sa kohë mbahet rele e barrierës e hapur (jog). Vlen për të gjitha relet e barrierave.", + inputsTitle: "Hyrjet — terminalet (buton, sensor)", + inputsHint: + "Hyrjet janë TERMINALE që hosti i LEXON: butoni i hyrjes dhe sensori i pranisë/radari. Secila i përket një barriere hyrëse — e gateron ose e nis atë rele.", + inputsIdleHigh: "Hyrjet në pushim HIGH", + inputsIdleHighHint: "Kjo pllakë i mban hyrjet HIGH në pushim (statusi 1111); një shtypje e ul në LOW.", + inputsForRelay: "Për rele {{relay}}", + inputsNoEntryRelay: "Asnjë rele hyrëse — shto një rele 'Hyrje' ose 'Hyrje + dalje' te Daljet që të caktosh terminalet.", relay: "Rele", entryButtonTerminal: "Butoni i hyrjes në terminalin", presenceInput: "Sensori i pranisë (terminali)",