diff --git a/src/pipeline.js b/src/pipeline.js index 4e9f84c..f10b1d3 100644 --- a/src/pipeline.js +++ b/src/pipeline.js @@ -2,31 +2,40 @@ import { emptySpec, isUsable, specsCompatible } from './spec.js'; import { decodePartNumber } from './pn-decoder.js'; import { extractFields } from './ocr-extract.js'; -async function safely(fn, fallback) { +// Barcode-Dekodierung ist schnell; ein Adapter, der laenger braucht, gilt +// als gescheitert. Grosszuegig genug, dass ein normaler Ablauf sie nie reisst. +const DEFAULT_BARCODE_TIMEOUT_MS = 1500; +// Texterkennung kann auf einem Handy durchaus einige Sekunden dauern - die +// Obergrenze muss so grosszuegig sein, dass ein normaler Ablauf auf einem +// langsamen Handy sie nie erreicht. +const DEFAULT_OCR_TIMEOUT_MS = 20000; + +const TIMED_OUT = Symbol('timed-out'); + +/** + * Wettlauf zwischen dem uebergebenen Versprechen und einem Zeitgeber. Ist der + * Zeitgeber schneller, wird TIMED_OUT geliefert, ohne dass der urspruengliche + * Aufruf abgebrochen wird - er darf spaeter noch einloesen, wir warten nur + * nicht mehr darauf. Der Zeitgeber wird in jedem Fall wieder aufgeraeumt, + * damit kein offener Timer den Node-Prozess am Beenden hindert. + */ +function withTimeout(promise, ms) { + let timer; + const timeout = new Promise((resolve) => { + timer = setTimeout(() => resolve(TIMED_OUT), ms); + }); + return Promise.race([promise, timeout]).finally(() => clearTimeout(timer)); +} + +async function safely(fn, fallback, timeoutMs) { try { - return await fn(); + const result = await withTimeout(fn(), timeoutMs); + return result === TIMED_OUT ? fallback : result; } catch { return fallback; } } -/** - * Fuehrt mehrere untereinander vertraegliche Specs zusammen: ein spaeterer - * Treffer fuellt nur Felder, die noch offen sind, ueberschreibt aber nie - * ein bereits gesetztes Feld. - */ -function mergeCompatibleSpecs(specs) { - const merged = { ...specs[0] }; - for (let i = 1; i < specs.length; i++) { - for (const field of Object.keys(merged)) { - if (merged[field] === null && specs[i][field] !== null) { - merged[field] = specs[i][field]; - } - } - } - return merged; -} - /** Prueft, ob alle Specs paarweise vertraeglich sind. */ function allCompatible(specs) { for (let i = 0; i < specs.length; i++) { @@ -39,10 +48,17 @@ function allCompatible(specs) { /** * Barcode zuerst, OCR nur als Rueckfallebene. + * @param {object} deps.decodeBarcodes und deps.runOcr sind erforderlich. + * deps.barcodeTimeoutMs und deps.ocrTimeoutMs sind optional und ueberschreiben + * die eingebauten Obergrenzen - fuer Tests, die einen haengenden Adapter + * simulieren, ohne tatsaechlich zu warten. * @returns {Promise<{spec: object, source: 'barcode'|'ocr'|'none', confidence: 'green'|'yellow'|'red'}>} */ export async function recognize(frame, deps) { - const decodedCodes = await safely(() => deps.decodeBarcodes(frame), []); + const barcodeTimeoutMs = deps.barcodeTimeoutMs ?? DEFAULT_BARCODE_TIMEOUT_MS; + const ocrTimeoutMs = deps.ocrTimeoutMs ?? DEFAULT_OCR_TIMEOUT_MS; + + const decodedCodes = await safely(() => deps.decodeBarcodes(frame), [], barcodeTimeoutMs); const codes = Array.isArray(decodedCodes) ? decodedCodes : []; let best = emptySpec(); @@ -62,13 +78,19 @@ export async function recognize(frame, deps) { } if (usable.length > 1) { if (allCompatible(usable)) { - return { spec: mergeCompatibleSpecs(usable), source: 'barcode', confidence: 'green' }; + // Vertraeglichkeit unter verwertbaren Barcode-Treffern setzt eine + // uebereinstimmende Teilenummer voraus (decodePartNumber setzt sie fuer + // jede nicht-leere Eingabe, specsCompatible vergleicht sie mit) - und + // da alle uebrigen Felder aus derselben Teilenummer abgeleitet werden, + // sind vertraegliche Treffer ohnehin gleich. Es gibt nichts aufzufuellen, + // der erste Treffer genuegt. + return { spec: usable[0], source: 'barcode', confidence: 'green' }; } // Mehrdeutigkeit zwischen verwertbaren Barcodes: nicht raten, Nutzer entscheidet. return { spec: emptySpec(), source: 'none', confidence: 'red' }; } - const ocrResult = await safely(() => deps.runOcr(frame), ''); + const ocrResult = await safely(() => deps.runOcr(frame), '', ocrTimeoutMs); const text = typeof ocrResult === 'string' ? ocrResult : ''; const fromOcr = extractFields(text); diff --git a/src/pn-tables.js b/src/pn-tables.js index acdb543..1524602 100644 --- a/src/pn-tables.js +++ b/src/pn-tables.js @@ -6,8 +6,7 @@ // Systematik und muessen vor produktivem Einsatz gegen reale Module bestaetigt // werden - siehe Task 14. Ein falscher Eintrag faellt auf dem Barcode-Weg // nicht automatisch auf, weil dort keine Texterkennung laeuft, gegen die er -// widersprechen koennte - die Eintraege muessen deshalb gegen reale Module -// geprueft werden. +// widersprechen koennte. export const VENDOR_TABLES = [ { diff --git a/test/pipeline.test.js b/test/pipeline.test.js index d05cb9d..f412d7f 100644 --- a/test/pipeline.test.js +++ b/test/pipeline.test.js @@ -115,7 +115,7 @@ test('mehrere unvertraegliche Barcodes ergeben rot statt eines ungeprueften erst assert.equal(ocrAufgerufen, false, 'bei mehrdeutigem Barcode-Ergebnis soll nicht geraten werden'); }); -test('mehrere untereinander vertraegliche Barcodes werden zusammengefuehrt und bleiben gruen', async () => { +test('mehrere untereinander vertraegliche Barcodes ergeben gruen (der erste Treffer entscheidet, es gibt nichts zusammenzufuehren)', async () => { let ocrAufgerufen = false; const result = await recognize({}, { // Zwei Lesungen desselben Etiketts (z.B. Mehrfachtreffer durch @@ -132,3 +132,26 @@ test('mehrere untereinander vertraegliche Barcodes werden zusammengefuehrt und b assert.equal(result.spec.speed, 'PC4-2400'); assert.equal(ocrAufgerufen, false, 'OCR darf bei gruenem Mehrfachtreffer nicht laufen'); }); + +// --- Beanstandung 1: haengender Adapter darf die Erkennung nicht dauerhaft blockieren --- + +test('haengender Barcode-Adapter blockiert nicht dauerhaft, OCR uebernimmt', { timeout: 500 }, async () => { + const result = await recognize({}, { + decodeBarcodes: () => new Promise(() => {}), // loest nie ein und wirft nie + runOcr: async () => OCR_TEXT, + barcodeTimeoutMs: 20, + }); + assert.equal(result.source, 'ocr'); + assert.equal(result.confidence, 'yellow'); + assert.equal(result.spec.capacityGb, 64); +}); + +test('haengender OCR-Adapter blockiert nicht dauerhaft, Erkennung ergibt rot', { timeout: 500 }, async () => { + const result = await recognize({}, { + decodeBarcodes: async () => [], + runOcr: () => new Promise(() => {}), // loest nie ein und wirft nie + ocrTimeoutMs: 20, + }); + assert.equal(result.source, 'none'); + assert.equal(result.confidence, 'red'); +});