Stand vor Protokollumbau

This commit is contained in:
2026-03-01 11:11:56 +01:00
parent c914333236
commit 4d88194f7d
17 changed files with 624 additions and 79 deletions

View File

@@ -1,20 +1,33 @@
<script lang="ts">
import { onMount } from 'svelte';
import { buzzer } from '../lib/buzzerStore';
import { connectToPort, initSerialListeners } from '../lib/buzzerActions';
import { connectToPort, disconnectBuzzer,initSerialListeners } from '../lib/buzzerActions';
import { PlugsIcon, PlugsConnectedIcon } from 'phosphor-svelte';
const BUZZER_FILTER = [
{ usbVendorId: 0x2fe3, usbProductId: 0x0001 },
];
onMount(() => {
initSerialListeners();
});
async function handleConnectClick() {
try {
// Wenn wir schon gekoppelt sind, aber nicht verbunden,
// wird navigator.serial.requestPort() den Picker zeigen.
const port = await navigator.serial.requestPort();
if ($buzzer.connected) {
console.log("Trenne verbindung zum aktuellen Buzzer...");
await disconnectBuzzer();
console.log("Verbindung getrennt");
}
const port = await navigator.serial.requestPort({ filters: BUZZER_FILTER });
console.log("Port ausgewählt, versuche Verbindung.", port.getInfo());
await connectToPort(port);
} catch (e) {
// Verhindert das Error-Logging, wenn der User einfach nur "Abbrechen" klickt
if (e instanceof Error && e.name === 'NotFoundError') {
console.log("Keine Verbindung ausgewählt, Abbruch durch Nutzer.");
return;
}
console.error("Verbindung abgebrochen", e);
}
}

View File

