This commit is contained in:
2026-02-28 10:06:36 +01:00
parent 8a124fe17d
commit 99dcbca8ac
8 changed files with 82 additions and 38 deletions

View File

@@ -3,7 +3,7 @@ import argparse
import sys
from core.config import load_config
from core.connection import BuzzerConnection, BuzzerError
from core.commands import info, ls, put, mkdir, rm, confirm, reboot, play
from core.commands import info, ls, put, mkdir, rm, confirm, reboot, play, check
def main():
parser = argparse.ArgumentParser(description="Edis Buzzer Host Tool")
@@ -42,6 +42,10 @@ def main():
play_parser = subparsers.add_parser("play", help="Spielt eine Datei auf dem Controller ab")
play_parser.add_argument("path", type=str, help="Pfad der abzuspielenden Datei (z.B. /lfs/a/neu)")
# Befehl: check
check_parser = subparsers.add_parser("check", help="Holt die CRC32 einer Datei und zeigt sie an")
check_parser.add_argument("path", type=str, help="Pfad der zu prüfenden Datei (z.B. /lfs/a/neu)")
# Befehl: confirm
confirm_parser = subparsers.add_parser("confirm", help="Bestätigt die aktuell laufende Firmware")
@@ -95,6 +99,12 @@ def main():
reboot.execute(conn)
elif args.command == "play":
play.execute(conn, path=args.path)
elif args.command == "check":
CRC32 = check.execute(conn, path=args.path)
if CRC32:
print(f"CRC32 von '{args.path}': 0x{CRC32['crc32']:08x}")
else:
print(f"Fehler: Keine CRC32-Information für '{args.path}' erhalten.")
elif args.command == "info" or args.command is None:
# Wurde kein Befehl oder explizit 'info' angegeben, sind wir hier schon fertig
pass

View File

@@ -351,6 +351,7 @@ int cmd_reboot_device()
void cmd_play(const char *filename)
{
LOG_DBG("Play command received with filename: '%s'", filename);
audio_stop();
audio_play(filename);
}
@@ -366,6 +367,7 @@ int cmd_check(const char *param)
return -ENOENT;
}
uint32_t crc32 = 0;
uint32_t start_time = k_uptime_get_32();
uint8_t buffer[256];
ssize_t read;
while ((read = fs_read(&file, buffer, sizeof(buffer))) > 0)
@@ -378,7 +380,8 @@ int cmd_check(const char *param)
LOG_ERR("Check failed: error reading file '%s': %d", param, (int)read);
return (int)read;
}
LOG_DBG("Check successful: file '%s' has CRC32 0x%08x", param, crc32);
uint32_t duration = k_uptime_get_32() - start_time;
LOG_DBG("Check successful: file '%s' has CRC32 0x%08x, check took %u ms", param, crc32, duration);
char response[64];
snprintf(response, sizeof(response), "CRC32 %s 0x%08x\n", param, crc32);
usb_write_buffer((const uint8_t *)response, strlen(response));

View File

@@ -14,6 +14,7 @@
"@tailwindcss/vite": "^4.2.1",
"astro": "^5.17.1",
"astro-icon": "^1.1.5",
"phosphor-svelte": "^3.1.0",
"svelte": "^5.53.5",
"tailwindcss": "^4.2.1",
"typescript": "^5.9.3"
@@ -4981,6 +4982,25 @@
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
"license": "MIT"
},
"node_modules/phosphor-svelte": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/phosphor-svelte/-/phosphor-svelte-3.1.0.tgz",
"integrity": "sha512-nldtxx+XCgNREvrb7O5xgDsefytXpSkPTx8Rnu3f2qQCUZLDV1rLxYSd2Jcwckuo9lZB1qKMqGR17P4UDC0PrA==",
"license": "MIT",
"dependencies": {
"estree-walker": "^3.0.3",
"magic-string": "^0.30.13"
},
"peerDependencies": {
"svelte": "^5.0.0 || ^5.0.0-next.96",
"vite": ">=5"
},
"peerDependenciesMeta": {
"vite": {
"optional": true
}
}
},
"node_modules/piccolore": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz",

View File

@@ -15,6 +15,7 @@
"@tailwindcss/vite": "^4.2.1",
"astro": "^5.17.1",
"astro-icon": "^1.1.5",
"phosphor-svelte": "^3.1.0",
"svelte": "^5.53.5",
"tailwindcss": "^4.2.1",
"typescript": "^5.9.3"

