zwischenstand

This commit is contained in:
2026-03-21 13:49:05 +01:00
parent b863b04505
commit 01448223ad
30 changed files with 1446 additions and 295 deletions

View File

@@ -1,3 +1,4 @@
add_subdirectory(fw_mgmt)
add_subdirectory(fs_mgmt) add_subdirectory(fs_mgmt)
add_subdirectory(ble_mgmt) add_subdirectory(ble_mgmt)
add_subdirectory(buzz_proto) add_subdirectory(buzz_proto)

View File

@@ -1,3 +1,4 @@
rsource "fw_mgmt/Kconfig"
rsource "fs_mgmt/Kconfig" rsource "fs_mgmt/Kconfig"
rsource "ble_mgmt/Kconfig" rsource "ble_mgmt/Kconfig"
rsource "buzz_proto/Kconfig" rsource "buzz_proto/Kconfig"

View File

@@ -35,6 +35,7 @@ enum buzz_data_type
BUZZ_DATA_PROTO_INFO = 0x01, BUZZ_DATA_PROTO_INFO = 0x01,
BUZZ_DATA_DEVICE_INFO = 0x02, BUZZ_DATA_DEVICE_INFO = 0x02,
BUZZ_DATA_FS_INFO = 0x03, BUZZ_DATA_FS_INFO = 0x03,
BUZZ_DATA_FW_INFO = 0x04,
BUZZ_DATA_FILE_GET = 0x20, BUZZ_DATA_FILE_GET = 0x20,
BUZZ_DATA_FILE_PUT = 0x21, BUZZ_DATA_FILE_PUT = 0x21,
@@ -88,6 +89,17 @@ struct __attribute__((packed)) buzz_resp_proto_version
uint16_t max_chunk_size; /* Little Endian */ uint16_t max_chunk_size; /* Little Endian */
}; };
/* Payload für die Geräteinformationen */
struct __attribute__((packed)) buzz_resp_device_info
{
uint8_t data_type; /* BUZZ_DATA_DEVICE_INFO */
uint8_t device_id[8]; /* EUI64 oder ähnliche eindeutige ID */
uint8_t board_name_length; /* Länge des Board-Namens */
uint8_t board_revision_length; /* Länge der Board-Revision */
uint8_t soc_name_length; /* Länge des SOC-Namens */
char data[]; /* Variabler String ohne Null-Terminierung: [board_name][board_revision][soc_name] */
};
/* Payload für die Dateisystem-Informationen */ /* Payload für die Dateisystem-Informationen */
struct __attribute__((packed)) buzz_resp_fs_info struct __attribute__((packed)) buzz_resp_fs_info
{ {
@@ -100,6 +112,16 @@ struct __attribute__((packed)) buzz_resp_fs_info
uint8_t data[]; /* Pfadnamen */ uint8_t data[]; /* Pfadnamen */
}; };
/* Payload für die Firmware-Infos */
struct __attribute__((packed)) buzz_resp_fw_info
{
uint8_t data_type; /* BUZZ_DATA_FW_INFO */
uint8_t fw_status; /* fw_state_t */
uint32_t slot1_size; /* Größe des Slot1-Partitionsbereichs (Little Endian) */
uint8_t fw_version_length; /* Länge der Firmware-Versionszeichenkette */
uint8_t kernel_version_length; /* Länge der Kernel-Versionszeichenkette */
char data[]; /* Variabler String ohne Null-Terminierung: [fw_version][kernel_version] */
};
/* Payload für das Entfernen einer Datei */ /* Payload für das Entfernen einer Datei */
struct __attribute__((packed)) buzz_rm_file_payload struct __attribute__((packed)) buzz_rm_file_payload
{ {
@@ -171,7 +193,6 @@ void buzz_proto_buf_free(uint8_t **buf);
/* Übergabe eines empfangenen Frames an den Protokoll-Thread */ /* Übergabe eines empfangenen Frames an den Protokoll-Thread */
int buzz_proto_submit_frame(struct buzz_frame_msg *msg); int buzz_proto_submit_frame(struct buzz_frame_msg *msg);
/* Gibt die Anzahl der freien Slabs zurück (abzüglich Reserve) */ /* Gibt die Anzahl der freien Slabs zurück (abzüglich Reserve) */
uint16_t buzz_proto_get_free_rx_slabs(void); uint16_t buzz_proto_get_free_rx_slabs(void);

View File

@@ -8,6 +8,7 @@
#include "buzz_proto.h" #include "buzz_proto.h"
#include "fs_mgmt.h" #include "fs_mgmt.h"
#include "fw_mgmt.h"
LOG_MODULE_REGISTER(buzz_proto, CONFIG_BUZZ_PROTO_LOG_LEVEL); LOG_MODULE_REGISTER(buzz_proto, CONFIG_BUZZ_PROTO_LOG_LEVEL);
K_MEM_SLAB_DEFINE(buzz_proto_slabs, CONFIG_BUZZ_PROTO_SLAB_SIZE, CONFIG_BUZZ_PROTO_SLAB_COUNT, 4); K_MEM_SLAB_DEFINE(buzz_proto_slabs, CONFIG_BUZZ_PROTO_SLAB_SIZE, CONFIG_BUZZ_PROTO_SLAB_COUNT, 4);
@@ -172,7 +173,7 @@ static void handle_proto_version_request(struct buzz_frame_msg *msg)
resp_data->data_type = BUZZ_DATA_PROTO_INFO; resp_data->data_type = BUZZ_DATA_PROTO_INFO;
resp_data->version = sys_cpu_to_le16(BUZZ_PROTO_VERSION); resp_data->version = sys_cpu_to_le16(BUZZ_PROTO_VERSION);
/* Dynamische Chunk-Größe basierend auf der aktuellen Transport-MTU berechnen */ /* Dynamische Chunk-Grösse basierend auf der aktuellen Transport-MTU berechnen */
uint16_t slab_payload = CONFIG_BUZZ_PROTO_SLAB_SIZE - sizeof(struct buzz_proto_header); uint16_t slab_payload = CONFIG_BUZZ_PROTO_SLAB_SIZE - sizeof(struct buzz_proto_header);
uint16_t transport_payload = 0; uint16_t transport_payload = 0;
@@ -192,6 +193,34 @@ static void handle_proto_version_request(struct buzz_frame_msg *msg)
} }
} }
void handle_device_info_request(struct buzz_frame_msg *msg) {
struct buzz_proto_header *hdr = (struct buzz_proto_header *)msg->data_ptr;
hdr->frame_type = BUZZ_FRAME_RESPONSE;
struct buzz_resp_device_info *resp_data = (struct buzz_resp_device_info *)(msg->data_ptr + sizeof(*hdr));
resp_data->data_type = BUZZ_DATA_DEVICE_INFO;
if (fw_mgmt_get_id(resp_data->device_id, sizeof(resp_data->device_id)) < 0) {
LOG_ERR("Failed to get device ID");
send_error_frame(msg, EIO);
return;
}
const char *board_name = fw_mgmt_get_board_name();
const char *board_rev = fw_mgmt_get_board_revision();
const char *soc_name = fw_mgmt_get_soc_name();
resp_data->board_name_length = MIN(strlen(board_name), 32); // Sicherheitsmassnahme gegen zu lange Namen
resp_data->board_revision_length = MIN(strlen(board_rev), 32); // Sicherheitsmassnahme gegen zu lange Namen
resp_data->soc_name_length = MIN(strlen(soc_name), 32); // Sicherheitsmassnahme gegen zu lange Namen
memcpy(resp_data->data, board_name, resp_data->board_name_length);
memcpy(resp_data->data + resp_data->board_name_length, board_rev, resp_data->board_revision_length);
memcpy(resp_data->data + resp_data->board_name_length + resp_data->board_revision_length, soc_name, resp_data->soc_name_length);
uint16_t payload_length = sizeof(struct buzz_resp_device_info) + resp_data->board_name_length + resp_data->board_revision_length + resp_data->soc_name_length;
hdr->payload_length = sys_cpu_to_le16(payload_length);
uint16_t total_len = sizeof(struct buzz_proto_header) + payload_length;
if (msg->reply_cb)
{
msg->reply_cb(msg->data_ptr, total_len);
}
}
void handle_fs_info_request(struct buzz_frame_msg *msg) void handle_fs_info_request(struct buzz_frame_msg *msg)
{ {
struct buzz_proto_header *hdr = (struct buzz_proto_header *)msg->data_ptr; struct buzz_proto_header *hdr = (struct buzz_proto_header *)msg->data_ptr;
@@ -234,6 +263,36 @@ void handle_fs_info_request(struct buzz_frame_msg *msg)
} }
} }
static void handle_fw_info_request(struct buzz_frame_msg *msg)
{
struct buzz_proto_header *hdr = (struct buzz_proto_header *)msg->data_ptr;
hdr->frame_type = BUZZ_FRAME_RESPONSE;
struct buzz_resp_fw_info *resp_data = (struct buzz_resp_fw_info *)(msg->data_ptr + sizeof(*hdr));
resp_data->data_type = BUZZ_DATA_FW_INFO;
resp_data->fw_status = fw_mgmt_get_fw_state();
resp_data->slot1_size = sys_cpu_to_le32(fw_mgmt_get_slot1_size());
const char *fw_version = fw_mgmt_get_fw_version_string();
const char *kernel_version = fw_mgmt_get_kernel_version_string();
resp_data->fw_version_length = MIN(strlen(fw_version), 32); // Sicherheitsmassnahme gegen zu lange Strings
resp_data->kernel_version_length = MIN(strlen(kernel_version), 32); // Sicherheitsmassnahme gegen zu lange Strings
memcpy(resp_data->data, fw_version, resp_data->fw_version_length);
memcpy(resp_data->data + resp_data->fw_version_length, kernel_version, resp_data->kernel_version_length);
uint16_t payload_length = sizeof(struct buzz_resp_fw_info) + resp_data->fw_version_length + resp_data->kernel_version_length;
hdr->payload_length = sys_cpu_to_le16(payload_length);
uint16_t total_len = sizeof(struct buzz_proto_header) + payload_length;
if (msg->reply_cb)
{
msg->reply_cb(msg->data_ptr, total_len);
}
}
static void handle_ls_request(struct buzz_frame_msg *msg) static void handle_ls_request(struct buzz_frame_msg *msg)
{ {
struct buzz_proto_header *hdr = (struct buzz_proto_header *)msg->data_ptr; struct buzz_proto_header *hdr = (struct buzz_proto_header *)msg->data_ptr;
@@ -619,6 +678,11 @@ static void handle_request(struct buzz_frame_msg *msg)
handle_proto_version_request(msg); handle_proto_version_request(msg);
break; break;
case BUZZ_DATA_DEVICE_INFO:
LOG_DBG("Received Device Info Request");
handle_device_info_request(msg);
break;
case BUZZ_DATA_FS_INFO: case BUZZ_DATA_FS_INFO:
LOG_DBG("Received FS Info Request"); LOG_DBG("Received FS Info Request");
handle_fs_info_request(msg); handle_fs_info_request(msg);
@@ -629,6 +693,11 @@ static void handle_request(struct buzz_frame_msg *msg)
handle_ls_request(msg); handle_ls_request(msg);
break; break;
case BUZZ_DATA_FW_INFO:
LOG_DBG("Received FW Info Request");
handle_fw_info_request(msg);
break;
case BUZZ_DATA_FILE_GET: case BUZZ_DATA_FILE_GET:
LOG_DBG("Received FILE_GET Request"); LOG_DBG("Received FILE_GET Request");
handle_file_get_request(msg, false); handle_file_get_request(msg, false);

View File

@@ -514,8 +514,8 @@ static void fs_thread_entry(void *p1, void *p2, void *p3)
LOG_WRN("Write timeout! Aborting transfer."); LOG_WRN("Write timeout! Aborting transfer.");
if (write_ctx.state == FS_STATE_RECEIVING_FILE) if (write_ctx.state == FS_STATE_RECEIVING_FILE)
{ {
// fs_mgmt_pm_close(&write_ctx.file); fs_mgmt_pm_close(&write_ctx.file);
// fs_mgmt_pm_unlink(write_ctx.filename); fs_mgmt_pm_unlink(write_ctx.filename);
} }
write_ctx.state = FS_STATE_IDLE; write_ctx.state = FS_STATE_IDLE;
continue; continue;
@@ -536,8 +536,8 @@ static void fs_thread_entry(void *p1, void *p2, void *p3)
memcpy(write_ctx.filename, msg.slab_ptr + msg.data_offset, msg.data_len); memcpy(write_ctx.filename, msg.slab_ptr + msg.data_offset, msg.data_len);
write_ctx.filename[msg.data_len] = '\0'; write_ctx.filename[msg.data_len] = '\0';
// fs_mgmt_pm_unlink(write_ctx.filename); fs_mgmt_pm_unlink(write_ctx.filename);
// rc = fs_mgmt_pm_open(&write_ctx.file, write_ctx.filename, FS_O_CREATE | FS_O_WRITE); rc = fs_mgmt_pm_open(&write_ctx.file, write_ctx.filename, FS_O_CREATE | FS_O_WRITE);
if (rc == 0) if (rc == 0)
{ {
@@ -568,7 +568,7 @@ static void fs_thread_entry(void *p1, void *p2, void *p3)
write_ctx.filename[msg.data_len] = '\0'; write_ctx.filename[msg.data_len] = '\0';
/* Datei öffnen: Nur Lese- und Schreibrechte, Datei muss bereits existieren */ /* Datei öffnen: Nur Lese- und Schreibrechte, Datei muss bereits existieren */
// int rc = fs_mgmt_pm_open(&write_ctx.file, write_ctx.filename, FS_O_READ | FS_O_WRITE); int rc = fs_mgmt_pm_open(&write_ctx.file, write_ctx.filename, FS_O_READ | FS_O_WRITE);
if (rc == 0) if (rc == 0)
{ {
@@ -583,7 +583,7 @@ static void fs_thread_entry(void *p1, void *p2, void *p3)
} }
/* Datei ab dem Ende der Audiodaten abschneiden (alte Tags entfernen) */ /* Datei ab dem Ende der Audiodaten abschneiden (alte Tags entfernen) */
// rc = fs_truncate(&write_ctx.file, audio_len); rc = fs_truncate(&write_ctx.file, audio_len);
if (rc != 0) if (rc != 0)
{ {
LOG_ERR("Failed to truncate file: %d", rc); LOG_ERR("Failed to truncate file: %d", rc);
@@ -593,7 +593,7 @@ static void fs_thread_entry(void *p1, void *p2, void *p3)
} }
/* File-Pointer exakt an das neue Ende (audio_len) setzen */ /* File-Pointer exakt an das neue Ende (audio_len) setzen */
// fs_seek(&write_ctx.file, audio_len, FS_SEEK_SET); fs_seek(&write_ctx.file, audio_len, FS_SEEK_SET);
write_ctx.state = FS_STATE_RECEIVING_TAGS; write_ctx.state = FS_STATE_RECEIVING_TAGS;
write_ctx.crc32 = 0; write_ctx.crc32 = 0;
@@ -622,8 +622,7 @@ static void fs_thread_entry(void *p1, void *p2, void *p3)
case FS_STATE_RECEIVING_FILE: case FS_STATE_RECEIVING_FILE:
if (msg.op == FS_WRITE_OP_FILE_CHUNK && msg.slab_ptr) if (msg.op == FS_WRITE_OP_FILE_CHUNK && msg.slab_ptr)
{ {
// ssize_t written = fs_write(&write_ctx.file, msg.slab_ptr + msg.data_offset, msg.data_len); ssize_t written = fs_write(&write_ctx.file, msg.slab_ptr + msg.data_offset, msg.data_len);
ssize_t written = msg.data_len; /* Zum Testen, da wir ja kein echtes FS-Backend haben */
if (written == msg.data_len) if (written == msg.data_len)
{ {
write_ctx.crc32 = crc32_ieee_update(write_ctx.crc32, msg.slab_ptr + msg.data_offset, msg.data_len); write_ctx.crc32 = crc32_ieee_update(write_ctx.crc32, msg.slab_ptr + msg.data_offset, msg.data_len);
@@ -649,7 +648,7 @@ static void fs_thread_entry(void *p1, void *p2, void *p3)
} }
else if (msg.op == FS_WRITE_OP_FILE_END) else if (msg.op == FS_WRITE_OP_FILE_END)
{ {
// fs_mgmt_pm_close(&write_ctx.file); fs_mgmt_pm_close(&write_ctx.file);
write_ctx.state = FS_STATE_IDLE; write_ctx.state = FS_STATE_IDLE;
if (write_ctx.crc32 == msg.metadata) if (write_ctx.crc32 == msg.metadata)
@@ -660,14 +659,14 @@ static void fs_thread_entry(void *p1, void *p2, void *p3)
else else
{ {
LOG_ERR("CRC Mismatch! Expected: 0x%08X, Got: 0x%08X", msg.metadata, write_ctx.crc32); LOG_ERR("CRC Mismatch! Expected: 0x%08X, Got: 0x%08X", msg.metadata, write_ctx.crc32);
// fs_mgmt_pm_unlink(write_ctx.filename); fs_mgmt_pm_unlink(write_ctx.filename);
buzz_proto_send_error_reusing_slab(msg.reply_cb, EBADMSG, msg.slab_ptr); buzz_proto_send_error_reusing_slab(msg.reply_cb, EBADMSG, msg.slab_ptr);
} }
} }
else if (msg.op == FS_WRITE_OP_ABORT) else if (msg.op == FS_WRITE_OP_ABORT)
{ {
// fs_mgmt_pm_close(&write_ctx.file); fs_mgmt_pm_close(&write_ctx.file);
// fs_mgmt_pm_unlink(write_ctx.filename); fs_mgmt_pm_unlink(write_ctx.filename);
write_ctx.state = FS_STATE_IDLE; write_ctx.state = FS_STATE_IDLE;
if (msg.slab_ptr) if (msg.slab_ptr)
buzz_proto_buf_free(&msg.slab_ptr); buzz_proto_buf_free(&msg.slab_ptr);
@@ -677,8 +676,7 @@ static void fs_thread_entry(void *p1, void *p2, void *p3)
case FS_STATE_RECEIVING_TAGS: case FS_STATE_RECEIVING_TAGS:
if (msg.op == FS_WRITE_OP_FILE_CHUNK && msg.slab_ptr) if (msg.op == FS_WRITE_OP_FILE_CHUNK && msg.slab_ptr)
{ {
// ssize_t written = fs_write(&write_ctx.file, msg.slab_ptr + msg.data_offset, msg.data_len); ssize_t written = fs_write(&write_ctx.file, msg.slab_ptr + msg.data_offset, msg.data_len);
ssize_t written = msg.data_len; /* Zum Testen, da wir ja kein echtes FS-Backend haben */
if (written == msg.data_len) if (written == msg.data_len)
{ {
write_ctx.crc32 = crc32_ieee_update(write_ctx.crc32, msg.slab_ptr + msg.data_offset, msg.data_len); write_ctx.crc32 = crc32_ieee_update(write_ctx.crc32, msg.slab_ptr + msg.data_offset, msg.data_len);
@@ -699,8 +697,8 @@ static void fs_thread_entry(void *p1, void *p2, void *p3)
else else
{ {
LOG_ERR("Flash write failed during tags transfer!"); LOG_ERR("Flash write failed during tags transfer!");
// fs_truncate(&write_ctx.file, write_ctx.audio_len); /* Rollback */ fs_truncate(&write_ctx.file, write_ctx.audio_len); /* Rollback */
// fs_mgmt_pm_close(&write_ctx.file); fs_mgmt_pm_close(&write_ctx.file);
write_ctx.state = FS_STATE_IDLE; write_ctx.state = FS_STATE_IDLE;
buzz_proto_send_error_reusing_slab(msg.reply_cb, EIO, msg.slab_ptr); buzz_proto_send_error_reusing_slab(msg.reply_cb, EIO, msg.slab_ptr);
} }
@@ -716,16 +714,16 @@ static void fs_thread_entry(void *p1, void *p2, void *p3)
else else
{ {
LOG_ERR("Tags CRC Mismatch! Expected: 0x%08X, Got: 0x%08X", msg.metadata, write_ctx.crc32); LOG_ERR("Tags CRC Mismatch! Expected: 0x%08X, Got: 0x%08X", msg.metadata, write_ctx.crc32);
// fs_truncate(&write_ctx.file, write_ctx.audio_len); /* Rollback */ fs_truncate(&write_ctx.file, write_ctx.audio_len); /* Rollback */
// fs_mgmt_pm_close(&write_ctx.file); fs_mgmt_pm_close(&write_ctx.file);
buzz_proto_send_error_reusing_slab(msg.reply_cb, EBADMSG, msg.slab_ptr); buzz_proto_send_error_reusing_slab(msg.reply_cb, EBADMSG, msg.slab_ptr);
} }
write_ctx.state = FS_STATE_IDLE; write_ctx.state = FS_STATE_IDLE;
} }
else if (msg.op == FS_WRITE_OP_ABORT) else if (msg.op == FS_WRITE_OP_ABORT)
{ {
// fs_truncate(&write_ctx.file, write_ctx.audio_len); /* Rollback */ fs_truncate(&write_ctx.file, write_ctx.audio_len); /* Rollback */
// fs_mgmt_pm_close(&write_ctx.file); fs_mgmt_pm_close(&write_ctx.file);
write_ctx.state = FS_STATE_IDLE; write_ctx.state = FS_STATE_IDLE;
if (msg.slab_ptr) if (msg.slab_ptr)
buzz_proto_buf_free(&msg.slab_ptr); buzz_proto_buf_free(&msg.slab_ptr);

View File

@@ -1,17 +1,16 @@
if(CONFIG_FS_MGMT) if(CONFIG_FW_MGMT)
zephyr_library() zephyr_library()
zephyr_library_sources(src/fs_mgmt.c) zephyr_library_sources(src/fw_mgmt.c)
zephyr_include_directories(include) zephyr_include_directories(include)
if(CONFIG_FILE_SYSTEM_LITTLEFS) if(CONFIG_MCUBOOT_IMG_MANAGER)
if(DEFINED ZEPHYR_LITTLEFS_MODULE_DIR) # img_mgmt.h pulls in <bootutil/image.h> and <zcbor_common.h>
zephyr_include_directories(${ZEPHYR_LITTLEFS_MODULE_DIR}) if(DEFINED ZEPHYR_MCUBOOT_MODULE_DIR)
elseif(DEFINED WEST_TOPDIR) zephyr_include_directories(${ZEPHYR_MCUBOOT_MODULE_DIR}/boot/bootutil/include)
zephyr_include_directories(${WEST_TOPDIR}/modules/fs/littlefs) zephyr_include_directories(${ZEPHYR_MCUBOOT_MODULE_DIR}/boot/zephyr/include)
endif() endif()
if(DEFINED ZEPHYR_ZCBOR_MODULE_DIR)
if(DEFINED ZEPHYR_BASE) zephyr_include_directories(${ZEPHYR_ZCBOR_MODULE_DIR}/include)
zephyr_include_directories(${ZEPHYR_BASE}/modules/littlefs)
endif() endif()
endif() endif()
endif() endif()

View File

@@ -1,64 +1,22 @@
menuconfig FS_MGMT menuconfig FW_MGMT
bool "File System Management" bool "Firmware Management"
select FLASH select FLASH
select FLASH_MAP select FLASH_MAP
select FILE_SYSTEM select STREAM_FLASH
select FILE_SYSTEM_LITTLEFS
select FILE_SYSTEM_MKFS
select FLASH_PAGE_LAYOUT select FLASH_PAGE_LAYOUT
select NORDIC_QSPI_NOR if SOC_SERIES_NRF52X && (SOC_NRF52840_QIAA || SOC_NRF52833_QIAA) select BOOTLOADER_MCUBOOT
select IMG_MANAGER
select MCUBOOT_IMG_MGR
select HWINFO
help help
Library for initializing and managing the file system. Library for firmware operations.
if FS_MGMT if FW_MGMT
config FS_MGMT_MOUNT_POINT config MCUBOOT_UTIL_LOG_LEVEL_ERR
string "Littlefs Mount Point" # config CONFIG_MCUMGR_GRP_IMG
default "/lfs" # default y
help module = FW_MGMT
Set the mount point for the Littlefs file system. Default is "/lfs". module-str = fw_mgmt
config FS_MGMT_AUDIO_SUBDIR
string "Audio File Path"
default "/a"
help
Set the path for the audio file within the file system. Default is "/a".
config FS_MGMT_SYSTEM_SUBDIR
string "System File Path"
default "/sys"
help
Set the path for the system file within the file system. Default is "/sys".
config FS_MGMT_THREAD_STACK_SIZE
int "File System Management Thread Stack Size"
default 2048
help
Set the stack size for the file system management thread. Default is 2048 bytes.
config FS_MGMT_THREAD_PRIORITY
int "File System Management Thread Priority"
default 6
help
Set the priority for the file system management thread. Default is 6.
if SOC_SERIES_NRF52X
config PM_PARTITION_REGION_LITTLEFS_EXTERNAL
default y
config PM_PARTITION_SIZE_LITTLEFS
default 0x1000000
endif # SOC_SERIES_NRF52X
config FS_LITTLEFS_READ_SIZE
default 256
config FS_LITTLEFS_PROG_SIZE
default 256
config FS_LITTLEFS_CACHE_SIZE
default 4096
config FS_LITTLEFS_LOOKAHEAD_SIZE
default 512
module = FS_MGMT
module-str = fs_mgmt
source "subsys/logging/Kconfig.template.log_config" source "subsys/logging/Kconfig.template.log_config"
endif # FS_MGMT endif # FW_MGMT

View File

@@ -0,0 +1,23 @@
#ifndef FW_MGMT_H
#define FW_MGMT_H
#include <zephyr/kernel.h>
typedef enum
{
FW_STATE_CONFIRMED = 0x00,
FW_STATE_PENDING = 0x01,
FW_STATE_TESTING = 0x02,
FW_STATE_UNKNOWN = 0xFF
} fw_state_t;
const char *fw_mgmt_get_fw_version_string(void);
const char *fw_mgmt_get_kernel_version_string(void);
const char *fw_mgmt_get_board_name();
const char *fw_mgmt_get_board_revision();
const char *fw_mgmt_get_soc_name();
int fw_mgmt_get_id(uint8_t *buffer, size_t length);
ssize_t fw_mgmt_get_slot1_size(void);
fw_state_t fw_mgmt_get_fw_state(void);
#endif /* FW_MGMT_H */

View File

@@ -0,0 +1,79 @@
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/dfu/mcuboot.h>
#include <zephyr/drivers/hwinfo.h>
#include <zephyr/mgmt/mcumgr/grp/img_mgmt/img_mgmt.h>
#include <version.h>
#include <app_version.h>
#include "fw_mgmt.h"
#define SLOT1_PARTITION_SIZE DT_REG_SIZE(DT_NODELABEL(slot1_partition))
static char fw_version_string[] = APP_VERSION_STRING;
static char kernel_version_string[] = KERNEL_VERSION_STRING;
static char board_name[] = CONFIG_BOARD;
static char board_revision[] = CONFIG_BOARD_REVISION;
static char soc_name[] = CONFIG_SOC;
static uint8_t hwid[8];
LOG_MODULE_REGISTER(fw_mgmt, CONFIG_FW_MGMT_LOG_LEVEL);
const char* fw_mgmt_get_fw_version_string(void)
{
return (const char*)fw_version_string;
}
const char* fw_mgmt_get_kernel_version_string(void)
{
return (const char*)kernel_version_string;
}
const char* fw_mgmt_get_board_name(void)
{
return (const char*)board_name;
}
const char* fw_mgmt_get_board_revision(void)
{
return (const char*)board_revision;
}
const char* fw_mgmt_get_soc_name(void)
{
return (const char*)soc_name;
}
int fw_mgmt_get_id(uint8_t *buffer, size_t length)
{
int ret = hwinfo_get_device_id(hwid, length);
if (ret < 0) {
LOG_ERR("Failed to get device EUI64: %d", ret);
return ret;
}
memcpy(buffer, hwid, sizeof(hwid));
return ret;
}
fw_state_t fw_mgmt_get_fw_state(void)
{
if (!boot_is_img_confirmed()) {
return FW_STATE_TESTING;
}
int swap_type = mcuboot_swap_type();
if (swap_type == BOOT_SWAP_TYPE_NONE) {
return FW_STATE_CONFIRMED;
} else if (swap_type == BOOT_SWAP_TYPE_TEST) {
return FW_STATE_PENDING;
} else {
LOG_ERR("Unexpected swap type: %d", swap_type);
return FW_STATE_UNKNOWN; // Fallback auf bestätigten Zustand bei unerwartetem Swap-Typ
}
return FW_STATE_UNKNOWN; // Fallback, sollte nie erreicht werden
}
ssize_t fw_mgmt_get_slot1_size(void)
{
return SLOT1_PARTITION_SIZE;
}

View File

@@ -1,33 +1,34 @@
### Logging ### Logging
CONFIG_LOG=y CONFIG_LOG=y
### Bootloader
CONFIG_BOOTLOADER_MCUBOOT=y
### File System ### File System
CONFIG_FS_MGMT=y CONFIG_FS_MGMT=y
# CONFIG_FS_MGMT_LOG_LEVEL_DBG=y
CONFIG_FS_LOG_LEVEL_WRN=y CONFIG_FS_LOG_LEVEL_WRN=y
### Bluetooth ### Bluetooth
CONFIG_BLE_MGMT=y CONFIG_BLE_MGMT=y
# Advertising 500ms - 1s # Advertising 500ms - 1s
CONFIG_BLE_MGMT_ADV_INT_MIN=160 CONFIG_BLE_MGMT_ADV_INT_MIN=160
CONFIG_BLE_MGMT_ADV_INT_MAX=320 CONFIG_BLE_MGMT_ADV_INT_MAX=320
## Buzzer protocol ### Firmware Management
CONFIG_FW_MGMT=y
CONFIG_FW_MGMT_LOG_LEVEL_DBG=y
CONFIG_HW_STACK_PROTECTION=y
CONFIG_RESET_ON_FATAL_ERROR=y
### Buzzer protocol
CONFIG_BUZZ_PROTO=y CONFIG_BUZZ_PROTO=y
CONFIG_BUZZ_PROTO_LOG_LEVEL_DBG=y CONFIG_BUZZ_PROTO_LOG_LEVEL_DBG=y
## Power management ## Power management
CONFIG_PM_DEVICE=y CONFIG_PM_DEVICE=y
## Shell # ## Shell
# CONFIG_SHELL=y # # CONFIG_SHELL=y
# CONFIG_FILE_SYSTEM_SHELL=y # # CONFIG_FILE_SYSTEM_SHELL=y
# Airtime-Maximierung # # Airtime-Maximierung
CONFIG_BT_CTLR_SDC_MAX_CONN_EVENT_LEN_DEFAULT=4000000 CONFIG_BT_CTLR_SDC_MAX_CONN_EVENT_LEN_DEFAULT=4000000
# MTU-Setup # MTU-Setup
@@ -58,3 +59,8 @@ CONFIG_BT_PERIPHERAL_PREF_MIN_INT=12
CONFIG_BT_PERIPHERAL_PREF_MAX_INT=40 CONFIG_BT_PERIPHERAL_PREF_MAX_INT=40
CONFIG_BT_PERIPHERAL_PREF_LATENCY=0 CONFIG_BT_PERIPHERAL_PREF_LATENCY=0
CONFIG_BT_PERIPHERAL_PREF_TIMEOUT=400 CONFIG_BT_PERIPHERAL_PREF_TIMEOUT=400
CONFIG_HEAP_MEM_POOL_SIZE=2048
CONFIG_BT_CENTRAL=n

View File

@@ -5,6 +5,7 @@
#include "fs_mgmt.h" #include "fs_mgmt.h"
#include "ble_mgmt.h" #include "ble_mgmt.h"
#include "buzz_proto.h" #include "buzz_proto.h"
#include "fw_mgmt.h"
LOG_MODULE_REGISTER(main); LOG_MODULE_REGISTER(main);
@@ -44,7 +45,13 @@ void ble_rx_cb(const uint8_t *data, uint16_t len)
int main(void) int main(void)
{ {
LOG_INF("Starting app on %s (SOC: %s)", CONFIG_BOARD, CONFIG_SOC); uint8_t hw_id[8];
LOG_INF("Starting app version %s (state: 0x%02x zephyr %s) on %s (Rev: %s, SOC: %s)", fw_mgmt_get_fw_version_string(), fw_mgmt_get_fw_state(), fw_mgmt_get_kernel_version_string(), fw_mgmt_get_board_name(), strlen(fw_mgmt_get_board_revision()) ? fw_mgmt_get_board_revision() : "N/A", fw_mgmt_get_soc_name());
if (fw_mgmt_get_id(hw_id, sizeof(hw_id)) >= 0) {
LOG_INF("Device EUI64: %02X%02X-%02X%02X-%02X%02X-%02X%02X", hw_id[0], hw_id[1], hw_id[2], hw_id[3], hw_id[4], hw_id[5], hw_id[6], hw_id[7]);
} else {
LOG_ERR("Failed to get device ID");
}
int rc; int rc;

View File

@@ -1,5 +1,4 @@
CONFIG_LOG=y CONFIG_LOG=y
CONFIG_MCUBOOT_LOG_LEVEL_DBG=y
# CONFIG_MCUBOOT_SERIAL=y # CONFIG_MCUBOOT_SERIAL=y
CONFIG_UART_CONSOLE=y CONFIG_UART_CONSOLE=y
# CONFIG_SINGLE_APPLICATION_SLOT=n # CONFIG_SINGLE_APPLICATION_SLOT=n

View File

@@ -1,7 +1,7 @@
/ { / {
aliases { aliases {
mcuboot-button0 = &button1; mcuboot-button0 = &button0;
mcuboot-led0 = &led1; mcuboot-led0 = &led0;
}; };
}; };

View File

@@ -3,7 +3,7 @@
Stand: 2026-03-18 Stand: 2026-03-18
Quelle: aktueller Implementierungsstand aus Firmware (`buzz_proto`, `fs_mgmt`, `ble_mgmt`) und Web-Client (`transport`, `parser`). Quelle: aktueller Implementierungsstand aus Firmware (`buzz_proto`, `fs_mgmt`, `ble_mgmt`) und Web-Client (`transport`, `parser`).
## 1. Ziel und Scope ## Ziel und Scope
Das Buzzer Protocol ist ein binäres Frame-Protokoll für Host <-> Device Kommunikation. Das Buzzer Protocol ist ein binäres Frame-Protokoll für Host <-> Device Kommunikation.
@@ -19,7 +19,7 @@ Nicht produktiv implementiert:
- `DEVICE_INFO` (`0x02`) - `DEVICE_INFO` (`0x02`)
- Firmware-Update (`FW_*`, `FW_UPDATE`) - Firmware-Update (`FW_*`, `FW_UPDATE`)
## 2. Transport und Grundregeln ## Transport und Grundregeln
- Alle Integer-Felder sind Little Endian. - Alle Integer-Felder sind Little Endian.
- Jedes Frame hat einen 3-Byte Header. - Jedes Frame hat einen 3-Byte Header.
@@ -32,9 +32,9 @@ BLE Service UUIDs:
- RX: `e517d988-bab5-4574-8479-97c6cb115ca1` - RX: `e517d988-bab5-4574-8479-97c6cb115ca1`
- TX: `e517d988-bab5-4574-8479-97c6cb115ca2` - TX: `e517d988-bab5-4574-8479-97c6cb115ca2`
## 3. Frame-Format ## Frame-Format
### 3.1 Header ### Header
```c ```c
uint8_t frame_type; uint8_t frame_type;
@@ -42,7 +42,7 @@ uint16_t payload_length; // LE
``` ```
### 3.2 Paketstruktur ### Paketstruktur
```mermaid ```mermaid
--- ---
@@ -54,7 +54,7 @@ packet
+40: "Payload (variable length)" +40: "Payload (variable length)"
``` ```
### 3.3 Maximalgröße ### Maximalgröße
Firmware-Buffer ist slab-basiert (`CONFIG_BUZZ_PROTO_SLAB_SIZE`). Firmware-Buffer ist slab-basiert (`CONFIG_BUZZ_PROTO_SLAB_SIZE`).
Der effektive Chunk für Transfers wird zusätzlich durch den Transport limitiert. Bei Bluetooth sind das zum Beispiel 3 Bytes: Der effektive Chunk für Transfers wird zusätzlich durch den Transport limitiert. Bei Bluetooth sind das zum Beispiel 3 Bytes:
@@ -65,7 +65,7 @@ Der effektive Chunk für Transfers wird zusätzlich durch den Transport limitier
Das Protokoll ist so ausgelegt, dass es mit mindestens 100 Bytes auskommen sollte. Das Protokoll ist so ausgelegt, dass es mit mindestens 100 Bytes auskommen sollte.
## 4. Frame-Typen ## Frame-Typen
| Wert | Name | Richtung | Bedeutung | | Wert | Name | Richtung | Bedeutung |
|---|---|---|---| |---|---|---|---|
@@ -84,13 +84,14 @@ Das Protokoll ist so ausgelegt, dass es mit mindestens 100 Bytes auskommen sollt
| `0x41` | `LS_ENTRY` | Device -> Host | Verzeichniseintrag | | `0x41` | `LS_ENTRY` | Device -> Host | Verzeichniseintrag |
| `0x42` | `LS_END` | Device -> Host | Ende Verzeichnis-Stream | | `0x42` | `LS_END` | Device -> Host | Ende Verzeichnis-Stream |
## 5. Data-Typen (`REQUEST`) ## Data-Typen (`REQUEST`)
| Wert | Name | Status | Beschreibung | | Wert | Name | Status | Beschreibung |
|---|---|---|---| |---|---|---|---|
| `0x01` | `PROTO_INFO` | aktiv | Protokollversion + max Chunkgröße | | `0x01` | `PROTO_INFO` | aktiv | Protokollversion + max Chunkgröße |
| `0x02` | `DEVICE_INFO` | reserviert | aktuell nicht bedient | | `0x02` | `DEVICE_INFO` | aktiv | Device-Infos (Board, Revision, SOC, ID) |
| `0x03` | `FS_INFO` | aktiv | Dateisystem- und Pfadinfos | | `0x03` | `FS_INFO` | aktiv | Dateisystem- und Pfadinfos |
| `0x04` | `FW_INFO` | aktiv | Info über Firmware-Status und -Version sowie Kernelversion |
| `0x20` | `FILE_GET` | aktiv | Datei vom Device streamen | | `0x20` | `FILE_GET` | aktiv | Datei vom Device streamen |
| `0x21` | `FILE_PUT` | aktiv | Datei zum Device hochladen | | `0x21` | `FILE_PUT` | aktiv | Datei zum Device hochladen |
| `0x22` | `TAGS_GET` | aktiv | nur Tag-Bereich streamen | | `0x22` | `TAGS_GET` | aktiv | nur Tag-Bereich streamen |
@@ -100,9 +101,9 @@ Das Protokoll ist so ausgelegt, dass es mit mindestens 100 Bytes auskommen sollt
| `0x30` | `FW_UPDATE` | reserviert | aktuell nicht bedient | | `0x30` | `FW_UPDATE` | reserviert | aktuell nicht bedient |
| `0x40` | `LS` | aktiv | Verzeichnisliste starten | | `0x40` | `LS` | aktiv | Verzeichnisliste starten |
## 6. Request/Response-Formate ## Request/Response-Formate
## 6.1 Generischer Request ### Generischer Request
```c ```c
uint8_t data_type; uint8_t data_type;
@@ -126,7 +127,7 @@ packet
+32: "Optional payload (variable length)" +32: "Optional payload (variable length)"
``` ```
## 6.2 `PROTO_INFO` (`0x01`) ### `PROTO_INFO` (`0x01`)
Request: keine Zusatzdaten. Request: keine Zusatzdaten.
@@ -150,7 +151,24 @@ packet
+16: "Max Chunk Size (LE)" +16: "Max Chunk Size (LE)"
``` ```
## 6.3 `FS_INFO` (`0x03`) ### `DEVICE_INFO` (`0x02`)
Request: keine Zusatzdaten.
Respone-Payload:
```c
uint8_t data_type; // 0x02
uint8_t[8] device_id;
uint8_t board_len;
uint8_t rev_len;
uint8_t soc_len;
uint8_t data[]; // board, rev und soc, ohne Nullterminierung
```
***Hinweis:*** In der aktuellen implementierung werden die Strings auf eine maximale Länge von 32 Zeichen beschränkt. Dies sollte für alle Fälle genügen.
### `FS_INFO` (`0x03`)
Request: keine Zusatzdaten. Request: keine Zusatzdaten.
@@ -163,7 +181,7 @@ uint32_t free_size; // LE
uint8_t max_path_length; uint8_t max_path_length;
uint8_t sys_path_length; uint8_t sys_path_length;
uint8_t audio_path_length; uint8_t audio_path_length;
uint8_t data[]; // sys_path + audio_path (beide nicht nullterminiert) uint8_t data[]; // sys_path + audio_path ohne Nullterminierung
``` ```
Im `data` folgen sich System- und Audiopfad ohne Abstand, und ohne 0-Terminierung (`\0`). Beispiel für Systempfad `/lfs/sys`und Audiopfad `/lfs/a`: Im `data` folgen sich System- und Audiopfad ohne Abstand, und ohne 0-Terminierung (`\0`). Beispiel für Systempfad `/lfs/sys`und Audiopfad `/lfs/a`:
@@ -213,7 +231,23 @@ Das Beispiel schaut in HEX so aus:
0x2F 0x6C 0x66 0x73 0x2F 0x73 0x79 0x73 0x2F 0x6C 0x66 0x73 0x2F 0x61 0x2F 0x6C 0x66 0x73 0x2F 0x73 0x79 0x73 0x2F 0x6C 0x66 0x73 0x2F 0x61
``` ```
## 6.4 `LS` (`0x40`) ### `FW_INFO` (`0x04`)
Request: keine Zusatzdaten
Response:
```c
uint8_t fw_status; /* 0x00: Confirmed, 0x01: Pending, 0x02: Testing, 0xFF: Unbekannt */
uint32_t slot1_size; /* (LE) Grösse des Firmware Update Slots */
uint8_t fw_version_len; /* Länge des Firmware-Versionsstring */
uint8_t kernel_version_len; /* Länge des Kernel-Versionsstrings */
uint8_t data[]; /* FW-Version und Kernelversion, ohne Nullterminierung */
```
***Hinweis:*** in der Aktuellen implementierung werden die Versionen auf 32 Zeichen limitiert.
### `LS` (`0x40`)
Request-Payload: Request-Payload:
@@ -222,7 +256,7 @@ uint8_t data_type; // 0x40
char path[]; // ohne Nullterminierung char path[]; // ohne Nullterminierung
``` ```
## 6.5 `FILE_GET` (`0x20`) und `TAGS_GET` (`0x22`) ### `FILE_GET` (`0x20`) und `TAGS_GET` (`0x22`)
Request-Payload: Request-Payload:
@@ -233,7 +267,7 @@ char path[]; // ohne Nullterminierung
Antwort ist ein Stream aus `FILE_START` -> `FILE_CHUNK`* -> `FILE_END`. Antwort ist ein Stream aus `FILE_START` -> `FILE_CHUNK`* -> `FILE_END`.
## 6.6 `FILE_PUT` (`0x21`) und `TAGS_PUT` (`0x23`) ### `FILE_PUT` (`0x21`) und `TAGS_PUT` (`0x23`)
Request-Payload: Request-Payload:
@@ -247,7 +281,7 @@ Danach sendet der Host:
- `FILE_CHUNK` Frames - `FILE_CHUNK` Frames
- abschließend `FILE_END` mit CRC32 - abschließend `FILE_END` mit CRC32
## 6.7 `RM_FILE` (`0x24`) ### `RM_FILE` (`0x24`)
Request-Payload: Request-Payload:
@@ -257,7 +291,7 @@ uint8_t path_length;
char path[]; // ohne Nullterminierung char path[]; // ohne Nullterminierung
``` ```
## 6.8 `RENAME_FILE` (`0x25`) ### `RENAME_FILE` (`0x25`)
Request-Payload: Request-Payload:
@@ -268,9 +302,9 @@ uint8_t new_path_length;
char paths[]; // old_path + new_path (jeweils ohne Nullterminierung) char paths[]; // old_path + new_path (jeweils ohne Nullterminierung)
``` ```
## 7. ACK / ERROR / SUCCESS ## ACK / ERROR / SUCCESS
## 7.1 ACK (`0x11`) ### ACK (`0x11`)
Payload: Payload:
@@ -285,7 +319,7 @@ Wichtig: Es gibt zwei Semantiken je nach Richtung.
- Upload (`FILE_PUT`, `TAGS_PUT`): - Upload (`FILE_PUT`, `TAGS_PUT`):
Device -> Host, Credits sind zusätzlich gewährte Tokens (Host addiert sie). Device -> Host, Credits sind zusätzlich gewährte Tokens (Host addiert sie).
## 7.2 ERROR (`0x12`) ### ERROR (`0x12`)
Payload: Payload:
@@ -310,7 +344,7 @@ Häufige Codes:
| `116` | `ETIMEDOUT` | Credit-/Stream-Timeout | | `116` | `ETIMEDOUT` | Credit-/Stream-Timeout |
| `134` | `ENOTSUP` | nicht unterstützt | | `134` | `ENOTSUP` | nicht unterstützt |
## 7.3 SUCCESS (`0x13`) ### SUCCESS (`0x13`)
Payload: Payload:
@@ -320,9 +354,7 @@ uint8_t data_type; // erfolgreich abgeschlossener Befehl
Wird derzeit u.a. für `FILE_PUT`, `TAGS_PUT`, `RM_FILE`, `RENAME_FILE` genutzt. Wird derzeit u.a. für `FILE_PUT`, `TAGS_PUT`, `RM_FILE`, `RENAME_FILE` genutzt.
## 8. Stream-Sequenzen (Mermaid) ## Verzeichnisliste (`LS`)
## 8.1 Verzeichnisliste (`LS`)
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
@@ -346,7 +378,7 @@ sequenceDiagram
Hinweis zum aktuellen Web-Client (März 2026): Für `LS` wird initial `ACK(64)` gesendet, ein dynamisches Nachfüllen ist noch nicht implementiert. Große Verzeichnisse können deshalb in `ETIMEDOUT` laufen. Hinweis zum aktuellen Web-Client (März 2026): Für `LS` wird initial `ACK(64)` gesendet, ein dynamisches Nachfüllen ist noch nicht implementiert. Große Verzeichnisse können deshalb in `ETIMEDOUT` laufen.
## 8.2 Datei-/Tag-Download (`FILE_GET`, `TAGS_GET`) ## Datei-/Tag-Download (`FILE_GET`, `TAGS_GET`)
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
@@ -369,7 +401,7 @@ sequenceDiagram
Note over Host: CRC prüfen Note over Host: CRC prüfen
``` ```
## 8.3 Datei-/Tag-Upload (`FILE_PUT`, `TAGS_PUT`) ## Datei-/Tag-Upload (`FILE_PUT`, `TAGS_PUT`)
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
@@ -395,9 +427,9 @@ sequenceDiagram
end end
``` ```
## 9. Payload-Frames im Detail ## Payload-Frames im Detail
## 9.1 `LS_ENTRY` (`0x41`) ### `LS_ENTRY` (`0x41`)
```c ```c
uint8_t type; // 0x00 file, 0x01 dir uint8_t type; // 0x00 file, 0x01 dir
@@ -406,13 +438,13 @@ uint8_t name_length;
char name[]; // ohne Nullterminierung char name[]; // ohne Nullterminierung
``` ```
## 9.2 `LS_END` (`0x42`) ### `LS_END` (`0x42`)
```c ```c
uint32_t total_entries; // LE uint32_t total_entries; // LE
``` ```
## 9.3 `FILE_START` (`0x20`) ### `FILE_START` (`0x20`)
```c ```c
uint32_t total_size; // LE uint32_t total_size; // LE
@@ -421,19 +453,19 @@ uint32_t total_size; // LE
`FILE_GET`: komplette Dateigröße. `FILE_GET`: komplette Dateigröße.
`TAGS_GET`: nur Tag-Teil der Datei. `TAGS_GET`: nur Tag-Teil der Datei.
## 9.4 `FILE_CHUNK` (`0x21`) ### `FILE_CHUNK` (`0x21`)
Payload sind rohe Nutzdatenbytes. Payload sind rohe Nutzdatenbytes.
## 9.5 `FILE_END` (`0x22`) ### `FILE_END` (`0x22`)
```c ```c
uint32_t crc32; // LE, IEEE CRC32 uint32_t crc32; // LE, IEEE CRC32
``` ```
## 10. Beispiel-Frames (Hex) ## Beispiel-Frames (Hex)
## 10.1 `PROTO_INFO` Request/Response ### `PROTO_INFO` Request/Response
Request: Request:
@@ -454,7 +486,7 @@ Interpretation:
- `01 00`: Version 1 - `01 00`: Version 1
- `FD 00`: max_chunk_size = 253 - `FD 00`: max_chunk_size = 253
## 10.2 `LS` Request für `/a` ### `LS` Request für `/a`
```text ```text
00 03 00 40 2F 61 00 03 00 40 2F 61
@@ -466,7 +498,7 @@ Interpretation:
- `40`: data_type LS - `40`: data_type LS
- `2F 61`: `/a` - `2F 61`: `/a`
## 11. Implementierungsnotizen ### Implementierungsnotizen
- Unknown `REQUEST.data_type` wird aktuell mit `ERROR(EINVAL)` beantwortet. - Unknown `REQUEST.data_type` wird aktuell mit `ERROR(EINVAL)` beantwortet.
- Unbekannte/unerwartete `frame_type` im aktiven Protokollthread führen zu `ERROR(EPROTO)`. - Unbekannte/unerwartete `frame_type` im aktiven Protokollthread führen zu `ERROR(EPROTO)`.

View File

@@ -4,7 +4,7 @@
"singleAttributePerLine": false, "singleAttributePerLine": false,
"overrides": [ "overrides": [
{ {
"files": ["*.svelte", "*.astro"], "files": ["*.svelte", "*.astro", "*.ts", "*.js", "*.tsx", "*.jsx"],
"options": { "options": {
"printWidth": 100 "printWidth": 100
} }

View File

@@ -0,0 +1,456 @@
<script lang="ts">
import { GearIcon, CloudArrowUpIcon, InfoIcon, CaretDownIcon } from "phosphor-svelte";
import { slide, fade } from "svelte/transition";
import { addToast } from "../lib/toast";
import { processAudioFile } from "../lib/audioProcessor";
import { saveLocalFile } from "../lib/db";
import { refreshLocal } from "../lib/sync";
import { updateLocalAudioFormat, updateLocalAudioCrc } from "../lib/tagHandler";
import { audioOptions } from "../lib/store";
import { AUDIO_PRESETS, type AudioPresetName } from "../lib/settings";
import { tooltip } from "../lib/actions/tooltip";
import { clickOutside } from "../lib/actions/clickOutside"; // Angenommen, diese Action existiert bereits (analog zu Ihren anderen Menüs).
let isFileDragOver = false;
let showSettings = false;
let showPresetDropdown = false; // Neuer Zustand für das Custom Dropdown
// Definitionen der Preset-Infos für Tooltips im Dropdown
const presetInfos: Record<AudioPresetName, { name: string; desc: string }> = {
normal: { name: "Normal", desc: "Für natürliche Sprache. Moderate Verdichtung." },
broadcast: { name: "Broadcast", desc: "Aggressiv, sehr laut (Radio-Style). Maximale Präsenz." },
custom: { name: "Benutzerdefiniert", desc: "Manuelle Anpassung aller Kompressor-Parameter." },
};
// Reagiert auf Preset-Wechsel
function handlePresetChange(preset: AudioPresetName) {
$audioOptions.preset = preset;
showPresetDropdown = false; // Dropdown nach Auswahl schließen
if (preset !== "custom") {
const p = AUDIO_PRESETS[preset];
$audioOptions = {
...$audioOptions,
compressorThreshold: p.compressorThreshold!,
compressorRatio: p.compressorRatio!,
compressorKnee: p.compressorKnee!,
compressorAttack: p.compressorAttack!,
compressorRelease: p.compressorRelease!,
};
}
}
// Setzt das Preset automatisch auf Custom, sobald ein Slider bewegt wird
function setCustomPreset() {
$audioOptions.preset = "custom";
}
async function handleDrop(event: DragEvent) {
isFileDragOver = false;
showSettings = false;
showPresetDropdown = false; // Dropdown sicherheitshalber schließen
const files = event.dataTransfer?.files;
if (!files || files.length === 0) return;
const fileArray = Array.from(files);
const audioFiles = fileArray.filter((file) => file.type.startsWith("audio/"));
const rawFiles = fileArray.filter((file) => file.type === ""); // Ohne MIME-Type
if (audioFiles.length > 0 || rawFiles.length > 0) {
// 1. Reguläre Dateien umwandeln & taggen
for (const file of audioFiles) {
try {
const { buffer, name } = await processAudioFile(file, $audioOptions);
await saveLocalFile(name, new Blob([buffer]), buffer.byteLength);
await updateLocalAudioFormat(name, { codec: 0, bitDepth: 16, sampleRate: 16000 });
await updateLocalAudioCrc(name);
addToast(`Gespeichert & Getaggt: ${name}`, "success");
} catch (err) {
addToast(`Fehler bei ${file.name}: ${err}`, "error");
}
}
// 2. Rohdaten bypassen (direkt speichern & taggen)
for (const file of rawFiles) {
try {
const name = file.name;
await saveLocalFile(name, file, file.size);
// TagHandler erkennt alte Tags automatisch und fügt System-Tags sicher hinzu
await updateLocalAudioFormat(name, { codec: 0, bitDepth: 16, sampleRate: 16000 });
await updateLocalAudioCrc(name);
addToast(`Roh-Datei importiert: ${name}`, "success");
} catch (err) {
addToast(`Fehler bei Roh-Import ${file.name}: ${err}`, "error");
}
}
refreshLocal();
} else {
addToast("Keine gültigen Dateien gefunden.", "error");
}
}
</script>
<svelte:window
on:keydown={(e) => {
if (e.key === "Escape") showSettings = false;
}}
/>
<section class="buzzer-card flex flex-col h-full transition-all duration-300 min-w-[320px]">
<div class="card-header" class:blur={isFileDragOver}>
<h3 class="card-title">Dateiverarbeitung</h3>
<button class="btn" aria-label="Einstellungen" on:click={() => (showSettings = !showSettings)}>
<GearIcon class="icon" />
</button>
</div>
<div
class="card-body flex flex-col flex-1"
role="region"
aria-label="Audiofile dropzone"
on:dragenter={() => (isFileDragOver = true)}
on:dragleave={() => (isFileDragOver = false)}
on:dragover|preventDefault
on:drop|preventDefault={handleDrop}
>
{#if !showSettings}
<div
class="flex justify-center items-center pointer-events-none p-4 pb-0"
in:fade={{ duration: 200 }}
>
<div
class="text-center text-2xs tracking-tight font-mono font-semibold transition-all"
class:blur={isFileDragOver}
>
<span class={$audioOptions.lowCut ? "text-emerald-500" : "text-text-muted"}>LOW CUT</span>
|
<span class={$audioOptions.compress ? "text-emerald-500" : "text-text-muted"}>
COMPRESSOR
</span>
|
<span class={$audioOptions.normalize ? "text-emerald-500" : "text-text-muted"}>
NORMALIZER
</span>
</div>
</div>
<div class="flex-1 p-4 pt-2">
<div
in:fade={{ duration: 200 }}
class="flex h-full items-center justify-center border-2 border-dashed rounded-lg pointer-events-none transition-all duration-300 min-h-[8rem]"
class:border-slate-300={!isFileDragOver}
class:border-indigo-500={isFileDragOver}
class:bg-indigo-50={isFileDragOver}
>
<div class="relative flex size-24 transition-transform" class:scale-110={isFileDragOver}>
<span
class="absolute inline-flex h-full w-full opacity-50"
class:animate-ping={isFileDragOver}
class:text-indigo-500={isFileDragOver}
class:text-transparent={!isFileDragOver}
>
<CloudArrowUpIcon class="w-full h-full" />
</span>
<span
class="relative inline-flex size-24 transition-colors"
class:text-slate-500={!isFileDragOver}
class:text-indigo-600={isFileDragOver}
>
<CloudArrowUpIcon class="w-full h-full transition-colors" />
</span>
</div>
</div>
</div>
{:else}
<div class="flex-1 flex flex-col bg-white h-full" in:slide={{ duration: 300 }}>
<div
class="flex items-center justify-between px-4 pt-4 pb-2 border-b border-slate-100 mb-4"
>
<h4 class="font-semibold text-sm text-slate-800">Audio-Optionen</h4>
</div>
<div class="space-y-5 flex-1 overflow-y-auto px-4 pb-4">
<div class="flex flex-col gap-2">
<div class="flex items-center gap-2">
<div
use:tooltip={{
text: "Entfernt tieffrequentes Rumpeln (Wind, Trittschall). Schont kleine Lautsprecher und verhindert Verzerrungen.",
pos: "right",
}}
>
<InfoIcon class="text-text-muted size-4" />
</div>
<label class="flex items-center space-x-2 text-sm font-medium cursor-pointer flex-1">
<input
type="checkbox"
bind:checked={$audioOptions.lowCut}
class="accent-indigo-600"
/>
<span>Low-Cut Filter</span>
</label>
</div>
{#if $audioOptions.lowCut}
<div class="pl-6 flex items-center gap-3">
<span class="text-xs text-text-muted w-10 text-right">
{$audioOptions.lowCutFreq} Hz
</span>
<input
type="range"
min="50"
max="300"
step="10"
bind:value={$audioOptions.lowCutFreq}
class="flex-1 accent-indigo-500 h-1"
/>
</div>
{/if}
</div>
<div class="flex flex-col gap-2">
<div class="flex items-center gap-2">
<div
use:tooltip={{
text: "Gleicht Lautstärkeunterschiede an. Macht leise Passagen lauter und fängt laute Spitzen ab.",
pos: "right",
}}
>
<InfoIcon class="text-text-muted size-4" />
</div>
<label class="flex items-center space-x-2 text-sm font-medium cursor-pointer flex-1">
<input
type="checkbox"
bind:checked={$audioOptions.compress}
class="accent-indigo-600"
/>
<span>Dynamik-Kompressor</span>
</label>
</div>
{#if $audioOptions.compress}
<div class="pl-6 flex flex-col gap-3">
<div class="relative" use:clickOutside={() => (showPresetDropdown = false)}>
<button
type="button"
class="flex items-center justify-between w-full text-sm border border-slate-300 rounded p-1.5 bg-slate-50 hover:bg-slate-100 transition"
on:click={() => (showPresetDropdown = !showPresetDropdown)}
>
<div class="flex items-center gap-2">
<div
use:tooltip={{ text: presetInfos[$audioOptions.preset].desc, pos: "right" }}
>
<InfoIcon class="text-text-muted size-4" />
</div>
<span>Preset: {presetInfos[$audioOptions.preset].name}</span>
</div>
<span class:rotate-180={showPresetDropdown}>
<CaretDownIcon class="size-4 text-slate-500 transition-transform" />
</span>
</button>
{#if showPresetDropdown}
<div
class="absolute top-full left-0 right-0 mt-1 bg-white border border-slate-200 rounded-md shadow-lg z-20 overflow-hidden"
transition:fade={{ duration: 150 }}
>
{#each Object.entries(presetInfos) as [key, info]}
<button
type="button"
class="flex items-center gap-2 w-full text-left text-sm px-3 py-2 hover:bg-indigo-50 transition"
class:bg-indigo-100={$audioOptions.preset === key}
class:font-medium={$audioOptions.preset === key}
on:click={() => handlePresetChange(key as AudioPresetName)}
>
<div use:tooltip={{ text: info.desc, pos: "right" }}>
<InfoIcon class="text-text-muted size-4" />
</div>
<span>{info.name}</span>
</button>
{/each}
</div>
{/if}
</div>
<div class="grid grid-cols-1 gap-2 text-xs">
<div
class="flex items-center gap-2"
class:opacity-50={$audioOptions.preset !== "custom"}
>
<span
class="w-20 cursor-help"
use:tooltip={{
text: "Pegel, ab dem der Kompressor zu arbeiten beginnt.",
pos: "right",
}}
>
Threshold
</span>
<span class="text-text-muted w-16 text-right">
{$audioOptions.compressorThreshold} dB
</span>
<input
type="range"
min="-60"
max="0"
step="1"
bind:value={$audioOptions.compressorThreshold}
disabled={$audioOptions.preset !== "custom"}
on:input={setCustomPreset}
class="flex-1 accent-indigo-500 h-1"
/>
</div>
<div
class="flex items-center gap-2"
class:opacity-50={$audioOptions.preset !== "custom"}
>
<span
class="w-20 cursor-help"
use:tooltip={{
text: "Stärke der Pegelreduzierung oberhalb des Thresholds.",
pos: "right",
}}
>
Ratio
</span>
<span class="text-text-muted w-16 text-right">
{$audioOptions.compressorRatio}:1
</span>
<input
type="range"
min="1"
max="20"
step="1"
bind:value={$audioOptions.compressorRatio}
disabled={$audioOptions.preset !== "custom"}
on:input={setCustomPreset}
class="flex-1 accent-indigo-500 h-1"
/>
</div>
<div
class="flex items-center gap-2"
class:opacity-50={$audioOptions.preset !== "custom"}
>
<span
class="w-20 cursor-help"
use:tooltip={{
text: "Reaktionszeit des Kompressors auf Pegelüberschreitungen.",
pos: "right",
}}
>
Attack
</span>
<span class="text-text-muted w-16 text-right">
{($audioOptions.compressorAttack * 1000).toFixed(0)} ms
</span>
<input
type="range"
min="0.001"
max="0.1"
step="0.001"
bind:value={$audioOptions.compressorAttack}
disabled={$audioOptions.preset !== "custom"}
on:input={setCustomPreset}
class="flex-1 accent-indigo-500 h-1"
/>
</div>
<div
class="flex items-center gap-2"
class:opacity-50={$audioOptions.preset !== "custom"}
>
<span
class="w-20 cursor-help"
use:tooltip={{
text: "Zeit, bis der Kompressor nach Unterschreitung des Thresholds aufhört zu arbeiten.",
pos: "right",
}}
>
Release
</span>
<span class="text-text-muted w-16 text-right">
{($audioOptions.compressorRelease * 1000).toFixed(0)} ms
</span>
<input
type="range"
min="0.01"
max="1"
step="0.01"
bind:value={$audioOptions.compressorRelease}
disabled={$audioOptions.preset !== "custom"}
on:input={setCustomPreset}
class="flex-1 accent-indigo-500 h-1"
/>
</div>
</div>
</div>
{/if}
</div>
<div class="flex flex-col gap-2">
<div class="flex items-center gap-2">
<div
use:tooltip={{
text: "Hebt das verarbeitete Signal linear an, bis der lauteste Peak exakt den Zielwert erreicht.",
pos: "right",
}}
>
<InfoIcon class="text-text-muted size-4" />
</div>
<label class="flex items-center space-x-2 text-sm font-medium cursor-pointer flex-1">
<input
type="checkbox"
bind:checked={$audioOptions.normalize}
class="accent-indigo-600"
/>
<span>Lautstärke normalisieren</span>
</label>
</div>
{#if $audioOptions.normalize}
<div class="pl-6 flex items-center gap-3">
<span class="text-xs text-text-muted w-10 text-right">
{$audioOptions.normalizeTargetDb} dB
</span>
<input
type="range"
min="-10"
max="0"
step="0.1"
bind:value={$audioOptions.normalizeTargetDb}
class="flex-1 accent-indigo-500 h-1"
/>
</div>
{/if}
</div>
<div class="text-xs text-text-muted bg-slate-100 p-3 rounded mt-4">
Hinweis: Die Konvertierung zu 16kHz Mono PCM erfolgt immer automatisch beim Speichern.
</div>
</div>
</div>
{/if}
</div>
</section>
<style>
@reference "../styles/app.css";
.btn {
@apply rounded-full transition-colors border-0 p-2;
&:not(:disabled) {
@apply hover:bg-slate-200 hover:shadow-sm;
}
}
/* Styling für die Range-Slider */
input[type="range"] {
@apply cursor-pointer;
}
input[type="range"]:disabled {
@apply cursor-not-allowed;
}
</style>

View File

@@ -1,46 +1,137 @@
<script lang="ts"> <script lang="ts">
import FlashUsage from "./FlashUsage.svelte" import FlashUsage from "./FlashUsage.svelte";
import { BatteryEmptyIcon, BatteryLowIcon, BatteryMediumIcon, BatteryHighIcon, BatteryFullIcon, BatteryChargingIcon } from "phosphor-svelte" import { deviceInfo, fwInfo } from "../lib/store";
import { FW_STATUS } from "../lib/protocol/constants";
import { tooltip } from "../lib/actions/tooltip";
import {
CheckCircleIcon,
WarningIcon,
BatteryEmptyIcon,
BatteryLowIcon,
BatteryMediumIcon,
BatteryHighIcon,
BatteryFullIcon,
BatteryChargingIcon,
} from "phosphor-svelte";
</script> </script>
<div class="text-sm"> <div class="text-sm">
<table> <table>
<tbody> <tbody>
<tr> <tr>
<tr> <td class="key">Modell</td>
<td class="key">
Modell
<td class="value"> <td class="value">
<td class="value"> {#if $deviceInfo}
{$deviceInfo.boardName}
{#if $deviceInfo.boardRevision}
<span class="text-muted">>Rev. {$deviceInfo.boardRevision}</span>
{/if}
{:else}
unbekannt
{/if}
</td> </td>
</tr> </tr>
<tr> <tr>
<tr> <td class="key">Version</td>
<td class="key"> <td class="value flex items-center gap-1">
Version {#if $fwInfo}
</td> <span
<td class="value"> use:tooltip={{
text: `Die Firmware-Slotgrösse beträgt <span class="font-medium">${$fwInfo.slot1Size / 1024} kB</span>. Wieso musst Du das wissen? Edi sorgt schon dafür, dass die Updates nicht zu gross sind.<br/> <div class="text-3xl align-center text-center pb-2">😈</div>`,
pos: "bottom",
}}
>
{$fwInfo.fwVersion}
</span>
{#if $fwInfo.fwStatus === FW_STATUS.CONFIRMED}
<span
use:tooltip={{
text: "Firmware ist bestätigt und damit dauerhaft nutzbar",
pos: "bottom",
}}
class="relative flex size-5"
>
<CheckCircleIcon
weight="fill"
class="text-emerald-500 relative inline-flex h-full w-full"
/>
</span>
{:else if $fwInfo.fwStatus === FW_STATUS.PENDING}
<span
use:tooltip={{
text: "Firmware ist nur hochgeladen und noch nicht bestätigt. Du kannst sie nach einem <b>reboot</b> testen. Wenn sie funktioniert, bestätige sie, damit sie dauerhaft nutzbar ist.<br/><b><i>Achtung:</i></b> der Reboot dauert einen Moment, da die Firmware erst umkopiert werden muss.",
pos: "bottom",
}}
class="relative flex size-5"
>
<WarningIcon
weight="fill"
class="text-amber-500 absolute inline-flex h-full w-full animate-ping"
/>
<WarningIcon
weight="fill"
class="text-amber-500 relative inline-flex h-full w-full"
/>
</span>
{:else if $fwInfo.fwStatus === FW_STATUS.TESTING}
<span
use:tooltip={{
text: "Die Firmware ist im Testmodus. Wenn sie gut funktioniert, dann <b>bestätige</b> sie, damit sie dauerhaft nutzbar ist. Wenn sie nicht richtig funktioniert, dann kannst Du den Buzzer <b>rebooten</b>, er kehrt dann zur vorhergehenden Version zurück.<br/><b><i>Achtung:</i></b> der Reboot dauert einen Moment, da die Firmware erst umkopiert werden muss.",
pos: "bottom",
}}
class="relative flex size-5"
>
<WarningIcon
weight="fill"
class="text-amber-600 absolute inline-flex h-full w-full animate-ping"
/>
<WarningIcon
weight="fill"
class="text-amber-600 relative inline-flex h-full w-full"
/>
</span>
{:else if $fwInfo.fwStatus === FW_STATUS.UNKNOWN}
<span
use:tooltip={{
text: `Die Firmware hat den unbekannten Status <span class="bold font-mono">0x${$fwInfo.fwStatus.toString(16).padStart(2, "0").toUpperCase()}.</span> Weiss der Teufel, was da wieder passiert ist... Vielleicht hilft ein Reboot? Oder Firmware neu flashen? Oder... hast Du ne Idee???`,
pos: "bottom",
}}
class="relative flex size-5"
>
<WarningIcon
weight="fill"
class="text-red-500 absolute inline-flex h-full w-full animate-ping"
/>
<WarningIcon
weight="fill"
class="text-red-500 relative inline-flex h-full w-full"
/>
</span>
{/if}
<span class="text-text-muted ml-1">(Zephyr {$fwInfo.kernelVersion})</span>
{:else}
unbekannt
{/if}
</td> </td>
</tr> </tr>
<tr> <tr>
<tr> <td class="key">HW-ID</td>
<td class="key">
HW-ID
<td class="value"> <td class="value">
<td class="value"> {#if $deviceInfo}
<span class="font-mono">{$deviceInfo.deviceId}</span>
{:else}
unbekannt
{/if}
</td> </td>
</tr> </tr>
<tr> <tr>
<tr> <td class="key">Batterie</td>
<td class="key">
Batterie
<td class="value flex items-center gap-2"> <td class="value flex items-center gap-2">
85% <BatteryChargingIcon weight="bold" class="w-5 h-5" /> 1200mAh 85% <BatteryChargingIcon weight="bold" class="w-5 h-5" /> 1200mAh
</td> </td>
</tr> </tr>
<tr> <tr>
<tr> <td class="key">Speicher</td>
<td class="key">
Speicher
<td class="value"> <td class="value">
<div class="py-1"> <div class="py-1">
<FlashUsage /> <FlashUsage />

View File

@@ -28,7 +28,7 @@
import { tagEditorState } from "../lib/store"; import { tagEditorState } from "../lib/store";
import { tooltip } from "../lib/actions/tooltip"; import { tooltip } from "../lib/actions/tooltip";
import { deleteRemoteFile } from "../lib/transport"; import { deleteRemoteFile } from "../lib/transport";
import { deleteLocalFile } from "../lib/db"; import { deleteLocalFile, playLocalFile } from "../lib/db";
import { refreshRemote, refreshLocal } from "../lib/sync"; import { refreshRemote, refreshLocal } from "../lib/sync";
import { addToast } from "../lib/toast"; import { addToast } from "../lib/toast";
@@ -56,6 +56,7 @@
case SyncState.UNKNOWN: case SyncState.UNKNOWN:
return { return {
icon: QuestionIcon, icon: QuestionIcon,
weight: "fill",
color: "text-amber-500", color: "text-amber-500",
variant: "warning", variant: "warning",
text: "Prüfsumme fehlt. Bitte Metadaten aktualisieren.", text: "Prüfsumme fehlt. Bitte Metadaten aktualisieren.",
@@ -63,20 +64,24 @@
case SyncState.SINGLE_SIDED: case SyncState.SINGLE_SIDED:
return { return {
icon: CircleIcon, icon: CircleIcon,
color: "text-green-600", weight: "bold",
color: "text-emerald-600",
variant: "info", variant: "info",
text: `Datei existiert nur ${type === "buzzer" ? "auf dem Buzzer" : "lokal"}.`, text: `Datei existiert nur ${type === "buzzer" ? "auf dem Buzzer" : "lokal"}.`,
}; };
case SyncState.SYNCED: case SyncState.SYNCED:
return { return {
icon: CheckCircleIcon, icon: CheckCircleIcon,
color: "text-green-600", weight: "fill",
color: "text-emerald-500",
variant: "info", variant: "info",
text: "Datei ist synchronisiert.", text: "Datei ist synchronisiert.",
}; };
case SyncState.CONFLICT: case SyncState.CONFLICT:
return { return {
icon: WarningIcon, icon: WarningIcon,
ping: true,
weight: "fill",
color: "text-amber-600", color: "text-amber-600",
variant: "warning", variant: "warning",
text: `Konflikt: Name/Tags weichen ab. Vergleiche mit <span class="text-medium text-on-surface bg-white rounded px-1">${syncStatus.linkedFiles[0]}</span> ${type === "buzzer" ? "lokal" : "auf dem Buzzer"}`, text: `Konflikt: Name/Tags weichen ab. Vergleiche mit <span class="text-medium text-on-surface bg-white rounded px-1">${syncStatus.linkedFiles[0]}</span> ${type === "buzzer" ? "lokal" : "auf dem Buzzer"}`,
@@ -88,6 +93,8 @@
: `Mehrfach vorhanden: Gleicher Inhalt auch in <span class="text-medium text-on-surface bg-white rounded px-1">${syncStatus.linkedFiles.join("</span> <span class='text-medium text-on-surface bg-white rounded px-1'>")}</span>`; : `Mehrfach vorhanden: Gleicher Inhalt auch in <span class="text-medium text-on-surface bg-white rounded px-1">${syncStatus.linkedFiles.join("</span> <span class='text-medium text-on-surface bg-white rounded px-1'>")}</span>`;
return { return {
icon: WarningCircleIcon, icon: WarningCircleIcon,
ping: true,
weight: "fill",
color: "text-red-600", color: "text-red-600",
variant: "danger", variant: "danger",
text: duplicateText, text: duplicateText,
@@ -158,6 +165,7 @@
></div> ></div>
{/if} {/if}
<!-- svelte-ignore a11y_mouse_events_have_key_events -->
<button <button
class="relative z-10 w-full text-left flex-1 px-3 py-1 pr-16 flex items-center border-l-4 transition-colors border-b border-b-border-card class="relative z-10 w-full text-left flex-1 px-3 py-1 pr-16 flex items-center border-l-4 transition-colors border-b border-b-border-card
{file.selected ? 'border-l-blue-600' : 'border-l-transparent'} {file.selected ? 'border-l-blue-600' : 'border-l-transparent'}
@@ -169,11 +177,15 @@
{$isTransferingRemote ? 'cursor-default' : ''} {$isTransferingRemote ? 'cursor-default' : ''}
{state === 'pending' ? 'grayscale opacity-80' : ''}" {state === 'pending' ? 'grayscale opacity-80' : ''}"
on:click={toggleSelection} on:click={toggleSelection}
on:mouseenter|stopPropagation={() => (menuOpen = true)}
on:mouseleave|stopPropagation={() => (menuOpen = false)}
on:blur|stopPropagation={() => (menuOpen = false)}
on:focus|stopPropagation={() => (menuOpen = true)}
disabled={$isTransferingRemote} disabled={$isTransferingRemote}
> >
<MusicNotesIcon weight="fill" class="mr-3 w-5 h-5 shrink-0" /> <MusicNotesIcon weight="fill" class="mr-2 w-5 h-5 shrink-0" />
<div class="flex flex-col flex-1 min-w-0 overflow-hidden"> <div class="flex flex-col flex-1 min-w-0 overflow-hidden pl-1">
<div class="flex items-center min-w-0"> <div class="flex items-center min-w-0">
<span class="font-light truncate min-w-0 text-sm"> <span class="font-light truncate min-w-0 text-sm">
{file.name || "Unbekannte Datei"} {file.name || "Unbekannte Datei"}
@@ -193,11 +205,26 @@
variant: statusConfig.variant, variant: statusConfig.variant,
}} }}
> >
{#if statusConfig.ping}
<span class="mr-1 relative inline-flex size-3.5">
<svelte:component <svelte:component
this={statusConfig.icon} this={statusConfig.icon}
weight="fill" weight={statusConfig.weight ? statusConfig.weight : "fill"}
class="opacity-57 text-amber-600 absolute inline-flex h-full w-full animate-ping"
/>
<svelte:component
this={statusConfig.icon}
weight={statusConfig.weight ? statusConfig.weight : "fill"}
class="shrink-0 w-3.5 h-3.5 {statusConfig.color}"
/>
</span>
{:else}
<svelte:component
this={statusConfig.icon}
weight={statusConfig.weight ? statusConfig.weight : "fill"}
class="mr-1 shrink-0 w-3.5 h-3.5 {statusConfig.color}" class="mr-1 shrink-0 w-3.5 h-3.5 {statusConfig.color}"
/> />
{/if}
</span> </span>
{/if} {/if}
@@ -222,26 +249,32 @@
</div> </div>
</button> </button>
<div class="menu-btn-grp group/menu" class:is-open={menuOpen}> <!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_mouse_events_have_key_events -->
<div
class="menu-btn-grp group/menu"
class:is-open={menuOpen}
on:mouseout|stopPropagation={() => (menuOpen = false)}
>
<div <div
class="flex items-center overflow-hidden transition-all duration-300 ease-in-out class="flex items-center overflow-hidden transition-all duration-300 ease-in-out
{menuOpen {menuOpen
? 'max-w-[120px] opacity-100' ? 'max-w-[120px] opacity-100'
: 'max-w-0 opacity-0 group-hover/menu:max-w-[120px] group-hover/menu:opacity-100'}" : 'max-w-0 opacity-0 group-hover/menu:max-w-[120px] group-hover/menu:opacity-100'}"
> >
<button <button class="menu-btn danger" title="Löschen" on:click|stopPropagation={handleDeleteClick}>
class="menu-btn danger"
title="Löschen"
on:click|stopPropagation={handleDeleteClick}
>
<TrashIcon class="list-menu-icon" /> <TrashIcon class="list-menu-icon" />
</button> </button>
<button <button
class="menu-btn" class="menu-btn"
title="Abspielen" title="Abspielen"
on:click|stopPropagation={() => { on:click|stopPropagation={() => {
console.log("Play", file.name); if (type === "buzzer") {
addToast;
} else {
playLocalFile(file.name);
menuOpen = false; menuOpen = false;
}
}} }}
> >
<PlayIcon class="list-menu-icon" /> <PlayIcon class="list-menu-icon" />

View File

@@ -10,6 +10,7 @@
import FileMenuOverlay from "./FileMenuOverlay.svelte"; import FileMenuOverlay from "./FileMenuOverlay.svelte";
import FileTagEditor from "./FileTagEditor.svelte"; import FileTagEditor from "./FileTagEditor.svelte";
import FileListMenu from "./FileListMenu.svelte"; import FileListMenu from "./FileListMenu.svelte";
import AudioDropzone from "./AudioDropzone.svelte";
let showOverlay = false; let showOverlay = false;
let isTransferFinished = false; let isTransferFinished = false;
@@ -18,6 +19,7 @@
let showBuzzerMenu = false; let showBuzzerMenu = false;
let editModeType: "local" | "buzzer" | null = null; let editModeType: "local" | "buzzer" | null = null;
let fileToEdit: string | null = null; let fileToEdit: string | null = null;
let isFileDragOver = false;
$: currentDevice = $pairedDevices.find((d) => d.id === $activeDeviceId); $: currentDevice = $pairedDevices.find((d) => d.id === $activeDeviceId);
@@ -57,26 +59,7 @@
</script> </script>
<div class="main-layout mt-16 lg:mt-20 pb-12"> <div class="main-layout mt-16 lg:mt-20 pb-12">
<section class="buzzer-card flex flex-col"> <AudioDropzone />
<div class="card-header">
<h3 class="card-title">Dateiverarbeitung</h3>
<button class="btn" aria-label="Einstellungen">
<GearIcon class="icon" />
</button>
</div>
<div class="card-body p-4">
<div class="flex justify-center items-center">
<div class="text-center text-2xs tracking-tight font-mono font-semibold">
16kHz 16bit MONO | <span class="text-emerald-700">NORMALIZER ON</span>
|
<span class="text-red-700">COMPRESSOR OFF</span>
</div>
</div>
<div class="flex items-center justify-center">
<CloudArrowUpIcon class="w-24 h-24 text-slate-500" />
</div>
</div>
</section>
<section class="buzzer-card flex flex-col"> <section class="buzzer-card flex flex-col">
<div class="card-header"> <div class="card-header">

View File

@@ -0,0 +1,23 @@
export function clickOutside(node: HTMLElement, callback: () => void) {
const handleClick = (event: MouseEvent | TouchEvent) => {
// Prüfen, ob der Klick auf ein Element außerhalb des Containers erfolgte
if (node && !node.contains(event.target as Node) && !event.defaultPrevented) {
callback();
}
};
// Event-Listener in der Capture-Phase hinzufügen (true)
document.addEventListener('click', handleClick, true);
document.addEventListener('touchstart', handleClick, { passive: true, capture: true });
return {
update(newCallback: () => void) {
callback = newCallback;
},
destroy() {
// Event-Listener beim Zerstören der Komponente entfernen
document.removeEventListener('click', handleClick, true);
document.removeEventListener('touchstart', handleClick, true);
}
};
}

View File

@@ -0,0 +1,131 @@
export interface AudioProcessingOptions {
lowCut: boolean;
lowCutFreq: number;
compress: boolean;
compressorThreshold: number;
compressorRatio: number;
compressorKnee: number;
compressorAttack: number;
compressorRelease: number;
normalize: boolean;
normalizeTargetDb: number;
}
export const DEFAULT_AUDIO_OPTIONS: AudioProcessingOptions = {
lowCut: true,
lowCutFreq: 150,
compress: true,
compressorThreshold: -45, // Drastisch gesenkt: Erfasst auch sehr leises Flüstern
compressorRatio: 8, // Starke Kompression (8:1)
compressorKnee: 10, // Härterer Übergang (Hard Knee), damit der Kompressor direkter zupackt
compressorAttack: 0.005, // Etwas langsamer (5ms), lässt die initialen Konsonanten für die Sprachverständlichkeit durch
compressorRelease: 0.15, // Schnelleres Release (150ms), zieht leise Passagen zwischen Wörtern schneller hoch
normalize: true,
normalizeTargetDb: -0.5,
};
/**
* Konvertiert Float32 Samples (-1.0 bis 1.0) in Int16 PCM (-32768 bis 32767)
*/
function floatToInt16(float32Data: Float32Array): Int16Array {
const int16Data = new Int16Array(float32Data.length);
for (let i = 0; i < float32Data.length; i++) {
// Hard-Clipping verhindern, falls Signale leicht übersteuern
const s = Math.max(-1, Math.min(1, float32Data[i]));
int16Data[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
}
return int16Data;
}
/**
* Führt eine Peak-Normalisierung basierend auf einem dBFS-Zielwert durch.
*/
function applyNormalization(buffer: AudioBuffer, targetDb: number) {
const data = buffer.getChannelData(0);
let maxPeak = 0;
// 1. Höchsten absoluten Peak finden
for (let i = 0; i < data.length; i++) {
const absValue = Math.abs(data[i]);
if (absValue > maxPeak) maxPeak = absValue;
}
// 2. Multiplikator berechnen und anwenden
if (maxPeak > 0) {
const targetLinear = Math.pow(10, targetDb / 20);
const factor = targetLinear / maxPeak;
for (let i = 0; i < data.length; i++) {
data[i] *= factor;
}
}
}
export async function processAudioFile(
file: File,
// Partial bedeutet: Es müssen nicht alle Felder übergeben werden
userOptions: Partial<AudioProcessingOptions> = {}
): Promise<{ buffer: ArrayBuffer; name: string }> {
// Fügt die Standardwerte mit den User-Werten zusammen.
// Was der User nicht mitschickt, wird durch Defaults aufgefüllt.
const options = { ...DEFAULT_AUDIO_OPTIONS, ...userOptions };
// Cast auf ArrayBuffer, um TypeScript bzgl. SharedArrayBuffer zu beruhigen
const rawArrayBuffer = await file.arrayBuffer() as ArrayBuffer;
const audioCtx = new window.AudioContext();
const sourceBuffer = await audioCtx.decodeAudioData(rawArrayBuffer);
// Ziel-Kontext: Exakt 16kHz, Mono (1 Kanal), Länge berechnet aus Originaldauer
const offlineCtx = new window.OfflineAudioContext(1, sourceBuffer.duration * 16000, 16000);
const source = offlineCtx.createBufferSource();
source.buffer = sourceBuffer;
let lastNode: AudioNode = source;
// 1. Low-Cut Filter (Highpass)
if (options.lowCut) {
const filter = offlineCtx.createBiquadFilter();
filter.type = "highpass";
// Jetzt ist options.lowCutFreq garantiert eine Zahl
filter.frequency.setValueAtTime(options.lowCutFreq, offlineCtx.currentTime);
lastNode.connect(filter);
lastNode = filter;
}
// 2. Dynamics Compressor
if (options.compress) {
const compressor = offlineCtx.createDynamicsCompressor();
// Hier knallt es nicht mehr, weil options.compressorThreshold existiert
compressor.threshold.setValueAtTime(options.compressorThreshold, offlineCtx.currentTime);
compressor.knee.setValueAtTime(options.compressorKnee, offlineCtx.currentTime);
compressor.ratio.setValueAtTime(options.compressorRatio, offlineCtx.currentTime);
compressor.attack.setValueAtTime(options.compressorAttack, offlineCtx.currentTime);
compressor.release.setValueAtTime(options.compressorRelease, offlineCtx.currentTime);
lastNode.connect(compressor);
lastNode = compressor;
}
console.log("AudioProcessor: Audioeffekte angewendet", { options });
// Kette abschließen und Rendering starten
lastNode.connect(offlineCtx.destination);
source.start(0);
const renderedBuffer = await offlineCtx.startRendering();
// 3. Normalisierung
if (options.normalize) {
applyNormalization(renderedBuffer, options.normalizeTargetDb);
}
// 4. In Zielformat (16-bit PCM) wandeln
const pcm16 = floatToInt16(renderedBuffer.getChannelData(0));
return {
buffer: pcm16.buffer as ArrayBuffer,
// Original-Dateiendung entfernen
name: file.name.replace(/\.[^/.]+$/, "")
};
}

View File

@@ -70,3 +70,64 @@ export async function getLocalFile(name: string): Promise<{name: string, blob: B
request.onerror = () => reject(request.error); request.onerror = () => reject(request.error);
}); });
} }
let currentSource: AudioBufferSourceNode | null = null;
export async function playLocalFile(name: string): Promise<void> {
// Falls bereits etwas spielt: Stoppen und aufräumen
if (currentSource) {
try {
currentSource.stop();
} catch (e) {
// Ignorieren, falls die Quelle bereits natürlich gestoppt war
}
currentSource = null;
}
const fileEntry = await getLocalFile(name);
if (!fileEntry) throw new Error('Datei nicht gefunden');
const fullBuffer = await fileEntry.blob.arrayBuffer();
let audioByteLength = fullBuffer.byteLength;
// Metadaten-Limit berechnen (Footer an EOF - 8)
if (fullBuffer.byteLength >= 8) {
const view = new DataView(fullBuffer);
const footerOffset = fullBuffer.byteLength - 8;
const magic = new TextDecoder().decode(new Uint8Array(fullBuffer, footerOffset + 4, 4));
if (magic === "TAG!") {
const totalTagSize = view.getUint16(footerOffset, true); //
if (totalTagSize <= fullBuffer.byteLength) {
audioByteLength = fullBuffer.byteLength - totalTagSize;
}
}
}
const numSamples = Math.floor(audioByteLength / 2);
const int16Data = new Int16Array(fullBuffer, 0, numSamples);
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const audioBuffer = audioCtx.createBuffer(1, numSamples, 16000);
const float32Data = audioBuffer.getChannelData(0);
for (let i = 0; i < numSamples; i++) {
float32Data[i] = int16Data[i] / 32768.0;
}
const source = audioCtx.createBufferSource();
source.buffer = audioBuffer;
source.connect(audioCtx.destination);
// Referenz speichern, bevor wir starten
currentSource = source;
// Wenn die Datei natürlich endet, Referenz löschen
source.onended = () => {
if (currentSource === source) {
currentSource = null;
}
};
source.start();
}

View File

@@ -24,7 +24,9 @@ export const FRAME = {
export const DATA = { export const DATA = {
PROTO_INFO: 0x01, PROTO_INFO: 0x01,
DEVICE_INFO: 0x02,
FS_INFO: 0x03, FS_INFO: 0x03,
FW_INFO: 0x04,
FILE_GET: 0x20, FILE_GET: 0x20,
FILE_PUT: 0x21, FILE_PUT: 0x21,
@@ -57,3 +59,10 @@ export const ZEPHYR_ERRORS: Record<number, ZephyrError> = {
88: { text: "Funktion im Buzzer nicht implementiert", zephyr: "ENOSYS" }, 88: { text: "Funktion im Buzzer nicht implementiert", zephyr: "ENOSYS" },
134: { text: "Operation nicht unterstützt", zephyr: "ENOTSUP" } 134: { text: "Operation nicht unterstützt", zephyr: "ENOTSUP" }
}; };
export const FW_STATUS = {
CONFIRMED: 0x00,
PENDING: 0x01,
TESTING: 0x02,
UNKNOWN: 0xFF,
}

View File

@@ -1,5 +1,5 @@
import { FRAME, DATA, ZEPHYR_ERRORS } from './constants'; import { FRAME, DATA, ZEPHYR_ERRORS } from './constants';
import { protocolInfo, fsInfo, transferStats, resetTransferStats, transferDetails } from '../store'; import { protocolInfo, deviceInfo, fsInfo, transferStats, fwInfo, resetTransferStats, transferDetails } from '../store';
import { addToast } from '../toast'; import { addToast } from '../toast';
import { SETTINGS } from '../settings'; import { SETTINGS } from '../settings';
import { crc32 } from './crc32'; import { crc32 } from './crc32';
@@ -48,12 +48,13 @@ export function parseIncomingFrame(view: DataView, sender: FrameSender) {
switch (frameType) { switch (frameType) {
case FRAME.RESPONSE: case FRAME.RESPONSE:
const dataType = view.getUint8(3); const dataType = view.getUint8(3);
switch (dataType) {
if (dataType === DATA.PROTO_INFO && payloadLength >= 5) { case DATA.PROTO_INFO:
const version = view.getUint16(4, true); const version = view.getUint16(4, true);
const maxChunkSize = view.getUint16(6, true); const maxChunkSize = view.getUint16(6, true);
protocolInfo.set({ version, maxChunkSize }); protocolInfo.set({ version, maxChunkSize });
} else if (dataType === DATA.FS_INFO && payloadLength >= 14) { break;
case DATA.FS_INFO:
const totalSizeBytes = view.getUint32(4, true); const totalSizeBytes = view.getUint32(4, true);
const freeSizeBytes = view.getUint32(8, true); const freeSizeBytes = view.getUint32(8, true);
const maxPathLength = view.getUint8(12); const maxPathLength = view.getUint8(12);
@@ -62,6 +63,27 @@ export function parseIncomingFrame(view: DataView, sender: FrameSender) {
const sysPath = new TextDecoder().decode(new Uint8Array(view.buffer, 15, sysPathLength)); const sysPath = new TextDecoder().decode(new Uint8Array(view.buffer, 15, sysPathLength));
const audioPath = new TextDecoder().decode(new Uint8Array(view.buffer, 15 + sysPathLength, audioPathLength)); const audioPath = new TextDecoder().decode(new Uint8Array(view.buffer, 15 + sysPathLength, audioPathLength));
fsInfo.set({ totalSize: totalSizeBytes / 1024 / 1024, freeSize: freeSizeBytes / 1024 / 1024, maxPathLength, sysPath, audioPath }); fsInfo.set({ totalSize: totalSizeBytes / 1024 / 1024, freeSize: freeSizeBytes / 1024 / 1024, maxPathLength, sysPath, audioPath });
break;
case DATA.DEVICE_INFO:
const deviceId = [0, 2, 4, 6]
.map(offset => view.getUint16(4 + offset, false).toString(16).padStart(4, '0').toUpperCase())
.join('-');
const boardNameLength = view.getUint8(12);
const boardRevisionLength = view.getUint8(13);
const socNameLength = view.getUint8(14);
const boardName = new TextDecoder().decode(new Uint8Array(view.buffer, 15, boardNameLength));
const boardRevision = new TextDecoder().decode(new Uint8Array(view.buffer, 15 + boardNameLength, boardRevisionLength));
const socName = new TextDecoder().decode(new Uint8Array(view.buffer, 15 + boardNameLength + boardRevisionLength, socNameLength));
deviceInfo.set({ deviceId, boardName, boardRevision, socName });
break;
case DATA.FW_INFO:
const fwStatus = view.getUint8(4);
const slot1Size = view.getUint32(5, true);
const fw_version_length = view.getUint8(9);
const kernel_version_length = view.getUint8(10);
const fwVersion = new TextDecoder().decode(new Uint8Array(view.buffer, 11, fw_version_length));
const kernelVersion = new TextDecoder().decode(new Uint8Array(view.buffer, 11 + fw_version_length, kernel_version_length));
fwInfo.set({ fwStatus, slot1Size, fwVersion, kernelVersion });
} }
break; break;
@@ -92,9 +114,10 @@ export function parseIncomingFrame(view: DataView, sender: FrameSender) {
console.warn(`LS Stream: Erwartete Anzahl Einträge laut Header: ${total}, tatsächlich empfangen: ${lsBuffer.length}`); console.warn(`LS Stream: Erwartete Anzahl Einträge laut Header: ${total}, tatsächlich empfangen: ${lsBuffer.length}`);
addToast(`Warnung: LS Stream erwartete ${total} Einträge, aber nur ${lsBuffer.length} empfangen.`, 'warning'); addToast(`Warnung: LS Stream erwartete ${total} Einträge, aber nur ${lsBuffer.length} empfangen.`, 'warning');
} else if (lsResolve) { } else if (lsResolve) {
lsResolve([...lsBuffer]); const currentResolve = lsResolve;
lsResolve = null; lsResolve = null;
lsReject = null; lsReject = null;
currentResolve([...lsBuffer]);
} }
break; break;
@@ -137,10 +160,12 @@ export function parseIncomingFrame(view: DataView, sender: FrameSender) {
addToast("Dateitransfer abgebrochen (Timeout)", "error"); addToast("Dateitransfer abgebrochen (Timeout)", "error");
if (fileGetReject) { const currentReject = fileGetReject;
fileGetReject(new Error("Timeout beim Dateitransfer"));
fileGetResolve = null; fileGetResolve = null;
fileGetReject = null; fileGetReject = null;
if (currentReject) {
currentReject(new Error("Timeout beim Dateitransfer"));
} }
return; return;
} }
@@ -206,34 +231,39 @@ export function parseIncomingFrame(view: DataView, sender: FrameSender) {
if (fileTransfer.mode === 'file') { if (fileTransfer.mode === 'file') {
const fileName = get(transferStats).currentFileName; const fileName = get(transferStats).currentFileName;
const currentResolve = fileGetResolve;
const currentReject = fileGetReject;
// Direkt hier aufräumen, um Race Conditions bei schnellen Folge-Transfers zu vermeiden
fileGetResolve = null;
fileGetReject = null;
saveLocalFile(fileName, fileBlob, fileTransfer.totalBytes) saveLocalFile(fileName, fileBlob, fileTransfer.totalBytes)
.then(() => { .then(() => {
refreshLocal(); refreshLocal();
if (fileGetResolve) { if (currentResolve) {
fileGetResolve({ success: true }); currentResolve({ success: true });
} }
}) })
.catch(err => { .catch(err => {
console.error("Datenbankfehler:", err); console.error("Datenbankfehler:", err);
addToast(`Speichern von ${fileName} fehlgeschlagen.`, 'error'); addToast(`Speichern von ${fileName} fehlgeschlagen.`, 'error');
if (fileGetReject) fileGetReject(err); if (currentReject) currentReject(err);
})
.finally(() => {
fileGetResolve = null;
fileGetReject = null;
}); });
} else { } else {
// TAGS Modus: Blob direkt zurückgeben, nichts speichern // TAGS Modus: Blob direkt zurückgeben, nichts speichern
if (fileGetResolve) fileGetResolve({ success: true, blob: fileBlob }); const currentResolve = fileGetResolve;
fileGetResolve = null; fileGetResolve = null;
fileGetReject = null; fileGetReject = null;
if (currentResolve) currentResolve({ success: true, blob: fileBlob });
} }
} else { } else {
console.error("[CRC] Mismatch! Datei beschädigt."); console.error("[CRC] Mismatch! Datei beschädigt.");
addToast("CRC Fehler: Datei wurde fehlerhaft übertragen.", "error"); addToast("CRC Fehler: Datei wurde fehlerhaft übertragen.", "error");
if (fileGetReject) fileGetReject(new Error("CRC Mismatch")); const currentReject = fileGetReject;
fileGetResolve = null; fileGetResolve = null;
fileGetReject = null; fileGetReject = null;
if (currentReject) currentReject(new Error("CRC Mismatch"));
} }
break; break;
@@ -260,16 +290,18 @@ export function parseIncomingFrame(view: DataView, sender: FrameSender) {
console.error(`Received error frame with code: 0x${errorCode.toString(16)}`); console.error(`Received error frame with code: 0x${errorCode.toString(16)}`);
showErrorToast(errorCode); showErrorToast(errorCode);
if (lsReject) { if (lsReject) {
lsReject(new Error(`Buzzer Error 0x${errorCode.toString(16)}`)); const currentReject = lsReject;
lsResolve = null; lsResolve = null;
lsReject = null; lsReject = null;
currentReject(new Error(`Buzzer Error 0x${errorCode.toString(16)}`));
} }
if (fileGetReject && fileTransfer.active) { if (fileGetReject && fileTransfer.active) {
if (fileTransfer.metricsTimer) clearInterval(fileTransfer.metricsTimer); if (fileTransfer.metricsTimer) clearInterval(fileTransfer.metricsTimer);
fileTransfer.active = false; fileTransfer.active = false;
fileGetReject(new Error(`Buzzer Error 0x${errorCode.toString(16)}`)); const currentReject = fileGetReject;
fileGetResolve = null; fileGetResolve = null;
fileGetReject = null; fileGetReject = null;
currentReject(new Error(`Buzzer Error 0x${errorCode.toString(16)}`));
} }
if (uploadState.active && uploadState.onError) { if (uploadState.active && uploadState.onError) {
uploadState.onError(new Error(`Buzzer Error 0x${errorCode.toString(16)}`)); uploadState.onError(new Error(`Buzzer Error 0x${errorCode.toString(16)}`));
@@ -293,6 +325,17 @@ export function buildProtocolInfoRequest(): ArrayBuffer {
return buffer; return buffer;
} }
export function buildDeviceInfoRequest(): ArrayBuffer {
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint8(0, FRAME.REQUEST);
view.setUint16(1, 1, true);
view.setUint8(3, DATA.DEVICE_INFO);
return buffer;
}
export function buildFSInfoRequest(): ArrayBuffer { export function buildFSInfoRequest(): ArrayBuffer {
const buffer = new ArrayBuffer(4); const buffer = new ArrayBuffer(4);
const view = new DataView(buffer); const view = new DataView(buffer);
@@ -304,6 +347,17 @@ export function buildFSInfoRequest(): ArrayBuffer {
return buffer; return buffer;
} }
export function buildFWInfoRequest(): ArrayBuffer {
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint8(0, FRAME.REQUEST);
view.setUint16(1, 1, true);
view.setUint8(3, DATA.FW_INFO);
return buffer;
}
export function buildLSRequest(path: string): ArrayBuffer { export function buildLSRequest(path: string): ArrayBuffer {
const encoder = new TextEncoder(); const encoder = new TextEncoder();
const pathBytes = encoder.encode(path); const pathBytes = encoder.encode(path);
@@ -342,9 +396,10 @@ function resetLsWatchdog() {
addToast("Verzeichnis-Streaming abgebrochen (Timeout)", "warning"); addToast("Verzeichnis-Streaming abgebrochen (Timeout)", "warning");
lsBuffer = []; lsBuffer = [];
if (lsReject) { if (lsReject) {
lsReject(new Error("Timeout beim Lesen des Verzeichnisses")); const currentReject = lsReject;
lsResolve = null; lsResolve = null;
lsReject = null; lsReject = null;
currentReject(new Error("Timeout beim Lesen des Verzeichnisses"));
} }
}, 3000); }, 3000);
} }

View File

@@ -1,3 +1,5 @@
import type { AudioProcessingOptions } from './types';
export const SETTINGS = { export const SETTINGS = {
storage: { storage: {
connectionKey: 'buzzer_connection_state' connectionKey: 'buzzer_connection_state'
@@ -14,3 +16,31 @@ export const SETTINGS = {
estimatedInterFileGapMs: 700, // Initialer Schätzwert für die Pause zwischen zwei Dateien estimatedInterFileGapMs: 700, // Initialer Schätzwert für die Pause zwischen zwei Dateien
}, },
}; };
export const AUDIO_PRESETS: Record<'normal' | 'broadcast', Partial<AudioProcessingOptions>> = {
normal: {
compressorThreshold: -45,
compressorRatio: 8,
compressorKnee: 10,
compressorAttack: 0.005,
compressorRelease: 0.15,
},
broadcast: {
// Noch aggressiver für maximale, konstante Lautstärke (Radio-Style)
compressorThreshold: -50,
compressorRatio: 12,
compressorKnee: 2,
compressorAttack: 0.002,
compressorRelease: 0.10,
}
};
export const DEFAULT_AUDIO_OPTIONS: AudioProcessingOptions = {
preset: 'normal',
lowCut: true,
lowCutFreq: 150,
compress: true,
...AUDIO_PRESETS.normal, // Initialisierung mit Normal-Werten
normalize: true,
normalizeTargetDb: -0.5,
} as AudioProcessingOptions;

View File

@@ -1,6 +1,6 @@
import { writable, derived } from 'svelte/store'; import { writable, derived } from 'svelte/store';
import{ type BuzzerFile, SyncState, type SyncStatus } from './types'; import{ type BuzzerFile, SyncState, type SyncStatus, type AudioProcessingOptions } from './types';
import { SETTINGS } from './settings'; import {DEFAULT_AUDIO_OPTIONS, SETTINGS } from './settings';
const CONNECTION_STATE_KEY = 'buzzer_connection_state'; const CONNECTION_STATE_KEY = 'buzzer_connection_state';
@@ -31,6 +31,13 @@ export interface ProtocolInfo {
maxChunkSize: number; maxChunkSize: number;
} }
export interface DeviceInfo {
deviceId: string;
boardName: string;
boardRevision: string;
socName: string;
}
export interface FsInfo { export interface FsInfo {
totalSize: number; totalSize: number;
freeSize: number; freeSize: number;
@@ -39,6 +46,13 @@ export interface FsInfo {
audioPath: string; audioPath: string;
} }
export interface FwInfo {
fwStatus: number;
slot1Size: number;
fwVersion: string;
kernelVersion: string;
}
export interface StorageUsage { export interface StorageUsage {
totalBytes: number; totalBytes: number;
freeBytes: number; freeBytes: number;
@@ -65,9 +79,11 @@ export const targetDeviceId = writable<string | null>(null);
export const pairedDevices = writable<BluetoothDevice[]>([]); export const pairedDevices = writable<BluetoothDevice[]>([]);
export const availableDevices = writable<Set<string>>(new Set()); // IDs der derzeit advertisierten Geräte export const availableDevices = writable<Set<string>>(new Set()); // IDs der derzeit advertisierten Geräte
// Protokoll- und Dateisystem-Metadaten aus dem Device // Metadaten aus dem Device
export const protocolInfo = writable<ProtocolInfo | null>(null); export const protocolInfo = writable<ProtocolInfo | null>(null);
export const deviceInfo = writable<DeviceInfo | null>(null);
export const fsInfo = writable<FsInfo | null>(null); export const fsInfo = writable<FsInfo | null>(null);
export const fwInfo = writable<FwInfo | null>(null);
// Dateilisten // Dateilisten
export const buzzerAudioFiles = writable<BuzzerFile[]>([]); export const buzzerAudioFiles = writable<BuzzerFile[]>([]);
@@ -258,7 +274,9 @@ export function resetRemote(): void {
isConnected.set(false); isConnected.set(false);
isConnecting.set(false); isConnecting.set(false);
protocolInfo.set(null); protocolInfo.set(null);
deviceInfo.set(null);
fsInfo.set(null); fsInfo.set(null);
fwInfo.set(null);
activeDeviceId.set(null); activeDeviceId.set(null);
buzzerAudioFiles.set([]); buzzerAudioFiles.set([]);
buzzerSysFiles.set([]); buzzerSysFiles.set([]);
@@ -396,3 +414,28 @@ export const syncStateMap = derived(
return result; return result;
} }
); );
function createAudioOptionsStore() {
// 1. Initialen Wert aus dem Local Storage laden
const stored = localStorage.getItem("edi_audio_options");
// Merge aus Defaults und gespeicherten Werten, falls neue Felder hinzukommen
const initialValue: AudioProcessingOptions = stored
? { ...DEFAULT_AUDIO_OPTIONS, ...JSON.parse(stored) }
: DEFAULT_AUDIO_OPTIONS;
const { subscribe, set, update } = writable<AudioProcessingOptions>(initialValue);
// 2. Bei jeder Änderung automatisch in den Local Storage schreiben
subscribe((currentValue) => {
localStorage.setItem("edi_audio_options", JSON.stringify(currentValue));
});
return {
subscribe,
set,
update,
};
}
export const audioOptions = createAudioOptionsStore();

View File

@@ -1,8 +1,7 @@
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { isConnected, fsInfo, buzzerAudioFiles, buzzerSysFiles, isTransferingRemote, isFetchingLocal, storageUsage, localAudioFiles, transferStats } from './store'; import { isConnected, deviceInfo, fsInfo, fwInfo, buzzerAudioFiles, buzzerSysFiles, isTransferingRemote, isFetchingLocal, storageUsage, localAudioFiles, transferStats } from './store';
import { requestProtocolInfo, requestFSInfo, fetchDirectory } from './transport'; import { requestProtocolInfo, requestFSInfo, fetchDirectory, getFile, putFile, deleteRemoteFile, requestDeviceInfo, requestFWInfo } from './transport';
import type { BuzzerFile } from './types'; import type { BuzzerFile } from './types';
import { getFile, putFile, deleteRemoteFile } from './transport';
import { addToast } from './toast'; import { addToast } from './toast';
import { getLocalFiles, deleteLocalFile, getLocalFile } from './db'; import { getLocalFiles, deleteLocalFile, getLocalFile } from './db';
import { parseAudioFileTags } from './tagHandler'; import { parseAudioFileTags } from './tagHandler';
@@ -28,6 +27,8 @@ export async function refreshRemote() {
try { try {
await requestProtocolInfo(); await requestProtocolInfo();
await requestFSInfo(); await requestFSInfo();
await requestFWInfo();
await requestDeviceInfo();
// Kurze Verzögerung für Store-Propagation // Kurze Verzögerung für Store-Propagation
await new Promise(r => setTimeout(r, 100)); await new Promise(r => setTimeout(r, 100));

View File

@@ -1,5 +1,5 @@
import { getLocalFiles, saveLocalFile, deleteLocalFile } from './db'; import { getLocalFiles, saveLocalFile, deleteLocalFile } from './db';
import type { SystemTags, MetadataTags } from './types'; import type { SystemTags, MetadataTags, AudioFormat } from './types';
import { addToast } from './toast'; import { addToast } from './toast';
import { crc32 } from './protocol/crc32'; import { crc32 } from './protocol/crc32';
import { getTags, putTags, renameRemoteFile } from './transport'; import { getTags, putTags, renameRemoteFile } from './transport';
@@ -349,3 +349,21 @@ export async function updateLocalAudioCrc(filename: string): Promise<number> {
return newCrc; return newCrc;
} }
export async function updateLocalAudioFormat(filename: string, audioFormat: AudioFormat ): Promise<AudioFormat> {
const files = await getLocalFiles();
const record = files.find(f => f.name === filename);
if (!record) throw new Error(`Datei ${filename} nicht gefunden.`);
// Tags auslesen
const { sysTags, metaTags } = await parseAudioFileTags(record.blob, filename);
// Neue CRC berechnen
console.log(`TagHandler: Aktualisiere Audioformat für ${filename}`, { oldFormat: sysTags.format, newFormat: audioFormat });
// Mit aktualisierter CRC speichern
const updatedSysTags = { ...sysTags, format: audioFormat };
await updateLocalFile(filename, filename, updatedSysTags, metaTags);
return audioFormat;
}

View File

@@ -1,4 +1,4 @@
import { buildLSRequest, buildProtocolInfoRequest, buildFSInfoRequest, setLsResolver, buildFileGetRequest, setFileGetResolver, buildTagsGetRequest, uploadState } from './protocol/parser'; import { buildLSRequest, buildProtocolInfoRequest, buildDeviceInfoRequest, buildFSInfoRequest, buildFWInfoRequest, setLsResolver, buildFileGetRequest, setFileGetResolver, buildTagsGetRequest, uploadState } from './protocol/parser';
import { crc32 } from './protocol/crc32'; import { crc32 } from './protocol/crc32';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { protocolInfo, transferStats, } from './store'; import { protocolInfo, transferStats, } from './store';
@@ -26,6 +26,8 @@ export async function handleTransportConnect(sender: FrameSender) {
// Basis-Informationen zwingend vorab laden // Basis-Informationen zwingend vorab laden
await requestProtocolInfo(); await requestProtocolInfo();
await requestFSInfo(); await requestFSInfo();
await requestDeviceInfo();
await requestFWInfo();
// Erst wenn diese Basisdaten da sind, wird die UI freigeschaltet // Erst wenn diese Basisdaten da sind, wird die UI freigeschaltet
isConnected.set(true); isConnected.set(true);
@@ -44,10 +46,18 @@ export async function requestProtocolInfo() {
await sendFrame(buildProtocolInfoRequest()); await sendFrame(buildProtocolInfoRequest());
} }
export async function requestDeviceInfo() {
await sendFrame(buildDeviceInfoRequest());
}
export async function requestFSInfo() { export async function requestFSInfo() {
await sendFrame(buildFSInfoRequest()); await sendFrame(buildFSInfoRequest());
} }
export async function requestFWInfo() {
await sendFrame(buildFWInfoRequest());
}
let isListing = false; let isListing = false;
export async function fetchDirectory(path: string): Promise<any[]> { export async function fetchDirectory(path: string): Promise<any[]> {

View File

@@ -41,3 +41,17 @@ export interface SyncStatus {
state: SyncState; state: SyncState;
linkedFiles: string[]; // Referenzen auf die Dateinamen der Gegenseite (für Tooltips) linkedFiles: string[]; // Referenzen auf die Dateinamen der Gegenseite (für Tooltips)
} }
export interface AudioProcessingOptions {
preset: 'normal' | 'broadcast' | 'custom';
lowCut: boolean;
lowCutFreq: number;
compress: boolean;
compressorThreshold: number;
compressorRatio: number;
compressorKnee: number;
compressorAttack: number;
compressorRelease: number;
normalize: boolean;
normalizeTargetDb: number;
}