zwischenstand
This commit is contained in:
@@ -4,6 +4,8 @@ list(APPEND ZEPHYR_EXTRA_MODULES ${CMAKE_CURRENT_SOURCE_DIR}/libs)
|
|||||||
|
|
||||||
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
|
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
|
||||||
|
|
||||||
project(firmware)
|
project(buzzer)
|
||||||
|
|
||||||
|
include(${ZEPHYR_BASE}/samples/subsys/usb/common/common.cmake)
|
||||||
|
|
||||||
target_sources(app PRIVATE src/main.c)
|
target_sources(app PRIVATE src/main.c)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
VERSION_MAJOR = 0
|
VERSION_MAJOR = 0
|
||||||
VERSION_MINOR = 0
|
VERSION_MINOR = 0
|
||||||
PATCHLEVEL = 1
|
PATCHLEVEL = 2
|
||||||
VERSION_TWEAK = 0
|
VERSION_TWEAK = 0
|
||||||
#if (IS_ENABLED(CONFIG_LOG))
|
#if (IS_ENABLED(CONFIG_LOG))
|
||||||
EXTRAVERSION = debug
|
EXTRAVERSION = debug
|
||||||
|
|||||||
@@ -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,12 +568,12 @@ 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)
|
||||||
{
|
{
|
||||||
ssize_t audio_len = fs_get_audio_data_len(&write_ctx.file);
|
// ssize_t audio_len = fs_get_audio_data_len(&write_ctx.file);
|
||||||
|
ssize_t audio_len = 0; /* Zum Testen, da wir ja kein echtes FS-Backend haben */
|
||||||
if (audio_len < 0)
|
if (audio_len < 0)
|
||||||
{
|
{
|
||||||
LOG_ERR("Failed to get audio length: %d", (int)audio_len);
|
LOG_ERR("Failed to get audio length: %d", (int)audio_len);
|
||||||
@@ -583,17 +583,17 @@ 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);
|
||||||
fs_mgmt_pm_close(&write_ctx.file);
|
// fs_mgmt_pm_close(&write_ctx.file);
|
||||||
buzz_proto_send_error_reusing_slab(msg.reply_cb, abs(rc), msg.slab_ptr);
|
buzz_proto_send_error_reusing_slab(msg.reply_cb, abs(rc), msg.slab_ptr);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 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,8 @@ 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 +649,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 +660,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 +677,8 @@ 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 +699,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 +716,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);
|
||||||
|
|||||||
17
firmware/libs/fw_mgmt/CMakeLists.txt
Normal file
17
firmware/libs/fw_mgmt/CMakeLists.txt
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
if(CONFIG_FS_MGMT)
|
||||||
|
zephyr_library()
|
||||||
|
zephyr_library_sources(src/fs_mgmt.c)
|
||||||
|
zephyr_include_directories(include)
|
||||||
|
|
||||||
|
if(CONFIG_FILE_SYSTEM_LITTLEFS)
|
||||||
|
if(DEFINED ZEPHYR_LITTLEFS_MODULE_DIR)
|
||||||
|
zephyr_include_directories(${ZEPHYR_LITTLEFS_MODULE_DIR})
|
||||||
|
elseif(DEFINED WEST_TOPDIR)
|
||||||
|
zephyr_include_directories(${WEST_TOPDIR}/modules/fs/littlefs)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(DEFINED ZEPHYR_BASE)
|
||||||
|
zephyr_include_directories(${ZEPHYR_BASE}/modules/littlefs)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
64
firmware/libs/fw_mgmt/Kconfig
Normal file
64
firmware/libs/fw_mgmt/Kconfig
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
menuconfig FS_MGMT
|
||||||
|
bool "File System Management"
|
||||||
|
select FLASH
|
||||||
|
select FLASH_MAP
|
||||||
|
select FILE_SYSTEM
|
||||||
|
select FILE_SYSTEM_LITTLEFS
|
||||||
|
select FILE_SYSTEM_MKFS
|
||||||
|
select FLASH_PAGE_LAYOUT
|
||||||
|
select NORDIC_QSPI_NOR if SOC_SERIES_NRF52X && (SOC_NRF52840_QIAA || SOC_NRF52833_QIAA)
|
||||||
|
help
|
||||||
|
Library for initializing and managing the file system.
|
||||||
|
|
||||||
|
if FS_MGMT
|
||||||
|
config FS_MGMT_MOUNT_POINT
|
||||||
|
string "Littlefs Mount Point"
|
||||||
|
default "/lfs"
|
||||||
|
help
|
||||||
|
Set the mount point for the Littlefs file system. Default is "/lfs".
|
||||||
|
|
||||||
|
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"
|
||||||
|
endif # FS_MGMT
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
### 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_MGMT_LOG_LEVEL_DBG=y
|
||||||
|
|||||||
1
firmware/sysbuild.conf
Normal file
1
firmware/sysbuild.conf
Normal file
@@ -0,0 +1 @@
|
|||||||
|
SB_CONFIG_BOOTLOADER_MCUBOOT=y
|
||||||
8
firmware/sysbuild/mcuboot.conf
Normal file
8
firmware/sysbuild/mcuboot.conf
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
CONFIG_LOG=y
|
||||||
|
CONFIG_MCUBOOT_LOG_LEVEL_DBG=y
|
||||||
|
# CONFIG_MCUBOOT_SERIAL=y
|
||||||
|
CONFIG_UART_CONSOLE=y
|
||||||
|
# CONFIG_SINGLE_APPLICATION_SLOT=n
|
||||||
|
# CONFIG_MCUBOOT_INDICATION_LED=y
|
||||||
|
# CONFIG_BOOT_SERIAL_CDC_ACM=y
|
||||||
|
# CONFIG_PM_PARTITION_SIZE_MCUBOOT=0x11000
|
||||||
13
firmware/sysbuild/mcuboot.overlay
Normal file
13
firmware/sysbuild/mcuboot.overlay
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/ {
|
||||||
|
aliases {
|
||||||
|
mcuboot-button0 = &button1;
|
||||||
|
mcuboot-led0 = &led1;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Step 2.1 - Configure CDC ACM */
|
||||||
|
&zephyr_udc0 {
|
||||||
|
cdc_acm_uart0: cdc_acm_uart0 {
|
||||||
|
compatible = "zephyr,cdc-acm-uart";
|
||||||
|
};
|
||||||
|
};
|
||||||
671
protocol.md
671
protocol.md
@@ -1,365 +1,474 @@
|
|||||||
# Buzzer Protocol (Wire Specification)
|
# Buzzer Protocol (Wire Specification)
|
||||||
|
|
||||||
## 1. Zweck und Geltungsbereich
|
Stand: 2026-03-18
|
||||||
Das Buzzer Protocol definiert ein transportunabhaengiges, binaeres Frame-Format fuer die Kommunikation zwischen Host und Device.
|
Quelle: aktueller Implementierungsstand aus Firmware (`buzz_proto`, `fs_mgmt`, `ble_mgmt`) und Web-Client (`transport`, `parser`).
|
||||||
Unterstuetzte Transporte sind aktuell BLE und USB CDC ACM/UART.
|
|
||||||
|
|
||||||
Das Protokoll spezifiziert:
|
## 1. Ziel und Scope
|
||||||
- Frame-Struktur (Header + Payload)
|
|
||||||
- Frametypen
|
|
||||||
- Datentypen fuer Request/Response
|
|
||||||
- Semantik fuer Stream-Transfers (Verzeichnisliste, Datei, Firmware)
|
|
||||||
|
|
||||||
## 2. Transport- und Codierungsregeln
|
Das Buzzer Protocol ist ein binäres Frame-Protokoll für Host <-> Device Kommunikation.
|
||||||
- Alle ganzzahligen Felder werden in Little Endian uebertragen.
|
|
||||||
- Die im Header angegebene `payload_length` bezieht sich ausschliesslich auf die Nutzdaten ohne Header.
|
Abgedeckte Funktionen:
|
||||||
- Bei UART kann optional eine Synchronisationssequenz `BUZZ` (`0x42 0x55 0x5A 0x5A`) vor einem Frame verwendet werden, um Framing nach Leitungsstoerungen zu resynchronisieren.
|
- Capability-Abfrage (`PROTO_INFO`)
|
||||||
|
- Dateisystem-Info (`FS_INFO`)
|
||||||
|
- Verzeichnisliste als Stream (`LS`)
|
||||||
|
- Datei-/Tag-Download (`FILE_GET`, `TAGS_GET`)
|
||||||
|
- Datei-/Tag-Upload (`FILE_PUT`, `TAGS_PUT`)
|
||||||
|
- Datei löschen / umbenennen (`RM_FILE`, `RENAME_FILE`)
|
||||||
|
|
||||||
|
Nicht produktiv implementiert:
|
||||||
|
- `DEVICE_INFO` (`0x02`)
|
||||||
|
- Firmware-Update (`FW_*`, `FW_UPDATE`)
|
||||||
|
|
||||||
|
## 2. Transport und Grundregeln
|
||||||
|
|
||||||
|
- Alle Integer-Felder sind Little Endian.
|
||||||
|
- Jedes Frame hat einen 3-Byte Header.
|
||||||
|
- `payload_length` enthält nur die Payload-Länge (ohne Header).
|
||||||
|
- Aktiver Produktiv-Transport ist BLE GATT (RX Write Without Response, TX Notify).
|
||||||
|
- Es darf genau ein Stream gleichzeitig aktiv sein (`LS`, `FILE_GET`, `FILE_PUT`, `TAGS_*`).
|
||||||
|
|
||||||
|
BLE Service UUIDs:
|
||||||
|
- Service: `e517d988-bab5-4574-8479-97c6cb115ca0`
|
||||||
|
- RX: `e517d988-bab5-4574-8479-97c6cb115ca1`
|
||||||
|
- TX: `e517d988-bab5-4574-8479-97c6cb115ca2`
|
||||||
|
|
||||||
## 3. Frame-Format
|
## 3. Frame-Format
|
||||||
|
|
||||||
### 3.1 Header (3 Byte)
|
### 3.1 Header
|
||||||
|
|
||||||
```c
|
```c
|
||||||
uint8_t frame_type
|
uint8_t frame_type;
|
||||||
uint16_t payload_length // Little Endian
|
uint16_t payload_length; // LE
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3.2 Gesamtframe
|
|
||||||
```
|
### 3.2 Paketstruktur
|
||||||
+------------------+-------------------------+
|
|
||||||
| Header (3 Byte) | Payload (optional) |
|
```mermaid
|
||||||
| frame_type (1B) | payload_length Byte |
|
---
|
||||||
| payload_len (2B) | |
|
title: "Basic Packet Structure"
|
||||||
+------------------+-------------------------+
|
---
|
||||||
|
packet
|
||||||
|
+8: "Frame type"
|
||||||
|
+16: "Payload length (LE)"
|
||||||
|
+40: "Payload (variable length)"
|
||||||
```
|
```
|
||||||
|
|
||||||
## 4. Frametypen
|
### 3.3 Maximalgröße
|
||||||
|
|
||||||
### 4.1 Steuer- und Anfrageframes
|
Firmware-Buffer ist slab-basiert (`CONFIG_BUZZ_PROTO_SLAB_SIZE`).
|
||||||
| Wert | Name | Richtung | Beschreibung |
|
Der effektive Chunk für Transfers wird zusätzlich durch den Transport limitiert. Bei Bluetooth sind das zum Beispiel 3 Bytes:
|
||||||
|--------|------------|----------------|---------------------------------------|
|
|
||||||
| `0x00` | `REQUEST` | Host → Device | Abfrage eines Datentyps |
|
|
||||||
| `0x10` | `RESPONSE` | Device → Host | Antwort auf `REQUEST` |
|
|
||||||
| `0x11` | `ACK` | Host ↔ Device | Flusskontrolle bei Stream-Transfers |
|
|
||||||
| `0x12` | `ERROR` | Device → Host | Fehlerantwort mit Fehlercode |
|
|
||||||
| `0x13` | `SUCCESS` | Device → Host | Bestaetigung einer Operation |
|
|
||||||
|
|
||||||
### 4.2 Datei-Transfer
|
`PROTO_INFO.max_chunk_size` wird dynamisch berechnet als:
|
||||||
| Wert | Name | Richtung | Beschreibung |
|
|
||||||
|--------|--------------|----------------|---------------------------------------------|
|
|
||||||
| `0x20` | `FILE_START` | Host ↔ Device | Beginn eines Dateitransfers |
|
|
||||||
| `0x21` | `FILE_CHUNK` | Host ↔ Device | Ein Datenblock des Dateitransfers |
|
|
||||||
| `0x22` | `FILE_END` | Host ↔ Device | Abschluss des Dateitransfers inkl. CRC32 |
|
|
||||||
|
|
||||||
### 4.3 Firmware-Transfer (reserviert, noch nicht implementiert)
|
`min(slab_size - 3, transport_max_payload - 3)`
|
||||||
| Wert | Name |
|
|
||||||
|--------|------------|
|
|
||||||
| `0x30` | `FW_START` |
|
|
||||||
| `0x31` | `FW_CHUNK` |
|
|
||||||
| `0x32` | `FW_END` |
|
|
||||||
|
|
||||||
### 4.4 Verzeichnisliste
|
Das Protokoll ist so ausgelegt, dass es mit mindestens 100 Bytes auskommen sollte.
|
||||||
| Wert | Name | Richtung | Beschreibung |
|
|
||||||
|--------|------------|----------------|---------------------------------|
|
|
||||||
| `0x40` | `LS_START` | Device → Host | Beginn des Listing-Streams |
|
|
||||||
| `0x41` | `LS_ENTRY` | Device → Host | Ein Verzeichniseintrag |
|
|
||||||
| `0x42` | `LS_END` | Device → Host | Ende des Listing-Streams |
|
|
||||||
|
|
||||||
## 5. Request/Response-Schema
|
## 4. Frame-Typen
|
||||||
|
|
||||||
|
| Wert | Name | Richtung | Bedeutung |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `0x00` | `REQUEST` | Host -> Device | API-Aufruf per `data_type` |
|
||||||
|
| `0x10` | `RESPONSE` | Device -> Host | Antwort auf `REQUEST` |
|
||||||
|
| `0x11` | `ACK` | Host <-> Device | Credit-basierte Flusskontrolle |
|
||||||
|
| `0x12` | `ERROR` | Device -> Host | Fehler mit errno-Code |
|
||||||
|
| `0x13` | `SUCCESS` | Device -> Host | Erfolgreicher Abschluss |
|
||||||
|
| `0x20` | `FILE_START` | Device -> Host | Start Download-Stream |
|
||||||
|
| `0x21` | `FILE_CHUNK` | Host <-> Device | Datenblock (Upload/Download) |
|
||||||
|
| `0x22` | `FILE_END` | Host <-> Device | Streamende mit CRC32 |
|
||||||
|
| `0x30` | `FW_START` | reserviert | nicht aktiv |
|
||||||
|
| `0x31` | `FW_CHUNK` | reserviert | aktuell `ENOSYS` |
|
||||||
|
| `0x32` | `FW_END` | reserviert | nicht aktiv |
|
||||||
|
| `0x40` | `LS_START` | Device -> Host | Start Verzeichnis-Stream |
|
||||||
|
| `0x41` | `LS_ENTRY` | Device -> Host | Verzeichniseintrag |
|
||||||
|
| `0x42` | `LS_END` | Device -> Host | Ende Verzeichnis-Stream |
|
||||||
|
|
||||||
|
## 5. Data-Typen (`REQUEST`)
|
||||||
|
|
||||||
|
| Wert | Name | Status | Beschreibung |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `0x01` | `PROTO_INFO` | aktiv | Protokollversion + max Chunkgröße |
|
||||||
|
| `0x02` | `DEVICE_INFO` | reserviert | aktuell nicht bedient |
|
||||||
|
| `0x03` | `FS_INFO` | aktiv | Dateisystem- und Pfadinfos |
|
||||||
|
| `0x20` | `FILE_GET` | aktiv | Datei vom Device streamen |
|
||||||
|
| `0x21` | `FILE_PUT` | aktiv | Datei zum Device hochladen |
|
||||||
|
| `0x22` | `TAGS_GET` | aktiv | nur Tag-Bereich streamen |
|
||||||
|
| `0x23` | `TAGS_PUT` | aktiv | nur Tag-Bereich schreiben |
|
||||||
|
| `0x24` | `RM_FILE` | aktiv | Datei löschen |
|
||||||
|
| `0x25` | `RENAME_FILE` | aktiv | Datei umbenennen |
|
||||||
|
| `0x30` | `FW_UPDATE` | reserviert | aktuell nicht bedient |
|
||||||
|
| `0x40` | `LS` | aktiv | Verzeichnisliste starten |
|
||||||
|
|
||||||
|
## 6. Request/Response-Formate
|
||||||
|
|
||||||
|
## 6.1 Generischer Request
|
||||||
|
|
||||||
### 5.1 Request (`frame_type = 0x00`)
|
|
||||||
Payload-Mindestformat:
|
|
||||||
```c
|
```c
|
||||||
uint8_t data_type // Nutzt enum buzz_data_type
|
uint8_t data_type;
|
||||||
// optional: datentypspezifische Parameter
|
// optional daten_typspezifische Parameter
|
||||||
```
|
```
|
||||||
|
|
||||||
Wire-Format:
|
Wire:
|
||||||
```
|
|
||||||
[0x00][payload_length LE][data_type][optional parameters]
|
```text
|
||||||
|
[0x00][payload_len LE][data_type][optional...]
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5.2 Response (`frame_type = 0x10`)
|
```mermaid
|
||||||
Payload-Mindestformat:
|
---
|
||||||
```c
|
title: "Generic Requst Structure"
|
||||||
uint8_t data_type // Echo des angefragten data_type
|
---
|
||||||
// danach: datentypspezifische Response-Daten
|
packet
|
||||||
|
+8: "Frame type REQUEST: 0x00"
|
||||||
|
+16: "Payload length (LE)"
|
||||||
|
+8: "Data type"
|
||||||
|
+32: "Optional payload (variable length)"
|
||||||
```
|
```
|
||||||
|
|
||||||
Wire-Format:
|
## 6.2 `PROTO_INFO` (`0x01`)
|
||||||
```
|
|
||||||
[0x10][payload_length LE][data_type][response payload]
|
|
||||||
```
|
|
||||||
|
|
||||||
## 6. Datentypen (Request/Response)
|
Request: keine Zusatzdaten.
|
||||||
|
|
||||||
Definierte `data_type`-Werte:
|
|
||||||
| Wert | Name | Beschreibung |
|
|
||||||
|--------|---------------|--------------------------------------|
|
|
||||||
| `0x01` | `PROTO_INFO` | Protokollversion und Chunk-Groesse |
|
|
||||||
| `0x02` | `DEVICE_INFO` | Geraeteinformationen (TBD) |
|
|
||||||
| `0x03` | `FS_INFO` | Dateisystem-Statistik und Pfadnamen |
|
|
||||||
| `0x20` | `FILE_GET` | Datei vom Device anfordern |
|
|
||||||
| `0x21` | `FILE_PUT` | Datei auf das Device hochladen |
|
|
||||||
| `0x22` | `TAGS_GET` | Metadaten-Tags anfordern |
|
|
||||||
| `0x23` | `TAGS_PUT` | Metadaten-Tags schreiben |
|
|
||||||
| `0x24` | `RM_FILE` | Datei loeschen |
|
|
||||||
| `0x25` | `RENAME_FILE` | Datei umbenennen |
|
|
||||||
| `0x30` | `FW_UPDATE` | Firmware-Update starten |
|
|
||||||
| `0x40` | `LS` | Verzeichnisliste starten |
|
|
||||||
|
|
||||||
### 6.1 `PROTO_INFO` (`0x01`)
|
|
||||||
Request-Parameter: keine
|
|
||||||
|
|
||||||
Response-Payload:
|
Response-Payload:
|
||||||
|
|
||||||
```c
|
```c
|
||||||
uint8_t data_type; // 0x01
|
uint8_t data_type; // 0x01
|
||||||
uint16_t version; // Protokollversion (LE)
|
uint16_t version; // LE
|
||||||
uint16_t max_chunk_size; // max. Nutzdaten pro Frame ohne Header (LE)
|
uint16_t max_chunk_size; // LE
|
||||||
```
|
```
|
||||||
|
|
||||||
Hinweis: `max_chunk_size` ergibt sich aus der internen Slab-Konfiguration (`CONFIG_BUZZ_PROTO_SLAB_SIZE - 3`).
|
```mermaid
|
||||||
|
---
|
||||||
|
title: "PROTO_INFO response structure"
|
||||||
|
---
|
||||||
|
packet
|
||||||
|
+8: "Frame type RESPONSE: 0x10"
|
||||||
|
+16: "Payload length (LE): 5"
|
||||||
|
+8: "Data type PROTO_INFO: 0x01"
|
||||||
|
+16: "Protocol Version (LE)"
|
||||||
|
+16: "Max Chunk Size (LE)"
|
||||||
|
```
|
||||||
|
|
||||||
### 6.2 `DEVICE_INFO` (`0x02`)
|
## 6.3 `FS_INFO` (`0x03`)
|
||||||
TBD
|
|
||||||
|
|
||||||
### 6.3 `FS_INFO` (`0x03`)
|
Request: keine Zusatzdaten.
|
||||||
Request-Parameter: keine
|
|
||||||
|
|
||||||
Response-Payload:
|
Response-Payload:
|
||||||
|
|
||||||
```c
|
```c
|
||||||
uint8_t data_type; // 0x03
|
uint8_t data_type; // 0x03
|
||||||
uint32_t total_size; // Gesamtgroesse Flash in Bytes (LE)
|
uint32_t total_size; // LE
|
||||||
uint32_t free_size; // Freier Speicher in Bytes (LE)
|
uint32_t free_size; // LE
|
||||||
uint8_t max_path_length; // Maximal erlaubte Pfadlaenge
|
uint8_t max_path_length;
|
||||||
uint8_t sys_path_length; // Laenge des System-Pfades (ohne 0-Terminator)
|
uint8_t sys_path_length;
|
||||||
uint8_t audio_path_length; // Laenge des Audio-Pfades (ohne 0-Terminator)
|
uint8_t audio_path_length;
|
||||||
uint8_t data[]; // sys_path gefolgt von audio_path, nicht nullterminiert
|
uint8_t data[]; // sys_path + audio_path (beide nicht nullterminiert)
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6.4 `LS` (`0x40`) — Verzeichnisliste anfordern
|
Im `data` folgen sich System- und Audiopfad ohne Abstand, und ohne 0-Terminierung (`\0`). Beispiel für Systempfad `/lfs/sys`und Audiopfad `/lfs/a`:
|
||||||
Startet einen LS-Stream fuer den angegebenen Pfad.
|
|
||||||
|
`/lfs/sys/lfs/a`
|
||||||
|
|
||||||
|
`sys_path_len` ist in diesem Beispiel 8 und `audio_path_len` ist 6.
|
||||||
|
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
---
|
||||||
|
title: "FS_INFO response structure"
|
||||||
|
---
|
||||||
|
packet
|
||||||
|
+8: "Frame type RESPONSE: 0x10"
|
||||||
|
+16: "Payload length (LE): variable, 12 + sys path length + audio path length"
|
||||||
|
+8: "Data type FS_INFO: 0x03"
|
||||||
|
+32: "Total size (LE)"
|
||||||
|
+32: "Free size (LE)"
|
||||||
|
+8: "Max path length"
|
||||||
|
+8: "Sys path length"
|
||||||
|
+8: "Audio path length"
|
||||||
|
+40: "Sys path + Audio Path (variable)"
|
||||||
|
```
|
||||||
|
|
||||||
|
Beispielpaket mit **8 MiB Flash**, wovon **7 MiB frei** sind und den Pfaden `/lfs/sys` und `/lfs/a`. Die **maximale Pfadlänge** sind **32** Zeichen:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
---
|
||||||
|
title: "FS_INFO response example"
|
||||||
|
---
|
||||||
|
packet
|
||||||
|
+8: "Frame type RESPONSE: 0x10"
|
||||||
|
+16: "Payload length (LE): 26 (0x1A 0x00)"
|
||||||
|
+8: "Data type FS_INFO: 0x03"
|
||||||
|
+32: "Total size (LE): 8388608 (0x00 0x00 0x80 0x00)"
|
||||||
|
+32: "Free size (LE): 7340032 (0x00 0x00 0x70 0x00)"
|
||||||
|
+8: "Max path length: 32"
|
||||||
|
+8: "Sys path length: 8"
|
||||||
|
+8: "Audio path length: 6"
|
||||||
|
+112: "data: '/lfs/sys/lfs/a'"
|
||||||
|
```
|
||||||
|
|
||||||
|
Das Beispiel schaut in HEX so aus:
|
||||||
|
```text
|
||||||
|
0x10 0x1A 0x00 0x03 0x00 0x00 0x80 0x00 0x00 0x00 0x70 0x00 0x20 0x08 0x06
|
||||||
|
0x2F 0x6C 0x66 0x73 0x2F 0x73 0x79 0x73 0x2F 0x6C 0x66 0x73 0x2F 0x61
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6.4 `LS` (`0x40`)
|
||||||
|
|
||||||
Request-Payload:
|
Request-Payload:
|
||||||
|
|
||||||
```c
|
```c
|
||||||
uint8_t data_type; // 0x40
|
uint8_t data_type; // 0x40
|
||||||
char path[]; // Pfad ohne 0-Terminator, Laenge ergibt sich aus payload_length - 1
|
char path[]; // ohne Nullterminierung
|
||||||
```
|
```
|
||||||
|
|
||||||
Wire-Format (Beispiel fuer Pfad `/a`):
|
## 6.5 `FILE_GET` (`0x20`) und `TAGS_GET` (`0x22`)
|
||||||
```
|
|
||||||
[0x00][0x03 0x00][0x40][0x2F 0x61]
|
|
||||||
```
|
|
||||||
|
|
||||||
Das Device antwortet mit dem LS-Stream (siehe Abschnitt 8).
|
|
||||||
|
|
||||||
### 6.5 `RM_FILE` (`0x24`) — Datei loeschen
|
|
||||||
Request-Payload:
|
Request-Payload:
|
||||||
|
|
||||||
|
```c
|
||||||
|
uint8_t data_type; // 0x20 oder 0x22
|
||||||
|
char path[]; // ohne Nullterminierung
|
||||||
|
```
|
||||||
|
|
||||||
|
Antwort ist ein Stream aus `FILE_START` -> `FILE_CHUNK`* -> `FILE_END`.
|
||||||
|
|
||||||
|
## 6.6 `FILE_PUT` (`0x21`) und `TAGS_PUT` (`0x23`)
|
||||||
|
|
||||||
|
Request-Payload:
|
||||||
|
|
||||||
|
```c
|
||||||
|
uint8_t data_type; // 0x21 oder 0x23
|
||||||
|
uint32_t total_size; // LE
|
||||||
|
char path[]; // ohne Nullterminierung
|
||||||
|
```
|
||||||
|
|
||||||
|
Danach sendet der Host:
|
||||||
|
- `FILE_CHUNK` Frames
|
||||||
|
- abschließend `FILE_END` mit CRC32
|
||||||
|
|
||||||
|
## 6.7 `RM_FILE` (`0x24`)
|
||||||
|
|
||||||
|
Request-Payload:
|
||||||
|
|
||||||
```c
|
```c
|
||||||
uint8_t data_type; // 0x24
|
uint8_t data_type; // 0x24
|
||||||
uint8_t path_length; // Laenge des Pfads
|
uint8_t path_length;
|
||||||
char path[]; // Pfad ohne 0-Terminator
|
char path[]; // ohne Nullterminierung
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6.6 `RENAME_FILE` (`0x25`) — Datei umbenennen
|
## 6.8 `RENAME_FILE` (`0x25`)
|
||||||
|
|
||||||
Request-Payload:
|
Request-Payload:
|
||||||
|
|
||||||
```c
|
```c
|
||||||
uint8_t data_type; // 0x25
|
uint8_t data_type; // 0x25
|
||||||
uint8_t old_path_length; // Laenge des alten Pfads
|
uint8_t old_path_length;
|
||||||
uint8_t new_path_length; // Laenge des neuen Pfads
|
uint8_t new_path_length;
|
||||||
char paths[]; // Alter Pfad, direkt gefolgt vom neuen Pfad (beide ohne 0-Terminator)
|
char paths[]; // old_path + new_path (jeweils ohne Nullterminierung)
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6.7 `FILE_PUT` (`0x21`) / `TAGS_PUT` (`0x23`) — Upload initiieren
|
## 7. ACK / ERROR / SUCCESS
|
||||||
Request-Payload:
|
|
||||||
```c
|
|
||||||
uint8_t data_type; // 0x21 (Datei) oder 0x23 (Tags)
|
|
||||||
uint32_t total_size; // Dateigroesse in Bytes (LE)
|
|
||||||
char path[]; // Zielpfad ohne 0-Terminator
|
|
||||||
```
|
|
||||||
|
|
||||||
## 7. ACK-, ERROR- und SUCCESS-Frames
|
## 7.1 ACK (`0x11`)
|
||||||
|
|
||||||
### 7.1 ACK (`frame_type = 0x11`) — Host ↔ Device
|
|
||||||
Wird waehrend eines laufenden Stream-Transfers gesendet, um der sendenden Seite Credits (Sendeerlaubnisse) zu erteilen.
|
|
||||||
Bei einem Download (`LS` oder `FILE_GET`) sendet der Host das ACK. Bei einem Upload (`FILE_PUT` oder `TAGS_PUT`) sendet das Device das ACK.
|
|
||||||
|
|
||||||
Format:
|
|
||||||
```c
|
|
||||||
// Header:
|
|
||||||
uint8_t frame_type; // 0x11
|
|
||||||
uint16_t payload_length; // 0x0002
|
|
||||||
|
|
||||||
// Payload:
|
|
||||||
uint16_t credits; // Anzahl der Entries, die das Device senden darf (LE)
|
|
||||||
```
|
|
||||||
|
|
||||||
Wire-Format (Beispiel: 64 Credits):
|
|
||||||
```
|
|
||||||
[0x11][0x02 0x00][0x40 0x00]
|
|
||||||
```
|
|
||||||
|
|
||||||
Semantik:
|
|
||||||
- Der Host sendet nach Empfang von `LS_START` initial Credits (typisch 64).
|
|
||||||
- Das Device dekrementiert seinen internen Credit-Zaehler mit jeder gesendeten `LS_ENTRY`.
|
|
||||||
- Bei 0 Credits wartet das Device auf ein weiteres ACK (Timeout: 5 × 500 ms, danach Abbruch).
|
|
||||||
- Der Host soll bei Bedarf weitere Credits nachsenden, bevor die bisherigen aufgebraucht sind.
|
|
||||||
|
|
||||||
### 7.2 ERROR (`frame_type = 0x12`) — Device → Host
|
|
||||||
Format:
|
|
||||||
```c
|
|
||||||
// Header:
|
|
||||||
uint8_t frame_type; // 0x12
|
|
||||||
uint16_t payload_length; // 0x0002
|
|
||||||
|
|
||||||
// Payload:
|
|
||||||
uint16_t error_code; // Positiver Zephyr-errno-Wert (LE)
|
|
||||||
```
|
|
||||||
|
|
||||||
Wire-Format (Beispiel: ENOENT = 2):
|
|
||||||
```
|
|
||||||
[0x12][0x02 0x00][0x02 0x00]
|
|
||||||
```
|
|
||||||
|
|
||||||
ERROR kann jederzeit als Antwort auf einen REQUEST oder waehrend eines Streams gesendet werden.
|
|
||||||
Ein ERROR-Frame waehrend eines aktiven LS-Streams beendet diesen implizit.
|
|
||||||
|
|
||||||
Fehlercode-Tabelle (Zephyr errno, positiver Wert):
|
|
||||||
| Code | Zephyr-Name | Bedeutung |
|
|
||||||
|------|----------------|---------------------------------------------|
|
|
||||||
| 1 | `EPERM` | Fehlende Berechtigung |
|
|
||||||
| 2 | `ENOENT` | Datei oder Verzeichnis nicht gefunden |
|
|
||||||
| 5 | `EIO` | Ein-/Ausgabefehler auf dem Flash |
|
|
||||||
| 12 | `ENOMEM` | Nicht genuegend Speicher frei |
|
|
||||||
| 16 | `EBUSY` | Geraet oder Ressource belegt |
|
|
||||||
| 22 | `EINVAL` | Ungültiges Argument oder Parameter |
|
|
||||||
| 24 | `EMFILE` | Zu viele offene Dateien |
|
|
||||||
| 28 | `ENOSPC` | Kein freier Speicherplatz mehr |
|
|
||||||
| 36 | `ENAMETOOLONG` | Dateiname oder Pfad zu lang |
|
|
||||||
| 88 | `ENOSYS` | Funktion nicht implementiert |
|
|
||||||
| 134 | `ENOTSUP` | Operation nicht unterstuetzt |
|
|
||||||
|
|
||||||
### 7.3 SUCCESS (`frame_type = 0x13`) — Device → Host
|
|
||||||
Bestaetigt den erfolgreichen Abschluss einer Operation, z. B. nach Beendigung eines Uploads oder einer Dateioperation (Loeschen, Umbenennen).
|
|
||||||
|
|
||||||
Format:
|
|
||||||
```c
|
|
||||||
// Header:
|
|
||||||
uint8_t frame_type; // 0x13
|
|
||||||
uint16_t payload_length; // 0x0001
|
|
||||||
|
|
||||||
// Payload:
|
|
||||||
uint8_t data_type; // Der Befehl, der erfolgreich war (z.B. 0x21 fuer FILE_PUT)
|
|
||||||
```
|
|
||||||
|
|
||||||
Wire-Format (Beispiel: Erfolg bei RM_FILE):
|
|
||||||
```
|
|
||||||
[0x13][0x01 0x00][0x24]
|
|
||||||
```
|
|
||||||
|
|
||||||
## 8. LS-Stream (Verzeichnisliste)
|
|
||||||
|
|
||||||
Der LS-Stream wird durch einen `REQUEST` mit `data_type = 0x40` ausgeloest und laeuft wie folgt ab:
|
|
||||||
|
|
||||||
```
|
|
||||||
Host Device
|
|
||||||
| |
|
|
||||||
|-- REQUEST (data_type=LS, path) -->|
|
|
||||||
| | (oeffnet Verzeichnis)
|
|
||||||
|<--------- LS_START (leer) --------|
|
|
||||||
| |
|
|
||||||
|------ ACK (credits=64) ---------->|
|
|
||||||
| |
|
|
||||||
|<-- LS_ENTRY (entry 1) ------------|
|
|
||||||
|<-- LS_ENTRY (entry 2) ------------|
|
|
||||||
| ... |
|
|
||||||
|<-- LS_ENTRY (entry 64) -----------| (credits = 0, Device wartet)
|
|
||||||
| |
|
|
||||||
|------ ACK (credits=64) ---------->|
|
|
||||||
| |
|
|
||||||
|<-- LS_ENTRY (entry 65) -----------|
|
|
||||||
| ... |
|
|
||||||
|<--------- LS_END -----------------|
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8.1 `LS_START` (`0x40`) — Device → Host
|
|
||||||
Signalisiert den Beginn des Streams. Keine Payload.
|
|
||||||
|
|
||||||
```
|
|
||||||
[0x40][0x00 0x00]
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8.2 `LS_ENTRY` (`0x41`) — Device → Host
|
|
||||||
Ein Eintrag pro Verzeichniselement.
|
|
||||||
|
|
||||||
Payload:
|
Payload:
|
||||||
|
|
||||||
```c
|
```c
|
||||||
uint8_t type; // 0x00 = Datei, 0x01 = Verzeichnis (buzz_fs_entry_type)
|
uint16_t credits; // LE
|
||||||
uint32_t size; // Dateigroesse in Bytes (LE); bei Verzeichnissen 0
|
|
||||||
uint8_t name_length; // Laenge des Namens (ohne 0-Terminator)
|
|
||||||
char name[]; // Datei-/Verzeichnisname, nicht nullterminiert
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`type`-Werte:
|
Wichtig: Es gibt zwei Semantiken je nach Richtung.
|
||||||
| Wert | Bedeutung |
|
|
||||||
|--------|---------------|
|
|
||||||
| `0x00` | Datei (FILE) |
|
|
||||||
| `0x01` | Verzeichnis (DIR) |
|
|
||||||
|
|
||||||
### 8.3 `LS_END` (`0x42`) — Device → Host
|
- Download (`LS`, `FILE_GET`, `TAGS_GET`):
|
||||||
Signalisiert das Ende des Streams.
|
Host -> Device, Credits werden im Device als absoluter neuer Stand gesetzt.
|
||||||
|
- Upload (`FILE_PUT`, `TAGS_PUT`):
|
||||||
|
Device -> Host, Credits sind zusätzlich gewährte Tokens (Host addiert sie).
|
||||||
|
|
||||||
|
## 7.2 ERROR (`0x12`)
|
||||||
|
|
||||||
Payload:
|
Payload:
|
||||||
|
|
||||||
```c
|
```c
|
||||||
uint32_t total_entries; // Gesamtzahl gesendeter Eintraege (LE)
|
uint16_t error_code; // positiver errno-Wert, LE
|
||||||
```
|
```
|
||||||
|
|
||||||
Der Host kann `total_entries` mit der empfangenen Anzahl von `LS_ENTRY`-Frames vergleichen, um Vollstaendigkeit zu pruefen.
|
Häufige Codes:
|
||||||
|
|
||||||
### 8.4 Fehler- und Timeoutbehandlung
|
| Code | Name | Bedeutung |
|
||||||
- Tritt ein Fehler beim Lesen auf, sendet das Device einen `ERROR`-Frame und beendet den Stream.
|
|---|---|---|
|
||||||
- Empfaengt das Device 5 Mal in Folge keine Credits innerhalb von je 500 ms (2,5 s gesamt), bricht es den Stream intern ab (kein ERROR-Frame, Stream wird still verworfen).
|
| `1` | `EPERM` | fehlende Rechte |
|
||||||
- Der Host sollte einen eigenen Watchdog implementieren; empfohlener Timeout: 3 s ohne empfangenen Frame.
|
| `2` | `ENOENT` | Datei/Ordner nicht gefunden |
|
||||||
|
| `5` | `EIO` | Flash-I/O-Fehler |
|
||||||
|
| `16` | `EBUSY` | Stream/Ressource belegt |
|
||||||
|
| `22` | `EINVAL` | ungültige Nutzdaten |
|
||||||
|
| `36` | `ENAMETOOLONG` | Pfad zu lang |
|
||||||
|
| `71` | `EPROTO` | unzulässiger Frame-Typ |
|
||||||
|
| `74` | `EBADMSG` | ungültiger Stream-Frame/CRC Fehler |
|
||||||
|
| `88` | `ENOSYS` | nicht implementiert |
|
||||||
|
| `90` | `EMSGSIZE` | max Payload ungültig |
|
||||||
|
| `116` | `ETIMEDOUT` | Credit-/Stream-Timeout |
|
||||||
|
| `134` | `ENOTSUP` | nicht unterstützt |
|
||||||
|
|
||||||
## 9. Beispiele
|
## 7.3 SUCCESS (`0x13`)
|
||||||
|
|
||||||
### 9.1 PROTO_INFO abfragen
|
Payload:
|
||||||
|
|
||||||
|
```c
|
||||||
|
uint8_t data_type; // erfolgreich abgeschlossener Befehl
|
||||||
|
```
|
||||||
|
|
||||||
|
Wird derzeit u.a. für `FILE_PUT`, `TAGS_PUT`, `RM_FILE`, `RENAME_FILE` genutzt.
|
||||||
|
|
||||||
|
## 8. Stream-Sequenzen (Mermaid)
|
||||||
|
|
||||||
|
## 8.1 Verzeichnisliste (`LS`)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Host
|
||||||
|
participant Device
|
||||||
|
|
||||||
|
Host->>Device: REQUEST(LS, path)
|
||||||
|
Device-->>Host: LS_START
|
||||||
|
Host->>Device: ACK(credits=64)
|
||||||
|
|
||||||
|
loop solange credits > 0
|
||||||
|
Device-->>Host: LS_ENTRY
|
||||||
|
end
|
||||||
|
|
||||||
|
alt Verzeichnis vollständig
|
||||||
|
Device-->>Host: LS_END(total_entries)
|
||||||
|
else keine Credits/Timeout
|
||||||
|
Device-->>Host: ERROR(ETIMEDOUT)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
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`)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Host
|
||||||
|
participant Device
|
||||||
|
|
||||||
|
Host->>Device: REQUEST(FILE_GET|TAGS_GET, path)
|
||||||
|
Device-->>Host: FILE_START(total_size)
|
||||||
|
Host->>Device: ACK(credits=128)
|
||||||
|
|
||||||
|
loop chunks
|
||||||
|
Device-->>Host: FILE_CHUNK(data)
|
||||||
|
Note over Host: Credits dekrementieren
|
||||||
|
alt Credits <= 64
|
||||||
|
Host->>Device: ACK(credits=128)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Device-->>Host: FILE_END(crc32)
|
||||||
|
Note over Host: CRC prüfen
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8.3 Datei-/Tag-Upload (`FILE_PUT`, `TAGS_PUT`)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Host
|
||||||
|
participant Device
|
||||||
|
|
||||||
|
Host->>Device: REQUEST(FILE_PUT|TAGS_PUT, total_size, path)
|
||||||
|
Device-->>Host: ACK(initial credits)
|
||||||
|
|
||||||
|
loop solange Host-Credits > 0
|
||||||
|
Host->>Device: FILE_CHUNK(data)
|
||||||
|
Note over Device: schreibt Flash, zählt unacked_chunks
|
||||||
|
alt ACK_WATERMARK erreicht
|
||||||
|
Device-->>Host: ACK(additional credits)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Host->>Device: FILE_END(crc32)
|
||||||
|
alt CRC korrekt
|
||||||
|
Device-->>Host: SUCCESS(FILE_PUT|TAGS_PUT)
|
||||||
|
else CRC/Write Fehler
|
||||||
|
Device-->>Host: ERROR(EBADMSG/EIO/...)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. Payload-Frames im Detail
|
||||||
|
|
||||||
|
## 9.1 `LS_ENTRY` (`0x41`)
|
||||||
|
|
||||||
|
```c
|
||||||
|
uint8_t type; // 0x00 file, 0x01 dir
|
||||||
|
uint32_t size; // LE
|
||||||
|
uint8_t name_length;
|
||||||
|
char name[]; // ohne Nullterminierung
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9.2 `LS_END` (`0x42`)
|
||||||
|
|
||||||
|
```c
|
||||||
|
uint32_t total_entries; // LE
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9.3 `FILE_START` (`0x20`)
|
||||||
|
|
||||||
|
```c
|
||||||
|
uint32_t total_size; // LE
|
||||||
|
```
|
||||||
|
|
||||||
|
`FILE_GET`: komplette Dateigröße.
|
||||||
|
`TAGS_GET`: nur Tag-Teil der Datei.
|
||||||
|
|
||||||
|
## 9.4 `FILE_CHUNK` (`0x21`)
|
||||||
|
|
||||||
|
Payload sind rohe Nutzdatenbytes.
|
||||||
|
|
||||||
|
## 9.5 `FILE_END` (`0x22`)
|
||||||
|
|
||||||
|
```c
|
||||||
|
uint32_t crc32; // LE, IEEE CRC32
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. Beispiel-Frames (Hex)
|
||||||
|
|
||||||
|
## 10.1 `PROTO_INFO` Request/Response
|
||||||
|
|
||||||
Request:
|
Request:
|
||||||
```
|
|
||||||
|
```text
|
||||||
00 01 00 01
|
00 01 00 01
|
||||||
```
|
```
|
||||||
- `00`: `REQUEST`
|
|
||||||
- `01 00`: `payload_length = 1`
|
|
||||||
- `01`: `data_type = PROTO_INFO`
|
|
||||||
|
|
||||||
Response (Beispielwerte):
|
Beispiel-Response:
|
||||||
```
|
|
||||||
|
```text
|
||||||
10 05 00 01 01 00 FD 00
|
10 05 00 01 01 00 FD 00
|
||||||
```
|
```
|
||||||
- `10`: `RESPONSE`
|
|
||||||
- `05 00`: `payload_length = 5`
|
|
||||||
- `01`: `data_type = PROTO_INFO`
|
|
||||||
- `01 00`: `version = 1`
|
|
||||||
- `FD 00`: `max_chunk_size = 253`
|
|
||||||
|
|
||||||
### 9.2 Verzeichnisliste `/a` anfordern
|
Interpretation:
|
||||||
|
- `10`: RESPONSE
|
||||||
|
- `05 00`: Payload 5 Byte
|
||||||
|
- `01`: PROTO_INFO
|
||||||
|
- `01 00`: Version 1
|
||||||
|
- `FD 00`: max_chunk_size = 253
|
||||||
|
|
||||||
Request:
|
## 10.2 `LS` Request für `/a`
|
||||||
```
|
|
||||||
|
```text
|
||||||
00 03 00 40 2F 61
|
00 03 00 40 2F 61
|
||||||
```
|
```
|
||||||
- `00`: `REQUEST`
|
|
||||||
- `03 00`: `payload_length = 3`
|
|
||||||
- `40`: `data_type = LS`
|
|
||||||
- `2F 61`: Pfad `/a`
|
|
||||||
|
|
||||||
Antwort (Sequenz):
|
Interpretation:
|
||||||
```
|
- `00`: REQUEST
|
||||||
40 00 00 // LS_START, keine Payload
|
- `03 00`: Payload 3 Byte
|
||||||
// Host sendet ACK mit Credits
|
- `40`: data_type LS
|
||||||
11 02 00 40 00 // ACK, 64 Credits
|
- `2F 61`: `/a`
|
||||||
// Device sendet Eintraege
|
|
||||||
41 0A 00 00 00 00 00 00 06 73 6F 75 6E 64 31 // LS_ENTRY: FILE, size=0, name="sound1" (gekuerzt)
|
## 11. Implementierungsnotizen
|
||||||
// ... weitere Eintraege ...
|
|
||||||
42 04 00 01 00 00 00 // LS_END, total_entries = 1
|
- Unknown `REQUEST.data_type` wird aktuell mit `ERROR(EINVAL)` beantwortet.
|
||||||
```
|
- Unbekannte/unerwartete `frame_type` im aktiven Protokollthread führen zu `ERROR(EPROTO)`.
|
||||||
|
- Stream-Timeout in Firmware erzeugt aktiv `ERROR(ETIMEDOUT)`.
|
||||||
|
- Upload-Timeout im FS-Thread (2 s Inaktivität) bricht intern ab; die Host-Seite sollte eigene Watchdogs haben.
|
||||||
@@ -8,13 +8,8 @@
|
|||||||
import { refreshLocal } from "../lib/sync";
|
import { refreshLocal } from "../lib/sync";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
GearIcon,
|
|
||||||
CloudArrowUpIcon,
|
|
||||||
ArrowClockwiseIcon,
|
|
||||||
DotsThreeVerticalIcon,
|
|
||||||
CheckSquareOffsetIcon,
|
CheckSquareOffsetIcon,
|
||||||
SquareIcon,
|
SquareIcon,
|
||||||
DownloadIcon,
|
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
FingerprintIcon,
|
FingerprintIcon,
|
||||||
} from "phosphor-svelte";
|
} from "phosphor-svelte";
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ export const SETTINGS = {
|
|||||||
},
|
},
|
||||||
ui: {
|
ui: {
|
||||||
toastDurationMs: 5000,
|
toastDurationMs: 5000,
|
||||||
transferUpdateIntervalMs: 1000,
|
transferUpdateIntervalMs: 200,
|
||||||
kbpsCalculationWindowMs: 1000,
|
speedSmoothingSamples: 50, // Anzahl der Messwerte für den gleitenden ETA-Durchschnitt
|
||||||
transferOverlayPersistMs: 4000,
|
transferOverlayPersistMs: 4000,
|
||||||
|
estimatedInterFileGapMs: 700, // Initialer Schätzwert für die Pause zwischen zwei Dateien
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -123,6 +123,7 @@ export const storageUsage = derived(
|
|||||||
|
|
||||||
// Für die Anzeige der Transferdetails (Dateiname, Fortschritt, Geschwindigkeit, ETA)
|
// Für die Anzeige der Transferdetails (Dateiname, Fortschritt, Geschwindigkeit, ETA)
|
||||||
export const transferStats = writable({
|
export const transferStats = writable({
|
||||||
|
isActive: false,
|
||||||
currentFileName: '',
|
currentFileName: '',
|
||||||
pendingFileName: '',
|
pendingFileName: '',
|
||||||
bytesDone: 0,
|
bytesDone: 0,
|
||||||
@@ -130,11 +131,13 @@ export const transferStats = writable({
|
|||||||
overallDone: 0,
|
overallDone: 0,
|
||||||
overallTotal: 0,
|
overallTotal: 0,
|
||||||
bulkStartTime: 0,
|
bulkStartTime: 0,
|
||||||
fileStartTime: 0
|
fileStartTime: 0,
|
||||||
|
filesRemaining: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
export const resetTransferStats = () => {
|
export const resetTransferStats = () => {
|
||||||
transferStats.set({
|
transferStats.set({
|
||||||
|
isActive: false,
|
||||||
currentFileName: '',
|
currentFileName: '',
|
||||||
pendingFileName: '',
|
pendingFileName: '',
|
||||||
bytesDone: 0,
|
bytesDone: 0,
|
||||||
@@ -142,41 +145,111 @@ export const resetTransferStats = () => {
|
|||||||
overallDone: 0,
|
overallDone: 0,
|
||||||
overallTotal: 0,
|
overallTotal: 0,
|
||||||
bulkStartTime: 0,
|
bulkStartTime: 0,
|
||||||
fileStartTime: 0
|
fileStartTime: 0,
|
||||||
|
filesRemaining: 0
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
let speedHistory: { bytes: number, time: number }[] = [];
|
let speedSamples: number[] = [];
|
||||||
|
let lastSampleTime = 0;
|
||||||
|
let lastSampleBytes = 0;
|
||||||
|
let lastCalculatedSpeedKbs = 0;
|
||||||
|
|
||||||
|
let gapStartTime = 0;
|
||||||
|
let gapTimes: number[] = [];
|
||||||
|
let currentAverageGapMs = SETTINGS.ui.estimatedInterFileGapMs;
|
||||||
|
let wasActivelyTransferring = false;
|
||||||
|
|
||||||
export const transferDetails = derived(transferStats, ($s) => {
|
export const transferDetails = derived(transferStats, ($s) => {
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
|
|
||||||
|
// Nur nullen, wenn wirklich kein Transfer mehr im Store steht.
|
||||||
|
// Erlaubt das saubere Ausfaden des UI mit 100% Werten, auch wenn isActive schon false ist.
|
||||||
if ($s.overallTotal === 0) {
|
if ($s.overallTotal === 0) {
|
||||||
speedHistory = [];
|
speedSamples = [];
|
||||||
|
lastSampleTime = 0;
|
||||||
|
lastSampleBytes = 0;
|
||||||
|
lastCalculatedSpeedKbs = 0;
|
||||||
|
|
||||||
|
gapStartTime = 0;
|
||||||
|
gapTimes = [];
|
||||||
|
currentAverageGapMs = SETTINGS.ui.estimatedInterFileGapMs;
|
||||||
|
wasActivelyTransferring = false;
|
||||||
|
|
||||||
return { filePercent: 0, totalPercent: 0, speedKbs: 0, fileEta: Infinity, totalEta: Infinity };
|
return { filePercent: 0, totalPercent: 0, speedKbs: 0, fileEta: Infinity, totalEta: Infinity };
|
||||||
}
|
}
|
||||||
|
|
||||||
speedHistory.push({ bytes: $s.overallDone, time: now });
|
const isActivelyTransferring = $s.bytesDone > 0 && $s.bytesDone < $s.bytesTotal;
|
||||||
speedHistory = speedHistory.filter(p => now - p.time < SETTINGS.ui.kbpsCalculationWindowMs);
|
|
||||||
|
|
||||||
let speedKbs = 0;
|
// --- 1. Gap Calculation (Realer Overhead zwischen den Dateien) ---
|
||||||
if (speedHistory.length > 1) {
|
if (!isActivelyTransferring) {
|
||||||
const first = speedHistory[0];
|
if (wasActivelyTransferring || gapStartTime === 0) {
|
||||||
const last = speedHistory[speedHistory.length - 1];
|
gapStartTime = now;
|
||||||
const timeDiff = (last.time - first.time) / 1000;
|
}
|
||||||
const bytesDiff = last.bytes - first.bytes;
|
} else {
|
||||||
if (timeDiff > 0) speedKbs = (bytesDiff / 1024) / timeDiff;
|
if (!wasActivelyTransferring && gapStartTime > 0) {
|
||||||
|
const gapMs = now - gapStartTime;
|
||||||
|
gapStartTime = 0;
|
||||||
|
if (gapMs > 0 && gapMs < 10000) { // Plausibilitäts-Check
|
||||||
|
gapTimes.push(gapMs);
|
||||||
|
currentAverageGapMs = gapTimes.reduce((a, b) => a + b, 0) / gapTimes.length;
|
||||||
|
console.debug(`[Transfer] Inter-file overhead gap: ${gapMs.toFixed(1)}ms. Neues Bulk-Average: ${currentAverageGapMs.toFixed(1)}ms`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wasActivelyTransferring = isActivelyTransferring;
|
||||||
|
|
||||||
|
// --- 2. Speed Calculation (Gleitender Durchschnitt) ---
|
||||||
|
if (isActivelyTransferring) {
|
||||||
|
if (lastSampleTime > 0) {
|
||||||
|
const timeDiff = (now - lastSampleTime) / 1000;
|
||||||
|
const updateIntervalSecs = SETTINGS.ui.transferUpdateIntervalMs / 1000;
|
||||||
|
if (timeDiff >= updateIntervalSecs) { // Dynamisches Fenster gemäß Config
|
||||||
|
const bytesDiff = $s.overallDone - lastSampleBytes;
|
||||||
|
if (bytesDiff >= 0) {
|
||||||
|
const currentSpeedKbs = (bytesDiff / 1024) / timeDiff;
|
||||||
|
speedSamples.push(currentSpeedKbs);
|
||||||
|
if (speedSamples.length > SETTINGS.ui.speedSmoothingSamples) {
|
||||||
|
speedSamples.shift();
|
||||||
|
}
|
||||||
|
lastCalculatedSpeedKbs = speedSamples.reduce((a, b) => a + b, 0) / speedSamples.length;
|
||||||
|
}
|
||||||
|
lastSampleTime = now;
|
||||||
|
lastSampleBytes = $s.overallDone;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lastSampleTime = now;
|
||||||
|
lastSampleBytes = $s.overallDone;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lastSampleTime = 0; // Friert den Speed ein
|
||||||
}
|
}
|
||||||
|
|
||||||
const speedBytesPerSec = speedKbs * 1024;
|
const speedBytesPerSec = lastCalculatedSpeedKbs * 1024;
|
||||||
|
const estimatedGapSecs = ($s.filesRemaining * currentAverageGapMs) / 1000;
|
||||||
|
|
||||||
|
// ETA Aufrunden (Math.ceil), damit die letzte Sekunde immer als "1s" und nicht "0s" angezeigt wird.
|
||||||
|
// Harter Fallback auf 0, sobald die Datei/der Bulk physisch 100% erreicht hat.
|
||||||
|
let fileEta = Infinity;
|
||||||
|
if ($s.bytesTotal > 0 && $s.bytesDone >= $s.bytesTotal) {
|
||||||
|
fileEta = 0;
|
||||||
|
} else if (speedBytesPerSec > 100) {
|
||||||
|
fileEta = Math.ceil(($s.bytesTotal - $s.bytesDone) / speedBytesPerSec);
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalEta = Infinity;
|
||||||
|
if ($s.overallTotal > 0 && $s.overallDone >= $s.overallTotal) {
|
||||||
|
totalEta = 0;
|
||||||
|
} else if (speedBytesPerSec > 100) {
|
||||||
|
totalEta = Math.ceil((($s.overallTotal - $s.overallDone) / speedBytesPerSec) + estimatedGapSecs);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filePercent: Math.round(($s.bytesTotal > 0 ? $s.bytesDone / $s.bytesTotal : 0) * 100),
|
filePercent: Math.round(($s.bytesTotal > 0 ? $s.bytesDone / $s.bytesTotal : 0) * 100),
|
||||||
totalPercent: Math.round(($s.overallTotal > 0 ? $s.overallDone / $s.overallTotal : 0) * 100),
|
totalPercent: Math.round(($s.overallTotal > 0 ? $s.overallDone / $s.overallTotal : 0) * 100),
|
||||||
speedKbs: parseFloat(speedKbs.toFixed(2)),
|
speedKbs: parseFloat(lastCalculatedSpeedKbs.toFixed(2)),
|
||||||
// Wenn Speed zu gering, direkt Infinity für das ∞ Symbol
|
fileEta,
|
||||||
fileEta: speedBytesPerSec > 100 ? ($s.bytesTotal - $s.bytesDone) / speedBytesPerSec : Infinity,
|
totalEta
|
||||||
totalEta: speedBytesPerSec > 100 ? ($s.overallTotal - $s.overallDone) / speedBytesPerSec : Infinity
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -111,9 +111,11 @@ export async function downloadSelectedFiles() {
|
|||||||
|
|
||||||
transferStats.update(s => ({
|
transferStats.update(s => ({
|
||||||
...s,
|
...s,
|
||||||
|
isActive: true,
|
||||||
overallTotal: totalBytes,
|
overallTotal: totalBytes,
|
||||||
overallDone: 0,
|
overallDone: 0,
|
||||||
bulkStartTime: bulkStart
|
bulkStartTime: bulkStart,
|
||||||
|
filesRemaining: files.length
|
||||||
}));
|
}));
|
||||||
|
|
||||||
isTransferingRemote.set(true);
|
isTransferingRemote.set(true);
|
||||||
@@ -122,10 +124,17 @@ export async function downloadSelectedFiles() {
|
|||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
console.debug(`Starte Download von: ${file.name}`);
|
console.debug(`Starte Download von: ${file.name}`);
|
||||||
|
|
||||||
transferStats.update(s => ({ ...s, pendingFileName: file.name }));
|
// Setzt die Einzel-Balken hart auf 0 und bereitet UI perfekt auf neue Datei vor
|
||||||
|
transferStats.update(s => ({
|
||||||
|
...s,
|
||||||
|
pendingFileName: file.name,
|
||||||
|
currentFileName: file.name,
|
||||||
|
bytesTotal: file.size,
|
||||||
|
bytesDone: 0,
|
||||||
|
filesRemaining: s.filesRemaining > 0 ? s.filesRemaining - 1 : 0
|
||||||
|
}));
|
||||||
|
|
||||||
const fullPath = `${pathPrefix}/${file.name}`;
|
const fullPath = `${pathPrefix}/${file.name}`;
|
||||||
await new Promise(r => setTimeout(r, SETTINGS.ui.transferUpdateIntervalMs)); // Kurze Verzögerung für UI-Update
|
|
||||||
await getFile(fullPath);
|
await getFile(fullPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,6 +149,7 @@ export async function downloadSelectedFiles() {
|
|||||||
} finally {
|
} finally {
|
||||||
transferStats.update(s => ({
|
transferStats.update(s => ({
|
||||||
...s,
|
...s,
|
||||||
|
isActive: false, // UI Overlay verstecken
|
||||||
overallDone: s.overallTotal,
|
overallDone: s.overallTotal,
|
||||||
}));
|
}));
|
||||||
isTransferingRemote.set(false);
|
isTransferingRemote.set(false);
|
||||||
@@ -208,18 +218,28 @@ export async function uploadSelectedFiles() {
|
|||||||
|
|
||||||
transferStats.update(s => ({
|
transferStats.update(s => ({
|
||||||
...s,
|
...s,
|
||||||
|
isActive: true,
|
||||||
overallTotal: totalBytes,
|
overallTotal: totalBytes,
|
||||||
overallDone: 0,
|
overallDone: 0,
|
||||||
bulkStartTime: bulkStart
|
bulkStartTime: bulkStart,
|
||||||
|
filesRemaining: files.length
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Wir nutzen isFetchingRemote als generischen "Transfer aktiv"-Trigger für das UI TODO: Namensänderung in isTransferring?
|
|
||||||
isTransferingRemote.set(true);
|
isTransferingRemote.set(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
console.debug(`Starte Upload von: ${file.name} (${(file.size / 1024).toFixed(1)} kB)`);
|
console.debug(`Starte Upload von: ${file.name} (${(file.size / 1024).toFixed(1)} kB)`);
|
||||||
transferStats.update(s => ({ ...s, pendingFileName: file.name }));
|
|
||||||
|
// Resetted die Store-Stats VOR der DB-Abfrage, UI glättet sich sofort
|
||||||
|
transferStats.update(s => ({
|
||||||
|
...s,
|
||||||
|
pendingFileName: file.name,
|
||||||
|
currentFileName: file.name,
|
||||||
|
bytesTotal: file.size,
|
||||||
|
bytesDone: 0,
|
||||||
|
filesRemaining: s.filesRemaining > 0 ? s.filesRemaining - 1 : 0
|
||||||
|
}));
|
||||||
|
|
||||||
const dbRecord = await getLocalFile(file.name);
|
const dbRecord = await getLocalFile(file.name);
|
||||||
if (!dbRecord || !dbRecord.blob) {
|
if (!dbRecord || !dbRecord.blob) {
|
||||||
@@ -227,7 +247,6 @@ export async function uploadSelectedFiles() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fullPath = `${pathPrefix}/${file.name}`;
|
const fullPath = `${pathPrefix}/${file.name}`;
|
||||||
await new Promise(r => setTimeout(r, SETTINGS.ui.transferUpdateIntervalMs));
|
|
||||||
|
|
||||||
await putFile(dbRecord.blob, fullPath, file.name);
|
await putFile(dbRecord.blob, fullPath, file.name);
|
||||||
}
|
}
|
||||||
@@ -245,6 +264,7 @@ export async function uploadSelectedFiles() {
|
|||||||
} finally {
|
} finally {
|
||||||
transferStats.update(s => ({
|
transferStats.update(s => ({
|
||||||
...s,
|
...s,
|
||||||
|
isActive: false,
|
||||||
overallDone: s.overallTotal, // Schließt den Ladebalken visuell sauber ab
|
overallDone: s.overallTotal, // Schließt den Ladebalken visuell sauber ab
|
||||||
}));
|
}));
|
||||||
isTransferingRemote.set(false);
|
isTransferingRemote.set(false);
|
||||||
|
|||||||
@@ -210,9 +210,9 @@ export async function putFile(fileBlob: Blob, remotePath: string, fileNameForUI:
|
|||||||
uploadState.credits--;
|
uploadState.credits--;
|
||||||
offset += chunkLen;
|
offset += chunkLen;
|
||||||
|
|
||||||
// UI gedrosselt updaten (z.B. alle 100ms)
|
// UI gedrosselt updaten (gemäß Settings)
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
if (now - lastUiUpdate > 100) {
|
if (now - lastUiUpdate > SETTINGS.ui.transferUpdateIntervalMs) {
|
||||||
transferStats.update(s => ({
|
transferStats.update(s => ({
|
||||||
...s,
|
...s,
|
||||||
bytesDone: offset,
|
bytesDone: offset,
|
||||||
@@ -223,7 +223,11 @@ export async function putFile(fileBlob: Blob, remotePath: string, fileNameForUI:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Abschließendes UI Update
|
// Abschließendes UI Update
|
||||||
transferStats.update(s => ({ ...s, bytesDone: fileData.length }));
|
transferStats.update(s => ({
|
||||||
|
...s,
|
||||||
|
bytesDone: fileData.length,
|
||||||
|
overallDone: s.overallDone + (fileData.length - s.bytesDone)
|
||||||
|
}));
|
||||||
|
|
||||||
// END Frame senden
|
// END Frame senden
|
||||||
const endBuffer = new ArrayBuffer(3 + 4);
|
const endBuffer = new ArrayBuffer(3 + 4);
|
||||||
|
|||||||
Reference in New Issue
Block a user