feat(setup): USB printer discovery — pick a real /dev/usb device
The kernel numbers usblp nodes by plug/boot order (park-buzi's printer
is lp1); the wizard hardcoded lp0 in labels/default and the admin had to
shell in and `ls /dev/usb`. Now:
- GET /api/setup/usb-printers enumerates /dev/usb/lpN (visible via the
compose bind-mount) and enriches each with the printer's self-reported
make/model from sysfs ieee1284_id (readable through Docker's ro /sys).
- The wizard's devicePath becomes a SELECT of printers actually present
("/dev/usb/lp1 — Xprinter XP-K200L"): a fresh form preselects the
first real device; a saved-but-unplugged path stays selectable,
flagged "saved — not present now"; zero found falls back to free text
+ a check-the-cable hint.
- Transport option label no longer hardcodes lp0.
Wiki: printer-usb-transport marked HARDWARE-VERIFIED (lab 2026-07-07:
full slip + feed + cut over USB — parity with TCP).
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -29,6 +29,7 @@ import {
|
||||
type RelayEvent,
|
||||
type RelaySpec,
|
||||
type TestResult,
|
||||
fetchUsbPrinters,
|
||||
} from "./api.js";
|
||||
import { Modal } from "./ui/Modal.js";
|
||||
|
||||
@@ -546,6 +547,28 @@ function DeviceForm({
|
||||
}
|
||||
return out;
|
||||
});
|
||||
// USB printers PRESENT on the box (/dev/usb/lpN + sysfs model) — fetched when a
|
||||
// printer form is on the USB transport, so devicePath becomes a SELECT of real
|
||||
// devices instead of a guessed path (the kernel may pick lp1 — park-buzi did).
|
||||
const [usbPrinters, setUsbPrinters] = useState<{ path: string; description: string | null }[] | null>(null);
|
||||
const usbTransport = isPrinter && String(config.transport ?? "tcp-ip") === "usb";
|
||||
useEffect(() => {
|
||||
if (!usbTransport) return;
|
||||
let alive = true;
|
||||
fetchUsbPrinters()
|
||||
.then((r) => {
|
||||
if (!alive) return;
|
||||
setUsbPrinters(r.printers);
|
||||
// Fresh form with no explicit path yet → preselect the first REAL device.
|
||||
if (r.printers.length > 0) {
|
||||
setConfig((c) => (c.devicePath == null ? { ...c, devicePath: r.printers[0]!.path } : c));
|
||||
}
|
||||
})
|
||||
.catch(() => alive && setUsbPrinters([]));
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, [usbTransport]);
|
||||
// Controllers: the unified relay map. Each relay reacts to an EVENT — entry/exit/both
|
||||
// (pulse a barrier) or radarAlert (drive an alert lamp). Alert relays carry a trigger
|
||||
// input + blink cadence; barriers carry no input wiring (that lives in `inputs` below).
|
||||
@@ -890,7 +913,32 @@ function DeviceForm({
|
||||
{f.label}
|
||||
{f.required ? " *" : ""}
|
||||
</label>
|
||||
{f.type === "select" ? (
|
||||
{f.key === "devicePath" && usbPrinters != null && usbPrinters.length > 0 ? (
|
||||
// Real devices found → a select (path + self-reported model). A saved
|
||||
// path that is NOT currently present stays selectable, flagged.
|
||||
<select
|
||||
className="select"
|
||||
value={String(config.devicePath ?? (f.default as string | undefined) ?? "")}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value;
|
||||
setConfig((c) => ({ ...c, devicePath: v }));
|
||||
resetStatus();
|
||||
}}
|
||||
>
|
||||
{(() => {
|
||||
const cur = String(config.devicePath ?? (f.default as string | undefined) ?? "");
|
||||
const missing = cur && !usbPrinters.some((u) => u.path === cur);
|
||||
return [
|
||||
...(missing ? [{ path: cur, description: t("setup.usbSavedMissing") }] : []),
|
||||
...usbPrinters,
|
||||
].map((u) => (
|
||||
<option key={u.path} value={u.path}>
|
||||
{u.description ? `${u.path} — ${u.description}` : u.path}
|
||||
</option>
|
||||
));
|
||||
})()}
|
||||
</select>
|
||||
) : f.type === "select" ? (
|
||||
<select
|
||||
className="select"
|
||||
value={String(config[f.key] ?? (f.default as string | number | undefined) ?? "")}
|
||||
@@ -945,6 +993,9 @@ function DeviceForm({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{f.key === "devicePath" && usbPrinters != null && usbPrinters.length === 0 && (
|
||||
<p className="hint mt-1">{t("setup.usbNoneFound")}</p>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user