This commit is contained in:
2026-03-07 08:51:50 +01:00
parent 4f3fbff258
commit f85143d7e5
60 changed files with 3245 additions and 1205 deletions

View File

@@ -1,50 +1,143 @@
<script lang="ts">
import { onMount } from 'svelte';
import { bus } from '../lib/bus/SerialBus';
import { buzzer } from '../lib/buzzerStore';
import { connectToPort, disconnectBuzzer,initSerialListeners } from '../lib/buzzerActions';
import { PlugsIcon, PlugsConnectedIcon } from 'phosphor-svelte';
import { GetProtocolCommand } from '../lib/protocol/commands/GetProtocol';
import { addToast } from '../lib/toastStore';
import {
PlugsIcon,
PlugsConnectedIcon,
CaretDownIcon,
BluetoothIcon,
TrashIcon,
PlusCircleIcon
} from 'phosphor-svelte';
import { slide } from 'svelte/transition';
import { initializeBuzzer } from '../lib/buzzerActions';
const BUZZER_FILTER = [
{ usbVendorId: 0x2fe3, usbProductId: 0x0001 },
];
onMount(() => {
initSerialListeners();
});
const BUZZER_FILTER = [{ usbVendorId: 0x1209, usbProductId: 0xEDED }];
let showMenu = false;
let menuElement: HTMLElement;
async function handleConnectClick() {
try {
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);
// Schließt das Menü bei Klick außerhalb
function handleOutsideClick(event: MouseEvent) {
if (showMenu && menuElement && !menuElement.contains(event.target as Node)) {
showMenu = false;
}
}
async function connectTo(port: SerialPort) {
try {
await bus.connect(port);
// Kurze Pause für die Hardware-Bereitschaft
await new Promise(r => setTimeout(r, 100));
// Logische Initialisierung starten
await initializeBuzzer();
} catch (e: any) {
console.error("Port-Fehler:", e);
}
}
async function handleMainAction() {
if ($buzzer.connected) {
await bus.disconnect();
buzzer.update(s => ({ ...s, connected: false }));
return;
}
const ports = await navigator.serial.getPorts();
if (ports.length > 0) {
await connectTo(ports[0]);
} else {
await pairNewDevice();
}
}
async function pairNewDevice() {
showMenu = false;
try {
const port = await navigator.serial.requestPort({ filters: BUZZER_FILTER });
await connectTo(port);
} catch (e) {
console.log("Pairing abgebrochen");
}
}
async function forgetDevice() {
showMenu = false;
const ports = await navigator.serial.getPorts();
for (const port of ports) {
if ('forget' in port) {
await (port as any).forget();
}
}
if ($buzzer.connected) {
await bus.disconnect();
buzzer.update(s => ({ ...s, connected: false }));
}
addToast("Geräte entkoppelt", "info");
}
onMount(() => {
window.addEventListener('click', handleOutsideClick);
return () => window.removeEventListener('click', handleOutsideClick);
});
</script>
<button
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'}"
>
{#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>
<div class="relative inline-flex shadow-lg" bind:this={menuElement}>
<button
on:click={handleMainAction}
class="flex items-center gap-3 px-5 py-2.5 rounded-l-xl transition-all border-r border-white/10
{$buzzer.connected
? 'bg-emerald-600 hover:bg-emerald-500 text-white'
: 'bg-slate-700 hover:bg-slate-600 text-slate-200'}"
>
{#if $buzzer.connected}
<PlugsConnectedIcon size={20} weight="fill" />
<span class="text-sm font-bold uppercase tracking-wide">Trennen</span>
{:else}
<PlugsIcon size={20} weight="bold" />
<span class="text-sm font-bold uppercase tracking-wide">Verbinden</span>
{/if}
</button>
<button
on:click={() => showMenu = !showMenu}
class="px-3 py-2.5 rounded-r-xl transition-all
{$buzzer.connected
? 'bg-emerald-600 hover:bg-emerald-500 text-white'
: 'bg-slate-700 hover:bg-slate-600 text-slate-200'}"
>
<CaretDownIcon size={16} weight="bold" class="transition-transform {showMenu ? 'rotate-180' : ''}" />
</button>
{#if showMenu}
<div
transition:slide={{ duration: 150 }}
class="absolute top-full right-0 mt-2 w-56 bg-slate-800 border border-slate-700 rounded-xl overflow-hidden z-50 shadow-2xl"
>
<div class="p-2 flex flex-col gap-1">
<button
on:click={pairNewDevice}
class="flex items-center gap-3 w-full px-3 py-2 text-sm text-slate-300 hover:bg-slate-700 rounded-lg transition-colors"
>
<PlusCircleIcon size={18} class="text-emerald-400" />
Neuen Buzzer koppeln
</button>
<div class="h-px bg-slate-700 my-1"></div>
<button
on:click={forgetDevice}
class="flex items-center gap-3 w-full px-3 py-2 text-sm text-rose-400 hover:bg-rose-500/10 rounded-lg transition-colors"
>
<TrashIcon size={18} />
Buzzer entkoppeln
</button>
</div>
</div>
{/if}
</button>
</div>

View File

@@ -1,17 +1,25 @@
<script>
import { buzzer } from '../lib/buzzerStore';
import { CpuIcon } from 'phosphor-svelte';
</script>
<div class="text-[11px] font-mono text-slate-400 space-y-1 relative z-10">
<p>
Firmware: <span class="text-indigo-300">{$buzzer.version}</span>
</p>
<p>
Protocol: <span class="text-indigo-300">{$buzzer.protocol}</span>
</p>
<p>
Status: <span class="{$buzzer.connected ? 'text-emerald-400' : 'text-red-400'}">
{$buzzer.connected ? 'Confirmed' : 'Disconnected'}
</span>
</p>
</div>
<div class="h-48 bg-indigo-950/20 border border-indigo-500/20 rounded-[2rem] p-6 relative overflow-hidden shrink-0">
<div class="absolute top-6 right-6 text-indigo-500 opacity-20">
<CpuIcon class="w-12 h-12" weight="fill" />
</div>
<h3 class="text-indigo-400 text-[10px] font-black uppercase tracking-[0.2em] mb-4">
Device Info
</h3>
<div class="text-[11px] font-mono text-slate-400 space-y-1 relative z-10 transition-all duration-300 {!$buzzer.connected ? 'blur-[1px] opacity-30 grayscale pointer-events-none' : ''}">
<p>
Firmware: <span class="text-indigo-300">{$buzzer.version}</span>
</p>
<p>
Protocol: <span class="text-indigo-300">{$buzzer.protocol}</span>
</p>
<p>
Status: <span class="{$buzzer.connected ? 'text-emerald-400' : 'text-red-400'}">
{$buzzer.connected ? 'Confirmed' : 'Disconnected'}
</span>
</p>
</div>
</div>

View File

@@ -14,7 +14,7 @@
$: isDisconnected = !$buzzer.connected;
</script>
<div class="flex flex-col gap-1.5 w-96 transition-all duration-700 {isDisconnected ? 'blur-sm opacity-30 grayscale pointer-events-none' : ''}">
<div class="flex flex-col gap-1.5 w-96 transition-all duration-300 {isDisconnected ? 'blur-[1px] opacity-30 grayscale pointer-events-none' : ''}">
<div class="h-3.5 w-full bg-slate-800 rounded-full overflow-hidden flex border border-slate-700 shadow-inner">
<div class="h-full bg-slate-300 transition-all duration-500" style="width: {pMeta}%"></div>

View File

@@ -1,10 +1,31 @@
<script lang="ts">
import { buzzer } from '../lib/buzzerStore';
import { playFile, deleteFile } from '../lib/buzzerActions';
import { MusicNotesIcon, WrenchIcon, PlayIcon, TrashIcon, ArrowsLeftRightIcon, QuestionMarkIcon } from "phosphor-svelte";
import { InfoIcon, MusicNotesIcon, WrenchIcon, PlayIcon, TrashIcon, ArrowsLeftRightIcon, QuestionMarkIcon } from "phosphor-svelte";
import { PlayFileCommand } from '../lib/protocol/commands/PlayFile';
export let file: { name: string, size: string, isSystem: boolean, crc32?: number};
export let selected = false;
async function handlePlay() {
try {
const cmd = new PlayFileCommand();
// 1. WICHTIG: Der Buzzer braucht den absoluten Pfad!
const fullPath = `/lfs/a/${file.name}`;
console.log("Sende Play-Befehl für:", fullPath);
const success = await cmd.execute(fullPath);
if (success) {
// Optional: Erfolg kurz im Log zeigen
console.log("Wiedergabe läuft...");
} else {
addToast(`Buzzer konnte ${file.name} nicht abspielen.`, "error");
}
} catch (e: any) {
addToast(`Fehler: ${e.message}`, "error");
}
}
</script>
<div
@@ -47,18 +68,25 @@
</div>
<div class="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity shrink-0 ml-4">
<button
class="p-2 hover:bg-gray-500/20 rounded-lg text-white-400 transition-colors"
title="Datei-Infos"
>
<InfoIcon size={16} />
</button>
<button
on:click|stopPropagation={() => playFile(file.name)}
on:click|stopPropagation={handlePlay}
class="p-2 hover:bg-blue-500/20 rounded-lg text-blue-400 transition-colors"
title="Play Sound"
title="Auf dem Buzzer abspielen"
>
<PlayIcon size={16} weight="fill" />
</button>
<button
on:click|stopPropagation={() => deleteFile(file.name)}
// on:click|stopPropagation={() => deleteFile(file.name)}
class="p-2 hover:bg-red-500/20 rounded-lg text-red-400 transition-colors"
title="Delete File"
title="Datei vom Buzzer löschen"
>
<TrashIcon size={16} weight="fill" />
</button>

View File

@@ -1,11 +1,11 @@
<script lang="ts">
import { buzzer } from '../lib/buzzerStore';
import { refreshFileList } from '../lib/buzzerActions';
import FileRow from './FileRow.svelte';
import { ArrowsCounterClockwiseIcon } from 'phosphor-svelte';
import { refreshFileList } from '../lib/buzzerActions';
async function handleRefresh() {
console.log("Aktualisiere Dateiliste...");
await refreshFileList();
await refreshFileList();
}
</script>

View File

@@ -0,0 +1,134 @@
import { SYNC_SEQ, FrameType } from '../protocol/constants';
import { buzzer } from '../buzzerStore';
class SerialBus {
public port: SerialPort | null = null;
private reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
private internalBuffer: Uint8Array = new Uint8Array(0);
async connect(port: SerialPort) {
if (this.port) await this.disconnect();
await port.open({ baudRate: 115200 });
this.port = port;
this.internalBuffer = new Uint8Array(0);
this.reader = null;
port.addEventListener('disconnect', () => {
console.warn("Hardware-Verbindung verloren!");
this.disconnect();
buzzer.update(s => ({ ...s, connected: false }));
});
(window as any).buzzerBus = this;
}
// Hilfsmethode: Stellt sicher, dass wir einen aktiven Reader haben ohne zu crashen
private async ensureReader() {
if (!this.port?.readable) throw new Error("Port nicht lesbar");
if (!this.reader) {
this.reader = this.port.readable.getReader();
}
}
async sendRequest(cmd: number, payload: Uint8Array = new Uint8Array(0)) {
if (!this.port?.writable) throw new Error("Port nicht bereit");
const writer = this.port.writable.getWriter();
const header = new Uint8Array([FrameType.REQUEST, cmd]);
const frame = new Uint8Array(SYNC_SEQ.length + header.length + payload.length);
frame.set(SYNC_SEQ, 0);
frame.set(header, SYNC_SEQ.length);
frame.set(payload, SYNC_SEQ.length + header.length);
await writer.write(frame);
writer.releaseLock();
}
async waitForSync(timeoutMs = 2000): Promise<boolean> {
await this.ensureReader();
const startTime = Date.now();
while (Date.now() - startTime < timeoutMs) {
// 1. Zuerst im Puffer schauen (verhindert Datenverlust zwischen Frames!)
for (let i = 0; i <= this.internalBuffer.length - SYNC_SEQ.length; i++) {
if (this.internalBuffer[i] === SYNC_SEQ[0] && this.internalBuffer[i+1] === SYNC_SEQ[1] &&
this.internalBuffer[i+2] === SYNC_SEQ[2] && this.internalBuffer[i+3] === SYNC_SEQ[3]) {
this.internalBuffer = this.internalBuffer.subarray(i + SYNC_SEQ.length);
return true;
}
}
// 2. Neue Daten lesen
const { value, done } = await this.reader!.read();
if (done) break;
if (value) {
const next = new Uint8Array(this.internalBuffer.length + value.length);
next.set(this.internalBuffer);
next.set(value, this.internalBuffer.length);
this.internalBuffer = next;
}
}
return false;
}
async readExact(len: number): Promise<Uint8Array> {
await this.ensureReader();
while (this.internalBuffer.length < len) {
const { value, done } = await this.reader!.read();
if (done || !value) throw new Error("Stream closed");
const next = new Uint8Array(this.internalBuffer.length + value.length);
next.set(this.internalBuffer);
next.set(value, this.internalBuffer.length);
this.internalBuffer = next;
}
const res = this.internalBuffer.subarray(0, len);
this.internalBuffer = this.internalBuffer.subarray(len);
return res;
}
public releaseReadLock() {
if (this.reader) {
this.reader.releaseLock();
this.reader = null;
}
}
async disconnect() {
this.releaseReadLock();
if (this.port) {
try { await this.port.close(); } catch (e) {}
this.port = null;
}
this.internalBuffer = new Uint8Array(0);
}
}
const existingBus = typeof window !== 'undefined' ? (window as any).buzzerBus : null;
export const bus: SerialBus = existingBus || new SerialBus();
if (typeof window !== 'undefined') {
(window as any).buzzerBus = bus;
(window as any).initDebug = async () => {
console.log("🚀 Lade Debug-Kommandos...");
// Nutze hier am besten absolute Pfade ab /src/
const [proto, settings, list, play, flash] = await Promise.all([
import('../protocol/commands/GetProtocol.ts'),
import('../protocol/commands/GetSettings.ts'),
import('../protocol/commands/ListDir.ts'),
import('../protocol/commands/PlayFile.ts'), // Pfad prüfen!
import('../protocol/commands/GetFlashInfo.ts') // Pfad prüfen!
]);
(window as any).GetProtocolCommand = proto.GetProtocolCommand;
(window as any).GetSettingCommand = settings.GetSettingCommand;
(window as any).ListDirCommand = list.ListDirCommand;
(window as any).PlayFileCommand = play.PlayFileCommand;
(window as any).GetFlashInfoCommand = flash.GetFlashInfoCommand;
console.log("✅ Alle Commands geladen und an window gebunden.");
};
(window as any).run = async (CommandClass: any, ...args: any[]) => {
const cmd = new CommandClass();
return await cmd.execute(...args);
};
}

View File

@@ -1,362 +1,46 @@
import { buzzer } from './buzzerStore';
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;
};
class SerialQueue {
private queue: Task[] = [];
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[]> {
if (key) {
this.queue = this.queue.filter(t => t.key !== key);
}
return new Promise((resolve) => {
const task = { command, priority, resolve, key };
if (priority === 1) {
const lastUserTaskIndex = this.queue.findLastIndex(t => t.priority === 1);
this.queue.splice(lastUserTaskIndex + 1, 0, task);
} else {
this.queue.push(task);
}
this.process();
});
}
private async process() {
if (this.isProcessing || !this.port || this.queue.length === 0) return;
this.isProcessing = true;
const task = this.queue.shift()!;
try {
const encoder = new TextEncoder();
const decoder = new TextDecoder();
// 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) {
// 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;
}
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);
task.resolve(lines.filter(l => l !== "OK" && !l.startsWith("ERR") && !l.startsWith(task.command)));
} catch (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();
/**
* Initialisiert die globalen Serial-Listener (aufgerufen beim Start der App)
* Initialisiert den Buzzer nach dem physikalischen Verbindungsaufbau.
*/
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) {
const port = ports[0];
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.');
}
}
}
export async function initializeBuzzer() {
try {
const version = await new GetProtocolCommand().execute();
if (version !== null) {
buzzer.update(s => ({ ...s, connected: true, protocol: version }));
// FIX 1: Flash-Info muss auch beim Start geladen werden!
await refreshFileList();
await refreshFlashInfo();
addToast(`Buzzer bereit (v${version})`, 'success');
return true;
}
} catch (e: any) {
console.error("Initialisierung fehlgeschlagen:", e);
addToast(`Fehler: ${e.message}`, "error");
await bus.disconnect();
buzzer.update(s => ({ ...s, connected: false }));
}
return false;
}
/**
* Kernfunktion für den Verbindungsaufbau
*/
export async function connectToPort(port: SerialPort) {
if (isConnecting || get(buzzer).connected) return;
isConnecting = true;
try {
// console.log("Versuche Verbindung mit Port:", port.getInfo());
await port.open({ baudRate: 115200 });
await delay(100);
setActivePort(port);
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");
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;
export async function refreshFlashInfo() {
try {
const flashInfo = await new GetFlashInfoCommand().execute();
if (flashInfo) {
const totalSize = (flashInfo.total_size / (1024 * 1024));
const freeSize = (flashInfo.free_size / (1024 * 1024));
const fwSlotSize = (flashInfo.fw_slot_size / 1024);
const maxPathLength = flashInfo.max_path_length;
buzzer.update(s => ({
...s,
storage: { total: totalSize, available: freeSize }, // FIX 2: "storage" korrekt geschrieben
fw_slot_size: fwSlotSize,
max_path_length: maxPathLength
}));
}
}
function handleDisconnect() {
setActivePort(null);
buzzer.update(s => ({
...s,
connected: false,
files: [] // Liste leeren, da Gerät weg
}));
}
// --- EXPORTE ---
export function setActivePort(port: SerialPort | null) {
queue.setPort(port);
}
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(';');
if (parts.length >= 6) {
const pageSize = parseInt(parts[2]);
const totalPages = parseInt(parts[3]);
const availablePages = parseInt(parts[4]);
// MB Berechnung mit dem korrekten Divisor (1024 * 1024)
const totalMB = (totalPages * pageSize) / 1048576;
const availableMB = (availablePages * pageSize) / 1048576;
buzzer.update(s => ({
...s,
version: parts[1],
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 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[];
// 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 (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]+)/);
if (match) {
const crc = parseInt(match[1], 16);
updateFileCrc(file.name, crc);
}
}
}
}
}
function updateFileCrc(name: string, crc: number) {
buzzer.update(s => ({
...s,
files: s.files.map(f => f.name === name ? { ...f, crc32: crc } : f)
}));
}
export async function playFile(filename: string) {
return queue.add(`play /lfs/a/${filename}`, 1, 'play');
}
export async function deleteFile(filename: string) {
if (!confirm(`Datei ${filename} wirklich löschen?`)) return;
await queue.add(`rm /lfs/a/${filename}`, 1);
await refreshFileList();
}
} catch (e) { // FIX 3: try/catch Block sauber schließen
console.error("Fehler beim Abrufen der Flash-Info:", e);
}
} // Funktion schließen

View File

@@ -2,15 +2,18 @@ import { writable } from 'svelte/store';
export const buzzer = writable({
connected: false,
version: 'v0.0.0',
protocol: 0,
build: 'unknown',
version: 'v0.0.0',
kernel_version: 'v0.0.0',
storage: {
total: 8.0, // 8 MB Flash laut Spezifikation
total: 8.0,
available: 0.0,
unknown: 8.0,
usedSys: 0.0,
usedAudio: 0.0
usedAudio: 0.0,
unknown: 0.0
},
files: [] as {name: string, size: string, crc32: number, isSystem: boolean, isSynced: boolean}[]
max_path_length: 15,
fw_slot_size: 0,
files: [] as {name: string, size: string, crc32: number | null, isSystem: boolean}[]
});

View File

@@ -0,0 +1,49 @@
import { bus } from '../../bus/SerialBus';
import { Command, FrameType } from '../constants';
import { BinaryUtils } from '../../utils/BinaryUtils';
export interface FlashInfo {
total_size: number; // in Bytes
free_size: number; // in Bytes
fw_slot_size: number; // in Bytes
ext_flash_erase_size: number; // in Bytes
int_flash_erase_size: number; // in Bytes
max_path_length: number;
}
export class GetFlashInfoCommand {
async execute(): Promise<FlashInfo | null> {
try {
await bus.sendRequest(Command.GET_FLASH_INFO);
if (await bus.waitForSync()) {
const typeArr = await bus.readExact(1);
if (typeArr[0] === FrameType.RESPONSE) {
// Wir lesen exakt 21 Bytes für die Flash-Info
const data = await bus.readExact(21);
const pageSize = BinaryUtils.readUint32LE(data.subarray(0, 4));
const totalSize = BinaryUtils.readUint32LE(data.subarray(4, 8)) * pageSize;
const freeSize = BinaryUtils.readUint32LE(data.subarray(8, 12)) * pageSize; // Aktuell haben wir keine Info über belegten Speicher, also annehmen, dass alles frei ist
const fwSlotSize = BinaryUtils.readUint32LE(data.subarray(12, 16));
const extEraseSize = BinaryUtils.readUint16LE(data.subarray(16, 18));
const intEraseSize = BinaryUtils.readUint16LE(data.subarray(18, 20));
const maxPathLength = data[20];
console.log("Flash Info:", { pageSize, totalSize, freeSize, fwSlotSize, extEraseSize, intEraseSize, maxPathLength });
return {
total_size: totalSize,
free_size: totalSize, // Aktuell haben wir keine Info über belegten Speicher, also annehmen, dass alles frei ist
fw_slot_size: fwSlotSize,
ext_flash_erase_size: extEraseSize,
int_flash_erase_size: intEraseSize,
max_path_length: maxPathLength
};
}
}
} catch (e) {
console.error("GetFlashInfoCommand failed:", e);
} finally {
bus.releaseReadLock();
}
return null;
}
}

View File

@@ -0,0 +1,26 @@
import { bus } from '../../bus/SerialBus';
import { Command, FrameType } from '../constants';
import { BinaryUtils } from '../../utils/BinaryUtils';
export class GetProtocolCommand {
async execute(): Promise<number | null> {
try {
await bus.sendRequest(Command.GET_PROTOCOL_VERSION);
if (await bus.waitForSync()) {
const typeArr = await bus.readExact(1);
if (typeArr[0] === FrameType.RESPONSE) {
// Wir lesen exakt 2 Bytes für die Version
const data = await bus.readExact(2);
// Nutze die neue Hilfsfunktion
return BinaryUtils.readUint16LE(data);
}
}
} catch (e) {
console.error("GetProtocolCommand failed:", e);
} finally {
bus.releaseReadLock();
}
return null;
}
}

View File

@@ -0,0 +1,37 @@
import { bus } from '../../bus/SerialBus';
import { Command, FrameType } from '../constants';
import { BinaryUtils } from '../../utils/BinaryUtils';
export class GetSettingCommand {
async execute(key: string): Promise<number | boolean | null> {
try {
const keyBuf = new TextEncoder().encode(key);
const payload = new Uint8Array(1 + keyBuf.length);
payload[0] = keyBuf.length;
payload.set(keyBuf, 1);
await bus.sendRequest(Command.GET_SETTING, payload);
if (await bus.waitForSync()) {
const typeArr = await bus.readExact(1);
if (typeArr[0] === FrameType.RESPONSE) {
const lenArr = await bus.readExact(1);
const valLen = lenArr[0];
const data = await bus.readExact(valLen);
// Typ-Konvertierung analog zu C/Python
if (key === "audio/vol" || key === "play/norepeat") {
return data[0] === 1 ? true : (key === "audio/vol" ? data[0] : false);
} else if (key === "settings/storage_interval") {
return BinaryUtils.readUint16LE(data);
}
}
}
} catch (e) {
console.error("GetSetting failed:", e);
} finally {
bus.releaseReadLock();
}
return null;
}
}

View File

@@ -0,0 +1,43 @@
// src/lib/protocol/commands/ListDir.ts
import { bus } from '../../bus/SerialBus';
import { Command, FrameType } from '../constants';
import { BinaryUtils } from '../../utils/BinaryUtils';
export class ListDirCommand {
async execute(path: string) {
try {
const p = new TextEncoder().encode(path);
const req = new Uint8Array(p.length + 1);
req[0] = p.length; req.set(p, 1);
await bus.sendRequest(Command.LIST_DIR, req);
const entries = [];
while (true) {
// Wichtig: Wir rufen waitForSync ohne releaseReadLock zwischendurch auf!
if (!(await bus.waitForSync())) break;
const type = (await bus.readExact(1))[0];
if (type === FrameType.LIST_START) continue;
if (type === FrameType.LIST_END) {
const expected = BinaryUtils.readUint16LE(await bus.readExact(2));
console.log(`Erwartet: ${expected}, Erhalten: ${entries.length}`);
return entries;
}
if (type === FrameType.LIST_CHUNK) {
const len = BinaryUtils.readUint16LE(await bus.readExact(2));
const data = await bus.readExact(len);
entries.push({
isDir: data[0] === 1,
size: data[0] === 1 ? null : BinaryUtils.readUint32LE(data.subarray(1, 5)),
name: new TextDecoder().decode(data.subarray(5)).replace(/\0/g, '')
});
}
if (type === FrameType.ERROR) break;
}
} finally {
bus.releaseReadLock(); // ERST HIER LOCK LÖSEN!
}
return null;
}
}

View File

@@ -0,0 +1,31 @@
import { bus } from '../../bus/SerialBus';
import { Command, FrameType } from '../constants';
export class PlayFileCommand {
async execute(path: string): Promise<boolean | null> {
try {
const p = new TextEncoder().encode(path);
// Wir brauchen: 1 Byte Flags + 1 Byte Länge + Pfad
const req = new Uint8Array(p.length + 2);
req[0] = 0x01; // 1. Byte: Flags (LSB: 1 = sofort abspielen)
req[1] = p.length; // 2. Byte: Länge für get_path() im C-Code
req.set(p, 2); // Ab 3. Byte: Der Pfad-String
await bus.sendRequest(Command.PLAY, req);
// Warten auf das ACK vom Board
if (await bus.waitForSync()) {
const typeArr = await bus.readExact(1);
if (typeArr[0] === FrameType.ACK) {
return true;
}
}
} catch (e) {
console.error("PlayFileCommand failed:", e);
} finally {
bus.releaseReadLock();
}
return null;
}
}

View File

@@ -0,0 +1,91 @@
export const SYNC_SEQ = new TextEncoder().encode('BUZZ');
export enum FrameType {
REQUEST = 0x01,
ACK = 0x10,
RESPONSE = 0x11,
STREAM_START = 0x12,
STREAM_CHUNK = 0x13,
STREAM_END = 0x14,
LIST_START = 0x15,
LIST_CHUNK = 0x16,
LIST_END = 0x17,
ERROR = 0xFF,
}
export enum Command {
GET_PROTOCOL_VERSION = 0x00,
GET_FIRMWARE_STATUS = 0x01,
GET_FLASH_INFO = 0x02,
CONFIRM_FIRMWARE = 0x03,
REBOOT = 0x04,
LIST_DIR = 0x10,
CRC32 = 0x11,
MKDIR = 0x12,
RM = 0x13,
STAT = 0x18,
RENAME = 0x19,
PUT_FILE = 0x20,
PUT_FW = 0x21,
GET_FILE = 0x22,
PUT_TAGS = 0x24,
GET_TAGS = 0x25,
PLAY = 0x30,
STOP = 0x31,
SET_SETTING = 0x40,
GET_SETTING = 0x41,
}
export const ERRORS: Record<number, string> = {
0x00: "NONE",
0x01: "INVALID_COMMAND",
0x02: "INVALID_PARAMETERS",
0x03: "COMMAND_TOO_LONG",
0x10: "FILE_NOT_FOUND",
0x11: "ALREADY_EXISTS",
0x12: "NOT_A_DIRECTORY",
0x13: "IS_A_DIRECTORY",
0x14: "ACCESS_DENIED",
0x15: "NO_SPACE",
0x16: "FILE_TOO_LARGE",
0x20: "IO_ERROR",
0x21: "TIMEOUT",
0x22: "CRC_MISMATCH",
0x23: "TRANSFER_ABORTED",
0x30: "NOT_SUPPORTED",
0x31: "BUSY",
0x32: "INTERNAL_ERROR",
};
export enum ErrorCode {
P_ERR_NONE = 0x00,
P_ERR_INVALID_COMMAND = 0x01,
P_ERR_INVALID_PARAMETERS = 0x02,
P_ERR_COMMAND_TOO_LONG = 0x03,
P_ERR_FILE_NOT_FOUND = 0x10,
P_ERR_ALREADY_EXISTS = 0x11,
P_ERR_NOT_A_DIRECTORY = 0x12,
P_ERR_IS_A_DIRECTORY = 0x13,
P_ERR_ACCESS_DENIED = 0x14,
P_ERR_NO_SPACE = 0x15,
P_ERR_FILE_TOO_LARGE = 0x16,
P_ERR_IO = 0x20,
P_ERR_TIMEOUT = 0x21,
P_ERR_CRC_MISMATCH = 0x22,
P_ERR_TRANSFER_ABORTED = 0x23,
P_ERR_NOT_SUPPORTED = 0x30,
P_ERR_BUSY = 0x31,
P_ERR_INTERNAL = 0x32,
};

View File

@@ -0,0 +1,34 @@
export class BinaryUtils {
/**
* Konvertiert 2 Bytes (Little Endian) in eine Zahl (uint16)
*/
static readUint16LE(data: Uint8Array, offset = 0): number {
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
return view.getUint16(offset, true); // true = Little Endian
}
/**
* Konvertiert 4 Bytes (Little Endian) in eine Zahl (uint32)
*/
static readUint32LE(data: Uint8Array, offset = 0): number {
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
return view.getUint32(offset, true);
}
/**
* Erstellt ein Uint8Array aus einer Zahl (uint16 LE)
*/
static writeUint16LE(value: number): Uint8Array {
const buf = new Uint8Array(2);
const view = new DataView(buf.buffer);
view.setUint16(0, value, true);
return buf;
}
static writeUint32LE(value: number): Uint8Array {
const buf = new Uint8Array(4);
const view = new DataView(buf.buffer);
view.setUint32(0, value, true);
return buf;
}
}

View File

@@ -67,17 +67,7 @@ import type { loadRenderers } from "astro:container";
</section>
<section class="flex-1 flex flex-col gap-6 min-h-0">
<div
class="h-48 bg-indigo-950/20 border border-indigo-500/20 rounded-[2rem] p-6 relative overflow-hidden shrink-0">
<div class="absolute top-6 right-6 text-indigo-500 opacity-20">
<Icon name="ph:cpu-fill" class="w-12 h-12" />
</div>
<h3 class="text-indigo-400 text-[10px] font-black uppercase tracking-[0.2em] mb-4">
Device Info
</h3>
<DeviceInfo client:load />
</div>
<DeviceInfo client:load />
<div class="flex-1 flex flex-col overflow-hidden">
<FileStorage client:load />
</div>