Beschreibt die neue Aufnahme-Schaltfläche ("Zählen"/"Nochmal zählen"), die
fünf Bilder je Aufnahme samt Median, die um die Einzelmessungen erweiterte
Diagnosezeile und das Zurücksetzen beim Verlassen der Funktion. Aufbau-
Tabelle, Testanzahl und der Prüfabschnitt "Am Gerät noch zu prüfen" folgen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
733 lines
48 KiB
Markdown
733 lines
48 KiB
Markdown
# RAM-Sortierhilfe
|
||
|
||
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 vier Funktionen — Strichcode,
|
||
QR-Code, Text erkennen oder Zählen (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. Die vierte Funktion, „Zählen", gehört nicht zum eigentlichen Sortieren:
|
||
Sie bucht nichts, sondern ermittelt auf Knopfdruck beim Auslegen von
|
||
Kleinteilen (z. B. Festplattenschrauben) deren Anzahl und zeigt sie an, bis
|
||
der Nutzer erneut antippt.
|
||
|
||
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
|
||
*aktuell* ihm zugeordneten Modulen gelesen wurden (`stack.codes`); diese Menge
|
||
wird nicht angesammelt, sondern aus den Codes der zugeordneten Einträge
|
||
(`entry.codes`) abgeleitet — genau wie der Stapelzähler (siehe „Sitzungssicherung
|
||
im Detail" unten). Sortiert der Nutzer ein Modul um oder entfernt es, verliert
|
||
der alte Stapel dessen Codes deshalb sofort wieder; ein bereinigter Fehler
|
||
zieht kein weiteres Modul mehr fälschlich an. Trägt ein neu gescanntes Modul
|
||
einen Code, den ein Stapel bereits kennt, ist dieser Stapel Kandidat — aber
|
||
nur, wenn die technischen Angaben beider Seiten sich dabei nicht widersprechen
|
||
(`specsCompatible` in `src/spec.js`, siehe unten); ein Codetreffer allein
|
||
genügt nicht, sonst könnte ein Code, der gar keine Teilenummer ist, zwei
|
||
technisch verschiedene Module zusammenführen (siehe „Grenzen"). Genau ein
|
||
solcher Kandidat gehört dorthin, auch wenn die technischen Angaben
|
||
unvollständig sind (`proposeAssignment` in `src/session.js`). Auf Etiketten
|
||
stehen oft zwei Codes, von denen einer die Seriennummer ist, die bei jedem
|
||
Modul anders ist: Sie läuft beim Codeabgleich einfach ins Leere, während die
|
||
danebenstehende Teilenummer trägt — kein Sonderfall, nur eine Konsequenz des
|
||
Mengenvergleichs.
|
||
|
||
Kennt kein Stapel einen der gelesenen Codes ohne Widerspruch (typischerweise
|
||
beim ersten Modul eines neuen Stapels, aber auch dann, wenn ein Codetreffer an
|
||
der Widerspruchsprüfung scheitert), vergleicht die App stattdessen über alle
|
||
Merkmale, die
|
||
bei Modul und Stapel bekannt sind — Kapazität, Bauform, Rank, Geschwindigkeit
|
||
und, sofern gelesen, Hersteller-Teilenummer (`specsCompatible` in
|
||
`src/spec.js`); ein auf einer Seite fehlendes Merkmal wird dabei übersprungen,
|
||
nicht als Widerspruch gewertet. Ein Stapel gilt dabei nur als Kandidat, wenn
|
||
er mit dem Modul mindestens ein tatsächlich erkanntes Merkmal teilt
|
||
(`sharesSetField` in `src/session.js`) — ein Stapel ganz ohne erkannte Angaben
|
||
(etwa aus einem manuell entschiedenen Rot-Fall) ist deshalb nie automatisch
|
||
treffend, sonst würde er lautlos jedes folgende Modul an sich ziehen. Passt
|
||
danach genau ein Stapel, wird das Modul zugewiesen; passen mehrere oder mehrere
|
||
über einen bekannten Code, fragt die App nach. Das hat eine bewusst in Kauf
|
||
genommene Folge: Konnte die Teilenummer nicht gelesen werden — der häufige
|
||
Fall bei überklebten oder beschädigten Etiketten — und passt anhand der
|
||
übrigen Merkmale nur ein Stapel, landet das Modul dort, auch wenn seine
|
||
tatsächliche Teilenummer eine andere wäre. Diese Abwägung ist Absicht: Ohne
|
||
sie müsste die App bei jedem zweiten Modul nachfragen, sobald die
|
||
Texterkennung keine Teilenummer liefert.
|
||
|
||
Die Erkennung läuft vollständig lokal im Browser — kein Server, keine Cloud,
|
||
keine Daten verlassen das Gerät.
|
||
|
||
## Funktionsweise
|
||
|
||
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 vier
|
||
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 Codesuche | Texterkennung |
|
||
|---|---|---|---|
|
||
| **Strichcode** | Code128, Code39, Code93, ITF, EAN-13, EAN-8, UPC-A, UPC-E, Codabar | ja | nein |
|
||
| **QR-Code** | QRCode, MicroQRCode, RMQRCode, DataMatrix, Aztec, PDF417 | ja | nein |
|
||
| **Text erkennen** | keine Codes | nein | ja, auf Knopfdruck |
|
||
| **Zählen** | keine Codes | nein | nein, dafür Zählung auf Knopfdruck (keine Buchung) |
|
||
|
||
„QR-Code" heißt auf Wunsch des Auftraggebers „QR-Code", liest aber bewusst
|
||
nicht nur QR-Varianten, sondern auch DataMatrix, Aztec und PDF417. Der
|
||
Grund: Die 2D-Codes auf den RAM-Etiketten sind DataMatrix, nicht QR — eine
|
||
auf QR beschränkte Funktion fände auf dieser Ware nichts. Die interne
|
||
Kennung heißt weiterhin `qrcode` (aus historischen Gründen, siehe
|
||
`src/scan-modes.js`), und der Name ist Absicht — es ist der Wunsch des
|
||
Auftraggebers für seinen Sprachgebrauch.
|
||
|
||
Eine einzige Aufnahme-Schaltfläche bedient beide Knopfdruck-Funktionen —
|
||
„Modul scannen" in „Text erkennen", „Zählen" bzw. (nach der ersten
|
||
Aufnahme seit Betreten der Funktion) „Nochmal zählen" in „Zählen" — statt
|
||
einer zweiten Schaltfläche daneben; nur Wortlaut und Rückruf unterscheiden
|
||
sich
|
||
(`setCaptureLabel` in `src/ui/scan-view.js`, verdrahtet in
|
||
`src/main.js`/`applyMode()`). In „Strichcode" und „QR-Code" bleibt sie
|
||
verborgen — dort sucht die App ohnehin laufend, ein Knopf hätte nichts zu
|
||
tun. Der Zielrahmen hebt sich in „Strichcode" und „QR-Code" hervor, sobald
|
||
ein passender Code im Bild ist; in „Text erkennen" und „Zählen" bleibt er
|
||
schlicht, weil dort nichts laufend erkannt wird. Trotzdem ist der Rahmen
|
||
auch dort wirksam: Sowohl „Modul scannen" als auch „Zählen" lesen genau den
|
||
Bildausschnitt innerhalb des Rahmens, in voller Kameraauflösung, nicht das
|
||
gesamte, heruntergerechnete Kamerabild (`grabFrameRegion` in
|
||
`src/camera.js` — derselbe Ausschnitt, den auch die laufende Barcode-Suche
|
||
benutzt). Ein Wechsel der Funktion während des Sortierens (z. B. bei
|
||
gemischter Ware) gilt sofort: laufende Suche startet oder stoppt, die
|
||
Schaltfläche erscheint, verschwindet oder ändert ihren Wortlaut.
|
||
|
||
Liegt beim Start eine gesicherte Sitzung vor, erscheint zuerst die Frage
|
||
nach dem Fortsetzen (siehe Punkt 6 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; „Zählen" bekommt beides leer hineingereicht
|
||
(`buildRecognitionAdapters` in `src/scan-recognition.js`). Die
|
||
Erkennungs-Pipeline selbst (`src/pipeline.js`, Punkte 1–3 und 5 unten)
|
||
bleibt dabei unverändert und kennt keine Funktionen — die drei
|
||
code-/textbasierten Funktionen unterscheiden sich ausschließlich darin,
|
||
welche Adapter ihr übergeben werden. „Zählen" ruft diese Pipeline im
|
||
normalen Betrieb gar nicht erst auf (siehe Punkt 4 unten) — es gibt dort ja
|
||
nichts zu buchen, das leere Adapterpaar dient nur als Absicherung für den
|
||
seltenen Ersatzweg ohne Kamera (Punkt 7).
|
||
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
|
||
(`recognize` in `src/pipeline.js`). Findet die anschließende Texterkennung
|
||
dazu auch keine Kapazität, ist das Ergebnis trotzdem grün, nur eben ohne
|
||
abgeleitete Angaben in der Rückmeldung. Ist einer der gelesenen Codes
|
||
bereits einem Stapel dieser Sitzung bekannt, steht die Zuordnung ohnehin
|
||
fest — dann entfällt auch die Texterkennung, weil sie nichts mehr beitragen
|
||
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. **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). Gelesen wird der
|
||
Bildausschnitt innerhalb des Zielrahmens, in voller Kameraauflösung
|
||
(`grabFrameRegion`, siehe Punkt 0) — nicht mehr das ganze, auf 1280 Pixel
|
||
heruntergerechnete Kamerabild. Das Bild 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. Die Zeichen-Whitelist von
|
||
Tesseract (`tessedit_char_whitelist` in `src/ocr.js`) lässt Groß- **und**
|
||
Kleinbuchstaben, Ziffern sowie die auf Etiketten üblichen Satz- und
|
||
Sonderzeichen zu — nicht mehr nur Großbuchstaben. Das OCR-Ergebnis wird
|
||
anschließend gegen die bekannten Spec-Werte abgeglichen, was die
|
||
typischen Verwechslungen (0/O, 1/I, 8/B, …) auflöst; das kann durch die
|
||
breitere Whitelist bei den Codeteilen (Kapazität, Rank, Geschwindigkeit,
|
||
Teilenummer) etwas an Genauigkeit kosten, weil Tesseract jetzt z. B.
|
||
zwischen „O“ und „o“ unterscheiden muss — eine bewusst in Kauf genommene
|
||
Abwägung, damit der vollständige Etikettentext lesbar wird. 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.
|
||
|
||
Der vollständige, ungefilterte Erkennungstext (nicht nur die daraus
|
||
verstandenen Felder) wird zusätzlich mitgeführt (`recognize` in
|
||
`src/pipeline.js` liefert ihn als `rawText` zurück) und je Eintrag
|
||
gespeichert — rein additiv, ohne jeden Einfluss auf die Stapelzuordnung
|
||
(siehe „Grenzen" unten). In Funktion „Text erkennen" bleibt die
|
||
Treffer-Rückmeldung deshalb stehen, bis der Nutzer sie wegtippt, und zeigt
|
||
den vollen erkannten Text an, statt sich nach gut einer Sekunde von selbst
|
||
auszublenden wie in den beiden Barcode-Funktionen (`showResult` in
|
||
`src/ui/result-overlay.js`). Der Rohtext jedes Eintrags lässt sich später
|
||
auch in der Sitzungsliste nachlesen.
|
||
4. **Zählen** (ausschließlich in Funktion „Zählen"). Reine Anzeige, ohne
|
||
jeden Bezug zur Sitzung — und, anders als die drei übrigen Funktionen mit
|
||
laufender Erkennung, auf **Aufnahme statt Dauerbetrieb**: Eine Zählung ist
|
||
eine Schätzung, kein eindeutiger Treffer wie ein Barcode; sie mehrmals pro
|
||
Sekunde neu anzuzeigen macht sie nicht genauer, nur unruhig (am
|
||
Nutzergerät sprang die Anzeige im Dauerbetrieb zwischen etwa 3 und 60, an
|
||
einem Standfoto derselben Teile lieferte dasselbe Verfahren stabil 18).
|
||
Antippen der Schaltfläche „Zählen“/„Nochmal zählen“ nimmt deshalb binnen
|
||
rund einer Sekunde fünf Bilder aus dem Zielrahmen des laufenden
|
||
Videobilds auf (`grabFrameRegion` in `src/camera.js` — bewusst nicht die
|
||
native Fotoaufnahme des Geräts, deren Bildausschnitt von der Vorschau
|
||
abweichen kann), zählt jedes einzeln (`countObjects` in
|
||
`src/count-objects.js`, unverändert) und zeigt als Ergebnis deren Median
|
||
(`combineCaptureCounts` in `src/count-capture.js`) — robust gegen ein
|
||
einzelnes verwackeltes oder mitten in eine Fokusregelung fallendes Bild,
|
||
ohne die frühere fortlaufende Glättung über die letzten neun Messungen
|
||
(`count-history.js`, entfallen: eine Aufnahme mit fünf Bildern leistet
|
||
dieselbe Robustheit bereits selbst). Während der rund einen Sekunde zeigt
|
||
die Statuszeile „zähle …“, damit der Nutzer die Kamera ruhig hält und den
|
||
Knopfdruck nicht für wirkungslos hält (`runCountCapture` in
|
||
`src/main.js`). Das Ergebnis bleibt danach stehen, bis der Nutzer erneut
|
||
antippt — verlässt er die Funktion oder wechselt zu einer anderen, werden
|
||
Ergebnis und Diagnosezeile zurückgesetzt. Es gibt in dieser Funktion keine
|
||
Treffer-Rückmeldung und keine Stapel-Zuweisung; die Zahl wird nirgends
|
||
gespeichert, nicht einmal flüchtig für die Dauer der Sitzung. Die
|
||
Diagnosezeile unter der großen Zahl (Antippen der Zahl blendet sie ein
|
||
oder aus) zeigt zusätzlich zu den übrigen Zwischenwerten (die sich auf die
|
||
*letzte* der fünf Einzelmessungen beziehen) die fünf Einzelmessungen
|
||
dieser Aufnahme selbst — daran erkennen Nutzer und Entwickler sofort, ob
|
||
das Ergebnis belastbar ist: Fünf Messungen von 17 bis 19 bedeuten etwas
|
||
anderes als fünf Messungen von 4 bis 50.
|
||
|
||
Kein Bilderkennungsmodell: Der **Otsu-Schwellwert** (`threshold.js`),
|
||
den die Texterkennung weiterhin nutzt, taugt für „Zählen" nicht — er
|
||
setzt voraus, dass Objekt- und Untergrundfläche etwa gleich groß sind.
|
||
Am echten Foto des Auftraggebers (rund zwanzig silberne
|
||
Festplattenschrauben auf dunklem Leder) belegen die Schrauben unter
|
||
einem Prozent der Fläche; Otsu rutschte deshalb auf den höchsten
|
||
Helligkeitswert und erklärte nur die hellsten Punkte der Ledernarbung
|
||
zum Objekt (gemessenes Fehlverhalten: 76712 statt rund zwanzig). Über
|
||
die Helligkeit allein sind Schraube und Untergrund an diesem Foto gar
|
||
nicht trennbar.
|
||
|
||
`countObjects` (`src/count-objects.js`) verwendet stattdessen
|
||
**örtlichen Kontrast**: Der Ausschnitt wird auf 800 Bildpunkte Breite
|
||
normiert (schmalere Ausschnitte bleiben unverändert — macht alle
|
||
folgenden Maße unabhängig von der Kameraauflösung), in Graustufen
|
||
gewandelt und mit einem Kastenfilter geglättet (unterdrückt feine
|
||
Untergrundnarbung, über ein Summenbild für radius-unabhängige
|
||
Laufzeit). Ein zweiter, deutlich größerer Kastenmittelwert liefert den
|
||
**örtlichen Hintergrund** an jeder Stelle — das fängt ungleichmäßige
|
||
Ausleuchtung ab. Ein Bildpunkt gilt als Objekt, wenn er um mehr als
|
||
einen festen Betrag heller **oder** dunkler ist als sein örtlicher
|
||
Hintergrund (beide Richtungen werden berechnet; es gewinnt die mit den
|
||
meisten gefundenen Einzelflächen — Details siehe unten). Anschließend
|
||
werden zusammenhängende Objektflächen ermittelt
|
||
(Connected-Component-Labeling mit einer eigenen Arbeitsliste statt
|
||
Rekursion, damit ein rund eine Viertelmillion Bildpunkte großer
|
||
Ausschnitt nicht in den Stapelüberlauf läuft), winzige Flächen als
|
||
Rauschen verworfen und nur Flächen ab einem festen **Anteil der
|
||
größten gefundenen Fläche** behalten — bewusst relativ, nicht absolut,
|
||
damit ein näher oder weiter weg gehaltenes Kamerabild die Zählung nicht
|
||
verstellt. Auffällig große Flächen (mehrere berührende Teile) werden
|
||
auf ein Vielfaches der typischen (Median-)Einzelteilgröße
|
||
hochgerechnet. Details, alle verwendeten Schwellen und die Messung am
|
||
echten Foto: `.superpowers/sdd/counting-rework-report.md`.
|
||
5. **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)
|
||
oder mehrdeutiger Stapelzuordnung fragt die App nach.
|
||
6. **Sitzungsfortsetzung beim Start.** Findet die App beim Laden eine in
|
||
`localStorage` gesicherte, nicht leere Sitzung vor, wird sie nicht
|
||
stillschweigend übernommen, sondern zur Fortsetzung oder zum Verwerfen
|
||
angeboten (`askResumeSession` in `src/ui/resume-dialog.js`, verdrahtet über
|
||
`initResumeOffer()` in `src/main.js`). Erst „fortsetzen“ stellt Stapel,
|
||
Zähler und die „Zuletzt“-Zeile wieder her; „verwerfen“ löscht den
|
||
gesicherten Stand und die App startet leer.
|
||
7. **Ersatzweg ohne Kamera.** Lässt sich die Kamera nicht starten (z. B.
|
||
verweigerte Berechtigung), bleibt die App bedienbar: eine dauerhaft
|
||
sichtbare Schaltfläche „Bild auswählen“ öffnet stattdessen die native
|
||
Dateiauswahl (`setFilePickerVisible` in `src/ui/scan-view.js`). Die App
|
||
versucht danach nicht automatisch erneut, die Kamera zu starten - der
|
||
Kamera-Hinweistext bleibt für den Rest der Sitzung stehen und wird nicht
|
||
von der OCR-Verfügbarkeitsanzeige überschrieben.
|
||
|
||
Barcode- und OCR-Adapter sind gegen hängende Aufrufe abgesichert: Die
|
||
Erkennungs-Pipeline (`src/pipeline.js`) bricht einen Barcode-Versuch nach 10
|
||
Sekunden und einen OCR-Versuch nach 20 Sekunden ab und behandelt das wie ein
|
||
leeres Ergebnis, statt die App blockiert zu lassen. Diese Grenzen sind großzügig
|
||
bemessen (sie müssen auch das einmalige Laden der WebAssembly-/Sprachmodule
|
||
beim ersten Scan einer Sitzung abdecken) und keine Leistungserwartung.
|
||
|
||
## Installation
|
||
|
||
```bash
|
||
npm install
|
||
```
|
||
|
||
## Entwicklung
|
||
|
||
```bash
|
||
npm run dev
|
||
```
|
||
|
||
Der Server bindet an alle Interfaces; VCH stellt eine Preview-URL mit SSL
|
||
bereit. **HTTPS ist Pflicht** — Kamerazugriff (`getUserMedia`) verlangt eine
|
||
gesicherte Verbindung; über eine unverschlüsselte `http://`-Adresse verweigert
|
||
der Browser die Kamera grundsätzlich, unabhängig davon, ob die App selbst
|
||
korrekt funktioniert.
|
||
|
||
## Tests
|
||
|
||
```bash
|
||
npm test
|
||
```
|
||
|
||
179 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, jetzt in
|
||
`threshold.js`) und -Feldextraktion, Stapel-Zuweisung, die Erkennungs-Pipeline
|
||
samt Zeitgrenzen, Mehrdeutigkeitsbehandlung und dem additiven OCR-Rohtext
|
||
(`rawText` wird nur zurückgegeben, wenn die Texterkennung tatsächlich lief,
|
||
und beeinflusst nachweislich nie die Stapelzuordnung), die Sitzungssicherung
|
||
samt Prüfung eines wiederhergestellten Zustands (einschließlich
|
||
`entry.rawText`), die vier Scan-Funktionen (`scan-modes.js` — Codearten je
|
||
Funktion, DataMatrix/Aztec/PDF417 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"/„Zählen"). Dazu die Zähl-Berechnung selbst
|
||
(`count-objects.js` — leeres Bild, einzelne und mehrere getrennte Flächen,
|
||
verworfenes Rauschen, Hochrechnung berührender Teile ohne ein nur leicht
|
||
größeres Einzelteil zu verdoppeln, helle wie dunkle Objekte, ein Bild ohne
|
||
Bildpunkte). Dazu die reine Zusammenfassung der fünf Einzelmessungen einer
|
||
Zaehl-Aufnahme zu deren Median (`count-capture.js` — auch für den Fall
|
||
weniger als fünf verwertbarer Bilder, sowie eine leere Messreihe, die
|
||
regulär wirft statt stillschweigend `NaN` zu liefern). 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
|
||
|
||
Zum lokalen Nachschauen ohne Passwort:
|
||
|
||
```bash
|
||
npm run build
|
||
npm run preview
|
||
```
|
||
|
||
`preview` hört auf `PORT` aus der Umgebung (Vorgabe `4173`, falls `PORT`
|
||
nicht gesetzt ist).
|
||
|
||
Im Betrieb läuft stattdessen `server.py` — siehe nächster Abschnitt.
|
||
|
||
## Passwortschutz
|
||
|
||
Die veröffentlichte Seite ist passwortgeschützt. Geprüft wird **im Server**,
|
||
nicht im Browser: Ohne gültiges Passwort gibt `server.py` nichts heraus —
|
||
weder die Seite noch das gebündelte Skript. Ein Passwortschutz allein im
|
||
Browser wäre wirkungslos, weil das Passwort dann im ausgelieferten Skript
|
||
stünde und dort mitgelesen werden könnte.
|
||
|
||
```bash
|
||
APP_PASSWORD=dein-passwort python3 server.py
|
||
```
|
||
|
||
- Das Passwort kommt ausschließlich aus der Umgebungsvariable `APP_PASSWORD`
|
||
(siehe `.env.example`). Es steht **nicht** im Quelltext.
|
||
- **Fehlt die Variable, startet der Server nicht** und meldet das im Klartext.
|
||
Das ist Absicht: Ein vergessener Eintrag soll auffallen, statt stillschweigend
|
||
eine ungeschützte Seite ins Netz zu stellen.
|
||
- Abgefragt wird per HTTP-Basic-Auth. Der Browser fragt **einmal pro Sitzung**,
|
||
nicht bei jedem Seitenaufruf — beim Sortieren wäre das sonst unbrauchbar.
|
||
- Der **Benutzername ist beliebig**; geprüft wird nur das Passwort. Beim
|
||
Vergleich wird eine zeitkonstante Prüfung verwendet.
|
||
- `/healthz` antwortet **ohne** Passwort mit `ok`. Das braucht die Überwachung
|
||
der Hosting-Umgebung, die den geschützten Dienst sonst für ausgefallen hielte.
|
||
Der Pfad gibt nichts über die Anwendung preis.
|
||
|
||
Das Passwort wandert bei einer Basic-Auth-Anmeldung mit jeder Anfrage über die
|
||
Leitung. Das ist unbedenklich, solange die Seite über HTTPS ausgeliefert wird —
|
||
worauf du bei der Veröffentlichung achten solltest.
|
||
|
||
## Technik
|
||
|
||
- Vanilla JavaScript (ES Modules), kein Framework
|
||
- [Vite](https://vite.dev/) als Entwicklungsserver und Bündler
|
||
- [`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
|
||
|
||
## Aufbau
|
||
|
||
| Datei | Verantwortung |
|
||
|---|---|
|
||
| `src/spec.js` | Spec-Objekt, bekannte Werte, Fingerabdruck, toleranter Vergleich (Verwechslungstabelle für OCR-Fehler) |
|
||
| `src/pn-tables.js` | Herstellertabellen für den Teilenummer-Decoder |
|
||
| `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`) — kennt keine Scan-Funktionen; liefert zusätzlich (additiv) den ungefilterten OCR-Rohtext (`rawText`), leer, wenn keine Texterkennung lief |
|
||
| `src/scan-modes.js` | Die vier Scan-Funktionen (Strichcode, QR-Code, Text erkennen, Zählen): Kennung, deutsche Beschriftung, gelesene Codearten, laufende Codesuche ja/nein, Texterkennung ja/nein — einzige Quelle, sowohl für die Auswahl-Oberfläche als auch für `main.js`. „Zählen" läuft wie „Text erkennen" auf Knopfdruck (`continuousSearch: false`) und braucht dafür kein eigenes Merkmal mehr |
|
||
| `src/scan-recognition.js` | Baut die an `recognize()` übergebenen Adapter anhand der gewählten Funktion: liest eine Funktion keine Codearten (leeres `barcodeFormats`, bei „Text erkennen" und „Zählen"), liefert die Barcode-Dekodierung sofort eine leere Liste ohne `zxing-wasm` anzustoßen; nur bei `useOcr` läuft die echte Texterkennung, sonst liefert sie sofort leeren Text ohne Tesseract anzustoßen (in „Zählen" ruft `recognize()` im normalen Betrieb ohnehin nicht auf — siehe `count-capture.js`) |
|
||
| `src/count-capture.js` | Reine Berechnung für die Aufnahme in Funktion „Zählen": bildet aus den (bis zu fünf) Einzelmessungen einer Aufnahme (`countObjects()` je Bild) deren Median (`combineCaptureCounts`) — robust gegen ein einzelnes verwackeltes oder unscharfes Bild, bleibt auch bei weniger als fünf verwertbaren Bildern sinnvoll |
|
||
| `src/threshold.js` | Otsu-Schwellwertbestimmung (Graustufen, Histogramm, Schwellwert) — reines Modul ohne Browser-Zugriff, aus `ocr.js` herausgezogen; gemeinsame Grundlage für `preprocess()` (ocr.js) und `countObjects()` (count-objects.js) |
|
||
| `src/count-objects.js` | Reine Berechnung für Funktion „Zählen": zählt zusammenhängende Objektflächen in einem Bildausschnitt über örtlichen Kontrast (Normierung auf 800 Bildpunkte Breite, Glättung und örtlicher Hintergrund je über Kastenfilter/Summenbild, Maske aus geglättetem Bild vs. örtlichem Hintergrund ± Marge, beide Polaritäten, Connected-Component-Labeling mit eigener Arbeitsliste, Rauschfilter, Massstab relativ zur größten Fläche, Hochrechnung berührender Teile über den Flächen-Median) — nutzt `threshold.js`/Otsu bewusst **nicht** mehr (siehe „Funktionsweise" oben) |
|
||
| `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); `entry.rawText` ist wie `entry.codes` optional und wird bei Fehlen als leerer Rohtext wiederhergestellt |
|
||
| `src/camera.js` | Kamerastart, Einzelbildaufnahme (herunterskaliert, für die Dateiauswahl), Rahmenausschnitt in voller Auflösung (`grabFrameRegion`, für die laufende Barcode-Suche, „Modul scannen" in „Text erkennen" **und** jedes der fünf Bilder einer Aufnahme in „Zählen"), Bilddatei-Ersatzweg |
|
||
| `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, über `threshold.js`) und Adapter zu `tesseract.js`; Zeichen-Whitelist deckt Groß- und Kleinbuchstaben, Ziffern sowie übliche Etiketten-Sonderzeichen ab |
|
||
| `src/ui/scan-view.js` | Scan-Ansicht: Kamera-Vorschau, antippbare Anzeige der gewählten Funktion (Wechsel), eine gemeinsame Aufnahme-Schaltfläche für beide Knopfdruck-Funktionen (sichtbar in „Text erkennen" und „Zählen", Wortlaut je nach Funktion und Zustand über `setCaptureLabel`), große Zählanzeige samt Diagnosezeile (nur in „Zählen" 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: vier große Flächen, eine je Funktion aus `src/scan-modes.js` |
|
||
| `src/ui/result-overlay.js` | Treffer-Rückmeldung (grün/gelb): blendet sich in „Strichcode"/„QR-Code" nach kurzer Zeit selbst aus; in „Text erkennen" bleibt sie stehen, zeigt den vollen OCR-Rohtext und wartet auf eine Eingabe, bevor sie schließt |
|
||
| `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 — läuft vor der Funktionswahl |
|
||
| `src/ui/session-list.js` | Sitzungsliste: Stapel-Übersicht, Umsortieren, Entfernen, Sitzung beenden, Anzeige des vollständigen OCR-Rohtexts je Eintrag (sofern vorhanden) |
|
||
| `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 |
|
||
| `src/styles.css` | Farbvariablen (Grün/Gelb/Rot der Ampel-Rückmeldung), Layout des Kamera-Vollbilds samt Zielrahmen, Stapel-Leiste und Aktionsknöpfe sowie die Overlays für Treffer-Rückmeldung, Rot-Dialog und Sitzungsliste |
|
||
| `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`, `storage`,
|
||
`scan-modes`, `scan-recognition`, `threshold`, `count-objects` und
|
||
`count-capture` 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
|
||
|
||
`src/storage.js` prüft eine aus `localStorage` geladene Sitzung gründlich,
|
||
bevor sie übernommen wird: Stapel- und Eintragslisten müssen die erwartete
|
||
Form haben, Stapelkennungen und Eintragsnummern müssen jeweils eindeutig
|
||
sein, jeder Eintrag muss auf einen existierenden Stapel zeigen, und
|
||
`nextEntryId` muss größer sein als jede vorhandene Eintragsnummer. Trifft
|
||
auch nur eine dieser Bedingungen nicht zu, wird die gesamte gespeicherte
|
||
Sitzung verworfen (die App startet leer), statt mit einem halb kaputten
|
||
Zustand lautlos weiterzuarbeiten. Der gespeicherte Stapelzähler selbst wird
|
||
nie übernommen, sondern beim Laden immer neu aus der tatsächlichen Zahl der
|
||
zugeordneten Einträge berechnet — genauso die Codemenge eines Stapels
|
||
(`stack.codes`): Sie wird nie aus dem gesicherten Wert übernommen, sondern
|
||
stets aus den Codes der ihm tatsächlich zugeordneten Einträge (`entry.codes`)
|
||
neu zusammengesetzt, dieselbe Ableitung, die `session.js` nach jedem
|
||
Umsortieren oder Entfernen eines Eintrags vornimmt. Sind `stack.codes` bzw.
|
||
`entry.codes` vorhanden, werden sie streng geprüft (Liste von
|
||
Zeichenketten); ein gesicherter Stand aus einer Fassung vor dieser Erweiterung
|
||
ohne diese Felder gilt dagegen nicht als unstimmig, sondern wird beim Laden
|
||
als Stapel bzw. Eintrag ohne bekannte Codes wiederhergestellt.
|
||
|
||
Nach demselben Muster ist `entry.rawText` (der volle OCR-Rohtext, siehe
|
||
„Funktionsweise" oben) optional: Ist er vorhanden, muss er eine Zeichenkette
|
||
sein; ein gesicherter Stand aus einer Fassung vor dieser Erweiterung ohne
|
||
dieses Feld gilt nicht als unstimmig und wird beim Laden als Eintrag ohne
|
||
Rohtext (leere Zeichenkette) wiederhergestellt. Anders als `stack.codes` wird
|
||
`entry.rawText` beim Laden nicht neu abgeleitet — er hängt an genau diesem
|
||
Eintrag und fließt in keine Stapel-Berechnung ein.
|
||
|
||
## Grenzen
|
||
|
||
- Keine Bestandsführung über Sitzungen hinweg, kein Export. Die Sicherung in
|
||
`localStorage` ist ausdrücklich nur ein Absturzschutz für die laufende
|
||
Sitzung, keine dauerhafte Datenhaltung.
|
||
- Das Laden der Seite braucht eine Verbindung; das Sortieren selbst nicht
|
||
(nach dem Laden läuft alles lokal im Browser).
|
||
- Der Codeabgleich (siehe oben) weiß nicht, ob ein gelesener Barcode-Inhalt
|
||
tatsächlich eine Teilenummer ist. Trägt ein Etikett daneben noch eine
|
||
Los- oder Wiederverkäufer-Kennung und teilen zwei technisch verschiedene
|
||
Module dieselbe, würde ein reiner Codetreffer sie fälschlich zusammenführen.
|
||
Dagegen schützt, dass ein Codetreffer allein nie genügt: Ein Stapel ist über
|
||
einen Code nur dann Kandidat, wenn die technischen Angaben ihm zusätzlich
|
||
nicht widersprechen (`specsCompatible`, siehe `proposeAssignment` in
|
||
`src/session.js`) — widerspricht der Stapel trotz passendem Code, fällt das
|
||
Modul auf einen eigenen Stapel zurück, statt lautlos falsch einsortiert zu
|
||
werden.
|
||
- Der Teilenummer-Decoder kennt bisher nur das Samsung-DDR4-Schema. Unbekannte
|
||
Schemata sind kein Fehler — sie führen automatisch zum OCR-Weg, und selbst
|
||
wenn dieser keine Kapazität findet, genügt die exakt gelesene Teilenummer
|
||
allein für Grün (siehe „Funktionsweise“ oben). Nur Kapazität, Bauform,
|
||
Rank und Geschwindigkeit hängen am Tabelleneintrag — die Wiedererkennung
|
||
eines bereits erfassten Moduls über seinen Barcode-Inhalt nicht.
|
||
- **Der Teilenummer-Decoder ist nur teilweise gegen reale Module geprüft.**
|
||
Belegt ist ausschließlich der Samsung-Eintrag `A8K40` (64 GB, 4DRx4) mit
|
||
dem Geschwindigkeitscode `CRC`, anhand des Referenzmoduls
|
||
`M386A8K40BM1-CRC4Y`. Alle übrigen Einträge in `src/pn-tables.js` —
|
||
die Bauform-Codes (`378`, `391`, `393`, `386`, `471`, `474`) und die
|
||
übrigen Geschwindigkeitscodes (`CPB`, `CTD`, `CVF`, `CWE`) — stammen aus
|
||
der veröffentlichten Samsung-Systematik und wurden **nicht** gegen ein
|
||
reales Etikett bestätigt. Hynix- und Micron-Module haben noch gar keinen
|
||
Tabelleneintrag und laufen vollständig über den OCR-Weg. Ein falscher
|
||
Tabelleneintrag fällt auf dem Barcode-Weg nicht automatisch auf, weil dort
|
||
keine Texterkennung gegenläuft, gegen die er widersprechen könnte — die
|
||
Validierung braucht deshalb echte Module, keine Code-Prüfung.
|
||
- Farb- und Schriftgestaltung ist bewusst schlicht gehalten und kann nach dem
|
||
ersten Einsatz am Tisch nachgezogen werden.
|
||
- Der volle OCR-Rohtext (`rawText`) dient ausschließlich der Anzeige für den
|
||
Nutzer (Treffer-Rückmeldung, Sitzungsliste). Die Stapelzuordnung stützt
|
||
sich bewusst weiterhin ausschließlich auf die daraus abgeleiteten,
|
||
verstandenen Spec-Felder — roher Erkennungstext schwankt zwischen
|
||
Aufnahmen (Zeilenumbrüche, zusätzlich erkannte Wörter, Groß-/
|
||
Kleinschreibung, …) und würde als Gruppierungsmerkmal für praktisch jedes
|
||
Modul einen eigenen Stapel erzeugen.
|
||
- **Funktion „Zählen" ist eine Schätzung, keine exakte Zählung** — sie wird
|
||
ungenau, wenn eine der folgenden Bedingungen zutrifft:
|
||
- **Der Untergrund muss sich abheben.** Das Verfahren erkennt Fundstücke
|
||
über den örtlichen Kontrast zu ihrer unmittelbaren Umgebung (siehe
|
||
„Funktionsweise" oben) — es braucht also eine Stelle, an der sich das
|
||
Fundstück *lokal* von dem unterscheidet, was um es herum liegt. Ein
|
||
Fundstück, das nahezu dieselbe Helligkeit wie der Tisch/das Leder
|
||
darunter hat, wird nicht zuverlässig gefunden.
|
||
- **Gleichmäßige Ausleuchtung hilft.** Der örtliche Hintergrund wird aus
|
||
einem Fenster um jeden Bildpunkt gebildet und fängt einseitigen
|
||
Lichteinfall dadurch besser ab als ein einzelner, fester Schwellwert
|
||
für das ganze Bild. Sehr harte Schlagschatten oder Reflexe können
|
||
trotzdem eigene, dem Fundstück ähnliche Kontrastflächen erzeugen (siehe
|
||
„mehrere gefundene Flächen" unten) und die Zählung verfälschen.
|
||
- **Auseinandergelegt zählt besser als aufgehäuft/berührend.** Mehrere
|
||
Teile, die sich berühren, bilden eine einzige zusammenhängende Fläche.
|
||
Die App rechnet diese über die typische Einzelteilgröße (Median der
|
||
gefundenen Flächen) auf ein Vielfaches hoch, aber je unregelmäßiger sich
|
||
die Teile überlappen (z. B. mehrere Schrauben, die sich gegenseitig
|
||
teilweise verdecken statt sich nur zu berühren), desto ungenauer wird
|
||
die hochgerechnete Fläche — und je dichter die Teile gedrängt liegen,
|
||
desto eher sieht das Umgebungsfenster (siehe oben) an ihrer eigenen
|
||
Stelle vor allem Nachbarteile statt echten Untergrund. Liegen Teile
|
||
übereinander gestapelt statt nebeneinander ausgelegt, sieht die Kamera
|
||
von oben nur die Fläche des obersten Teils — verdeckte Teile werden
|
||
nicht mitgezählt.
|
||
- **Stark unterschiedlich große Teile verfälschen die Hochrechnung.** Die
|
||
Hochrechnung setzt voraus, dass die meisten Fundstücke ungefähr gleich
|
||
groß sind (der Median dient als Maßstab für „ein Teil"). Liegen im
|
||
selben Rahmen sowohl deutlich größere als auch deutlich kleinere Teile
|
||
derselben Sorte (oder gar unterschiedliche Teile gemischt), kann die
|
||
Rauschschwelle ein tatsächliches kleines Teil verwerfen oder die
|
||
Hochrechnung ein einzelnes großes Teil als mehrere zählen.
|
||
- Details, die verwendeten Schwellenwerte, ihre Begründung und die
|
||
Messung am echten Foto (~20 Festplattenschrauben auf Leder, für das
|
||
das alte Otsu-basierte Verfahren 76712 statt rund zwanzig meldete)
|
||
stehen in `.superpowers/sdd/counting-rework-report.md`.
|
||
|
||
## Herstellertabellen erweitern
|
||
|
||
`src/pn-tables.js` ist tabellengesteuert. Ein neuer Hersteller oder ein neuer
|
||
Dichte-/Geschwindigkeitscode braucht einen Eintrag mit `pattern`,
|
||
`formFactor`, `density` und `speed` — keine Codeänderung an `pn-decoder.js`.
|
||
|
||
Vorgehen für jedes real vorliegende Modul:
|
||
|
||
1. Hersteller-Teilenummer vom Etikett ablesen (Barcode oder Klartext) sowie
|
||
die dazugehörige Klartextzeile mit Kapazität, Rank/Bauform und
|
||
Geschwindigkeit.
|
||
2. Einen Testfall in `test/pn-decoder.test.js` ergänzen, der die erwarteten
|
||
Spec-Felder für genau diese Teilenummer festhält.
|
||
3. Den passenden Tabelleneintrag (bzw. die passende Zeile in einer
|
||
bestehenden Tabelle) in `src/pn-tables.js` ergänzen, bis der Testfall
|
||
grün ist.
|
||
4. Den Kommentar am Tabellenkopf aktualisieren, welche Einträge damit belegt
|
||
sind.
|
||
|
||
Ohne reale Module bleiben unbestätigte Einträge unbestätigt — sie sollten
|
||
nicht "auf Verdacht" ergänzt werden, weil ein falscher Eintrag auf dem
|
||
Barcode-Weg (siehe „Grenzen") nicht mehr auffällt.
|
||
|
||
## Am Gerät noch zu prüfen
|
||
|
||
Diese Umgebung hat keinen Browser und kein Handy. Die folgenden Punkte
|
||
wurden während der Umsetzung deshalb nur durch Quelltext-Lektüre, `node
|
||
--check`, `npm test` und `npm run build` abgesichert, aber nie am echten
|
||
Gerät gesehen. Sie sollten vor dem produktiven Einsatz am Tisch geprüft
|
||
werden:
|
||
|
||
**Kamera und Aufnahme**
|
||
- Preview-URL auf dem Handy öffnen (muss `https://` sein), Rückkamera-Bild
|
||
erscheint im Vorschaubereich; die Statuszeile bleibt leer, solange Kamera
|
||
und Texterkennung verfügbar sind.
|
||
- Verweigerte Kamera-Berechtigung: verständliche Fehlermeldung erscheint
|
||
*und* die Aufnahme-LED des Geräts erlischt tatsächlich wieder.
|
||
- Aufnahme-Knopf antippen, bevor das Kamerabild bereit ist: verständliche
|
||
Meldung statt technischer Fehler.
|
||
- Datei-Auswahl als Ersatzweg (ohne Kamera) öffnet den nativen Dialog und
|
||
hinterlässt keine leere Fläche im Layout.
|
||
|
||
**Funktionswahl**
|
||
- Erster Start ohne gesicherte Sitzung: Auswahl mit vier großen Flächen
|
||
(„Strichcode", „QR-Code", „Text erkennen", „Zählen") 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: Rahmen hebt sich hervor, Treffer wird gebucht (DataMatrix
|
||
liest diese Funktion jetzt mit). Einen echten QR-Code vor dieselbe Funktion
|
||
halten: ebenfalls Treffer.
|
||
- „Text erkennen" wählen: kein automatisches Hervorheben des Rahmens (er
|
||
bleibt durchgehend schlicht), „Modul scannen"-Knopf sichtbar und löst die
|
||
Texterkennung aus. Der Rahmen ist dort trotzdem wirksam: nur der
|
||
Bildausschnitt innerhalb des Rahmens wird gelesen (nicht das ganze
|
||
Kamerabild) — mit Text außerhalb des Rahmens auf demselben Etikett prüfen,
|
||
dass er **nicht** erkannt wird, während Text innerhalb des Rahmens
|
||
vollständig erscheint.
|
||
- 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 in Funktion „Strichcode" scannen:
|
||
`M386A8K40BM1-CRC4Y` wird korrekt als Teilenummer übernommen, grüne
|
||
Rückmeldung mit `STAPEL A`, `64GB 4DRx4 PC4-2400 LRDIMM`.
|
||
- 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). Prüfen, dass die
|
||
Rückmeldung dabei den **vollständigen** erkannten Text zeigt (Groß- und
|
||
Kleinbuchstaben, nicht nur die von der App verstandenen Felder wie
|
||
`32GB 2Rx4 PC4-2666`), stehen bleibt, bis sie weggetippt wird, und bei
|
||
langem Text lesbar bleibt (per Scrollen erreichbar, wächst nicht aus dem
|
||
Bildschirm heraus). Danach in der Sitzungsliste nachsehen, dass derselbe
|
||
Rohtext beim Eintrag zum Nachlesen erscheint.
|
||
- Da der Bildausschnitt für die Texterkennung jetzt deutlich größer ist als
|
||
das bisher übergebene, auf 1280 Pixel herunterskalierte Vollbild
|
||
(`grabFrameRegion` statt `grabFrame`, siehe „Funktionsweise"), dauert ein
|
||
einzelner Erkennungsversuch länger als vorher — beobachten, wie lange ein
|
||
normaler Scan auf einem durchschnittlichen Handy tatsächlich braucht, und
|
||
ob er dabei spürbar unter der 20-Sekunden-Zeitgrenze der Pipeline bleibt
|
||
(siehe dazu auch den folgenden Punkt zur Ladezeit des Tesseract-Arbeiters,
|
||
die zusätzlich in dieselbe Zeitgrenze fällt).
|
||
- Ladezeit des WASM-Barcode-Moduls (`zxing-wasm`) beim allerersten Scan einer
|
||
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.
|
||
**Zählen (Aufnahme statt Dauerbetrieb)**
|
||
- „Zählen" wählen, Kamera auf ausgelegte Kleinteile richten (z. B. silberne
|
||
Festplattenschrauben auf dunklem Untergrund): Es erscheint **keine**
|
||
Zahl und **keine** fortlaufende Aktualisierung, sondern eine gut
|
||
erreichbare Schaltfläche mit der Aufschrift „Zählen". Kein roter Dialog,
|
||
keine Treffer-Rückmeldung, keine Stapel-Zuweisung — der Zielrahmen bleibt
|
||
sichtbar und zeigt, was beim nächsten Antippen gezählt wird.
|
||
- „Zählen" antippen: Für rund eine Sekunde zeigt die Statuszeile „zähle …“,
|
||
danach erscheint eine große Zahl im Bild und bleibt stehen — sie
|
||
aktualisiert sich **nicht** von selbst weiter. Die Schaltfläche trägt jetzt
|
||
die Aufschrift „Nochmal zählen".
|
||
- Während der rund einen Sekunde Kamera und Teile ruhig halten (leichte
|
||
Bewegung zwischendurch simulieren) und danach prüfen, dass das Ergebnis
|
||
plausibel bleibt — die App nimmt in dieser Zeit fünf Bilder auf und zeigt
|
||
deren Median.
|
||
- Die Zahl antippen: Diagnosezeile blendet sich ein und zeigt (in dieser
|
||
Reihenfolge) `Roh: …`, `Messungen: …` (die fünf Einzelmessungen dieser
|
||
Aufnahme, z. B. `17, 18, 19, 18, 20`), `Bild: …`, `Flächen: …`,
|
||
`Größte Fläche: …`, `Typische Größe: …`, `Art: …`, `Dauer: …`. Nochmaliges
|
||
Antippen blendet sie wieder aus. Prüfen, dass die fünf Messungen tatsächlich
|
||
eng beieinanderliegen, wenn die Teile ruhig lagen, und spürbar streuen,
|
||
wenn während der Aufnahme bewegt wurde.
|
||
- „Nochmal zählen" antippen: neue Aufnahme, neues Ergebnis ersetzt das alte
|
||
vollständig (Zahl **und** Diagnosezeile).
|
||
- Zur Funktionswahl wechseln und zurück zu „Zählen": Zahl und Diagnosezeile
|
||
sind zurückgesetzt (nichts angezeigt), Schaltfläche zeigt wieder „Zählen"
|
||
statt „Nochmal zählen". Dasselbe beim Wechsel zu einer anderen Funktion und
|
||
zurück.
|
||
- Zugänge zur Sitzungsliste und zum Funktionswechsel bleiben auch in dieser
|
||
Funktion erreichbar (Anzeige der Funktion antippen, Stapel-Leiste antippen)
|
||
— außer während die rund einssekündige Aufnahme selbst läuft, genau wie bei
|
||
einem laufenden Scan in „Text erkennen".
|
||
- Aufnahme antippen, bevor das Kamerabild bereit ist (z. B. gleich nach dem
|
||
Start): verständliche Meldung „Zählen nicht möglich — Kamerabild noch
|
||
nicht bereit" statt Absturz oder stiller Nichtreaktion.
|
||
- Gegenprobe der Grenzen (siehe „Grenzen" oben): Teile eng aneinanderlegen
|
||
(Hochrechnung), Teile stapeln (nur oberste Schicht zählt), einseitige
|
||
Beleuchtung, geringer Kontrast zum Untergrund — beobachten, wo die Zahl
|
||
spürbar daneben liegt, bisher nur an synthetischen Testbildern geprüft, nie
|
||
an einem echten Foto.
|
||
- 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 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 Zeichen-Whitelist der Texterkennung (Groß-/
|
||
Kleinbuchstaben, Ziffern, übliche Satz-/Sonderzeichen) auf echten Etiketten
|
||
keine tatsächlich benötigten Zeichen ausschließt, und ob die Erkennung der
|
||
Codeteile (Kapazität, Rank, Geschwindigkeit, Teilenummer) durch die jetzt
|
||
mögliche Groß-/Kleinschreibung noch verlässlich genug bleibt.
|
||
- 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
|
||
Seriennummer die Zuordnung trägt.
|
||
|
||
**Bedienung und Anzeige**
|
||
- Zielrahmen liegt sinnvoll über dem Kamerabild, `object-fit: cover` füllt
|
||
es passend.
|
||
- Stapel-Leiste (`A: 12`, `B: 4`) aus rund einem Meter Entfernung mit einer
|
||
Hand lesbar.
|
||
- In „Strichcode"/„QR-Code": Grüne/gelbe Rückmeldung erscheint nach dem Scan
|
||
und verschwindet nach rund 1200 ms von selbst; bei zwei Scans deutlich
|
||
unter 1200 ms Abstand bleibt jeweils nur eine Rückmeldung sichtbar und die
|
||
App bleibt bedienbar.
|
||
- In „Text erkennen": Rückmeldung bleibt stehen, bis sie über die
|
||
„weiter"-Schaltfläche (mindestens 56px hoch) weggetippt wird — kein
|
||
automatisches Ausblenden. Ein weiterer Scan, bevor die vorherige
|
||
Rückmeldung weggetippt wurde, ersetzt sie sofort durch die neue, ohne die
|
||
App dauerhaft zu sperren.
|
||
- Alle Bedienflächen (Scan-, Rückgängig-, Stapel-Knöpfe) mit dem Daumen
|
||
erreichbar, während die andere Hand das Modul hält.
|
||
- Rückgängig-Knopf: Screenreader liest "Letzten Scan zurücknehmen" vor.
|
||
- Rot-Dialog bei Mehrdeutigkeit: vier klar unterscheidbare Flächen ("Stapel
|
||
A/B …", "neuer Stapel", "nochmal scannen"); bei vielen Stapel-Kandidaten
|
||
bleibt alles per Scrollen erreichbar, ohne dass die Seite dahinter
|
||
mitrollt; Tastatur-/Screenreader-Fokus bleibt innerhalb des Dialogs.
|
||
|
||
**Sitzung**
|
||
- Ein Modul zweimal scannen: beide Male derselbe Stapel, Zähler steigt.
|
||
- Rücknahme-Fläche nimmt genau den zuletzt erfassten Eintrag zurück, Zähler
|
||
sinkt entsprechend.
|
||
- Seite neu laden während einer laufenden Sitzung: Dialog „Gesicherte
|
||
Sitzung gefunden“ erscheint; erst nach „fortsetzen“ bleiben Stapel und
|
||
Zähler erhalten und die „Zuletzt“-Zeile zeigt wieder den tatsächlich
|
||
letzten Eintrag; „verwerfen“ führt zu einem leeren Start.
|
||
- Sitzungsliste öffnen, Eintrag umsortieren, Eintrag entfernen: Ansicht baut
|
||
sich korrekt neu auf.
|
||
- "Sitzung beenden": Ansicht wird leer, und ein anschließendes Neuladen
|
||
stellt **keine** alte Sitzung wieder her; ein danach neu gescanntes Modul
|
||
wird aber wieder normal gegen Neuladen abgesichert.
|
||
- Schnelles Mehrfachtippen auf die Stapel-Leiste öffnet nur eine
|
||
Sitzungsliste, kein Übereinanderlegen mehrerer Listen.
|