Compare commits

..
4 Commits
Author SHA1 Message Date
TanerUslu ebff173443 Doku: drei Scan-Funktionen statt automatischem Barcode/OCR-Ausweichen
Funktionsweise-Abschnitt, Technik- und Aufbau-Tabelle sowie die
Geraete-Checkliste auf die neue Funktionswahl gebracht. Haelt fest,
dass in "Text erkennen" mangels Barcode-Teilenummer allein die
technischen Angaben ueber die Stapelzuordnung entscheiden, und dass
"QR-Code" bewusst kein DataMatrix liest. Testanzahl auf 143 aktualisiert.
2026-07-29 13:37:18 +02:00
TanerUslu 2e619ed64d Funktionswahl vor der Kamera, Wechsel und Knopf-Sichtbarkeit je Funktion
Neuer Vollbild-Dialog (src/ui/mode-dialog.js) mit drei grossen Flaechen,
gebaut nach dem Muster der bestehenden Dialoge. Ablauf beim Start: erst
die Fortsetzen-Frage (falls eine gesicherte Sitzung vorliegt), dann die
Funktionswahl, erst danach startet die Kamera.

Die gewaehlte Funktion steht antippbar in der Scan-Ansicht und laesst
sich dort jederzeit wechseln - der Wechsel gilt sofort: laufende Suche
startet/stoppt, "Modul scannen" erscheint nur noch in Funktion "Text
erkennen", der Zielrahmen wird beim Wechsel weg von laufender Suche
zurueckgesetzt. pipeline.js bleibt unangetastet; main.js reicht je nach
Funktion nur unterschiedliche Adapter hinein (scan-recognition.js).
2026-07-29 13:37:12 +02:00
TanerUslu 2eb9d1ecf8 Barcode-Adapter nimmt Codearten entgegen, Adapter-Weiche je Funktion
decodeBarcodes(imageData, formats) bekommt die zu suchenden Codearten
uebergeben; ohne Angabe unveraendertes Verhalten (Code128 + DataMatrix).

