Stand vor Protokollumbau

This commit is contained in:
2026-03-01 11:11:56 +01:00
parent c914333236
commit 4d88194f7d
17 changed files with 624 additions and 79 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, check
from core.commands import info, ls, put, mkdir, rm, confirm, reboot, play, check, get_tag
def main():
parser = argparse.ArgumentParser(description="Edis Buzzer Host Tool")
@@ -52,6 +52,10 @@ def main():
# Befehl: reboot
reboot_parser = subparsers.add_parser("reboot", help="Startet den Buzzer neu")
# Befehl: get_tag
get_tag_parser = subparsers.add_parser("get_tag", help="Holt die Tags einer Datei")
get_tag_parser.add_argument("path", type=str, help="Pfad der Datei (z.B. /lfs/a/neu)")
# Argumente parsen
args = parser.parse_args()
config = load_config(args)
@@ -105,6 +109,14 @@ def main():
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 == "get_tag":
tags = get_tag.execute(conn, path=args.path)
if tags:
print(f"Tags von '{args.path}':")
for key, value in tags.items():
print(f" {key}: {value}")
else:
print(f"Fehler: Keine Tags 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

5
buzzer_tool/config.yaml Normal file
View File

@@ -0,0 +1,5 @@
# config.yaml
serial:
port: "COM17"
baudrate: 250000
timeout: 10

View File

@@ -8,13 +8,10 @@ def execute(conn, path: str) -> dict:
raise BuzzerError("Keine Antwort auf 'check' empfangen.")
parts = lines[0].split()
if len(parts) != 3 or parts[0] != "CRC32":
if len(parts) != 1:
raise BuzzerError(f"Unerwartetes Check-Format: {lines[0]}")
if parts[1] != path:
raise BuzzerError(f"Unerwarteter Pfad in Check-Antwort: {parts[1]} (erwartet: {path})")
crc32 = int(parts[2], 16)
crc32 = int(parts[0], 16)
return {
"crc32": crc32

View File

@@ -0,0 +1,56 @@
# core/commands/get_tag.py
from core.connection import BuzzerError
from core.util import hex_to_bytearray
def execute(conn, path: str) -> dict:
"""Holt Tags einer Datei und gibt sie als strukturiertes Dictionary zurück."""
lines = conn.send_command("get_tag " + path)
if not lines:
raise BuzzerError("Keine Antwort auf 'get_tag' empfangen.")
parts = lines[0].split()
if len(parts) != 1:
raise BuzzerError(f"Unerwartetes get_tag-Format: {lines[0]}")
data = hex_to_bytearray(parts[0])
if data is None:
raise BuzzerError("Ungültiger Hex-String in get_tag-Antwort.")
pos = 0
tags = {}
while pos < len(data):
tag_type = data[pos]
pos += 1
if pos >= len(data):
raise BuzzerError(f"Unerwartetes Ende des Hex-Strings bei get_tag (Tag-Typ erwartet). Position: {pos}/{len(data)}")
match tag_type:
case 0x01: # Kommentar
length = data[pos]
pos += 1
if pos + length > len(data):
raise BuzzerError(f"Unerwartetes Ende des Hex-Strings bei get_tag (Kommentar erwartet). Position: {pos}")
comment = data[pos:pos+length].decode('utf-8')
pos += length
tags["comment"] = comment
case 0x02: # Author
length = data[pos]
pos += 1
if pos + length > len(data):
raise BuzzerError(f"Unerwartetes Ende des Hex-Strings bei get_tag (Author erwartet). Position: {pos}")
author = data[pos:pos+length].decode('utf-8')
pos += length
tags["author"] = author
case 0x10: # CRC32
if pos + 4 > len(data):
raise BuzzerError(f"Unerwartetes Ende des Hex-Strings bei get_tag (CRC32 erwartet). Position: {pos}")
crc32 = int.from_bytes(data[pos:pos+4], byteorder='big')
pos += 4
tags["crc32"] = hex(crc32)
case _: # Default / Unbekannter Tag
tags[f"unknown_0x{tag_type:02x}"] = tag_value_raw.hex()
return tags

25
buzzer_tool/core/util.py Normal file
View File

@@ -0,0 +1,25 @@
def hex_to_bytearray(hex_string):
"""
Wandelt einen Hex-String (z.B. "deadbeef") in ein bytearray um.
Entfernt vorher Leerzeichen und prüft auf Gültigkeit.
"""
try:
# Whitespace entfernen (falls vorhanden)
clean_hex = hex_string.strip().replace(" ", "")
# Konvertierung
return bytearray.fromhex(clean_hex)
except ValueError as e:
print(f"Fehler bei der Konvertierung: {e}")
return None
def string_to_hexstring(text):
"""
Wandelt einen String in einen UTF-8-kodierten Hex-String um.
"""
# 1. String zu UTF-8 Bytes
utf8_bytes = text.encode('utf-8')
# 2. Bytes zu Hex-String
return utf8_bytes.hex()

View File

@@ -3,9 +3,10 @@ mcuboot:
size: 0xC000
region: flash_primary
# Primary Slot: Start bleibt 0xC000, Größe jetzt 200KB (0x32000)
mcuboot_primary:
address: 0xC000
size: 0x25000
size: 0x32000
region: flash_primary
mcuboot_pad:
@@ -13,16 +14,19 @@ mcuboot_pad:
size: 0x200
region: flash_primary
# Die App startet nach dem Padding des Primary Slots
app:
address: 0xC200
size: 0x24E00
size: 0x31E00 # (0x32000 - 0x200)
region: flash_primary
# Secondary Slot: Startet jetzt bei 0xC000 + 0x32000 = 0x3E000
mcuboot_secondary:
address: 0x31000
size: 0x25000
address: 0x3E000
size: 0x32000
region: flash_primary
# External Flash bleibt unverändert
littlefs_storage:
address: 0x0
size: 0x800000

View File

@@ -243,6 +243,8 @@ void audio_thread(void *arg1, void *arg2, void *arg3)
continue;
}
ssize_t file_size = fs_get_audio_data_len(&file);
LOG_INF("Playing: %s", filename);
io_status(true);
@@ -269,7 +271,7 @@ void audio_thread(void *arg1, void *arg2, void *arg3)
break;
}
ssize_t bytes_read = fs_read(&file, block, AUDIO_BLOCK_SIZE / 2);
ssize_t bytes_read = fs_read_audio(&file, block, AUDIO_BLOCK_SIZE / 2, file_size);
if (bytes_read <= 0)
{

View File

@@ -3,8 +3,11 @@
#include <zephyr/dfu/flash_img.h>
#include <zephyr/dfu/mcuboot.h>
#include <zephyr/pm/device.h>
#include <fs.h>
LOG_MODULE_REGISTER(buzz_fs, LOG_LEVEL_INF);
#include <utils.h>
LOG_MODULE_REGISTER(buzz_fs, LOG_LEVEL_DBG);
#define STORAGE_PARTITION_ID FIXED_PARTITION_ID(littlefs_storage)
#define SLOT1_ID FIXED_PARTITION_ID(slot1_partition)
@@ -167,6 +170,126 @@ int fs_pm_mkdir(const char *path)
fs_pm_flash_suspend();
return rc;
}
ssize_t fs_get_audio_data_len(struct fs_file_t *fp) {
uint8_t footer[6];
off_t file_size;
fs_seek(fp, 0, FS_SEEK_END);
file_size = fs_tell(fp);
fs_seek(fp, 0, FS_SEEK_SET);
if (file_size < 6) return file_size;
fs_seek(fp, -6, FS_SEEK_END);
if (fs_read(fp, footer, 6) == 6) {
if (memcmp(&footer[2], "TAG!", 4) == 0) {
uint16_t tag_len = footer[0] | (footer[1] << 8);
if (tag_len <= file_size) {
fs_seek(fp, 0, FS_SEEK_SET);
return file_size - tag_len;
}
}
}
fs_seek(fp, 0, FS_SEEK_SET);
return file_size;
}
ssize_t fs_read_audio(struct fs_file_t *fp, void *buffer, size_t len, size_t audio_limit) {
off_t current_pos = fs_tell(fp);
if (current_pos >= audio_limit) {
return 0; // "Virtuelles" Dateiende erreicht
}
size_t remaining = audio_limit - current_pos;
size_t to_read = (len < remaining) ? len : remaining;
return fs_read(fp, buffer, to_read);
}
int fs_write_hex_tag(struct fs_file_t *fp, const char *hex_str) {
size_t hex_len = strlen(hex_str);
// Ein Hex-String muss eine gerade Anzahl an Zeichen haben
if (hex_len % 2 != 0) return -EINVAL;
size_t payload_len = hex_len / 2;
uint16_t total_footer_len = (uint16_t)(payload_len + 6);
// 1. Audio-Ende bestimmen und dorthin seeken
size_t audio_limit = fs_get_audio_data_len(fp);
fs_seek(fp, audio_limit, FS_SEEK_SET);
// 2. Payload Byte für Byte konvertieren und schreiben
for (size_t i = 0; i < hex_len; i += 2) {
int high = hex2int(hex_str[i]);
int low = hex2int(hex_str[i+1]);
if (high < 0 || low < 0) return -EINVAL; // Ungültiges Hex-Zeichen
uint8_t byte = (uint8_t)((high << 4) | low);
fs_write(fp, &byte, 1);
}
// 3. Die 2 Bytes Länge schreiben (Little Endian)
uint8_t len_bytes[2];
len_bytes[0] = (uint8_t)(total_footer_len & 0xFF);
len_bytes[1] = (uint8_t)((total_footer_len >> 8) & 0xFF);
fs_write(fp, len_bytes, 2);
// 4. Magic Bytes schreiben
fs_write(fp, "TAG!", 4);
// 5. Datei am aktuellen Punkt abschneiden
off_t current_pos = fs_tell(fp);
return fs_truncate(fp, current_pos);
}
int fs_read_hex_tag(struct fs_file_t *fp, char *hex_str, size_t hex_str_size) {
if (hex_str == NULL || hex_str_size == 0) {
return -EINVAL;
}
hex_str[0] = '\0';
// Dateigröße ermitteln
fs_seek(fp, 0, FS_SEEK_END);
off_t file_size = fs_tell(fp);
// Audio-Limit finden (Anfang des Payloads)
size_t audio_limit = fs_get_audio_data_len(fp);
// Prüfen, ob überhaupt ein Tag existiert (audio_limit < file_size)
if (audio_limit >= file_size) {
// Kein Tag vorhanden -> leerer String
return 0;
}
// Die Payload-Länge ist: Gesamtgröße - Audio - 6 Bytes (Länge + Magic)
size_t payload_len = file_size - audio_limit - 6;
if ((payload_len * 2U) + 1U > hex_str_size) {
return -ENOMEM; // Nicht genug Platz im Zielpuffer
}
// Zum Anfang des Payloads springen
fs_seek(fp, audio_limit, FS_SEEK_SET);
uint8_t byte;
for (size_t i = 0; i < payload_len; i++) {
if (fs_read(fp, &byte, 1) != 1) {
return -EIO;
}
// Jedes Byte als zwei Hex-Zeichen in den Zielpuffer schreiben
hex_str[i * 2] = int2hex(byte >> 4);
hex_str[i * 2 + 1] = int2hex(byte & 0x0F);
}
hex_str[payload_len * 2] = '\0';
return 0;
}
int flash_get_slot_info(slot_info_t *info) {
if (slot1_info.size != 0) {
*info = slot1_info;

View File

@@ -82,6 +82,40 @@ int fs_pm_statvfs(const char *path, struct fs_statvfs *stat);
*/
int fs_pm_mkdir(const char *path);
/**
* @brief Gets the length of the audio data in a file, accounting for any metadata tags
* @param fp Pointer to an open fs_file_t structure representing the audio file
* @return Length of the audio data in bytes, or negative error code on failure
*/
int fs_get_audio_data_len(struct fs_file_t *fp);
/**
* @brief Reads audio data from a file, ensuring that it does not read past the audio data limit
* @param fp Pointer to an open fs_file_t structure representing the audio file
* @param buffer Pointer to the buffer to read data into
* @param len Maximum number of bytes to read
* @param audio_limit Maximum byte offset for audio data (e.g. file size minus metadata)
* @return Number of bytes read, or negative error code on failure
*/
int fs_read_audio(struct fs_file_t *fp, void *buffer, size_t len, size_t audio_limit);
/**
* @brief Writes a hexadecimal string as a metadata tag at the end of an audio file
* @param fp Pointer to an open fs_file_t structure representing the audio file
* @param hex_str Null-terminated string containing hexadecimal characters (0-9, a-f, A-F)
* @return 0 on success, negative error code on failure
*/
int fs_write_hex_tag(struct fs_file_t *fp, const char *hex_str);
/**
* @brief Reads a hexadecimal string from a metadata tag at the end of an audio file
* @param fp Pointer to an open fs_file_t structure representing the audio file
* @param hex_str Buffer to be filled with the hexadecimal string (must be large enough to hold the data)
* @param hex_str_size Size of the hex_str buffer
* @return 0 on success, negative error code on failure
*/
int fs_read_hex_tag(struct fs_file_t *fp, char *hex_str, size_t hex_str_size);
/**
* @brief Retrieves information about the firmware slot, such as start address and size
* @param info Pointer to slot_info_t structure to be filled with slot information

View File

@@ -15,7 +15,7 @@
#define PROTOCOL_VERSION 2
LOG_MODULE_REGISTER(protocol, LOG_LEVEL_DBG);
LOG_MODULE_REGISTER(protocol, LOG_LEVEL_INF);
#define PROTOCOL_STACK_SIZE 2048
#define PROTOCOL_PRIORITY 6
@@ -319,6 +319,63 @@ int cmd_rm(const char *path)
return rc;
}
int cmd_set_tag(const char *param)
{
LOG_DBG("SET_TAG command received with parameter: '%s'", param);
if (param == NULL || param[0] == '\0')
{
LOG_ERR("SET_TAG command requires a non-empty parameter");
return -EINVAL;
}
uint8_t tag_buffer[256];
uint8_t filename[64];
int rc = sscanf(param, "%63[^;];%255[^\n]", filename, tag_buffer);
if (rc != 2) {
LOG_ERR("Invalid parameters for SET_TAG command (got %d): '%s'", rc, param);
return -EINVAL;
}
struct fs_file_t file;
fs_file_t_init(&file);
rc = fs_pm_open(&file, (const char *)filename, FS_O_READ | FS_O_WRITE);
if (rc < 0) {
LOG_ERR("Failed to open file '%s' for SET_TAG: %d", filename, rc);
return rc;
}
rc = fs_write_hex_tag(&file, (const char *)tag_buffer);
fs_pm_close(&file);
if (rc < 0) {
LOG_ERR("Failed to write tag to file '%s': %d", filename, rc);
return rc;
}
LOG_DBG("Tag written successfully to file '%s'", filename);
return 0;
}
int cmd_get_tag(const char *param)
{
LOG_DBG("GET_TAG command received");
struct fs_file_t file;
fs_file_t_init(&file);
int rc = fs_pm_open(&file, param, FS_O_READ);
if (rc < 0) {
LOG_ERR("Failed to open file '%s' for GET_TAG: %d", param, rc);
return rc;
}
uint8_t tag_buffer[256];
rc = fs_read_hex_tag(&file, tag_buffer, sizeof(tag_buffer));
fs_pm_close(&file);
if (rc < 0) {
LOG_ERR("Failed to read tag from file '%s': %d", param, rc);
return rc;
}
LOG_DBG("Tag read successfully from file '%s': '%s'", param, tag_buffer);
LOG_HEXDUMP_DBG(tag_buffer, strlen((char *)tag_buffer), "Tag content");
usb_write_buffer(tag_buffer, strlen((char *)tag_buffer));
usb_write_char('\n');
return 0;
}
int cmd_confirm_firmware()
{
if (!boot_is_img_confirmed())
@@ -370,7 +427,8 @@ int cmd_check(const char *param)
uint32_t start_time = k_uptime_get_32();
uint8_t buffer[256];
ssize_t read;
while ((read = fs_read(&file, buffer, sizeof(buffer))) > 0)
ssize_t file_size = fs_get_audio_data_len(&file);
while ((read = fs_read_audio(&file, buffer, sizeof(buffer), file_size)) > 0)
{
crc32 = crc32_ieee_update(crc32, buffer, read);
}
@@ -383,7 +441,7 @@ int cmd_check(const char *param)
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);
snprintf(response, sizeof(response), "0x%08x\n", crc32);
usb_write_buffer((const uint8_t *)response, strlen(response));
return 0;
}
@@ -506,6 +564,28 @@ void execute_current_command(void)
send_error(protocol_map_error(rc));
}
break;
case CMD_SET_TAG:
LOG_DBG("Executing SET_TAG command");
rc = cmd_set_tag((char *)buffer);
if (rc == 0)
{
send_ok();
}
else
{
send_error(protocol_map_error(rc));
}
break;
case CMD_GET_TAG:
LOG_DBG("Executing GET_TAG command");
rc = cmd_get_tag((char *)buffer);
if (rc == 0) {
send_ok();
}
else {
send_error(protocol_map_error(rc));
}
break;
default:
LOG_ERR("No execution logic for command %d", current_command);
send_error(P_ERR_NOT_SUPPORTED);
@@ -576,6 +656,15 @@ protocol_state_t reading_command(uint8_t byte)
{
LOG_DBG("Received CHECK command");
current_command = CMD_CHECK;
} else if (strcmp((char *)buffer, "gett") == 0)
{
LOG_DBG("Received GETT command");
current_command = CMD_GET_TAG;
}
else if (strcmp((char *)buffer, "sett") == 0)
{
LOG_DBG("Received SETT command");
current_command = CMD_SET_TAG;
}
else
{

View File

@@ -18,6 +18,8 @@ typedef enum {
CMD_CONFIRM,
CMD_REBOOT,
CMD_PLAY,
CMD_SET_TAG,
CMD_GET_TAG,
CMD_CHECK,
/* Weitere Kommandos folgen hier */
} protocol_cmd_t;

View File

@@ -50,4 +50,16 @@ uint8_t get_reboot_status(void)
printk("Reboot status detected: 0x%02x\n", status);
}
return status;
}
int hex2int(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1; // Fehlerhaftes Zeichen
}
char int2hex(uint8_t i) {
if (i < 10) return '0' + i;
return 'a' + (i - 10);
}