View File

@@ -1,42 +1,58 @@
<script lang="ts">
import { buzzer } from '../lib/buzzerStore';
import { playFile, deleteFile } from '../lib/buzzerActions';
import { MusicNotesIcon, WrenchIcon, PlayIcon, TrashIcon, ArrowsLeftRightIcon, QuestionMarkIcon } from "phosphor-svelte";
// Die Datei-Daten werden vom Parent (FileStorage) übergeben
export let file: { name: string, size: string, isSystem: boolean };
export let file: { name: string, size: string, isSystem: boolean, crc32?: number};
export let selected = false;
// Wir benötigen den Port für die Kommandos
// In einem echten Szenario würden wir den Port in einem Store speichern.
// Hier nehmen wir an, er wird über die Actions gehandelt.
</script>
<div
class="flex items-center justify-between p-3 cursor-pointer transition-all group border-b border-slate-700/30
{selected ? 'bg-blue-600/20 border-l-4 border-blue-500 shadow-[inset_0_0_20px_rgba(59,130,246,0.1)]' : 'hover:bg-slate-700/40 border-l-4 border-transparent'}"
>
<div class="flex items-center gap-4 overflow-hidden">
<div class="flex-1 flex items-center gap-4 min-w-0">
<div class="text-2xl {selected ? 'text-blue-400' : 'text-slate-500'} shrink-0">
{#if file.isSystem}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M225.9,103.11l-40.45,40.44a8,8,0,0,1-11.31,0L148,117.41a8,8,0,0,1,0-11.31l40.44-40.45a72,72,0,0,0-85.3,109.11l-51.5,51.5a24,24,0,0,0,33.94,33.94l51.5-51.5A72,72,0,0,0,225.9,103.11ZM160,80a8,8,0,1,1-8-8A8,8,0,0,1,160,80Z"></path></svg>
<WrenchIcon size={24} weight="fill" />
{:else}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M210.3,56.34l-80-24A8,8,0,0,0,120,40V148.26A48,48,0,1,0,136,184V98.71l69.7,20.91a8,8,0,0,0,10.3-7.62V64A8,8,0,0,0,210.3,56.34ZM88,216a32,32,0,1,1,32-32A32,32,0,0,1,88,216Zm112-112.71L136,83.89V49.11L200,68.31Z"></path></svg>
<MusicNotesIcon size={24} weight="fill" />
{/if}
</div>
<div class="flex flex-col overflow-hidden">
<span class="text-sm truncate {selected ? 'font-bold text-white' : 'text-slate-200'}">{file.name}</span>
<span class="text-[10px] text-slate-500 font-mono tracking-tighter uppercase">{file.size}</span>
<div class="flex-1 flex flex-col min-w-0">
<span class="text-sm truncate overflow-hidden {selected ? 'font-bold text-white' : 'text-slate-200'}">
{file.name}
</span>
<div class="flex items-center gap-2">
<div class="relative group/sync flex items-center">
{#if !file.crc32 || isNaN(file.crc32)}
<QuestionMarkIcon size={12} weight="bold" class="text-slate-700" />
{:else}
<ArrowsLeftRightIcon size={12} weight="bold" class="text-slate-300" />
{/if}
<div class="absolute bottom-full left-1/2 -translate-x-1/2 mb-1 px-2 py-1 bg-slate-900 text-[10px] text-white rounded opacity-0 group-hover/sync:opacity-100 pointer-events-none transition-opacity whitespace-nowrap z-50 border border-slate-700 shadow-xl">
{file.crc32 ? `CRC32: 0x${file.crc32.toString(16).toUpperCase()}` : 'CRC unbekannt'}
<div class="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-slate-900"></div>
</div>
</div>
<span class="text-[10px] text-slate-500 font-mono tracking-tighter uppercase">
{file.size}
</span>
</div>
</div>
</div>
<div class="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity shrink-0">
<div class="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity shrink-0 ml-4">
<button
on:click|stopPropagation={() => playFile(file.name)}
class="p-2 hover:bg-blue-500/20 rounded-lg text-blue-400 transition-colors"
title="Play Sound"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256"><path d="M240,128a15.74,15.74,0,0,1-7.6,13.51L88.32,229.75a16,16,0,0,1-16.2,0A15.86,15.86,0,0,1,64,216.24V39.76a15.86,15.86,0,0,1,8.12-13.51,16,16,0,0,1,16.2,0L232.4,114.49A15.74,15.74,0,0,1,240,128Z"></path></svg>
<PlayIcon size={16} weight="fill" />
</button>
<button
@@ -44,7 +60,7 @@
class="p-2 hover:bg-red-500/20 rounded-lg text-red-400 transition-colors"
title="Delete File"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg>
<TrashIcon size={16} weight="fill" />
</button>
</div>
</div>

View File

@@ -2,10 +2,9 @@
import { buzzer } from '../lib/buzzerStore';
import { refreshFileList } from '../lib/buzzerActions';
import FileRow from './FileRow.svelte';
import { ArrowsCounterClockwiseIcon } from 'phosphor-svelte';
async function handleRefresh() {
// Falls wir den Port im Store haben, hier nutzen
// ansonsten wird er über die Actions verwaltet
console.log("Aktualisiere Dateiliste...");
await refreshFileList();
}
</script>
@@ -19,9 +18,10 @@
<button
on:click={handleRefresh}
class="text-slate-500 hover:text-blue-400 transition-colors active:rotate-180 duration-500">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256">
<rect width="256" height="256" fill="none"/><polyline points="88 96 40 96 40 48" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M40,96,68.28,67.72A88,88,0,0,1,192,67" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><polyline points="168 160 216 160 216 208" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M216,160l-28.28,28.28A88,88,0,0,1,64,189" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/> </svg>
class="text-slate-500 hover:text-blue-400 transition-colors active:rotate-180 duration-500"
title="Refresh File List"
>
<ArrowsCounterClockwiseIcon size={16} weight="fill" />
</button>
</div>
@@ -44,14 +44,4 @@
</div>
{/if}
</div>
<div class="absolute bottom-6 right-6 text-slate-700 opacity-5 pointer-events-none">
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" fill="currentColor" viewBox="0 0 256 256">
<path d="M208,40V216a16,16,0,0,1-16,16H64a16,16,0,0,1-16-16V40A16,16,0,0,1,64,24H192A16,16,0,0,1,208,40Z" opacity="0.2"></path>
<path d="M208,40V216a16,16,0,0,1-16,16H64a16,16,0,0,1-16-16V40A16,16,0,0,1,64,24H192A16,16,0,0,1,208,40Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"></path>
<line x1="80" y1="24" x2="80" y2="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"></line>
<line x1="128" y1="24" x2="128" y2="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"></line>
<line x1="176" y1="24" x2="176" y2="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"></line>
</svg>
</div>
</div>

View File

@@ -1,4 +1,5 @@
// src/lib/buzzerActions.ts
import { nan } from 'astro:schema';
import { buzzer } from './buzzerStore';
let activePort: SerialPort | null = null;
@@ -53,7 +54,10 @@ export async function updateDeviceInfo(port: SerialPort) {
// WICHTIG: Auch hier "export"
export async function refreshFileList(port: SerialPort) {
if (!port) return;
if (!port) {
if (!activePort) return;
port = activePort;
}
let audioSize = 0;
let sysSize = 0;
const audioFiles: {name: string, size: string, crc32: number, isSystem: boolean}[] = [];
@@ -73,7 +77,7 @@ export async function refreshFileList(port: SerialPort) {
if (path.startsWith('/lfs/a')) {
audioSize += size;
console.log("Audio-Datei gefunden:", name);
audioFiles.push({ name, size: (size / 1024).toFixed(1) + " KB", crc32: 0, isSystem: false });
audioFiles.push({ name, size: (size / 1024).toFixed(1) + " KB", crc32: NaN, isSystem: false, isSynced: false });
} else if (path.startsWith('/lfs/sys')) {
sysSize += size;
}

View File

@@ -7,10 +7,10 @@ export const buzzer = writable({
build: 'unknown',
storage: {
total: 8.0, // 8 MB Flash laut Spezifikation
unknown: 8.0,
available: 0.0,
unknown: 8.0,
usedSys: 0.0,
usedAudio: 0.0
},
files: [] as {name: string, size: string, crc32: number, isSystem: boolean}[]
files: [] as {name: string, size: string, crc32: number, isSystem: boolean, isSynced: boolean}[]
});