sync
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
134
webpage/src/lib/bus/SerialBus.ts
Normal file
134
webpage/src/lib/bus/SerialBus.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
@@ -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}[]
|
||||
});
|
||||
49
webpage/src/lib/protocol/commands/GetFlashInfo.ts
Normal file
49
webpage/src/lib/protocol/commands/GetFlashInfo.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
26
webpage/src/lib/protocol/commands/GetProtocol.ts
Normal file
26
webpage/src/lib/protocol/commands/GetProtocol.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
37
webpage/src/lib/protocol/commands/GetSettings.ts
Normal file
37
webpage/src/lib/protocol/commands/GetSettings.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
43
webpage/src/lib/protocol/commands/ListDir.ts
Normal file
43
webpage/src/lib/protocol/commands/ListDir.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
31
webpage/src/lib/protocol/commands/PlayFile.ts
Normal file
31
webpage/src/lib/protocol/commands/PlayFile.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
91
webpage/src/lib/protocol/constants.ts
Normal file
91
webpage/src/lib/protocol/constants.ts
Normal 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,
|
||||
};
|
||||
34
webpage/src/lib/utils/BinaryUtils.ts
Normal file
34
webpage/src/lib/utils/BinaryUtils.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user