@@ -0,0 +1,36 @@
<script lang="ts">
import { toasts, removeToast } from '../lib/toastStore';
import { flip } from 'svelte/animate';
import { fade, fly } from 'svelte/transition';
import { CheckCircleIcon, XCircleIcon, InfoIcon, WarningIcon, XIcon } from 'phosphor-svelte';
</script>
<div class="fixed bottom-6 right-6 z-[100] flex flex-col-reverse gap-3 pointer-events-none w-80">
{#each $toasts as toast (toast.id)}
<div
animate:flip={{ duration: 300 }}
in:fly={{ x: 50, duration: 400 }}
out:fade={{ duration: 200 }}
class="pointer-events-auto flex items-start gap-3 p-4 rounded-2xl border shadow-2xl backdrop-blur-md
{toast.type === 'success' ? 'bg-emerald-500/10 border-emerald-500/50 text-emerald-200' : ''}
{toast.type === 'error' ? 'bg-red-500/10 border-red-500/50 text-red-200' : ''}
{toast.type === 'warning' ? 'bg-amber-500/10 border-amber-500/50 text-amber-200' : ''}
{toast.type === 'info' ? 'bg-blue-500/10 border-blue-500/50 text-blue-200' : ''}"
>
<div class="shrink-0 mt-0.5">
{#if toast.type === 'success'}<CheckCircleIcon weight="fill" size={20} />{/if}
{#if toast.type === 'error'}<XCircleIcon weight="fill" size={20} />{/if}
{#if toast.type === 'warning'}<WarningIcon weight="fill" size={20} />{/if}
{#if toast.type === 'info'}<InfoIcon weight="fill" size={20} />{/if}
</div>
<div class="flex-1 text-xs font-medium leading-relaxed">
{toast.message}
</div>
<button on:click={() => removeToast(toast.id)} class="shrink-0 opacity-50 hover:opacity-100 transition-opacity">
<XIcon size={16} />
</button>
</div>
{/each}
</div>

View File

@@ -3,12 +3,13 @@ import { get } from 'svelte/store';
import { addToast } from './toastStore';
let isConnecting = false;
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
type Task = {
command: string;
priority: number; // 0 = Hintergrund, 1 = User-Aktion
resolve: (lines: string[]) => void;
key?: string;
key?: string;
};
class SerialQueue {
@@ -16,6 +17,9 @@ class SerialQueue {
private isProcessing = false;
private port: SerialPort | null = null;
private reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
private writer: WritableStreamDefaultWriter<Uint8Array> | null = null;
setPort(port: SerialPort | null) { this.port = port; }
async add(command: string, priority = 1, key?: string): Promise<string[]> {
@@ -43,38 +47,81 @@ class SerialQueue {
try {
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const writer = this.port.writable.getWriter();
const reader = this.port.readable.getReader();
await writer.write(encoder.encode(task.command + "\n"));
writer.releaseLock();
// Reader und Writer in der Instanz speichern
this.writer = this.port.writable.getWriter();
this.reader = this.port.readable.getReader();
await this.writer.write(encoder.encode(task.command + "\n"));
// Writer sofort wieder freigeben nach dem Senden
this.writer.releaseLock();
this.writer = null;
let raw = "";
while (true) {
const { value, done } = await reader.read();
// Hier könnte die Queue hängen bleiben, wenn das Gerät nicht antwortet
const { value, done } = await this.reader.read();
if (done) break;
raw += decoder.decode(value);
if (raw.includes("OK") || raw.includes("ERR")) break; // Auf ERR achten!
if (raw.includes("OK") || raw.includes("ERR")) break;
}
reader.releaseLock();
const lines = raw.split('\n').map(l => l.trim()).filter(l => l);
// AUTOMATISCHE FEHLERERKENNUNG
this.reader.releaseLock();
this.reader = null;
const lines = raw.split('\n').map(l => l.trim()).filter(l => l);
const errorLine = lines.find(l => l.startsWith("ERR"));
if (errorLine) {
addToast(`Gerätefehler: ${errorLine}`, 'error', 5000);
}
if (errorLine) addToast(`Gerätefehler: ${errorLine}`, 'error', 5000);
task.resolve(lines.filter(l => l !== "OK" && !l.startsWith("ERR") && !l.startsWith(task.command)));
} catch (e) {
addToast("Kommunikationsfehler (Serial Port)", "error");
console.error(e);
// Im Fehlerfall Locks sicher aufheben
this.cleanupLocks();
if (e instanceof Error && e.name !== 'AbortError') {
console.error("Queue Error:", e);
}
} finally {
this.isProcessing = false;
this.process();
}
}
// Hilfsmethode zum Aufräumen der Sperren
private cleanupLocks() {
if (this.reader) {
try { this.reader.releaseLock(); } catch { }
this.reader = null;
}
if (this.writer) {
try { this.writer.releaseLock(); } catch { }
this.writer = null;
}
}
async close() {
this.queue = [];
if (this.port) {
try {
// Erst die Streams abbrechen, um laufende Reads zu beenden
if (this.reader) {
await this.reader.cancel();
}
if (this.writer) {
await this.writer.abort();
}
// Dann die Locks freigeben
this.cleanupLocks();
await this.port.close();
// console.log("Port erfolgreich geschlossen");
} catch (e) {
console.error("Port-Fehler beim Schließen:", e);
this.cleanupLocks();
}
this.port = null;
}
this.isProcessing = false;
}
}
const queue = new SerialQueue();
@@ -87,7 +134,7 @@ export function initSerialListeners() {
// 1. Wenn ein bereits gekoppeltes Gerät eingesteckt wird
navigator.serial.addEventListener('connect', (event) => {
console.log('Neues Gerät erkannt, starte Auto-Connect...');
// console.log('Neues Gerät erkannt, starte Auto-Connect...');
autoConnect();
});
@@ -103,14 +150,23 @@ export async function autoConnect() {
const ports = await navigator.serial.getPorts();
if (ports.length > 0) {
// Wir nehmen den ersten verfügbaren Port
const port = ports[0];
console.log('Gekoppeltes Gerät gefunden, verbinde...');
try {
await connectToPort(port);
} catch (e) {
console.warn('Auto-Connect fehlgeschlagen:', e);
const retryDelays = [100, 500];
// Erster Versuch + 2 Retries = max 3 Versuche
for (let i = 0; i <= retryDelays.length; i++) {
try {
// console.log("Auto-Connect Versuch mit Port:", port.getInfo());
await connectToPort(port);
return; // Erfolg!
} catch (e) {
if (i < retryDelays.length) {
// console.log(`Reconnect Versuch ${i + 1} fehlgeschlagen, warte ${retryDelays[i]}ms...`);
await delay(retryDelays[i]);
} else {
console.error('Auto-Connect nach Retries endgültig fehlgeschlagen.');
}
}
}
}
}
@@ -123,29 +179,49 @@ export async function connectToPort(port: SerialPort) {
isConnecting = true;
try {
// console.log("Versuche Verbindung mit Port:", port.getInfo());
await port.open({ baudRate: 115200 });
port.addEventListener('disconnect', () => {
addToast("Buzzer-Verbindung verloren!", "warning");
handleDisconnect();
});
await delay(100);
setActivePort(port);
buzzer.update(s => ({ ...s, connected: true }));
// Kleiner Timeout, damit die UI Zeit zum Hydrieren hat
setTimeout(() => {
try {
// Validierung: Antwortet das Teil auf "info"?
const success = await Promise.race([
updateDeviceInfo(port),
new Promise<boolean>((_, reject) => setTimeout(() => reject(new Error("Timeout")), 1500))
]);
if (!success) throw new Error("Kein Buzzer");
port.addEventListener('disconnect', () => {
addToast("Buzzer-Verbindung verloren!", "warning");
handleDisconnect();
});
buzzer.update(s => ({ ...s, connected: true }));
addToast("Buzzer erfolgreich verbunden", "success");
}, 100);
await updateDeviceInfo(port);
await refreshFileList();
} catch (e) {
console.error("Connect error:", e);
// Nur Toasten, wenn es kein "Already Open" Fehler ist
if (!(e instanceof Error && e.name === 'InvalidStateError')) {
addToast("Verbindung fehlgeschlagen", "error");
await refreshFileList();
} catch (validationError) {
addToast("Buzzer-Validierung fehlgeschlagen!", "error");
await disconnectBuzzer();
try {
if ('forget' in port) { // Check für Browser-Support
await (port as any).forget();
console.log("Gerät wurde erfolgreich entkoppelt.");
}
} catch (forgetError) {
console.error("Entkoppeln fehlgeschlagen:", forgetError);
}
throw new Error("Device ist kein gültiger Buzzer");
throw validationError; // Fehler an den äußeren Block weitergeben
}
} catch (e) {
setActivePort(null);
// Hier landen wir, wenn der User den Port-Dialog abbricht oder die Validierung fehlschlägt
throw e;
} finally {
isConnecting = false;
}
@@ -153,8 +229,8 @@ export async function connectToPort(port: SerialPort) {
function handleDisconnect() {
setActivePort(null);
buzzer.update(s => ({
...s,
buzzer.update(s => ({
...s,
connected: false,
files: [] // Liste leeren, da Gerät weg
}));
@@ -166,9 +242,12 @@ export function setActivePort(port: SerialPort | null) {
queue.setPort(port);
}
// Diese Funktion fehlte im letzten Block!
export async function updateDeviceInfo(port: SerialPort) {
// Wir nutzen hier direkt die Queue für Konsistenz
export async function disconnectBuzzer() {
await queue.close();
handleDisconnect();
}
export async function updateDeviceInfo(port: SerialPort): Promise<boolean> {
const lines = await queue.add("info", 1);
if (lines.length > 0) {
const parts = lines[0].split(';');
@@ -177,8 +256,9 @@ export async function updateDeviceInfo(port: SerialPort) {
const totalPages = parseInt(parts[3]);
const availablePages = parseInt(parts[4]);
const totalMB = (totalPages * pageSize) / (1024 * 1024);
const availableMB = (availablePages * pageSize) / (1024 * 1024);
// MB Berechnung mit dem korrekten Divisor (1024 * 1024)
const totalMB = (totalPages * pageSize) / 1048576;
const availableMB = (availablePages * pageSize) / 1048576;
buzzer.update(s => ({
...s,
@@ -186,32 +266,72 @@ export async function updateDeviceInfo(port: SerialPort) {
protocol: parseInt(parts[0]),
storage: { ...s.storage, total: totalMB, available: availableMB }
}));
return true; // Validierung erfolgreich
}
}
return false; // Keine gültigen Daten erhalten
}
export async function refreshFileList() {
let totalSystemBytes = 0;
let totalAudioBytes = 0;
// 1. System-Größe abfragen (nur Summieren, keine Liste speichern)
const syslines = await queue.add("ls /lfs/sys", 1, 'ls');
syslines.forEach(line => {
const parts = line.split(',');
if (parts.length >= 2) {
totalSystemBytes += parseInt(parts[1]);
}
});
// 2. Audio-Files abfragen und Liste für das UI erstellen
const lines = await queue.add("ls /lfs/a", 1, 'ls');
const audioFiles = lines.map(line => {
const parts = line.split(',');
if (parts.length < 3) return null;
const [type, size, name] = parts;
return {
name,
size: (parseInt(size) / 1024).toFixed(1) + " KB",
crc32: 0,
isSystem: false
const size = parseInt(parts[1]);
totalAudioBytes += size;
return {
name: parts[2],
size: (size / 1024).toFixed(1) + " KB",
crc32: 0,
isSystem: false
};
}).filter(f => f !== null) as any[];
buzzer.update(s => ({ ...s, files: audioFiles }));
// 3. Den Store mit MB-Werten aktualisieren
buzzer.update(s => {
// Konvertierung in MB (1024 * 1024 = 1048576)
const audioMB = totalAudioBytes / 1048576;
const sysMB = totalSystemBytes / 1048576;
const usedTotalMB = s.storage.total - s.storage.available;
const unknownMB = Math.max(0, usedTotalMB - audioMB - sysMB);
console.log(`Storage: Total ${s.storage.total} MB, Used ${usedTotalMB.toFixed(2)} MB, Audio ${audioMB.toFixed(2)} MB, System ${sysMB.toFixed(2)} MB, Unknown ${unknownMB.toFixed(2)} MB`);
return {
...s,
files: audioFiles,
storage: {
...s.storage,
usedSys: sysMB,
usedAudio: audioMB,
unknown: unknownMB
}
};
});
startBackgroundCrcCheck();
}
async function startBackgroundCrcCheck() {
const currentFiles = get(buzzer).files;
for (const file of currentFiles) {
if (!file.crc32) {
if (true) {//(!file.crc32) {
const tagresponse = await queue.add(`gett /lfs/a/${file.name}`, 0);
if (tagresponse.length > 0) {
console.log(`Tag für ${file.name}:`, tagresponse[0]);
}
const response = await queue.add(`check /lfs/a/${file.name}`, 0);
if (response.length > 0) {
const match = response[0].match(/0x([0-9a-fA-F]+)/);

View File

@@ -8,7 +8,7 @@ import ConnectButton from "../components/ConnectButton.svelte";
import SerialWarning from "../components/SerialWarning.svelte";
import FileStorage from "../components/FileStorage.svelte";
import DeviceInfo from "../components/DeviceInfo.svelte";
import ToastContainer from "../components/ToastContainer.svelte";
import ToastContainer from '../components/ToastContainer.svelte';
import type { loadRenderers } from "astro:container";
---