- README/.vch-description overstated the stacking rule as a hard "identical specs AND identical part number" requirement. In reality specsCompatible (src/spec.js) only compares fields known on both sides, so an unread part number is skipped rather than compared; proposeAssignment (src/session.js) only prompts when multiple stacks match. Documented the actual rule and its deliberate trade-off. - Added a sentence on how pipeline.js handles multiple barcodes with an unknown numbering scheme that disagree: no part number is taken over and OCR alone decides, without prompting. - Added src/styles.css to the "Aufbau" file table. - Added the two barcode-adapter device checks from .superpowers/sdd/task-9-report.md (first-load timing, reload after an interrupted first load) to "Am Gerät noch zu prüfen"; reviewed task-8 and task-10..13 reports and confirmed their other device checks were already present.
266 lines
14 KiB
Markdown
266 lines
14 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. Verglichen wird ü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. Passt danach genau ein Stapel, wird das Modul zugewiesen; passen
|
|
mehrere, fragt die App nach (`proposeAssignment` in `src/session.js`). 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
|
|
|
|
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 und das Ergebnis
|
|
stützt sich allein auf die Texterkennung, ohne dass die App nachfragt.
|
|
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.
|
|
3. **OCR als Rückfallebene.** Nur wenn kein Barcode eindeutig lesbar war (z. B.
|
|
überklebtes oder beschädigtes Etikett). 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.
|
|
4. **Stapel-Zuweisung.** Grün (Barcode) und Gelb (OCR) laufen ohne Eingabe
|
|
durch; nur bei roter Konfidenz (nichts Eindeutiges erkannt oder
|
|
widersprüchliche Barcodes) oder mehrdeutiger Stapelzuordnung fragt die
|
|
App nach.
|
|
|
|
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
|
|
```
|
|
|
|
97 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.
|
|
|
|
## Produktion
|
|
|
|
```bash
|
|
npm run build
|
|
npm run preview
|
|
```
|
|
|
|
`preview` hört auf `PORT` aus der Umgebung (Vorgabe `4173`, falls `PORT`
|
|
nicht gesetzt ist).
|
|
|
|
## 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) für Code-128 und DataMatrix
|
|
- [`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 |
|
|
| `src/pipeline.js` | Barcode → Teilenummer → OCR → Ampelfarbe, inkl. Zeitgrenzen und Mehrdeutigkeitsbehandlung |
|
|
| `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/ocr.js` | Bildaufbereitung (Otsu) und Adapter zu `tesseract.js` |
|
|
| `src/ui/` | Ansichten: Scan-Ansicht, Ergebnis-Einblendung, Rot-Dialog bei Mehrdeutigkeit, Sitzungsliste |
|
|
| `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 |
|
|
|
|
`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.
|
|
|
|
### 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.
|
|
|
|
## 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 Teilenummer-Decoder kennt bisher nur das Samsung-DDR4-Schema. Unbekannte
|
|
Schemata sind kein Fehler — sie führen automatisch zum OCR-Weg.
|
|
- **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.
|
|
|
|
## 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, "Kamera läuft" wird angezeigt.
|
|
- 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.
|
|
|
|
**Barcode- und OCR-Erkennung**
|
|
- Samsung-Referenzmodul scannen: Code-128 *und* DataMatrix werden erkannt,
|
|
`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).
|
|
- 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)?
|
|
- 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?
|
|
- 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 Code: "kein Barcode gefunden" statt Fehler oder Absturz.
|
|
|
|
**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.
|
|
- 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.
|
|
- 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: Stapel und Zähler bleiben
|
|
erhalten.
|
|
- 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.
|