This commit is contained in:
2026-02-28 11:59:16 +01:00
parent 1b850d8de8
commit c914333236
9 changed files with 208 additions and 78 deletions

View File

@@ -1,58 +1,37 @@
<script lang="ts">
import { onMount } from 'svelte';
import { buzzer } from '../lib/buzzerStore';
import { updateDeviceInfo, refreshFileList, setActivePort } from '../lib/buzzerActions';
import { set } from 'astro:schema';
import { connectToPort, initSerialListeners } from '../lib/buzzerActions';
import { PlugsIcon, PlugsConnectedIcon } from 'phosphor-svelte';
let port: SerialPort | null = null;
const filters = [{ usbVendorId: 0x2fe3, usbProductId: 0x0001 }];
onMount(() => {
initSerialListeners();
});
async function connect() {
async function handleConnectClick() {
try {
port = await navigator.serial.requestPort({ filters });
await port.open({ baudRate: 115200 });
// Store aktualisieren
buzzer.update(b => ({ ...b, connected: true }));
if (port) {
setActivePort(port); // Aktiven Port in den Actions setzen
await updateDeviceInfo(port);
await refreshFileList(port);
}
// Wenn wir schon gekoppelt sind, aber nicht verbunden,
// wird navigator.serial.requestPort() den Picker zeigen.
const port = await navigator.serial.requestPort();
await connectToPort(port);
} catch (e) {
setActivePort(null); // Port zurücksetzen
console.error("Verbindung fehlgeschlagen:", e);
buzzer.update(b => ({ ...b, connected: false }));
}
}
async function disconnect() {
if (port) {
await port.close();
port = null;
buzzer.update(b => ({ ...b, connected: false }));
console.error("Verbindung abgebrochen", e);
}
}
</script>
<button
on:click={$buzzer.connected ? disconnect : connect}
class="flex items-center gap-2 px-6 py-2 {$buzzer.connected ? 'bg-blue-600 hover:bg-blue-500' : 'bg-emerald-600 hover:bg-emerald-500'} text-white text-xs font-black rounded-full transition-all shadow-lg active:scale-95 uppercase tracking-widest"
on:click={handleConnectClick}
class="flex items-center gap-2 px-4 py-2 rounded-xl transition-all
{$buzzer.connected
? 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/50'
: 'bg-slate-700 hover:bg-slate-600 text-slate-200 border border-slate-600'}"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256">
{#if $buzzer.connected}
<rect width="256" height="256" fill="none"/>
<rect x="63.03" y="88.4" width="129.94" height="79.2" rx="24" transform="translate(-53.02 128) rotate(-45)" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/>
<line x1="88" y1="88" x2="168" y2="168" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/>
<line x1="232" y1="24" x2="173.94" y2="82.06" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/>
<line x1="82.06" y1="173.94" x2="24" y2="232" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/>
{:else}
<rect width="256" height="256" fill="none"/>
<line x1="144" y1="144" x2="120" y2="168" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/>
<path d="M132,180l-29,29a24,24,0,0,1-33.94,0L47,186.91A24,24,0,0,1,47,153l29-29" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/>
<line x1="197.94" y1="58.06" x2="232" y2="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/>
<path d="M180,132l29-29a24,24,0,0,0,0-33.94L186.91,47A24,24,0,0,0,153,47L124,76" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/>
{/if}
</svg>
{$buzzer.connected ? 'Disconnect' : 'Connect'}
{#if $buzzer.connected}
<PlugsConnectedIcon size={18} weight="fill" />
<span class="text-xs font-bold uppercase tracking-wider text-emerald-300">Verbunden</span>
{:else}
<PlugsIcon size={18} weight="bold" />
<span class="text-xs font-bold uppercase tracking-wider">Verbinden</span>
{/if}
</button>

View File

@@ -1,5 +1,8 @@
import { buzzer } from './buzzerStore';
import { get } from 'svelte/store';
import { addToast } from './toastStore';
let isConnecting = false;
type Task = {
command: string;
@@ -51,12 +54,22 @@ class SerialQueue {
const { value, done } = await reader.read();
if (done) break;
raw += decoder.decode(value);
if (raw.includes("OK")) break;
if (raw.includes("OK") || raw.includes("ERR")) break; // Auf ERR achten!
}
reader.releaseLock();
task.resolve(raw.split('\n').map(l => l.trim()).filter(l => l && l !== "OK" && !l.startsWith(task.command)));
const lines = raw.split('\n').map(l => l.trim()).filter(l => l);
// AUTOMATISCHE FEHLERERKENNUNG
const errorLine = lines.find(l => l.startsWith("ERR"));
if (errorLine) {
addToast(`Gerätefehler: ${errorLine}`, 'error', 5000);
}
task.resolve(lines.filter(l => l !== "OK" && !l.startsWith("ERR") && !l.startsWith(task.command)));
} catch (e) {
console.error("Queue Error:", e);
addToast("Kommunikationsfehler (Serial Port)", "error");
console.error(e);
} finally {
this.isProcessing = false;
this.process();
@@ -66,6 +79,87 @@ class SerialQueue {
const queue = new SerialQueue();
/**
* Initialisiert die globalen Serial-Listener (aufgerufen beim Start der App)
*/
export function initSerialListeners() {
if (typeof navigator === 'undefined' || !navigator.serial) return;
// 1. Wenn ein bereits gekoppeltes Gerät eingesteckt wird
navigator.serial.addEventListener('connect', (event) => {
console.log('Neues Gerät erkannt, starte Auto-Connect...');
autoConnect();
});
// Beim Laden der Seite prüfen, ob wir bereits Zugriff auf Geräte haben
autoConnect();
}
/**
* Versucht eine Verbindung zu bereits gekoppelten Geräten herzustellen
*/
export async function autoConnect() {
if (typeof navigator === 'undefined' || !navigator.serial) return;
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);
}
}
}
/**
* Kernfunktion für den Verbindungsaufbau
*/
export async function connectToPort(port: SerialPort) {
if (isConnecting || get(buzzer).connected) return;
isConnecting = true;
try {
await port.open({ baudRate: 115200 });
port.addEventListener('disconnect', () => {
addToast("Buzzer-Verbindung verloren!", "warning");
handleDisconnect();
});
setActivePort(port);
buzzer.update(s => ({ ...s, connected: true }));
// Kleiner Timeout, damit die UI Zeit zum Hydrieren hat
setTimeout(() => {
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");
}
} finally {
isConnecting = false;
}
}
function handleDisconnect() {
setActivePort(null);
buzzer.update(s => ({
...s,
connected: false,
files: [] // Liste leeren, da Gerät weg
}));
}
// --- EXPORTE ---
export function setActivePort(port: SerialPort | null) {

View File

@@ -0,0 +1,27 @@
import { writable } from 'svelte/store';
export type ToastType = 'success' | 'error' | 'info' | 'warning';
export interface Toast {
id: number;
message: string;
type: ToastType;
duration?: number;
}
export const toasts = writable<Toast[]>([]);
export function addToast(message: string, type: ToastType = 'info', duration = 3000) {
const id = Date.now();
toasts.update(all => [{ id, message, type, duration }, ...all]);
if (duration > 0) {
setTimeout(() => {
removeToast(id);
}, duration);
}
}
export function removeToast(id: number) {
toasts.update(all => all.filter(t => t.id !== id));
}

View File

@@ -8,6 +8,8 @@ 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 type { loadRenderers } from "astro:container";
---
<html lang="de">
@@ -18,7 +20,7 @@ import DeviceInfo from "../components/DeviceInfo.svelte";
<body class="bg-slate-900 selection:bg-blue-500/30">
<SerialWarning client:load />
<StatusModal visible={false} />
<ToastContainer client:load />
<div class="min-h-screen max-w-[1600px] min-w-[1024px] mx-auto text-slate-100 flex flex-col font-sans">
<header
class="h-16 border-b border-slate-700 bg-slate-800 grid grid-cols-3 items-center px-8 shrink-0 shadow-lg">