feat(setup): "Test ANPR" probe on ANPR-enabled cameras
Adds a bottom-of-modal "Test ANPR" button (shown only when a camera's Plate recognition opt-in is checked) that captures a live snapshot off the camera and runs it through the vision service, reporting the plate read + confidence + elapsed time, or which stage failed. - New POST /api/setup/test-anpr: builds the camera from the unsaved config (no DB write/device change, like /test), captures a snapshot, runs vision.analyze. Fail-soft like the runtime path (snapshot.ts): camera/vision failures are reported results, never a 500. - Thread the existing VisionClient into setupRoutes; add an isCamera() type guard to @parking/devices. - Web: testAnpr() client + AnprTestResult; button, hint, result line. - i18n keys in sq + en (Catalog parity). Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -7,8 +7,10 @@ import {
|
||||
fetchBackendIps,
|
||||
fetchCatalog,
|
||||
fetchState,
|
||||
testAnpr,
|
||||
testDevice,
|
||||
unassignDevice,
|
||||
type AnprTestResult,
|
||||
type Assignment,
|
||||
type BackendIpCandidate,
|
||||
type Catalog,
|
||||
@@ -352,6 +354,10 @@ function DeviceForm({
|
||||
const [tested, setTested] = useState<TestResult | null>(null);
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [testError, setTestError] = useState<string | null>(null);
|
||||
// ANPR probe (camera + anpr on): snapshot → vision analyze, reported below.
|
||||
const [anprResult, setAnprResult] = useState<AnprTestResult | null>(null);
|
||||
const [anprTesting, setAnprTesting] = useState(false);
|
||||
const [anprError, setAnprError] = useState<string | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [saveError, setSaveError] = useState<string | null>(null);
|
||||
const [found, setFound] = useState<DiscoveredDevice[] | null>(null);
|
||||
@@ -442,6 +448,8 @@ function DeviceForm({
|
||||
setTested(null);
|
||||
setTestError(null);
|
||||
setSaveError(null);
|
||||
setAnprResult(null);
|
||||
setAnprError(null);
|
||||
}
|
||||
|
||||
async function test() {
|
||||
@@ -458,6 +466,23 @@ function DeviceForm({
|
||||
}
|
||||
}
|
||||
|
||||
// End-to-end ANPR probe: capture a frame off this camera and run the vision service
|
||||
// on it, reporting plate + time (or the failure stage). Only meaningful for an
|
||||
// ANPR-enabled camera; never blocks save.
|
||||
async function testAnprNow() {
|
||||
if (!selected) return;
|
||||
setAnprTesting(true);
|
||||
setAnprError(null);
|
||||
setAnprResult(null);
|
||||
try {
|
||||
setAnprResult(await testAnpr(selected.id, mergedScalarConfig()));
|
||||
} catch (e) {
|
||||
setAnprError((e as Error).message);
|
||||
} finally {
|
||||
setAnprTesting(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!selected) return;
|
||||
// Bound devices must point at a controller relay (binding is optional in the
|
||||
@@ -641,6 +666,40 @@ function DeviceForm({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CAMERA + ANPR on: a bottom-of-modal end-to-end probe — capture a frame and
|
||||
run the vision service on it, reporting the plate read + how long it took. */}
|
||||
{isCamera && anpr && (
|
||||
<div className="mt-3 rounded-term border border-term-border bg-term-bg p-2">
|
||||
<button type="button" className="btn btn-sm" onClick={testAnprNow} disabled={anprTesting}>
|
||||
{anprTesting ? t("setup.anprTesting") : t("setup.testAnpr")}
|
||||
</button>
|
||||
<p className="hint mt-1">{t("setup.testAnprHint")}</p>
|
||||
|
||||
{anprError && <p className="mt-2 text-[12px] text-term-red">{t("setup.testFailed", { error: anprError })}</p>}
|
||||
{anprResult &&
|
||||
(anprResult.ok ? (
|
||||
<div className="mt-2 text-[12px] text-term-green">
|
||||
{t("setup.anprOk", {
|
||||
plate: anprResult.plate,
|
||||
confidence: Math.round(anprResult.confidence * 100),
|
||||
ms: anprResult.tookMs,
|
||||
})}
|
||||
{anprResult.lowConfidence && (
|
||||
<span className="ml-1 text-term-amber">{t("setup.anprLowConfidence")}</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-2 text-[12px] text-term-amber">
|
||||
⚠ {t(`setup.anprFail.${anprResult.reason}`, { defaultValue: anprResult.reason })}
|
||||
{anprResult.detail && <span className="text-term-muted"> — {anprResult.detail}</span>}
|
||||
{anprResult.tookMs != null && (
|
||||
<span className="text-term-muted"> ({t("setup.anprTookMs", { ms: anprResult.tookMs })})</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{backendIps && backendIps.length > 0 && (
|
||||
<div className="mt-3">
|
||||
<div className="field max-w-md">
|
||||
|
||||
@@ -280,6 +280,34 @@ export function testDevice(driverId: string, config: DeviceConfig): Promise<Test
|
||||
});
|
||||
}
|
||||
|
||||
/** Result of an end-to-end ANPR probe on a camera: snapshot → vision analyze. */
|
||||
export type AnprTestResult =
|
||||
| {
|
||||
ok: true;
|
||||
plate: string;
|
||||
confidence: number;
|
||||
region: string | null;
|
||||
lowConfidence: boolean;
|
||||
modelVersion: string;
|
||||
tookMs: number;
|
||||
}
|
||||
| {
|
||||
ok: false;
|
||||
/** vision-disabled | snapshot-failed | no-plate */
|
||||
reason: string;
|
||||
detail?: string;
|
||||
tookMs?: number;
|
||||
};
|
||||
|
||||
/** Take a live snapshot off the camera and run ANPR on it — without saving. Reports
|
||||
* whether a plate was extracted, the read, and how long it took. */
|
||||
export function testAnpr(driverId: string, config: DeviceConfig): Promise<AnprTestResult> {
|
||||
return apiFetch<AnprTestResult>("/api/setup/test-anpr", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ driverId, config }),
|
||||
});
|
||||
}
|
||||
|
||||
export interface BackendIpCandidate {
|
||||
ip: string;
|
||||
iface: string;
|
||||
|
||||
@@ -344,6 +344,16 @@ export const en: Catalog = {
|
||||
anpr: "Plate recognition (ANPR)",
|
||||
anprHint:
|
||||
"Enable to scan plates on this camera: the vision service reads the plate from a snapshot and feeds it as a read (advisory only — it never opens a barrier on its own). Requires the vision service running.",
|
||||
testAnpr: "Test ANPR",
|
||||
anprTesting: "Testing ANPR…",
|
||||
testAnprHint:
|
||||
"Takes a live snapshot from this camera and tries to read a plate, reporting the result and the time it took. Point a plate at the camera first.",
|
||||
anprOk: "✓ Read plate {{plate}} — {{confidence}}% confidence, {{ms}} ms",
|
||||
anprLowConfidence: "(low confidence — advisory only)",
|
||||
anprTookMs: "{{ms}} ms",
|
||||
"anprFail.vision-disabled": "Vision service is disabled — enable it (VISION_ENABLED) to test ANPR.",
|
||||
"anprFail.snapshot-failed": "Couldn't take a snapshot from the camera (offline or unreachable).",
|
||||
"anprFail.no-plate": "No plate found in the snapshot.",
|
||||
whichBarrier: "Which barrier does this device serve?",
|
||||
controller: "Controller",
|
||||
choose: "Choose…",
|
||||
|
||||
@@ -354,6 +354,16 @@ export const sq = {
|
||||
anpr: "Njohja e targave (ANPR)",
|
||||
anprHint:
|
||||
"Aktivizo që ky aparat të skanojë targat: shërbimi i vizionit lexon targën nga pamja dhe e dërgon si lexim (vetëm këshillues — nuk hap vetë barrierën). Kërkon shërbimin e vizionit aktiv.",
|
||||
testAnpr: "Testo ANPR",
|
||||
anprTesting: "Duke testuar ANPR…",
|
||||
testAnprHint:
|
||||
"Merr një pamje të drejtpërdrejtë nga kjo kamerë dhe përpiqet të lexojë një targë, duke raportuar rezultatin dhe kohën e nevojshme. Vendos një targë para kamerës më parë.",
|
||||
anprOk: "✓ Targa u lexua {{plate}} — {{confidence}}% besueshmëri, {{ms}} ms",
|
||||
anprLowConfidence: "(besueshmëri e ulët — vetëm këshillues)",
|
||||
anprTookMs: "{{ms}} ms",
|
||||
"anprFail.vision-disabled": "Shërbimi i vizionit është çaktivizuar — aktivizoje (VISION_ENABLED) për ta testuar ANPR.",
|
||||
"anprFail.snapshot-failed": "Nuk u mor dot pamje nga kamera (jashtë linje ose e paarritshme).",
|
||||
"anprFail.no-plate": "Nuk u gjet asnjë targë në pamje.",
|
||||
// Binding picker.
|
||||
whichBarrier: "Cilën barrierë shërben kjo pajisje?",
|
||||
controller: "Kontrolluesi",
|
||||
|
||||
Reference in New Issue
Block a user