View File

@@ -21,4 +21,19 @@ void reboot_with_status(uint8_t status_code);
* @return The reboot status code set before the last reboot, or 0 if no status was set.
*/
uint8_t get_reboot_status();
#endif // UTILS_H
/**
* @brief Converts a hexadecimal character to its integer value.
* @param c The hexadecimal character (0-9, a-f, A-F) to convert.
* @return The integer value of the hexadecimal character, or -1 if the character is not a valid hexadecimal digit.
*/
int hex2int(char c);
/**
* @brief Converts an integer value to its hexadecimal character representation.
* @param i The integer value to convert (0-15).
* @return The hexadecimal character representation of the integer value.
*/
char int2hex(uint8_t i);
#endif // UTILS_H

View File

@@ -1,20 +1,33 @@
<script lang="ts">
import { onMount } from 'svelte';
import { buzzer } from '../lib/buzzerStore';
import { connectToPort, initSerialListeners } from '../lib/buzzerActions';
import { connectToPort, disconnectBuzzer,initSerialListeners } from '../lib/buzzerActions';
import { PlugsIcon, PlugsConnectedIcon } from 'phosphor-svelte';
const BUZZER_FILTER = [
{ usbVendorId: 0x2fe3, usbProductId: 0x0001 },
];
onMount(() => {
initSerialListeners();
});
async function handleConnectClick() {
try {
// Wenn wir schon gekoppelt sind, aber nicht verbunden,
// wird navigator.serial.requestPort() den Picker zeigen.
const port = await navigator.serial.requestPort();
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);
}
}

