refactor(setup): split the controller form into Outputs and Inputs sections
The controller editor mixed outputs and inputs in one flat "Relays" block — relay direction, the entry-button terminal, and the presence/radar terminal all on the same row, with the lamp orphaned below. Reorganize into two labelled sections: - Outputs — relays (barriers + lamp): relay # + direction, the button-light relay, and "Pulse open (ms)" (a relay hold-time, NOT an input setting — answers a recurring confusion). - Inputs — terminals (button, sensor): per entry relay, the button + presence/radar terminals (kind, active-low) and cooldown, each labelled "For relay N", plus the board-wide "Inputs idle HIGH". UI-only: storage stays config.relays[] (+ config.buttonLight), so saved booth configs keep working with no migration. pulseMs/inputRestingHigh are pulled out of the generic field loop and rendered in their section. i18n parity (sq + en). Also passes the device id to testDevice() so an edited device's stored relay password re-merges on Test connection (pairs with the secure-merge server change). Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
+235
-135
@@ -496,7 +496,7 @@ function DeviceForm({
|
|||||||
setTestError(null);
|
setTestError(null);
|
||||||
setTested(null);
|
setTested(null);
|
||||||
try {
|
try {
|
||||||
setTested(await testDevice(selected.id, mergedScalarConfig()));
|
setTested(await testDevice(selected.id, mergedScalarConfig(), editing?.id));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setTestError((e as Error).message);
|
setTestError((e as Error).message);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -598,7 +598,12 @@ function DeviceForm({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{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" ? (
|
f.type === "boolean" ? (
|
||||||
// Boolean config field → a real checkbox (stores a true/false boolean, not
|
// 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.
|
// 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. */}
|
{/* CONTROLLER — OUTPUTS: the relays (barriers + the button lamp) + pulse time. */}
|
||||||
{isController && <RelayEditor relays={relays} onChange={setRelays} />}
|
|
||||||
|
|
||||||
{/* CONTROLLER: optional button-lamp output on a spare relay (radar + camera driven). */}
|
|
||||||
{isController && (
|
{isController && (
|
||||||
<ButtonLightEditor relays={relays} value={buttonLight} onChange={setButtonLight} />
|
<OutputEditor
|
||||||
|
relays={relays}
|
||||||
|
onChange={setRelays}
|
||||||
|
buttonLight={buttonLight}
|
||||||
|
onButtonLightChange={setButtonLight}
|
||||||
|
pulseMs={config.pulseMs as number | undefined}
|
||||||
|
onPulseMsChange={(v) => {
|
||||||
|
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 && (
|
||||||
|
<InputEditor
|
||||||
|
relays={relays}
|
||||||
|
onChange={setRelays}
|
||||||
|
inputsIdleHigh={config.inputRestingHigh as boolean | undefined}
|
||||||
|
onInputsIdleHighChange={(v) => {
|
||||||
|
setConfig((c) => ({ ...c, inputRestingHigh: v }));
|
||||||
|
resetStatus();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* BOUND device: which controller + relay it sits at. */}
|
{/* 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)
|
// ── Controller OUTPUTS (relays) ────────────────────────────────────────────
|
||||||
* the input terminal its entry button is wired to. */
|
// A relay is an OUTPUT: it opens a barrier (or drives the button lamp). This section
|
||||||
function RelayEditor({ relays, onChange }: { relays: RelaySpec[]; onChange: (r: RelaySpec[]) => void }) {
|
// 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();
|
const { t } = useTranslation();
|
||||||
function update(i: number, patch: Partial<RelaySpec>) {
|
function update(i: number, patch: Partial<RelaySpec>) {
|
||||||
onChange(relays.map((r, idx) => (idx === i ? { ...r, ...patch } : r)));
|
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) {
|
function remove(i: number) {
|
||||||
onChange(relays.filter((_, idx) => idx !== i));
|
onChange(relays.filter((_, idx) => idx !== i));
|
||||||
}
|
}
|
||||||
|
const barrierRelays = new Set(relays.map((r) => r.relay));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="my-2 rounded-term border border-term-border bg-term-bg p-2">
|
<div className="my-2 rounded-term border border-term-border bg-term-bg p-2">
|
||||||
<strong className="text-[12px] uppercase tracking-wider text-term-text">{t("setup.relaysTitle")}</strong>
|
<strong className="text-[12px] uppercase tracking-wider text-term-text">{t("setup.outputsTitle")}</strong>
|
||||||
<p className="hint mt-0.5 mb-2">{t("setup.relaysHint")}</p>
|
<p className="hint mt-0.5 mb-2">{t("setup.outputsHint")}</p>
|
||||||
|
|
||||||
|
{/* Pulse-open time applies to every barrier relay (how long it's held open). */}
|
||||||
|
<label className="my-1 inline-flex items-center gap-1.5 text-[12px] text-term-muted" title={t("setup.pulseOpenHint")}>
|
||||||
|
{t("setup.pulseOpenMs")}
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={100}
|
||||||
|
value={pulseMs ?? ""}
|
||||||
|
placeholder="500"
|
||||||
|
className="input input-sm w-20"
|
||||||
|
onChange={(e) => onPulseMsChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{/* Barrier relays: number + direction. (Input terminals are in the Inputs section.) */}
|
||||||
{relays.map((r, i) => (
|
{relays.map((r, i) => (
|
||||||
<div key={i} className="my-1 flex flex-wrap items-center gap-2">
|
<div key={i} className="my-1 flex flex-wrap items-center gap-2">
|
||||||
<label className="inline-flex items-center gap-1.5 text-[12px] text-term-muted">
|
<label className="inline-flex items-center gap-1.5 text-[12px] text-term-muted">
|
||||||
@@ -832,7 +893,130 @@ function RelayEditor({ relays, onChange }: { relays: RelaySpec[]; onChange: (r:
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
{(r.direction === "entry" || r.direction === "both") && (
|
{relays.length > 1 && (
|
||||||
|
<button type="button" className="btn btn-ghost btn-sm" onClick={() => remove(i)}>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<button type="button" className="btn btn-sm mt-1" onClick={add}>
|
||||||
|
{t("setup.addRelay")}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 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). */}
|
||||||
|
<div className="mt-3 flex flex-wrap items-center gap-3 border-t border-term-border pt-2">
|
||||||
|
<span className="text-[12px] text-term-muted" title={t("setup.buttonLightHint")}>
|
||||||
|
{t("setup.buttonLight")}
|
||||||
|
</span>
|
||||||
|
<label className="inline-flex items-center gap-1.5 text-[12px] text-term-muted">
|
||||||
|
{t("setup.buttonLightRelay")}
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={buttonLight?.relay ?? ""}
|
||||||
|
placeholder="—"
|
||||||
|
className="input input-sm w-16"
|
||||||
|
onChange={(e) =>
|
||||||
|
onButtonLightChange(e.target.value === "" ? null : { ...buttonLight, relay: Number(e.target.value) })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{buttonLight?.relay != null && barrierRelays.has(buttonLight.relay) && (
|
||||||
|
<span className="text-[11px] text-term-amber">{t("setup.buttonLightBarrierWarn")}</span>
|
||||||
|
)}
|
||||||
|
{buttonLight?.relay != null && (
|
||||||
|
<>
|
||||||
|
<label className="inline-flex items-center gap-1.5 text-[12px] text-term-muted">
|
||||||
|
{t("setup.blinkOnMs")}
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={50}
|
||||||
|
value={buttonLight.blinkOnMs ?? ""}
|
||||||
|
placeholder="500"
|
||||||
|
className="input input-sm w-20"
|
||||||
|
onChange={(e) =>
|
||||||
|
onButtonLightChange({ ...buttonLight, blinkOnMs: e.target.value === "" ? undefined : Number(e.target.value) })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="inline-flex items-center gap-1.5 text-[12px] text-term-muted">
|
||||||
|
{t("setup.blinkOffMs")}
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={50}
|
||||||
|
value={buttonLight.blinkOffMs ?? ""}
|
||||||
|
placeholder="500"
|
||||||
|
className="input input-sm w-20"
|
||||||
|
onChange={(e) =>
|
||||||
|
onButtonLightChange({ ...buttonLight, blinkOffMs: e.target.value === "" ? undefined : Number(e.target.value) })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 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<RelaySpec>) {
|
||||||
|
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 (
|
||||||
|
<div className="my-2 rounded-term border border-term-border bg-term-bg p-2">
|
||||||
|
<strong className="text-[12px] uppercase tracking-wider text-term-text">{t("setup.inputsTitle")}</strong>
|
||||||
|
<p className="hint mt-0.5 mb-2">{t("setup.inputsHint")}</p>
|
||||||
|
|
||||||
|
{/* Board-wide resting level (idle HIGH vs LOW) — an input property. */}
|
||||||
|
<label className="my-1 inline-flex items-start gap-2 text-[12px] text-term-muted">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="mt-0.5"
|
||||||
|
checked={inputsIdleHigh ?? true}
|
||||||
|
onChange={(e) => onInputsIdleHighChange(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
<span className="font-semibold text-term-text">{t("setup.inputsIdleHigh")}</span>
|
||||||
|
<span className="hint mt-0.5 block">{t("setup.inputsIdleHighHint")}</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{entryRelays.length === 0 ? (
|
||||||
|
<p className="hint">{t("setup.inputsNoEntryRelay")}</p>
|
||||||
|
) : (
|
||||||
|
entryRelays.map(({ r, i }) => (
|
||||||
|
<div key={i} className="my-1 flex flex-wrap items-center gap-2 border-t border-term-border pt-2">
|
||||||
|
<span className="text-[11px] uppercase tracking-wider text-term-amber">
|
||||||
|
{t("setup.inputsForRelay", { relay: r.relay })}
|
||||||
|
</span>
|
||||||
<label className="inline-flex items-center gap-1.5 text-[12px] text-term-muted">
|
<label className="inline-flex items-center gap-1.5 text-[12px] text-term-muted">
|
||||||
{t("setup.entryButtonTerminal")}
|
{t("setup.entryButtonTerminal")}
|
||||||
<input
|
<input
|
||||||
@@ -844,8 +1028,6 @@ function RelayEditor({ relays, onChange }: { relays: RelaySpec[]; onChange: (r:
|
|||||||
onChange={(e) => update(i, { button: e.target.value === "" ? undefined : Number(e.target.value) })}
|
onChange={(e) => update(i, { button: e.target.value === "" ? undefined : Number(e.target.value) })}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
)}
|
|
||||||
{(r.direction === "entry" || r.direction === "both") && (
|
|
||||||
<label className="inline-flex items-center gap-1.5 text-[12px] text-term-muted" title={t("setup.presenceInputHint")}>
|
<label className="inline-flex items-center gap-1.5 text-[12px] text-term-muted" title={t("setup.presenceInputHint")}>
|
||||||
{t("setup.presenceInput")}
|
{t("setup.presenceInput")}
|
||||||
<input
|
<input
|
||||||
@@ -854,131 +1036,49 @@ function RelayEditor({ relays, onChange }: { relays: RelaySpec[]; onChange: (r:
|
|||||||
value={r.presenceInput ?? ""}
|
value={r.presenceInput ?? ""}
|
||||||
placeholder="—"
|
placeholder="—"
|
||||||
className="input input-sm w-16"
|
className="input input-sm w-16"
|
||||||
onChange={(e) =>
|
onChange={(e) => update(i, { presenceInput: e.target.value === "" ? undefined : Number(e.target.value) })}
|
||||||
update(i, { presenceInput: e.target.value === "" ? undefined : Number(e.target.value) })
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
)}
|
{/* Sensor kind + active-level — only once a presence terminal is set. */}
|
||||||
{/* Presence sensor kind + active-level — only meaningful once a terminal is set. */}
|
{!!r.presenceInput && (
|
||||||
{(r.direction === "entry" || r.direction === "both") && !!r.presenceInput && (
|
<>
|
||||||
<>
|
<label className="inline-flex items-center gap-1.5 text-[12px] text-term-muted">
|
||||||
<label className="inline-flex items-center gap-1.5 text-[12px] text-term-muted">
|
{t("setup.presenceKind")}
|
||||||
{t("setup.presenceKind")}
|
<select
|
||||||
<select
|
value={r.presenceKind ?? "loop"}
|
||||||
value={r.presenceKind ?? "loop"}
|
className="input input-sm w-24"
|
||||||
className="input input-sm w-24"
|
onChange={(e) => update(i, { presenceKind: e.target.value as "loop" | "radar" })}
|
||||||
onChange={(e) => update(i, { presenceKind: e.target.value as "loop" | "radar" })}
|
>
|
||||||
>
|
<option value="loop">{t("setup.presenceKindLoop")}</option>
|
||||||
<option value="loop">{t("setup.presenceKindLoop")}</option>
|
<option value="radar">{t("setup.presenceKindRadar")}</option>
|
||||||
<option value="radar">{t("setup.presenceKindRadar")}</option>
|
</select>
|
||||||
</select>
|
</label>
|
||||||
</label>
|
<label className="inline-flex items-center gap-1.5 text-[12px] text-term-muted" title={t("setup.presenceActiveLowHint")}>
|
||||||
<label className="inline-flex items-center gap-1.5 text-[12px] text-term-muted" title={t("setup.presenceActiveLowHint")}>
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={!!r.presenceActiveLow}
|
||||||
|
onChange={(e) => update(i, { presenceActiveLow: e.target.checked || undefined })}
|
||||||
|
/>
|
||||||
|
{t("setup.presenceActiveLow")}
|
||||||
|
</label>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{/* Cooldown fallback only when no presence sensor is wired. */}
|
||||||
|
{!r.presenceInput && (
|
||||||
|
<label className="inline-flex items-center gap-1.5 text-[12px] text-term-muted" title={t("setup.entryCooldownHint")}>
|
||||||
|
{t("setup.entryCooldown")}
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="number"
|
||||||
checked={!!r.presenceActiveLow}
|
min={0}
|
||||||
onChange={(e) => update(i, { presenceActiveLow: e.target.checked || undefined })}
|
value={r.entryCooldownSec ?? ""}
|
||||||
|
placeholder="—"
|
||||||
|
className="input input-sm w-16"
|
||||||
|
onChange={(e) => update(i, { entryCooldownSec: e.target.value === "" ? undefined : Number(e.target.value) })}
|
||||||
/>
|
/>
|
||||||
{t("setup.presenceActiveLow")}
|
|
||||||
</label>
|
</label>
|
||||||
</>
|
)}
|
||||||
)}
|
</div>
|
||||||
{(r.direction === "entry" || r.direction === "both") && !r.presenceInput && (
|
))
|
||||||
<label className="inline-flex items-center gap-1.5 text-[12px] text-term-muted" title={t("setup.entryCooldownHint")}>
|
|
||||||
{t("setup.entryCooldown")}
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={0}
|
|
||||||
value={r.entryCooldownSec ?? ""}
|
|
||||||
placeholder="—"
|
|
||||||
className="input input-sm w-16"
|
|
||||||
onChange={(e) =>
|
|
||||||
update(i, { entryCooldownSec: e.target.value === "" ? undefined : Number(e.target.value) })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
{relays.length > 1 && (
|
|
||||||
<button type="button" className="btn btn-ghost btn-sm" onClick={() => remove(i)}>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<button type="button" className="btn btn-sm mt-1" onClick={add}>
|
|
||||||
{t("setup.addRelay")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 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 (
|
|
||||||
<div className="mt-2 flex flex-wrap items-center gap-3 rounded-term border border-term-border p-2">
|
|
||||||
<span className="text-[12px] text-term-muted" title={t("setup.buttonLightHint")}>
|
|
||||||
{t("setup.buttonLight")}
|
|
||||||
</span>
|
|
||||||
<label className="inline-flex items-center gap-1.5 text-[12px] text-term-muted">
|
|
||||||
{t("setup.buttonLightRelay")}
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
value={value?.relay ?? ""}
|
|
||||||
placeholder="—"
|
|
||||||
className="input input-sm w-16"
|
|
||||||
onChange={(e) =>
|
|
||||||
onChange(e.target.value === "" ? null : { ...value, relay: Number(e.target.value) })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
{value?.relay != null && barrierRelays.has(value.relay) && (
|
|
||||||
<span className="text-[11px] text-term-amber">{t("setup.buttonLightBarrierWarn")}</span>
|
|
||||||
)}
|
|
||||||
{value?.relay != null && (
|
|
||||||
<>
|
|
||||||
<label className="inline-flex items-center gap-1.5 text-[12px] text-term-muted">
|
|
||||||
{t("setup.blinkOnMs")}
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={50}
|
|
||||||
value={value.blinkOnMs ?? ""}
|
|
||||||
placeholder="500"
|
|
||||||
className="input input-sm w-20"
|
|
||||||
onChange={(e) =>
|
|
||||||
onChange({ ...value, blinkOnMs: e.target.value === "" ? undefined : Number(e.target.value) })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="inline-flex items-center gap-1.5 text-[12px] text-term-muted">
|
|
||||||
{t("setup.blinkOffMs")}
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={50}
|
|
||||||
value={value.blinkOffMs ?? ""}
|
|
||||||
placeholder="500"
|
|
||||||
className="input input-sm w-20"
|
|
||||||
onChange={(e) =>
|
|
||||||
onChange({ ...value, blinkOffMs: e.target.value === "" ? undefined : Number(e.target.value) })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -359,6 +359,18 @@ export const en: Catalog = {
|
|||||||
relaysTitle: "Relays on this controller",
|
relaysTitle: "Relays on this controller",
|
||||||
relaysHint:
|
relaysHint:
|
||||||
"Each relay opens one barrier. Set its direction; for transient entry, set which input terminal the entry button is wired to.",
|
"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",
|
relay: "Relay",
|
||||||
entryButtonTerminal: "Entry button on terminal",
|
entryButtonTerminal: "Entry button on terminal",
|
||||||
presenceInput: "Presence sensor (terminal)",
|
presenceInput: "Presence sensor (terminal)",
|
||||||
|
|||||||
@@ -368,6 +368,18 @@ export const sq = {
|
|||||||
relaysTitle: "Relet në këtë kontrollues",
|
relaysTitle: "Relet në këtë kontrollues",
|
||||||
relaysHint:
|
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.",
|
"Ç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",
|
relay: "Rele",
|
||||||
entryButtonTerminal: "Butoni i hyrjes në terminalin",
|
entryButtonTerminal: "Butoni i hyrjes në terminalin",
|
||||||
presenceInput: "Sensori i pranisë (terminali)",
|
presenceInput: "Sensori i pranisë (terminali)",
|
||||||
|
|||||||
Reference in New Issue
Block a user