feat(v5): Etappe W-1 — Laufwerk: Wache, Typ, Auswerfen mit Nachsehen
Ampel / ampel (push) Successful in 1m21s

kern/laufwerk in drei Schichten: codes.ts (Steuercodes HERGELEITET wie
win_ioctl.py, gegen die Doku-Zahlen getestet), win32.ts (der einzige
koffi-Ort im Kern, Muster aus beweise/laufwerk.js), disc.ts (Logik gegen
die LaufwerkApi-Schnittstelle — classify mit den cdrom.py-Schwellen,
Geraete-Info mit ready/empty/unknown + Klartext-Grund, Auswurf als
entriegeln→auswerfen→NACHSEHEN mit injizierbarem Warten). Dazu wache.ts
im 3-Sekunden-Takt (§ 5 Plan A): Uebergangslogik pur, gescheiterte Runde
behaelt den Stand (R2) und meldet laut (R4).

Fenster zeigt Laufwerke live (Modell, Typ, Groesse, Grund) mit
Auswerfen-Knopf; Ereigniszeilen fuer Disc rein/raus und Fehler.

Bündel-Falle gefunden und behoben: Ein woertliches require überlebt
Rollup nicht — win32 wird per dynamischem import() als eigener Chunk
gebaut; der Fehler war dank R4 im Fenster sichtbar statt still.

Gemessen am echten BU40N (30.08.2026, RIPPY_MESSUNG=1):
  Laufwerk G: status=ready typ=bluray groesse=33759690752
  modell=HL-DT-ST BD-RE BU40N 1.03 serial=0025114C0149
Der Auswurf-Beweis (messung.auswurf) folgt am Ende der Sitzung — der
BU40N kann die Schublade nicht selbst einziehen.