View File

@@ -0,0 +1,36 @@
<script lang="ts">
import { toasts, removeToast } from '../lib/toastStore';
import { flip } from 'svelte/animate';
import { fade, fly } from 'svelte/transition';
import { CheckCircleIcon, XCircleIcon, InfoIcon, WarningIcon, XIcon } from 'phosphor-svelte';
</script>
<div class="fixed bottom-6 right-6 z-[100] flex flex-col-reverse gap-3 pointer-events-none w-80">
{#each $toasts as toast (toast.id)}
<div
animate:flip={{ duration: 300 }}
in:fly={{ x: 50, duration: 400 }}
out:fade={{ duration: 200 }}
class="pointer-events-auto flex items-start gap-3 p-4 rounded-2xl border shadow-2xl backdrop-blur-md
{toast.type === 'success' ? 'bg-emerald-500/10 border-emerald-500/50 text-emerald-200' : ''}
{toast.type === 'error' ? 'bg-red-500/10 border-red-500/50 text-red-200' : ''}
{toast.type === 'warning' ? 'bg-amber-500/10 border-amber-500/50 text-amber-200' : ''}
{toast.type === 'info' ? 'bg-blue-500/10 border-blue-500/50 text-blue-200' : ''}"
>
<div class="shrink-0 mt-0.5">
{#if toast.type === 'success'}<CheckCircleIcon weight="fill" size={20} />{/if}
{#if toast.type === 'error'}<XCircleIcon weight="fill" size={20} />{/if}
{#if toast.type === 'warning'}<WarningIcon weight="fill" size={20} />{/if}
{#if toast.type === 'info'}<InfoIcon weight="fill" size={20} />{/if}
</div>
<div class="flex-1 text-xs font-medium leading-relaxed">
{toast.message}
</div>
<button on:click={() => removeToast(toast.id)} class="shrink-0 opacity-50 hover:opacity-100 transition-opacity">
<XIcon size={16} />
</button>
</div>
{/each}
</div>

View File

@@ -3,12 +3,13 @@ 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;
key?: string;
};
class SerialQueue {
@@ -16,6 +17,9 @@ class SerialQueue {
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[]> {
@@ -43,38 +47,81 @@ class SerialQueue {
try {
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const writer = this.port.writable.getWriter();
const reader = this.port.readable.getReader();
await writer.write(encoder.encode(task.command + "\n"));
writer.releaseLock();
// 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) {
const { value, done } = await reader.read();
// 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; // Auf ERR achten!
if (raw.includes("OK") || raw.includes("ERR")) break;
}
reader.releaseLock();
const lines = raw.split('\n').map(l => l.trim()).filter(l => l);
// AUTOMATISCHE FEHLERERKENNUNG
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);
}
if (errorLine) addToast(`Gerätefehler: ${errorLine}`, 'error', 5000);
task.resolve(lines.filter(l => l !== "OK" && !l.startsWith("ERR") && !l.startsWith(task.command)));
} catch (e) {
addToast("Kommunikationsfehler (Serial Port)", "error");
console.error(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();
@@ -87,7 +134,7 @@ export function initSerialListeners() {
// 1. Wenn ein bereits gekoppeltes Gerät eingesteckt wird
navigator.serial.addEventListener('connect', (event) => {
console.log('Neues Gerät erkannt, starte Auto-Connect...');
// console.log('Neues Gerät erkannt, starte Auto-Connect...');
autoConnect();
});
@@ -103,14 +150,23 @@ export async function autoConnect() {
const ports = await navigator.serial.getPorts();
if (ports.length > 0) {
// Wir nehmen den ersten verfügbaren Port
const port = ports[0];
console.log('Gekoppeltes Gerät gefunden, verbinde...');
try {
await connectToPort(port);
} catch (e) {
console.warn('Auto-Connect fehlgeschlagen:', e);
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.');
}
}
}
}
}
@@ -123,29 +179,49 @@ export async function connectToPort(port: SerialPort) {
isConnecting = true;
try {
// console.log("Versuche Verbindung mit Port:", port.getInfo());
await port.open({ baudRate: 115200 });
port.addEventListener('disconnect', () => {
addToast("Buzzer-Verbindung verloren!", "warning");
handleDisconnect();
});
await delay(100);
setActivePort(port);
buzzer.update(s => ({ ...s, connected: true }));
// Kleiner Timeout, damit die UI Zeit zum Hydrieren hat
setTimeout(() => {
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");
}, 100);
await updateDeviceInfo(port);
await refreshFileList();
} catch (e) {
console.error("Connect error:", e);
// Nur Toasten, wenn es kein "Already Open" Fehler ist
if (!(e instanceof Error && e.name === 'InvalidStateError')) {
addToast("Verbindung fehlgeschlagen", "error");
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;
}
@@ -153,8 +229,8 @@ export async function connectToPort(port: SerialPort) {
function handleDisconnect() {
setActivePort(null);
buzzer.update(s => ({
...s,
buzzer.update(s => ({
...s,
connected: false,
files: [] // Liste leeren, da Gerät weg
}));
@@ -166,9 +242,12 @@ export function setActivePort(port: SerialPort | null) {
queue.setPort(port);
}
// Diese Funktion fehlte im letzten Block!
export async function updateDeviceInfo(port: SerialPort) {
// Wir nutzen hier direkt die Queue für Konsistenz
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(';');
@@ -177,8 +256,9 @@ export async function updateDeviceInfo(port: SerialPort) {
const totalPages = parseInt(parts[3]);
const availablePages = parseInt(parts[4]);
const totalMB = (totalPages * pageSize) / (1024 * 1024);
const availableMB = (availablePages * pageSize) / (1024 * 1024);
// MB Berechnung mit dem korrekten Divisor (1024 * 1024)
const totalMB = (totalPages * pageSize) / 1048576;
const availableMB = (availablePages * pageSize) / 1048576;
buzzer.update(s => ({
...s,
@@ -186,32 +266,72 @@ export async function updateDeviceInfo(port: SerialPort) {
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 [type, size, name] = parts;
return {
name,
size: (parseInt(size) / 1024).toFixed(1) + " KB",
crc32: 0,
isSystem: false
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[];
buzzer.update(s => ({ ...s, files: audioFiles }));
// 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 (!file.crc32) {
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]+)/);

View File

@@ -8,7 +8,7 @@ import ConnectButton from "../components/ConnectButton.svelte";
import SerialWarning from "../components/SerialWarning.svelte";
import FileStorage from "../components/FileStorage.svelte";
import DeviceInfo from "../components/DeviceInfo.svelte";
import ToastContainer from "../components/ToastContainer.svelte";
import ToastContainer from '../components/ToastContainer.svelte';
import type { loadRenderers } from "astro:container";
---