buildRecognitionAdapters() baut daraus und aus dem echten OCR-Adapter
die an recognize() (pipeline.js, unveraendert) uebergebenen Adapter:
in Strichcode/QR-Code liefert die Texterkennung sofort leeren Text ohne
Tesseract anzustossen, in Text-erkennen liefert die Barcode-Dekodierung
sofort eine leere Liste ohne zxing-wasm anzustossen.
2026-07-29 13:37:05 +02:00
TanerUslu 9ef038ca98 Reines Funktionen-Modul fuer die drei Scan-Funktionen
Kennung, deutsche Beschriftung, Codearten, laufende Suche und
Texterkennung je Funktion an einer einzigen Stelle (src/scan-modes.js),
ohne Browser-Zugriff und damit unter Node pruefbar. Funktion "QR-Code"
enthaelt bewusst kein DataMatrix.
2026-07-29 13:36:58 +02:00
10 changed files with 624 additions and 79 deletions
+146 -56
View File
@@ -4,6 +4,11 @@ Browser-App, die per Handykamera gebrauchte Server-RAM-Module erkennt und
beim physischen Sortieren am Tisch anleitet: Modul vor die Kamera halten, die
App nennt den Stapel.
Vor dem Scannen wählt der Nutzer eine von drei Funktionen — Strichcode,
QR-Code oder Text erkennen (siehe „Funktionsweise" unten). Jede Funktion nutzt
ausschließlich ihre eigene Quelle; anders als früher weicht die App nicht mehr
automatisch auf Texterkennung aus, wenn kein Code gefunden wird.
Zwei Module gelten als zusammengehörig, wenn sie denselben Barcode-Inhalt
tragen — ein Barcode ist exakt gelesen, das ist der zuverlässigste Maßstab.
Jeder Stapel führt dafür die Menge der Barcode-Zeichenketten, die bei seinen
@@ -51,16 +56,57 @@ keine Daten verlassen das Gerät.
## Funktionsweise
1. **Barcode zuerst.** Code-128 und DataMatrix werden aus dem Kamerabild
dekodiert (`zxing-wasm`). Das ist exakt, im Gegensatz zu Texterkennung.
Werden mehrere Barcodes im Bild gefunden, die sich widersprechen (nicht
dieselbe Teilenummer), gilt das als mehrdeutig — die App rät nicht,
sondern fragt nach. Das gilt nur für Barcodes mit einem bekannten
Nummernschema; widersprechen sich mehrere Barcodes mit unbekanntem
Nummernschema, wird keine ihrer Teilenummern übernommen.
2. **Teilenummer-Decoder.** Aus einer Hersteller-PN wie `M386A8K40BM1-CRC4Y`
werden Kapazität, Bauform und Geschwindigkeit tabellengesteuert
abgeleitet. Gelingt das, entfällt OCR vollständig, das Ergebnis ist grün.
0. **Funktion wählen.** Vor dem ersten Scan (und jederzeit erneut über die
antippbare Anzeige in der Scan-Ansicht) wählt der Nutzer eine von drei
Funktionen. Kennung, Beschriftung, gelesene Codearten und Verhalten stehen
ausschließlich in `src/scan-modes.js` — es gibt keine zweite Stelle, an der
das steht:
| Funktion | Liest | Laufende Suche | Texterkennung |
|---|---|---|---|
| **Strichcode** | Code128, Code39, Code93, ITF, EAN-13, EAN-8, UPC-A, UPC-E, Codabar | ja | nein |
| **QR-Code** | ausschließlich QRCode, MicroQRCode, RMQRCode | ja | nein |
| **Text erkennen** | keine Codes | nein | ja, auf Knopfdruck |
„QR-Code" liest bewusst **kein** DataMatrix — auch wenn die 2D-Codes auf
eigenen RAM-Etiketten DataMatrix sind. Der reguläre Arbeitsablauf läuft über
„Strichcode", weil dort die Teilenummer im Strichcode steht; „QR-Code" ist
für andere Ware gedacht. Diese Beschränkung ist eine ausdrückliche
Entscheidung, keine Lücke.
„Modul scannen" erscheint nur in „Text erkennen" — in „Strichcode" und
„QR-Code" sucht die App ohnehin laufend, ein Knopf ohne Texterkennung hätte
dort nichts zu tun. Der Zielrahmen hebt sich in „Strichcode" und „QR-Code"
hervor, sobald ein passender Code im Bild ist; in „Text erkennen" bleibt er
schlicht, weil dort nichts laufend erkannt wird. Ein Wechsel der Funktion
während des Sortierens (z. B. bei gemischter Ware) gilt sofort: laufende
Suche startet oder stoppt, der Knopf erscheint oder verschwindet.
Liegt beim Start eine gesicherte Sitzung vor, erscheint zuerst die Frage
nach dem Fortsetzen (siehe Punkt 5 unten) und erst danach die Funktionswahl
— die Kamera startet in jedem Fall erst, nachdem beides geklärt ist.
Technisch bekommen „Strichcode" und „QR-Code" eine Texterkennung
hineingereicht, die sofort leeren Text liefert, ohne Tesseract anzustoßen;
„Text erkennen" bekommt eine Barcode-Dekodierung hineingereicht, die sofort
eine leere Liste liefert (`buildRecognitionAdapters` in
`src/scan-recognition.js`). Die Erkennungs-Pipeline selbst (`src/pipeline.js`,
Punkte 14 unten) bleibt dabei unverändert und kennt keine Funktionen — die
drei Funktionen unterscheiden sich ausschließlich darin, welche Adapter ihr
übergeben werden.
1. **Barcode-Dekodierung** (nur in „Strichcode" und „QR-Code"). Die zur
Funktion gehörenden Codearten werden laufend aus dem Kamerabild dekodiert
(`zxing-wasm`, `decodeBarcodes` in `src/barcode.js` bekommt die Codearten
der gewählten Funktion übergeben). Das ist exakt, im Gegensatz zu
Texterkennung. Werden mehrere Barcodes im Bild gefunden, die sich
widersprechen (nicht dieselbe Teilenummer), gilt das als mehrdeutig — die
App rät nicht, sondern fragt nach. Das gilt nur für Barcodes mit einem
bekannten Nummernschema; widersprechen sich mehrere Barcodes mit
unbekanntem Nummernschema, wird keine ihrer Teilenummern übernommen.
2. **Teilenummer-Decoder** (nur relevant in „Strichcode"/„QR-Code" — „Text
erkennen" liest nie einen Barcode, siehe Punkt 0). Aus einer Hersteller-PN
wie `M386A8K40BM1-CRC4Y` werden Kapazität, Bauform und Geschwindigkeit
tabellengesteuert abgeleitet. Gelingt das, entfällt OCR vollständig, das Ergebnis ist grün.
Kennt der Decoder das Nummernschema nicht (siehe „Grenzen“ unten), ist das
allein noch kein Grund für Rot: Zum Sortieren muss keine Kapazität bekannt
sein, eine exakt gelesene Teilenummer genügt, um ein Modul wiederzuerkennen
@@ -72,17 +118,23 @@ keine Daten verlassen das Gerät.
könnte, das über die Zuordnung entscheidet. Bei einem tatsächlich neuen
Modul läuft sie dagegen wie gehabt, denn dort liefert sie die lesbare
Beschriftung des neuen Stapels.
3. **OCR als Rückfallebene.** Nur wenn kein Barcode eindeutig lesbar war oder
sein Nummernschema unbekannt ist (z. B. überklebtes/beschädigtes Etikett
oder ein noch nicht in `src/pn-tables.js` hinterlegter Hersteller). Das
Kamerabild wird dafür in Graustufen gewandelt und mit einem
**Otsu-Schwellwert** in Schwarz/Weiß aufbereitet: Der Schwellwert wird aus
der Helligkeitsverteilung des gesamten Bildes bestimmt, nicht aus einer
festen Kontrastspreizung zwischen hellstem und dunkelstem Pixel. Das macht
die Aufbereitung robust gegen einzelne Lichtreflexe auf glänzenden
Metalletiketten, die eine reine Min/Max-Spreizung leicht kippen würden.
Das OCR-Ergebnis wird anschließend gegen die bekannten Spec-Werte
abgeglichen, was die typischen Verwechslungen (0/O, 1/I, 8/B, …) auflöst.
3. **Texterkennung** (ausschließlich in „Text erkennen", ausgelöst durch
„Modul scannen"). Anders als früher ist das kein automatischer Rückfall
vom Barcode-Weg mehr, sondern die einzige Quelle dieser Funktion — sie
liest ohnehin nie einen Barcode (siehe Punkt 0). Das Kamerabild wird dafür
in Graustufen gewandelt und mit einem **Otsu-Schwellwert** in Schwarz/Weiß
aufbereitet: Der Schwellwert wird aus der Helligkeitsverteilung des
gesamten Bildes bestimmt, nicht aus einer festen Kontrastspreizung
zwischen hellstem und dunkelstem Pixel. Das macht die Aufbereitung robust
gegen einzelne Lichtreflexe auf glänzenden Metalletiketten, die eine reine
Min/Max-Spreizung leicht kippen würden. Das OCR-Ergebnis wird anschließend
gegen die bekannten Spec-Werte abgeglichen, was die typischen
Verwechslungen (0/O, 1/I, 8/B, …) auflöst. Da in dieser Funktion nie eine
per Barcode gelesene Teilenummer vorliegt, entscheiden hier ausschließlich
die aus dem erkannten Text abgeleiteten technischen Angaben (Kapazität,
Bauform, Rank, Geschwindigkeit) über die Stapelzuordnung
(`specsCompatible` in `src/spec.js`) — anders als in „Strichcode"/
„QR-Code", wo meist die exakte Teilenummer trägt.
4. **Stapel-Zuweisung.** Grün (Barcode) und Gelb (OCR) laufen ohne Eingabe
durch; nur bei roter Konfidenz (nichts Eindeutiges erkannt, widersprüchliche
Barcodes oder widersprüchliche unbekannte Barcodes ohne jede OCR-Kapazität)
@@ -133,14 +185,19 @@ korrekt funktioniert.
npm test
```
124 Tests, alle grün (Stand dieses Dokuments). Getestet werden ausschließlich
143 Tests, alle grün (Stand dieses Dokuments). Getestet werden ausschließlich
die reinen Module ohne Browser-Zugriff: Teilenummer-Decoder, Spec-Normalisierung
und toleranter Vergleich, OCR-Bildaufbereitung (Otsu-Schwellwert) und
-Feldextraktion, Stapel-Zuweisung, die Erkennungs-Pipeline samt Zeitgrenzen
und Mehrdeutigkeitsbehandlung, sowie die Sitzungssicherung samt Prüfung eines
wiederhergestellten Zustands. Kamera, Barcode-/OCR-Adapter selbst und
Oberfläche laufen nur im echten Browser und werden dort manuell geprüft
(siehe „Am Gerät noch zu prüfen" unten) — `node --test` kennt kein DOM.
und Mehrdeutigkeitsbehandlung, die Sitzungssicherung samt Prüfung eines
wiederhergestellten Zustands, die drei Scan-Funktionen (`scan-modes.js`
Codearten je Funktion, kein DataMatrix in „QR-Code") sowie die je Funktion
tatsächlich aufgerufenen Adapter (`scan-recognition.js` — Texterkennung läuft
nachweislich nie in „Strichcode"/„QR-Code", Barcode-Dekodierung nachweislich
nie in „Text erkennen"). Kamera, Barcode-/OCR-Adapter selbst, die
Auswahl-Oberfläche und die Verdrahtung in `main.js` laufen nur im echten
Browser und werden dort manuell geprüft (siehe „Am Gerät noch zu prüfen"
unten) — `node --test` kennt kein DOM.
## Produktion
@@ -189,7 +246,9 @@ worauf du bei der Veröffentlichung achten solltest.
- Vanilla JavaScript (ES Modules), kein Framework
- [Vite](https://vite.dev/) als Entwicklungsserver und Bündler
- [`zxing-wasm`](https://github.com/Sec-ant/zxing-wasm) für Code-128 und DataMatrix
- [`zxing-wasm`](https://github.com/Sec-ant/zxing-wasm) zur Barcode-Dekodierung;
welche Codearten gesucht werden, gibt die gewählte Funktion vor
(`src/scan-modes.js`)
- [`tesseract.js`](https://tesseract.projectnaptha.com/) für Texterkennung
- `node:test` für Tests, ohne zusätzliche Test-Bibliothek
@@ -202,15 +261,18 @@ worauf du bei der Veröffentlichung achten solltest.
| `src/pn-decoder.js` | Teilenummer → Spec-Felder |
| `src/ocr-extract.js` | OCR-Rohtext → Spec-Felder |
| `src/session.js` | Stapel halten, vorschlagen, buchen, umsortieren, zurücknehmen — inklusive der aus den Einträgen abgeleiteten, je Stapel bekannten Barcode-Inhalte (`stack.codes`), die vor dem Vergleich der technischen Angaben über die Zuordnung entscheiden, sofern diese Angaben nicht widersprechen |
| `src/pipeline.js` | Barcode → Teilenummer → OCR → Ampelfarbe, inkl. Zeitgrenzen, Mehrdeutigkeitsbehandlung und Kurzschluss über einen bereits bekannten Barcode-Inhalt (`deps.isKnownCode`) |
| `src/pipeline.js` | Barcode → Teilenummer → OCR → Ampelfarbe, inkl. Zeitgrenzen, Mehrdeutigkeitsbehandlung und Kurzschluss über einen bereits bekannten Barcode-Inhalt (`deps.isKnownCode`) — unverändert; kennt keine Scan-Funktionen |
| `src/scan-modes.js` | Die drei Scan-Funktionen (Strichcode, QR-Code, Text erkennen): Kennung, deutsche Beschriftung, gelesene Codearten, laufende Suche ja/nein, Texterkennung ja/nein — einzige Quelle, sowohl für die Auswahl-Oberfläche als auch für `main.js` |
| `src/scan-recognition.js` | Baut die an `recognize()` übergebenen Adapter anhand der gewählten Funktion: in „Strichcode"/„QR-Code" liefert die Texterkennung sofort leeren Text ohne Tesseract anzustoßen, in „Text erkennen" liefert die Barcode-Dekodierung sofort eine leere Liste ohne `zxing-wasm` anzustoßen |
| `src/storage.js` | Absturzschutz der laufenden Sitzung: sichert und lädt aus `localStorage`, verwirft beim Laden jeden in sich unstimmigen Zustand vollständig (siehe unten) |
| `src/camera.js` | Kamerastart, Einzelbildaufnahme, Bilddatei-Ersatzweg |
| `src/barcode.js` | Adapter zu `zxing-wasm` |
| `src/barcode.js` | Adapter zu `zxing-wasm`; `decodeBarcodes(imageData, formats)` bekommt die Codearten übergeben, ohne Angabe wie bisher (Code128 + DataMatrix) |
| `src/ocr.js` | Bildaufbereitung (Otsu) und Adapter zu `tesseract.js` |
| `src/ui/scan-view.js` | Scan-Ansicht: Kamera-Vorschau, Scan-Knopf, Stapel-Leiste, „Zuletzt"-Zeile, Ersatzweg-Schaltfläche bei fehlender Kamera |
| `src/ui/scan-view.js` | Scan-Ansicht: Kamera-Vorschau, antippbare Anzeige der gewählten Funktion (Wechsel), Scan-Knopf (nur in „Text erkennen" sichtbar), Stapel-Leiste, „Zuletzt"-Zeile, Ersatzweg-Schaltfläche bei fehlender Kamera |
| `src/ui/mode-dialog.js` | Vollbild-Auswahl der Scan-Funktion vor dem Start und beim Wechsel: drei große Flächen, eine je Funktion aus `src/scan-modes.js` |
| `src/ui/result-overlay.js` | Kurze Treffer-Rückmeldung (grün/gelb), blendet sich nach kurzer Zeit selbst wieder aus |
| `src/ui/ambiguous-dialog.js` | Rot-Dialog bei roter Konfidenz oder mehrdeutiger Stapelzuordnung |
| `src/ui/resume-dialog.js` | Dialog beim Start: gesicherte Sitzung fortsetzen oder verwerfen |
| `src/ui/resume-dialog.js` | Dialog beim Start: gesicherte Sitzung fortsetzen oder verwerfen — läuft vor der Funktionswahl |
| `src/ui/session-list.js` | Sitzungsliste: Stapel-Übersicht, Umsortieren, Entfernen, Sitzung beenden |
| `src/ui/describe-spec.js` | Gemeinsame Kurzbeschreibung eines Specs (Kapazität, Rank, Geschwindigkeit, Bauform), wahlweise mit Platzhaltern für fehlende Felder; von Treffer-Rückmeldung, „Zuletzt"-Zeile und Sitzungsliste gemeinsam genutzt |
| `src/main.js` | Verdrahtung aller Module zur lauffähigen App |
@@ -218,11 +280,11 @@ worauf du bei der Veröffentlichung achten solltest.
| `server.py` | Auslieferung im Betrieb: statischer Server für `dist/` mit Passwortabfrage vor jeder Datei, plus passwortfreier Gesundheitspfad `/healthz`. Nur Python-Standardbibliothek, keine zusätzliche Abhängigkeit |
| `.vch/deploy.yaml` | Bau- und Startbefehl sowie Gesundheitspfad für die Hosting-Umgebung |
`spec`, `pn-decoder`, `ocr-extract`, `session`, `pipeline` und `storage` sind
reine Funktionen ohne Browser-Zugriff (kein `window`, `document` oder
`localStorage` direkt) und deshalb vollständig mit `node:test` prüfbar.
`camera.js`, `barcode.js`, `ocr.js` und `src/ui/` brauchen einen echten
Browser und werden nur manuell geprüft.
`spec`, `pn-decoder`, `ocr-extract`, `session`, `pipeline`, `storage`,
`scan-modes` und `scan-recognition` sind reine Funktionen ohne Browser-Zugriff
(kein `window`, `document` oder `localStorage` direkt) und deshalb vollständig
mit `node:test` prüfbar. `camera.js`, `barcode.js`, `ocr.js` und `src/ui/`
brauchen einen echten Browser und werden nur manuell geprüft.
### Sitzungssicherung im Detail
@@ -325,40 +387,68 @@ werden:
- Datei-Auswahl als Ersatzweg (ohne Kamera) öffnet den nativen Dialog und
hinterlässt keine leere Fläche im Layout.
**Funktionswahl (neu)**
- Erster Start ohne gesicherte Sitzung: Auswahl mit drei großen Flächen
(„Strichcode", „QR-Code", „Text erkennen") erscheint, *bevor* die Kamera
startet — kein Kamerabild sichtbar, solange keine Funktion gewählt ist.
- Erster Start *mit* gesicherter Sitzung: erst die Frage „Gesicherte Sitzung
gefunden" (fortsetzen/verwerfen), erst danach die Funktionswahl.
- „Strichcode" wählen: laufende Suche erkennt Code-128/Code-39/EAN/UPC/…,
kein „Modul scannen"-Knopf sichtbar, Zielrahmen hebt sich bei erkanntem
Code hervor.
- „QR-Code" wählen und ein Etikett mit dem eigenen DataMatrix-Code vor die
Kamera halten: **bewusst kein Treffer** (kein DataMatrix in dieser
Funktion) — Rahmen bleibt neutral. Einen echten QR-Code vor dieselbe
Funktion halten: Rahmen hebt sich hervor, Treffer wird gebucht.
- „Text erkennen" wählen: kein automatisches Hervorheben des Rahmens (er
bleibt durchgehend schlicht), „Modul scannen"-Knopf sichtbar und löst die
Texterkennung aus.
- Anzeige der gewählten Funktion in der Scan-Ansicht antippen: Auswahl
erscheint erneut, nach der Wahl gilt der Wechsel sofort — laufende Suche
startet/stoppt, Knopf erscheint/verschwindet, **ohne** die Seite neu zu
laden.
- Anzeige der gewählten Funktion aus rund einem Meter Entfernung lesbar.
**Barcode- und OCR-Erkennung**
- Samsung-Referenzmodul scannen: Code-128 *und* DataMatrix werden erkannt,
- Samsung-Referenzmodul in Funktion „Strichcode" scannen:
`M386A8K40BM1-CRC4Y` wird korrekt als Teilenummer übernommen, grüne
Rückmeldung mit `STAPEL A`, `64GB 4DRx4 PC4-2400 LRDIMM`.
- Etikett ohne lesbaren Barcode fotografieren: OCR-Weg greift, die
Otsu-Aufbereitung liefert auf einem echten, glänzenden Etikettenfoto
tatsächlich brauchbaren Text (bisher nur an synthetischen Testbildern
geprüft, nie an einem echten Foto).
- In Funktion „Text erkennen" ein Etikett fotografieren und „Modul scannen"
antippen: die Otsu-Aufbereitung liefert auf einem echten, glänzenden
Etikettenfoto tatsächlich brauchbaren Text (bisher nur an synthetischen
Testbildern geprüft, nie an einem echten Foto).
- Ladezeit des WASM-Barcode-Moduls (`zxing-wasm`) beim allerersten Scan einer
Sitzung beobachten — bleibt sie deutlich unter der 10-Sekunden-Zeitgrenze
der Pipeline, und braucht ein zweiter, schnell nachfolgender Scan nicht
erneut die volle Ladezeit (Beleg, dass die Vorbereitung tatsächlich nur
einmal läuft)?
Sitzung in Funktion „Strichcode" oder „QR-Code" beobachten — bleibt sie
deutlich unter der 10-Sekunden-Zeitgrenze der Pipeline, und braucht ein
zweiter, schnell nachfolgender Scan nicht erneut die volle Ladezeit (Beleg,
dass die Vorbereitung tatsächlich nur einmal läuft)? In Funktion „Text
erkennen" darf dieses Laden dagegen gar nicht erst anlaufen, da dort nie
`decodeBarcodes` aufgerufen wird.
- Verbindung während des allerersten Scans einer Sitzung unterbrechen (WASM
lädt per CDN), danach mit wiederhergestellter Verbindung erneut scannen:
Das muss einen echten neuen Ladeversuch auslösen statt dauerhaft mit
demselben Fehler zu scheitern.
- Ladezeit des Tesseract-Arbeiters beim allerersten Scan einer Sitzung
beobachten — bleibt sie auf einem normalen Handy deutlich unter der
20-Sekunden-Zeitgrenze der Pipeline?
- Ladezeit des Tesseract-Arbeiters beim ersten Antippen von „Modul scannen"
in Funktion „Text erkennen" beobachten — bleibt sie auf einem normalen
Handy deutlich unter der 20-Sekunden-Zeitgrenze der Pipeline? In den
Funktionen „Strichcode"/„QR-Code" darf dieses Laden nie anlaufen, auch
nicht bei einem gänzlich unbekannten Etikett, da `runOcr` dort nie
aufgerufen wird.
- Verfügbarkeitsanzeige der Texterkennung: bleibt nach einem erfolgreichen
Ladevorgang dauerhaft "verfügbar", springt nach einem erzwungenen
Fehlschlag (z. B. Flugmodus beim ersten Laden) beim nächsten Erfolg wieder
darauf zurück?
- Stichprobe, ob die feste Zeichen-Whitelist der Texterkennung auf echten
Etiketten keine tatsächlich benötigten Zeichen ausschließt.
- Bild ohne lesbaren Barcode und ohne verwertbaren OCR-Text: Rot-Dialog
erscheint statt Fehler oder Absturz.
- Modul mit unbekanntem Nummernschema (z. B. ein Hynix-Modul) scannen: grüne
Rückmeldung statt Rot, auch wenn keine Kapazität abgeleitet werden kann.
Dasselbe Modul ein zweites Mal scannen: landet auf demselben Stapel — und
die Rückmeldung erscheint spürbar schneller als beim ersten Scan, weil die
Texterkennung diesmal übersprungen wird (nur am Gerät beobachtbar, da
`node --test` keine echte Ladezeit misst).
- Funktion „Strichcode"/„QR-Code": kein passender Code im Bild führt zu
Rot-Dialog (statt Fehler oder Absturz) — hier ohne jeden OCR-Versuch.
Funktion „Text erkennen": kein verwertbarer OCR-Text führt ebenfalls zu
Rot, hier ohne jeden Barcode-Versuch.
- In Funktion „Strichcode" ein Modul mit unbekanntem Nummernschema (z. B. ein
Hynix-Modul) scannen: grüne Rückmeldung statt Rot, auch wenn keine
Kapazität abgeleitet werden kann (die Texterkennung ist in dieser Funktion
ohnehin stumm geschaltet und liefert nie eine Kapazität nach). Dasselbe
Modul ein zweites Mal scannen: landet auf demselben Stapel.
- Etikett mit zwei Barcodes (Teile- *und* Seriennummer) mehrfach scannen:
beide Scans landen auf demselben Stapel, obwohl die Seriennummer bei realen
Modulen nie identisch ist — das belegt, dass die Teilenummer und nicht die
+11 -3
View File
@@ -25,8 +25,14 @@ async function ensureReady() {
await readyPromise;
}
// Bisheriges Verhalten (Code-128 und DataMatrix), falls der Aufrufer keine
// eigene Liste von Codearten angibt - main.js reicht seit den Scan-Funktionen
// (siehe scan-modes.js) stets eine eigene Liste durch, aber ohne Angabe
// bleibt der Adapter unveraendert nutzbar (z. B. fuer kuenftige Aufrufer).
const DEFAULT_FORMATS = ['Code128', 'DataMatrix'];
/**
* Dekodiert Code-128 und DataMatrix aus einem Einzelbild.
* Dekodiert Barcodes aus einem Einzelbild.
* Barcode-Dekodierung ist exakt - deshalb ist sie die bevorzugte Quelle
* gegenueber der fehleranfaelligen Texterkennung.
*
@@ -35,12 +41,14 @@ async function ensureReady() {
* Ausgang und kein Fehler.
*
* @param {ImageData} imageData Einzelbild aus der Kamera.
* @param {string[]} [formats] Zu suchende Codearten (zxing-wasm-Formatnamen,
* siehe scan-modes.js). Ohne Angabe wie bisher: Code128 und DataMatrix.
* @returns {Promise<string[]>}
*/
export async function decodeBarcodes(imageData) {
export async function decodeBarcodes(imageData, formats = DEFAULT_FORMATS) {
await ensureReady();
const results = await readBarcodes(imageData, {
formats: ['Code128', 'DataMatrix'],
formats,
tryHarder: true,
maxNumberOfSymbols: 4,
});
+79 -3
View File
@@ -5,6 +5,8 @@ import { decodeBarcodes } from './barcode.js';
import { runOcr, isOcrAvailable } from './ocr.js';
import { recognize } from './pipeline.js';
import { normalizeToken } from './spec.js';
import { getScanMode } from './scan-modes.js';
import { buildRecognitionAdapters } from './scan-recognition.js';
import {
createSession, proposeAssignment, commitAssignment,
undoLast, moveEntry, removeEntry, nextStackId,
@@ -14,6 +16,7 @@ import { renderScanView } from './ui/scan-view.js';
import { showResult } from './ui/result-overlay.js';
import { askForStack } from './ui/ambiguous-dialog.js';
import { askResumeSession } from './ui/resume-dialog.js';
import { askForScanMode } from './ui/mode-dialog.js';
import { renderSessionList } from './ui/session-list.js';
import { describeSpec } from './ui/describe-spec.js';
@@ -38,6 +41,14 @@ let busy = false;
// Scan-Durchlaufs ueberschrieben werden.
let cameraUnavailableMessage = null;
// Vor dem ersten Start der Kamera waehlt der Nutzer eine der drei Scan-
// Funktionen (siehe scan-modes.js); erst danach steht diese Kennung fest.
// Waehrend applyMode() noch nicht gelaufen ist, darf weder die Dauersuche
// noch ein Scan-Durchlauf ausgeloest werden - beide sind bis dahin durch das
// Auswahl-Overlay (searchPaused()) bzw. durch busy/den fehlenden Kamerastart
// ohnehin unerreichbar.
let currentModeId = null;
const view = renderScanView(app, {
onCapture: () => processCapture(() => grabFrame(view.video)),
onUndo: () => {
@@ -56,8 +67,44 @@ const view = renderScanView(app, {
openSessionList();
},
onPickFile: (file) => processCapture(() => imageDataFromFile(file)),
onChangeMode: () => {
// Waehrend eine Erkennung laeuft, bleibt auch der Funktionswechsel
// unerreichbar - aus demselben Grund wie bei Rueckgaengig/Sitzungsliste.
if (busy) return;
openModeDialog();
},
});
/**
* Uebernimmt eine (neu gewaehlte) Scan-Funktion: Anzeige, Sichtbarkeit von
* "Modul scannen" und - bei einem Wechsel weg von laufender Codesuche - der
* Zielrahmen wird sofort zurueckgesetzt, statt auf den naechsten Durchlauf
* der Dauersuche zu warten.
* @param {string} modeId
*/
function applyMode(modeId) {
currentModeId = modeId;
const mode = getScanMode(modeId);
view.setMode(mode.label);
// "Modul scannen" hat nur dort etwas zu tun, wo es keine laufende Suche
// gibt (Funktion 3) - siehe Kommentar in scan-view.js/setCaptureVisible.
view.setCaptureVisible(!mode.continuousSearch);
if (!mode.continuousSearch) {
view.setFrameDetected(false);
}
}
/**
* Oeffnet die Funktionswahl (auch fuer den Wechsel aus der laufenden
* Scan-Ansicht heraus, nicht nur beim Start). Solange das Overlay offen ist,
* pausiert die Dauersuche von selbst (siehe searchPaused()).
*/
async function openModeDialog() {
closeOverlays();
const modeId = await askForScanMode(app, { current: currentModeId });
applyMode(modeId);
}
/** Spiegelt den Sitzungsstand in die Oberflaeche und sichert ihn gegen Neuladen ab. */
function syncView() {
view.setStacks(session.stacks);
@@ -98,9 +145,14 @@ async function processCapture(getFrame) {
(stack) => Array.isArray(stack.codes) && stack.codes.includes(normalized),
);
};
// Welcher Adapter tatsaechlich etwas tut, haengt von der gewaehlten
// Scan-Funktion ab (siehe scan-modes.js/scan-recognition.js) - recognize()
// selbst (pipeline.js) bleibt unveraendert und kennt keine Funktionen.
const { decodeBarcodes: decode, runOcr: recognizeText } =
buildRecognitionAdapters(currentModeId, { decodeBarcodes, runOcr });
const { spec, source, confidence, codes } = await recognize(
frame,
{ decodeBarcodes, runOcr, isKnownCode },
{ decodeBarcodes: decode, runOcr: recognizeText, isKnownCode },
);
const plan = proposeAssignment(targetSession, spec, codes);
@@ -218,6 +270,11 @@ function searchPaused() {
* nichts tun.
*/
async function runSearchAttempt() {
// Funktion 3 (Text erkennen) hat keine laufende Suche - siehe scan-modes.js.
// Ein Wechsel dorthin setzt den Rahmen bereits in applyMode() zurueck;
// hier genuegt es, schlicht nichts zu tun.
if (!getScanMode(currentModeId).continuousSearch) return;
if (searchPaused()) {
view.setFrameDetected(false);
return;
@@ -235,7 +292,7 @@ async function runSearchAttempt() {
let codes;
try {
codes = await decodeBarcodes(region);
codes = await decodeBarcodes(region, getScanMode(currentModeId).barcodeFormats);
} catch {
view.setFrameDetected(false);
return;
@@ -331,10 +388,14 @@ function openSessionList() {
});
}
/** Startet die Kamera und, sobald sie brauchbare Bilder liefert, die Dauersuche. */
function startCameraAndSearch() {
startCamera(view.video)
.then(() => {
// Erst ab hier liefert die Kamera brauchbare Bilder - die Dauersuche
// laeuft fortan im Hintergrund, ohne dass der Nutzer etwas tun muss.
// runSearchAttempt() selbst haelt sich zurueck, wenn die gewaehlte
// Funktion keine laufende Suche vorsieht (Funktion 3).
continuousSearchLoop();
})
.catch((error) => {
@@ -345,6 +406,7 @@ startCamera(view.video)
view.setStatus(cameraUnavailableMessage, true);
view.setFilePickerVisible(true);
});
}
/**
* Bietet eine beim Start gefundene, gesicherte Sitzung zur Fortsetzung an,
@@ -367,4 +429,18 @@ async function initResumeOffer() {
syncView();
}
initResumeOffer();
/**
* Ablauf beim Start: erst - falls vorhanden - die Frage nach dem Fortsetzen
* einer gesicherten Sitzung, danach die Wahl der Scan-Funktion, und erst
* danach die Kamera. So sieht der Nutzer die Funktionswahl nie, bevor die
* Fortsetzen-Frage (falls noetig) beantwortet ist, und die Kamera startet
* nie, bevor feststeht, welche Codearten sie ueberhaupt suchen soll.
*/
async function boot() {
await initResumeOffer();
const modeId = await askForScanMode(app);
applyMode(modeId);
startCameraAndSearch();
}
boot();
+68
View File
@@ -0,0 +1,68 @@
// Die drei Scan-Funktionen, die der Nutzer vor dem Scannen waehlt. Reines
// Modul ohne Browser-Zugriff (kein window/document/navigator) - einzige
// Quelle fuer Kennung, Beschriftung, Codearten und Verhalten je Funktion.
// Sowohl die Auswahl-Oberflaeche als auch main.js lesen ausschliesslich von
// hier - es darf keine zweite Stelle geben, an der Codearten oder
// Beschriftungen stehen.
//
// Ausdrueckliche Entscheidung des Auftraggebers (nicht abaendern): Funktion 2
// liest streng nur QR (QRCode, MicroQRCode, RMQRCode) - kein DataMatrix, auch
// wenn die 2D-Codes auf den eigenen RAM-Etiketten DataMatrix sind. Der
// Arbeitsablauf des Auftraggebers laeuft ueber Funktion 1 (Teilenummer im
// Strichcode); Funktion 2 dient anderer Ware.
/**
* @typedef {object} ScanMode
* @property {string} id Kennung, intern verwendet (main.js, Speicherung der Wahl).
* @property {string} label Deutsche Beschriftung fuer die Oberflaeche.
* @property {string[]} barcodeFormats An decodeBarcodes() zu uebergebende
* Codearten (zxing-wasm-Formatnamen). Leer, wenn die Funktion keine
* Barcode-Dekodierung nutzt.
* @property {boolean} continuousSearch Ob waehrend die Kamera laeuft
* fortwaehrend nach Codes gesucht wird (statt nur auf Knopfdruck).
* @property {boolean} useOcr Ob die Texterkennung (Tesseract) genutzt wird.
*/
/** @type {ScanMode[]} */
export const SCAN_MODES = [
{
id: 'barcode',
label: 'Strichcode',
// Eindimensionale Codes, wie sie auf Teilenummern-Etiketten vorkommen.
barcodeFormats: ['Code128', 'Code39', 'Code93', 'ITF', 'EAN13', 'EAN8', 'UPCA', 'UPCE', 'Codabar'],
continuousSearch: true,
useOcr: false,
},
{
id: 'qrcode',
label: 'QR-Code',
// Ausschliesslich QR-Varianten - bewusst kein DataMatrix, siehe Kommentar oben.
barcodeFormats: ['QRCode', 'MicroQRCode', 'RMQRCode'],
continuousSearch: true,
useOcr: false,
},
{
id: 'ocr',
label: 'Text erkennen',
// Keine Codes - allein die Texterkennung, auf Knopfdruck.
barcodeFormats: [],
continuousSearch: false,
useOcr: true,
},
];
/**
* Liefert die Funktion zu einer Kennung.
* @param {string} id
* @returns {ScanMode}
* @throws {Error} wenn die Kennung keiner der drei Funktionen entspricht -
* ein Programmfehler (main.js darf nie eine fremde Kennung durchreichen),
* kein regulaerer Ausgang.
*/
export function getScanMode(id) {
const mode = SCAN_MODES.find((candidate) => candidate.id === id);
if (!mode) {
throw new Error(`Unbekannte Scan-Funktion: ${id}`);
}
return mode;
}
+34
View File
@@ -0,0 +1,34 @@
// Baut die Adapter fuer recognize() (pipeline.js) anhand der gewaehlten
// Scan-Funktion (siehe scan-modes.js). Reines Modul ohne Browser-Zugriff -
// direkt unter Node pruefbar. pipeline.js selbst bleibt unveraendert: die
// Funktionen unterscheiden sich ausschliesslich darin, welche Adapter
// hineingereicht werden.
//
// Funktionen 1/2 (laufende Codesuche): echte Barcode-Dekodierung mit den zur
// Funktion gehoerenden Codearten; die Texterkennung liefert sofort leeren
// Text, ohne dass runOcr ueberhaupt aufgerufen wird.
// Funktion 3 (Text erkennen): echte Texterkennung; die Barcode-Dekodierung
// liefert sofort eine leere Liste, ohne dass decodeBarcodes ueberhaupt
// aufgerufen wird.
import { getScanMode } from './scan-modes.js';
/**
* @param {string} modeId Kennung aus scan-modes.js.
* @param {{decodeBarcodes: Function, runOcr: Function}} adapters echte,
* browser-gebundene Adapter (aus barcode.js / ocr.js).
* @returns {{decodeBarcodes: Function, runOcr: Function}} an recognize()
* durchzureichende Adapter, passend zur gewaehlten Funktion.
*/
export function buildRecognitionAdapters(modeId, adapters) {
const mode = getScanMode(modeId);
return {
decodeBarcodes: mode.useOcr
? async () => []
: (imageData) => adapters.decodeBarcodes(imageData, mode.barcodeFormats),
runOcr: mode.useOcr
? adapters.runOcr
: async () => '',
};
}
+30
View File
@@ -21,6 +21,19 @@ body {
#app { display: flex; flex-direction: column; height: 100dvh; }
.mode-button {
min-height: 56px;
width: 100%;
border: none;
border-bottom: 1px solid var(--line);
background: #1c2027;
color: var(--fg);
font-size: 15px;
font-weight: 600;
text-align: left;
padding: 0 14px;
}
.camera { position: relative; flex: 1; overflow: hidden; background: #000; }
.camera video { width: 100%; height: 100%; object-fit: cover; }
@@ -115,5 +128,22 @@ body {
.choices { display: grid; gap: 12px; width: 100%; max-width: 420px; }
.choices button { min-height: 64px; font-size: 20px; }
/* Auswahl der Scan-Funktion vor dem Scannen (mode-dialog.js): drei grosse,
gleich gestaltete Flaechen statt schmaler Schaltflaechen. */
.mode-tile {
min-height: 96px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
padding: 12px;
}
.mode-tile-label { font-size: 24px; font-weight: 700; }
.mode-tile-hint { font-size: 14px; opacity: 0.8; }
.mode-tile-current { outline: 3px solid var(--fg); outline-offset: -3px; }
.status { padding: 8px 12px; font-size: 14px; color: var(--muted); }
.status.warn { color: #f0b429; }
+95
View File
@@ -0,0 +1,95 @@
import { SCAN_MODES } from '../scan-modes.js';
/**
* Kurzer Hinweis je Funktion, direkt aus ihren Merkmalen abgeleitet (siehe
* scan-modes.js) - keine zweite Stelle mit eigenem Text je Funktion.
*/
function describeMode(mode) {
if (mode.useOcr) return 'Auf Knopfdruck';
return 'Läuft von selbst, sobald ein Code im Bild ist';
}
/**
* Vollbild-Auswahl der Scan-Funktion: drei grosse Flaechen, eine je Funktion
* aus scan-modes.js (einzige Quelle fuer Kennung und Beschriftung). Gebaut
* nach demselben Muster wie die anderen Vollbild-Dialoge (ambiguous-dialog.js,
* resume-dialog.js): Geschwister waehrend der Anzeige per inert unerreichbar,
* Fokus kehrt danach zurueck. Reine Darstellung - kennt weder Kamera noch
* Erkennung.
*
* Wird sowohl vor dem ersten Start der Kamera verwendet als auch fuer den
* Wechsel aus der laufenden Scan-Ansicht heraus (siehe main.js).
*
* @param {HTMLElement} root
* @param {{current?: string}} [args] Kennung der aktuell gewaehlten Funktion,
* falls es sich um einen Wechsel handelt - hebt die passende Flaeche hervor.
* @returns {Promise<string>} Kennung der gewaehlten Funktion (siehe scan-modes.js).
*/
export function askForScanMode(root, { current } = {}) {
const overlay = document.createElement('div');
overlay.className = 'overlay';
overlay.style.background = 'var(--bg)';
overlay.setAttribute('role', 'dialog');
overlay.setAttribute('aria-modal', 'true');
overlay.setAttribute('aria-labelledby', 'mode-dialog-heading');
overlay.innerHTML = `
<div class="stack-id" id="mode-dialog-heading" style="font-size:28px">Funktion wählen</div>
<div class="choices mode-choices" id="choices"></div>
`;
const choices = overlay.querySelector('#choices');
// Siehe ambiguous-dialog.js / resume-dialog.js: waehrend der Dialog offen
// ist, werden die Geschwisterelemente per inert unerreichbar gemacht und
// danach wieder freigegeben.
const previouslyFocused = document.activeElement;
const madeInert = [];
for (const sibling of root.children) {
if (!sibling.hasAttribute('inert')) {
sibling.setAttribute('inert', '');
madeInert.push(sibling);
}
}
root.appendChild(overlay);
return new Promise((resolve) => {
let settled = false;
const finish = (modeId) => {
if (settled) return;
settled = true;
overlay.remove();
for (const sibling of madeInert) {
sibling.removeAttribute('inert');
}
if (
previouslyFocused &&
typeof previouslyFocused.focus === 'function' &&
document.contains(previouslyFocused)
) {
previouslyFocused.focus();
} else {
root.focus?.();
}
resolve(modeId);
};
let firstButton = null;
for (const mode of SCAN_MODES) {
const button = document.createElement('button');
button.className = 'action mode-tile';
if (mode.id === current) button.classList.add('mode-tile-current');
button.setAttribute('aria-pressed', String(mode.id === current));
button.innerHTML = `
<span class="mode-tile-label">${mode.label}</span>
<span class="mode-tile-hint"></span>
`;
button.querySelector('.mode-tile-hint').textContent = describeMode(mode);
button.addEventListener('click', () => finish(mode.id));
choices.appendChild(button);
firstButton = firstButton ?? button;
}
firstButton?.focus();
});
}
+29 -3
View File
@@ -4,14 +4,15 @@ import { FRAME_INSET } from '../camera.js';
* Baut die Hauptansicht auf und liefert Aktualisierungsfunktionen zurueck.
* Kennt weder Kamera noch Erkennung - alles kommt ueber die Rueckrufe.
*/
export function renderScanView(root, { onCapture, onUndo, onOpenList, onPickFile }) {
export function renderScanView(root, { onCapture, onUndo, onOpenList, onPickFile, onChangeMode }) {
root.innerHTML = `
<button class="mode-button" id="mode-button" type="button"></button>
<div class="camera">
<video id="preview" playsinline muted></video>
<div class="frame"></div>
</div>
<div class="status" id="status"></div>
<button class="action capture" id="capture">Modul scannen</button>
<button class="action capture" id="capture" hidden>Modul scannen</button>
<button class="action secondary" id="pick-file-button" hidden>Bild auswählen</button>
<div class="stacks" id="stacks"></div>
<div class="last">
@@ -27,6 +28,8 @@ export function renderScanView(root, { onCapture, onUndo, onOpenList, onPickFile
const fileEl = root.querySelector('#file');
const pickFileButtonEl = root.querySelector('#pick-file-button');
const frameEl = root.querySelector('.frame');
const captureEl = root.querySelector('#capture');
const modeButtonEl = root.querySelector('#mode-button');
// FRAME_INSET (camera.js) ist die einzige Quelle der Rahmenmasse - sowohl
// fuer diesen sichtbaren Rahmen als auch fuer den nativen Ausschnitt
@@ -36,8 +39,9 @@ export function renderScanView(root, { onCapture, onUndo, onOpenList, onPickFile
`${FRAME_INSET.top * 100}% ${FRAME_INSET.right * 100}% ` +
`${FRAME_INSET.bottom * 100}% ${FRAME_INSET.left * 100}%`;
root.querySelector('#capture').addEventListener('click', onCapture);
captureEl.addEventListener('click', onCapture);
root.querySelector('#undo').addEventListener('click', onUndo);
modeButtonEl.addEventListener('click', onChangeMode);
// Klick auf die sichtbare Schaltflaeche ist selbst die Nutzergeste, die
// die versteckte Dateiauswahl oeffnen darf - anders als ein Aufruf aus dem
// Code heraus, den Handy-Browser blockieren.
@@ -74,6 +78,28 @@ export function renderScanView(root, { onCapture, onUndo, onOpenList, onPickFile
lastEl.textContent = text;
},
/**
* Zeigt die gewaehlte Scan-Funktion in der Ansicht an - antippbar, um zu
* wechseln (siehe onChangeMode). Beschriftung kommt vom Aufrufer, der sie
* seinerseits aus scan-modes.js liest; diese Ansicht kennt die Funktionen
* selbst nicht.
* @param {string} label
*/
setMode(label) {
modeButtonEl.textContent = `${label} ändern ⌄`;
modeButtonEl.setAttribute('aria-label', `Scan-Funktion wechseln (aktuell: ${label})`);
},
/**
* "Modul scannen" hat nur in Funktionen mit Texterkennung auf Knopfdruck
* etwas zu tun (Funktion 3) - in Funktionen mit laufender Codesuche sucht
* die App ohnehin durchgehend, ein Knopf dafuer waere wirkungslos.
* @param {boolean} visible
*/
setCaptureVisible(visible) {
captureEl.hidden = !visible;
},
/** @param {string} text @param {boolean} warn */
setStatus(text, warn = false) {
statusEl.textContent = text;
+39
View File
@@ -0,0 +1,39 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { SCAN_MODES, getScanMode } from '../src/scan-modes.js';
test('genau drei Funktionen in fester Reihenfolge', () => {
assert.equal(SCAN_MODES.length, 3);
assert.deepEqual(SCAN_MODES.map((mode) => mode.id), ['barcode', 'qrcode', 'ocr']);
});
test('Funktion 1 (Strichcode): eindimensionale Codearten, laufende Suche, keine Texterkennung', () => {
const mode = getScanMode('barcode');
assert.equal(mode.label, 'Strichcode');
assert.deepEqual(mode.barcodeFormats, [
'Code128', 'Code39', 'Code93', 'ITF', 'EAN13', 'EAN8', 'UPCA', 'UPCE', 'Codabar',
]);
assert.equal(mode.continuousSearch, true);
assert.equal(mode.useOcr, false);
});
test('Funktion 2 (QR-Code): ausschliesslich QR-Varianten, kein DataMatrix', () => {
const mode = getScanMode('qrcode');
assert.equal(mode.label, 'QR-Code');
assert.deepEqual(mode.barcodeFormats, ['QRCode', 'MicroQRCode', 'RMQRCode']);
assert.ok(!mode.barcodeFormats.includes('DataMatrix'), 'Funktion 2 darf kein DataMatrix lesen');
assert.equal(mode.continuousSearch, true);
assert.equal(mode.useOcr, false);
});
test('Funktion 3 (Text erkennen): keine Codearten, keine laufende Suche, Texterkennung', () => {
const mode = getScanMode('ocr');
assert.equal(mode.label, 'Text erkennen');
assert.deepEqual(mode.barcodeFormats, []);
assert.equal(mode.continuousSearch, false);
assert.equal(mode.useOcr, true);
});
test('unbekannte Kennung wirft statt undefined zurueckzugeben', () => {
assert.throws(() => getScanMode('unbekannt'));
});
+79
View File
@@ -0,0 +1,79 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { buildRecognitionAdapters } from '../src/scan-recognition.js';
import { recognize } from '../src/pipeline.js';
test('Funktion 1: Texterkennung wird nicht aufgerufen, auch wenn kein Code gefunden wird', async () => {
let ocrCalled = false;
const adapters = buildRecognitionAdapters('barcode', {
decodeBarcodes: async () => [],
runOcr: async () => { ocrCalled = true; return 'irgendein Text'; },
});
const result = await recognize({}, adapters);
assert.equal(ocrCalled, false, 'runOcr darf in Funktion 1 nie aufgerufen werden');
assert.equal(result.confidence, 'red');
});
test('Funktion 2: Texterkennung wird nicht aufgerufen, auch wenn kein Code gefunden wird', async () => {
let ocrCalled = false;
const adapters = buildRecognitionAdapters('qrcode', {
decodeBarcodes: async () => [],
runOcr: async () => { ocrCalled = true; return 'irgendein Text'; },
});
const result = await recognize({}, adapters);
assert.equal(ocrCalled, false, 'runOcr darf in Funktion 2 nie aufgerufen werden');
assert.equal(result.confidence, 'red');
});
test('Funktion 3: Barcode-Dekodierung wird nicht aufgerufen', async () => {
let barcodeCalled = false;
const adapters = buildRecognitionAdapters('ocr', {
decodeBarcodes: async () => { barcodeCalled = true; return []; },
runOcr: async () => '64GB 4DRx4 PC4-2400T-LD1-11-MC0 M386A8K40BM1-CRC4Y 1908',
});
const result = await recognize({}, adapters);
assert.equal(barcodeCalled, false, 'decodeBarcodes darf in Funktion 3 nie aufgerufen werden');
assert.equal(result.source, 'ocr');
assert.equal(result.confidence, 'yellow');
});
test('Funktion 1: ein gelesener Strichcode fuehrt zu Gruen', async () => {
const adapters = buildRecognitionAdapters('barcode', {
decodeBarcodes: async () => ['M386A8K40BM1-CRC4Y'],
runOcr: async () => '',
});
const result = await recognize({}, adapters);
assert.equal(result.source, 'barcode');
assert.equal(result.confidence, 'green');
});
test('Funktion 3: kein brauchbarer Text fuehrt zu Rot, nicht zu Gruen', async () => {
const adapters = buildRecognitionAdapters('ocr', {
decodeBarcodes: async () => { throw new Error('darf nicht aufgerufen werden'); },
runOcr: async () => 'Made in Philippines',
});
const result = await recognize({}, adapters);
assert.equal(result.source, 'none');
assert.equal(result.confidence, 'red');
});
test('decodeBarcodes bekommt die Codearten der jeweiligen Funktion uebergeben', async () => {
let receivedFormats = null;
const adapters = buildRecognitionAdapters('qrcode', {
decodeBarcodes: async (_imageData, formats) => { receivedFormats = formats; return []; },
runOcr: async () => '',
});
await adapters.decodeBarcodes({});
assert.deepEqual(receivedFormats, ['QRCode', 'MicroQRCode', 'RMQRCode']);
});
test('Funktion 3 uebergibt keine Codearten, weil decodeBarcodes gar nicht aufgerufen wird', async () => {
let called = false;
const adapters = buildRecognitionAdapters('ocr', {
decodeBarcodes: async () => { called = true; return []; },
runOcr: async () => '',
});
const codes = await adapters.decodeBarcodes({});
assert.deepEqual(codes, []);
assert.equal(called, false);
});