import { canonical, emptySpec, isUsable, specsCompatible } from './spec.js'; import { decodePartNumber } from './pn-decoder.js'; import { extractFields } from './ocr-extract.js'; // Die Barcode-Dekodierung selbst ist normalerweise schnell. Diese Grenze ist // keine Leistungserwartung, sondern nur eine Absicherung gegen einen dauerhaft // haengenden Adapter. Sie muss auch das einmalige Laden des WebAssembly-Moduls // beim ersten Scan einer Sitzung abdecken, das auf einem Handy mehrere Sekunden // dauern kann. const DEFAULT_BARCODE_TIMEOUT_MS = 10000; // 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 { const result = await withTimeout(fn(), timeoutMs); return result === TIMED_OUT ? fallback : result; } catch { return fallback; } } /** Prueft, ob alle Specs paarweise vertraeglich sind. */ function allCompatible(specs) { for (let i = 0; i < specs.length; i++) { for (let j = i + 1; j < specs.length; j++) { if (!specsCompatible(specs[i], specs[j])) return false; } } return true; } /** * Barcode zuerst, OCR nur als Rueckfallebene. * @param {object} deps Adapter- und Zeitgrenzen-Bundel: 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 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 : []; const usable = []; const unusable = []; for (const code of codes) { const decoded = decodePartNumber(code); if (isUsable(decoded)) { usable.push(decoded); } else { unusable.push(decoded); } } // Auch unter den nicht verwertbaren Barcodes (unbekanntes Nummernschema) // gilt: Vorrang vor der gelesenen Teilenummer nur, wenn er eindeutig ist. // Genau ein Code - oder mehrere mit uebereinstimmender Teilenummer - behaelt // seinen Vorrang samt abgeleiteter Felder. Widersprechen sich mehrere // untereinander, erhaelt keiner Vorrang; es wird nichts aus ihnen uebernommen. let best = emptySpec(); const withPartNumber = unusable.filter((decoded) => decoded.partNumber !== null); if (withPartNumber.length > 0) { const first = withPartNumber[0]; const allSameCode = withPartNumber.every( (decoded) => canonical(decoded.partNumber) === canonical(first.partNumber) ); if (allSameCode) best = first; } if (usable.length === 1) { return { spec: usable[0], source: 'barcode', confidence: 'green' }; } if (usable.length > 1) { if (allCompatible(usable)) { // 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), '', ocrTimeoutMs); const text = typeof ocrResult === 'string' ? ocrResult : ''; const fromOcr = extractFields(text); // Der Barcode ist die exaktere Quelle: seine Teilenummer gewinnt. const merged = { ...fromOcr }; if (best.partNumber !== null) merged.partNumber = best.partNumber; for (const field of ['capacityGb', 'formFactor', 'rank', 'speed']) { if (merged[field] === null && best[field] !== null) merged[field] = best[field]; } if (isUsable(merged)) { return { spec: merged, source: 'ocr', confidence: 'yellow' }; } return { spec: merged, source: 'none', confidence: 'red' }; }