zwischenstand
This commit is contained in:
@@ -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)
|
||||||
@@ -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"
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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
|
||||||
23
firmware/libs/fw_mgmt/include/fw_mgmt.h
Normal file
23
firmware/libs/fw_mgmt/include/fw_mgmt.h
Normal 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 */
|
||||||
79
firmware/libs/fw_mgmt/src/fw_mgmt.c
Normal file
79
firmware/libs/fw_mgmt/src/fw_mgmt.c
Normal 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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/ {
|
/ {
|
||||||
aliases {
|
aliases {
|
||||||
mcuboot-button0 = &button1;
|
mcuboot-button0 = &button0;
|
||||||
mcuboot-led0 = &led1;
|
mcuboot-led0 = &led0;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
108
protocol.md
108
protocol.md
@@ -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)`.
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
456
webpage/src/components/AudioDropzone.svelte
Normal file
456
webpage/src/components/AudioDropzone.svelte
Normal 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>
|
||||||
@@ -1,49 +1,140 @@
|
|||||||
<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>
|
||||||
|
|
||||||
<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">
|
||||||
<td class="value flex items-center gap-2">
|
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">
|
||||||
<div class="py-1">
|
<FlashUsage />
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
23
webpage/src/lib/actions/clickOutside.ts
Normal file
23
webpage/src/lib/actions/clickOutside.ts
Normal 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
131
webpage/src/lib/audioProcessor.ts
Normal file
131
webpage/src/lib/audioProcessor.ts
Normal 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(/\.[^/.]+$/, "")
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -59,7 +59,7 @@ export async function deleteLocalFile(name: string): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLocalFile(name: string): Promise<{name: string, blob: Blob, size: number} | undefined> {
|
export async function getLocalFile(name: string): Promise<{ name: string, blob: Blob, size: number } | undefined> {
|
||||||
const db = await initDB();
|
const db = await initDB();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const tx = db.transaction(STORE_NAME, 'readonly');
|
const tx = db.transaction(STORE_NAME, 'readonly');
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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();
|
||||||
@@ -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));
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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[]> {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user