Stand vor Protokollumbau
This commit is contained in:
@@ -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
5
buzzer_tool/config.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
# config.yaml
|
||||
serial:
|
||||
port: "COM17"
|
||||
baudrate: 250000
|
||||
timeout: 10
|
||||
@@ -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
|
||||
|
||||
56
buzzer_tool/core/commands/get_tag.py
Normal file
56
buzzer_tool/core/commands/get_tag.py
Normal 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
25
buzzer_tool/core/util.py
Normal 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()
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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]+)/);
|
||||
|
||||
@@ -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";
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user