52 Tests gruen (Steuercodes, classify, Auswurf-Ablauf, Geraete-Info,
Wache, Smoke weiterhin gruen mit Laufwerks-Kachel im Bild).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-08-30 17:28:46 +02:00
co-authored by Claude Fable 5
parent 6ed8035dcc
commit a45069af5e
13 changed files with 1377 additions and 73 deletions
+76 -4
View File
@@ -1,7 +1,7 @@
// Der Kern — läuft als Electron utilityProcess (KONZEPT-WINDOWS.md § 4.1).
// Hier passiert die ganze Arbeit; das Fenster zeigt nur an. In W-0 kann der
// Kern: die Datenbank öffnen (node:sqlite, § 3.4), sich beim Haupt melden und
// über einen direkten MessagePort mit dem Fenster reden (Ping → Pong).
// Hier passiert die ganze Arbeit; das Fenster zeigt nur an. Stand W-1:
// Datenbank (node:sqlite, § 3.4), Disc-Wache im 3-Sekunden-Takt (§ 5),
// Auswerfen mit Nachsehen (R3), Ping/Pong über den direkten MessagePort.
//
// parentPort/MessagePortMain: Schnittstelle laut Electron-Doku »utilityProcess«
// und »MessagePorts in Electron« — dieselbe Verdrahtung wie im gemessenen
@@ -13,6 +13,9 @@ import {
type DbStatus,
type KernNachricht,
} from '../gemeinsam/nachrichten'
import type { LaufwerkApi } from './laufwerk/codes'
import { auswerfenMitGrund, geraeteInfo, kennung, listOptischeLaufwerke } from './laufwerk/disc'
import { DiscWache } from './laufwerk/wache'
import { Datenbank } from './speicher/db'
// --datenbank=<pfad> kommt vom Haupt (der Kern kennt Electrons app.getPath
@@ -57,6 +60,66 @@ function anFenster(nachricht: KernNachricht): void {
fensterPort?.postMessage(nachricht)
}
function anHauptUndFenster(nachricht: KernNachricht): void {
process.parentPort.postMessage(nachricht)
anFenster(nachricht)
}
// ── Laufwerke (W-1) ─────────────────────────────────────────────────────
// koffi wird erst HIER geladen, über einen echten dynamischen import():
// Rollup legt win32.ts damit als eigenen Chunk neben kern.js (ein
// wörtliches require überlebte das Bündeln nicht — im Smoke-Lauf am
// 30.08.2026 sichtbar geworden). Scheitert das Laden (falsche Plattform,
// kaputtes Paket), läuft der Kern weiter und der Grund steht im Fenster (R4).
let laufwerkApi: LaufwerkApi | null = null
let laufwerkFehler: string | null = null
let wache: DiscWache | null = null
const laufwerkeBereit: Promise<void> = (async () => {
try {
if (process.platform !== 'win32') {
throw new Error('Die Laufwerks-Schicht läuft nur unter Windows (KONZEPT § 14).')
}
const { win32Api } = await import('./laufwerk/win32')
laufwerkApi = win32Api()
wache = new DiscWache(
() => listOptischeLaufwerke(laufwerkApi!).map((g) => geraeteInfo(laufwerkApi!, g)),
(meldung) => {
anHauptUndFenster({ art: 'laufwerke', liste: meldung.laufwerke, ereignisse: meldung.ereignisse })
},
(fehler) => {
// R4: Eine gescheiterte Runde ist eine laute Zeile — der letzte
// Stand im Fenster bleibt stehen (R2), die Wache läuft weiter.
console.error(`[kern] Disc-Wache: Runde fehlgeschlagen: ${String(fehler)}`)
anFenster({ art: 'kern-fehler', text: `Disc-Wache: ${String(fehler)}` })
},
)
wache.starten()
} catch (fehler) {
laufwerkFehler = String(fehler)
console.error(`[kern] Laufwerke: ${laufwerkFehler}`)
}
})()
async function auswerfen(id: string): Promise<void> {
await laufwerkeBereit
if (laufwerkApi === null) {
anFenster({ art: 'auswurf-ergebnis', id, grund: 'kein-zugriff', fehler: laufwerkFehler })
return
}
const geraete = listOptischeLaufwerke(laufwerkApi)
const geraet = geraete.find((g) => kennung(g) === id)
if (geraet === undefined) {
anFenster({ art: 'auswurf-ergebnis', id, grund: 'kein-zugriff', fehler: `Laufwerk ${id}: gibt es nicht` })
return
}
const { grund, fehler } = await auswerfenMitGrund(laufwerkApi, geraet)
anFenster({ art: 'auswurf-ergebnis', id, grund, fehler })
// Die Wache merkt den neuen Zustand spätestens in der nächsten Runde;
// eine sofortige Runde macht das Fenster ohne 3-Sekunden-Wartezeit frisch.
wache?.runde()
}
process.parentPort.on('message', (ereignis) => {
const nachricht: unknown = ereignis.data
if (istHauptNachricht(nachricht) && nachricht.art === 'fenster-port') {
@@ -67,13 +130,22 @@ process.parentPort.on('message', (ereignis) => {
if (fensterPort === null) return
fensterPort.on('message', (portEreignis) => {
const frage: unknown = portEreignis.data
if (istFensterNachricht(frage) && frage.art === 'ping') {
if (!istFensterNachricht(frage)) return
if (frage.art === 'ping') {
anFenster({ art: 'pong', zeit: frage.zeit })
} else if (frage.art === 'auswerfen') {
void auswerfen(frage.id)
}
})
fensterPort.start()
// Damit das Fenster nach dem Verbinden sofort den Stand kennt:
anFenster(bereitNachricht())
void laufwerkeBereit.then(() => {
if (laufwerkFehler !== null) {
anFenster({ art: 'kern-fehler', text: `Laufwerke: ${laufwerkFehler}` })
}
anFenster({ art: 'laufwerke', liste: wache?.aktuellerStand() ?? [], ereignisse: [] })
})
}
})