sync
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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]+)/);
|
||||
|
||||
@@ -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";
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user