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 import sys
from core.config import load_config from core.config import load_config
from core.connection import BuzzerConnection, BuzzerError 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(): def main():
parser = argparse.ArgumentParser(description="Edis Buzzer Host Tool") 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 = 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)") 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 # Befehl: confirm
confirm_parser = subparsers.add_parser("confirm", help="Bestätigt die aktuell laufende Firmware") confirm_parser = subparsers.add_parser("confirm", help="Bestätigt die aktuell laufende Firmware")
@@ -95,6 +99,12 @@ def main():
reboot.execute(conn) reboot.execute(conn)
elif args.command == "play": elif args.command == "play":
play.execute(conn, path=args.path) 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: elif args.command == "info" or args.command is None:
# Wurde kein Befehl oder explizit 'info' angegeben, sind wir hier schon fertig # Wurde kein Befehl oder explizit 'info' angegeben, sind wir hier schon fertig
pass pass

View File

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

View File

@@ -14,6 +14,7 @@
"@tailwindcss/vite": "^4.2.1", "@tailwindcss/vite": "^4.2.1",
"astro": "^5.17.1", "astro": "^5.17.1",
"astro-icon": "^1.1.5", "astro-icon": "^1.1.5",
"phosphor-svelte": "^3.1.0",
"svelte": "^5.53.5", "svelte": "^5.53.5",
"tailwindcss": "^4.2.1", "tailwindcss": "^4.2.1",
"typescript": "^5.9.3" "typescript": "^5.9.3"
@@ -4981,6 +4982,25 @@
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
"license": "MIT" "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": { "node_modules/piccolore": {
"version": "0.1.3", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz",

View File

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

View File

@@ -1,42 +1,58 @@
<script lang="ts"> <script lang="ts">
import { buzzer } from '../lib/buzzerStore'; import { buzzer } from '../lib/buzzerStore';
import { playFile, deleteFile } from '../lib/buzzerActions'; 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, crc32?: number};
export let file: { name: string, size: string, isSystem: boolean };
export let selected = false; 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> </script>
<div <div
class="flex items-center justify-between p-3 cursor-pointer transition-all group border-b border-slate-700/30 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'}" {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"> <div class="text-2xl {selected ? 'text-blue-400' : 'text-slate-500'} shrink-0">
{#if file.isSystem} {#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} {: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} {/if}
</div> </div>
<div class="flex flex-col overflow-hidden"> <div class="flex-1 flex flex-col min-w-0">
<span class="text-sm truncate {selected ? 'font-bold text-white' : 'text-slate-200'}">{file.name}</span> <span class="text-sm truncate overflow-hidden {selected ? 'font-bold text-white' : 'text-slate-200'}">
<span class="text-[10px] text-slate-500 font-mono tracking-tighter uppercase">{file.size}</span> {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>
</div> </div>
<div class="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity shrink-0"> <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 ml-4">
<button <button
on:click|stopPropagation={() => playFile(file.name)} on:click|stopPropagation={() => playFile(file.name)}
class="p-2 hover:bg-blue-500/20 rounded-lg text-blue-400 transition-colors" class="p-2 hover:bg-blue-500/20 rounded-lg text-blue-400 transition-colors"
title="Play Sound" 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>
<button <button
@@ -44,7 +60,7 @@
class="p-2 hover:bg-red-500/20 rounded-lg text-red-400 transition-colors" class="p-2 hover:bg-red-500/20 rounded-lg text-red-400 transition-colors"
title="Delete File" 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> </button>
</div> </div>
</div> </div>

View File

@@ -2,10 +2,9 @@
import { buzzer } from '../lib/buzzerStore'; import { buzzer } from '../lib/buzzerStore';
import { refreshFileList } from '../lib/buzzerActions'; import { refreshFileList } from '../lib/buzzerActions';
import FileRow from './FileRow.svelte'; import FileRow from './FileRow.svelte';
import { ArrowsCounterClockwiseIcon } from 'phosphor-svelte';
async function handleRefresh() { async function handleRefresh() {
// Falls wir den Port im Store haben, hier nutzen console.log("Aktualisiere Dateiliste...");
// ansonsten wird er über die Actions verwaltet
await refreshFileList(); await refreshFileList();
} }
</script> </script>
@@ -19,9 +18,10 @@
<button <button
on:click={handleRefresh} on:click={handleRefresh}
class="text-slate-500 hover:text-blue-400 transition-colors active:rotate-180 duration-500"> 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"> title="Refresh File List"
<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> >
<ArrowsCounterClockwiseIcon size={16} weight="fill" />
</button> </button>
</div> </div>
@@ -44,14 +44,4 @@
</div> </div>
{/if} {/if}
</div> </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> </div>

View File

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

View File

@@ -7,10 +7,10 @@ export const buzzer = writable({
build: 'unknown', build: 'unknown',
storage: { storage: {
total: 8.0, // 8 MB Flash laut Spezifikation total: 8.0, // 8 MB Flash laut Spezifikation
unknown: 8.0,
available: 0.0, available: 0.0,
unknown: 8.0,
usedSys: 0.0, usedSys: 0.0,
usedAudio: 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}[]
}); });