diff --git a/firmware/CMakeLists.txt b/firmware/CMakeLists.txt index a7ddef2..9a8df2b 100644 --- a/firmware/CMakeLists.txt +++ b/firmware/CMakeLists.txt @@ -4,6 +4,8 @@ list(APPEND ZEPHYR_EXTRA_MODULES ${CMAKE_CURRENT_SOURCE_DIR}/libs) 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) diff --git a/firmware/VERSION b/firmware/VERSION index 23e4333..3bc071b 100644 --- a/firmware/VERSION +++ b/firmware/VERSION @@ -1,6 +1,6 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 -PATCHLEVEL = 1 +PATCHLEVEL = 2 VERSION_TWEAK = 0 #if (IS_ENABLED(CONFIG_LOG)) EXTRAVERSION = debug diff --git a/firmware/libs/fs_mgmt/src/fs_mgmt.c b/firmware/libs/fs_mgmt/src/fs_mgmt.c index f92cb64..1f98dee 100644 --- a/firmware/libs/fs_mgmt/src/fs_mgmt.c +++ b/firmware/libs/fs_mgmt/src/fs_mgmt.c @@ -514,8 +514,8 @@ static void fs_thread_entry(void *p1, void *p2, void *p3) LOG_WRN("Write timeout! Aborting transfer."); if (write_ctx.state == FS_STATE_RECEIVING_FILE) { - fs_mgmt_pm_close(&write_ctx.file); - fs_mgmt_pm_unlink(write_ctx.filename); + // fs_mgmt_pm_close(&write_ctx.file); + // fs_mgmt_pm_unlink(write_ctx.filename); } write_ctx.state = FS_STATE_IDLE; 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); write_ctx.filename[msg.data_len] = '\0'; - fs_mgmt_pm_unlink(write_ctx.filename); - rc = fs_mgmt_pm_open(&write_ctx.file, write_ctx.filename, FS_O_CREATE | FS_O_WRITE); + // fs_mgmt_pm_unlink(write_ctx.filename); + // rc = fs_mgmt_pm_open(&write_ctx.file, write_ctx.filename, FS_O_CREATE | FS_O_WRITE); 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'; /* 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) { - 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) { 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) */ - rc = fs_truncate(&write_ctx.file, audio_len); + // rc = fs_truncate(&write_ctx.file, audio_len); if (rc != 0) { 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); break; } /* 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.crc32 = 0; @@ -622,8 +622,8 @@ static void fs_thread_entry(void *p1, void *p2, void *p3) case FS_STATE_RECEIVING_FILE: 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) { 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) { - fs_mgmt_pm_close(&write_ctx.file); + // fs_mgmt_pm_close(&write_ctx.file); write_ctx.state = FS_STATE_IDLE; if (write_ctx.crc32 == msg.metadata) @@ -660,14 +660,14 @@ static void fs_thread_entry(void *p1, void *p2, void *p3) else { 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); } } else if (msg.op == FS_WRITE_OP_ABORT) { - fs_mgmt_pm_close(&write_ctx.file); - fs_mgmt_pm_unlink(write_ctx.filename); + // fs_mgmt_pm_close(&write_ctx.file); + // fs_mgmt_pm_unlink(write_ctx.filename); write_ctx.state = FS_STATE_IDLE; if (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: 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) { 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 { LOG_ERR("Flash write failed during tags transfer!"); - fs_truncate(&write_ctx.file, write_ctx.audio_len); /* Rollback */ - fs_mgmt_pm_close(&write_ctx.file); + // fs_truncate(&write_ctx.file, write_ctx.audio_len); /* Rollback */ + // fs_mgmt_pm_close(&write_ctx.file); write_ctx.state = FS_STATE_IDLE; 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 { 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_mgmt_pm_close(&write_ctx.file); + // fs_truncate(&write_ctx.file, write_ctx.audio_len); /* Rollback */ + // fs_mgmt_pm_close(&write_ctx.file); buzz_proto_send_error_reusing_slab(msg.reply_cb, EBADMSG, msg.slab_ptr); } write_ctx.state = FS_STATE_IDLE; } else if (msg.op == FS_WRITE_OP_ABORT) { - fs_truncate(&write_ctx.file, write_ctx.audio_len); /* Rollback */ - fs_mgmt_pm_close(&write_ctx.file); + // fs_truncate(&write_ctx.file, write_ctx.audio_len); /* Rollback */ + // fs_mgmt_pm_close(&write_ctx.file); write_ctx.state = FS_STATE_IDLE; if (msg.slab_ptr) buzz_proto_buf_free(&msg.slab_ptr); diff --git a/firmware/libs/fw_mgmt/CMakeLists.txt b/firmware/libs/fw_mgmt/CMakeLists.txt new file mode 100644 index 0000000..d4c5abb --- /dev/null +++ b/firmware/libs/fw_mgmt/CMakeLists.txt @@ -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() \ No newline at end of file diff --git a/firmware/libs/fw_mgmt/Kconfig b/firmware/libs/fw_mgmt/Kconfig new file mode 100644 index 0000000..5b656db --- /dev/null +++ b/firmware/libs/fw_mgmt/Kconfig @@ -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 \ No newline at end of file diff --git a/firmware/prj.conf b/firmware/prj.conf index c2b6b6e..e453327 100644 --- a/firmware/prj.conf +++ b/firmware/prj.conf @@ -1,6 +1,9 @@ ### Logging CONFIG_LOG=y +### Bootloader +CONFIG_BOOTLOADER_MCUBOOT=y + ### File System CONFIG_FS_MGMT=y # CONFIG_FS_MGMT_LOG_LEVEL_DBG=y diff --git a/firmware/sysbuild.conf b/firmware/sysbuild.conf new file mode 100644 index 0000000..721a76f --- /dev/null +++ b/firmware/sysbuild.conf @@ -0,0 +1 @@ +SB_CONFIG_BOOTLOADER_MCUBOOT=y \ No newline at end of file diff --git a/firmware/sysbuild/mcuboot.conf b/firmware/sysbuild/mcuboot.conf new file mode 100644 index 0000000..2e10fa4 --- /dev/null +++ b/firmware/sysbuild/mcuboot.conf @@ -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 diff --git a/firmware/sysbuild/mcuboot.overlay b/firmware/sysbuild/mcuboot.overlay new file mode 100644 index 0000000..972c222 --- /dev/null +++ b/firmware/sysbuild/mcuboot.overlay @@ -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"; + }; +}; \ No newline at end of file diff --git a/protocol.md b/protocol.md index 2beb39b..1548acf 100644 --- a/protocol.md +++ b/protocol.md @@ -1,365 +1,474 @@ # Buzzer Protocol (Wire Specification) -## 1. Zweck und Geltungsbereich -Das Buzzer Protocol definiert ein transportunabhaengiges, binaeres Frame-Format fuer die Kommunikation zwischen Host und Device. -Unterstuetzte Transporte sind aktuell BLE und USB CDC ACM/UART. +Stand: 2026-03-18 +Quelle: aktueller Implementierungsstand aus Firmware (`buzz_proto`, `fs_mgmt`, `ble_mgmt`) und Web-Client (`transport`, `parser`). -Das Protokoll spezifiziert: -- Frame-Struktur (Header + Payload) -- Frametypen -- Datentypen fuer Request/Response -- Semantik fuer Stream-Transfers (Verzeichnisliste, Datei, Firmware) +## 1. Ziel und Scope -## 2. Transport- und Codierungsregeln -- Alle ganzzahligen Felder werden in Little Endian uebertragen. -- Die im Header angegebene `payload_length` bezieht sich ausschliesslich auf die Nutzdaten ohne Header. -- Bei UART kann optional eine Synchronisationssequenz `BUZZ` (`0x42 0x55 0x5A 0x5A`) vor einem Frame verwendet werden, um Framing nach Leitungsstoerungen zu resynchronisieren. +Das Buzzer Protocol ist ein binäres Frame-Protokoll für Host <-> Device Kommunikation. + +Abgedeckte Funktionen: +- 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.1 Header (3 Byte) +### 3.1 Header + ```c -uint8_t frame_type -uint16_t payload_length // Little Endian +uint8_t frame_type; +uint16_t payload_length; // LE ``` -### 3.2 Gesamtframe -``` -+------------------+-------------------------+ -| Header (3 Byte) | Payload (optional) | -| frame_type (1B) | payload_length Byte | -| payload_len (2B) | | -+------------------+-------------------------+ + +### 3.2 Paketstruktur + +```mermaid +--- +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 -| Wert | Name | Richtung | Beschreibung | -|--------|------------|----------------|---------------------------------------| -| `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 | +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: -### 4.2 Datei-Transfer -| 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 | +`PROTO_INFO.max_chunk_size` wird dynamisch berechnet als: -### 4.3 Firmware-Transfer (reserviert, noch nicht implementiert) -| Wert | Name | -|--------|------------| -| `0x30` | `FW_START` | -| `0x31` | `FW_CHUNK` | -| `0x32` | `FW_END` | +`min(slab_size - 3, transport_max_payload - 3)` -### 4.4 Verzeichnisliste -| 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 | +Das Protokoll ist so ausgelegt, dass es mit mindestens 100 Bytes auskommen sollte. -## 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 -uint8_t data_type // Nutzt enum buzz_data_type -// optional: datentypspezifische Parameter +uint8_t data_type; +// optional daten_typspezifische Parameter ``` -Wire-Format: -``` -[0x00][payload_length LE][data_type][optional parameters] +Wire: + +```text +[0x00][payload_len LE][data_type][optional...] ``` -### 5.2 Response (`frame_type = 0x10`) -Payload-Mindestformat: -```c -uint8_t data_type // Echo des angefragten data_type -// danach: datentypspezifische Response-Daten +```mermaid +--- +title: "Generic Requst Structure" +--- +packet ++8: "Frame type REQUEST: 0x00" ++16: "Payload length (LE)" ++8: "Data type" ++32: "Optional payload (variable length)" ``` -Wire-Format: -``` -[0x10][payload_length LE][data_type][response payload] -``` +## 6.2 `PROTO_INFO` (`0x01`) -## 6. Datentypen (Request/Response) - -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 +Request: keine Zusatzdaten. Response-Payload: + ```c uint8_t data_type; // 0x01 -uint16_t version; // Protokollversion (LE) -uint16_t max_chunk_size; // max. Nutzdaten pro Frame ohne Header (LE) +uint16_t version; // 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`) -TBD +## 6.3 `FS_INFO` (`0x03`) -### 6.3 `FS_INFO` (`0x03`) -Request-Parameter: keine +Request: keine Zusatzdaten. Response-Payload: + ```c -uint8_t data_type; // 0x03 -uint32_t total_size; // Gesamtgroesse Flash in Bytes (LE) -uint32_t free_size; // Freier Speicher in Bytes (LE) -uint8_t max_path_length; // Maximal erlaubte Pfadlaenge -uint8_t sys_path_length; // Laenge des System-Pfades (ohne 0-Terminator) -uint8_t audio_path_length; // Laenge des Audio-Pfades (ohne 0-Terminator) -uint8_t data[]; // sys_path gefolgt von audio_path, nicht nullterminiert +uint8_t data_type; // 0x03 +uint32_t total_size; // LE +uint32_t free_size; // LE +uint8_t max_path_length; +uint8_t sys_path_length; +uint8_t audio_path_length; +uint8_t data[]; // sys_path + audio_path (beide nicht nullterminiert) ``` -### 6.4 `LS` (`0x40`) — Verzeichnisliste anfordern -Startet einen LS-Stream fuer den angegebenen Pfad. +Im `data` folgen sich System- und Audiopfad ohne Abstand, und ohne 0-Terminierung (`\0`). Beispiel für Systempfad `/lfs/sys`und Audiopfad `/lfs/a`: + +`/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: + ```c -uint8_t data_type; // 0x40 -char path[]; // Pfad ohne 0-Terminator, Laenge ergibt sich aus payload_length - 1 +uint8_t data_type; // 0x40 +char path[]; // ohne Nullterminierung ``` -Wire-Format (Beispiel fuer Pfad `/a`): -``` -[0x00][0x03 0x00][0x40][0x2F 0x61] -``` +## 6.5 `FILE_GET` (`0x20`) und `TAGS_GET` (`0x22`) -Das Device antwortet mit dem LS-Stream (siehe Abschnitt 8). - -### 6.5 `RM_FILE` (`0x24`) — Datei loeschen Request-Payload: + ```c -uint8_t data_type; // 0x24 -uint8_t path_length; // Laenge des Pfads -char path[]; // Pfad ohne 0-Terminator +uint8_t data_type; // 0x20 oder 0x22 +char path[]; // ohne Nullterminierung ``` -### 6.6 `RENAME_FILE` (`0x25`) — Datei umbenennen +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; // 0x25 -uint8_t old_path_length; // Laenge des alten Pfads -uint8_t new_path_length; // Laenge des neuen Pfads -char paths[]; // Alter Pfad, direkt gefolgt vom neuen Pfad (beide ohne 0-Terminator) +uint8_t data_type; // 0x21 oder 0x23 +uint32_t total_size; // LE +char path[]; // ohne Nullterminierung ``` -### 6.7 `FILE_PUT` (`0x21`) / `TAGS_PUT` (`0x23`) — Upload initiieren +Danach sendet der Host: +- `FILE_CHUNK` Frames +- abschließend `FILE_END` mit CRC32 + +## 6.7 `RM_FILE` (`0x24`) + 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 +uint8_t data_type; // 0x24 +uint8_t path_length; +char path[]; // ohne Nullterminierung ``` -## 7. ACK-, ERROR- und SUCCESS-Frames +## 6.8 `RENAME_FILE` (`0x25`) -### 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. +Request-Payload: -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) +uint8_t data_type; // 0x25 +uint8_t old_path_length; +uint8_t new_path_length; +char paths[]; // old_path + new_path (jeweils ohne Nullterminierung) ``` -Wire-Format (Beispiel: 64 Credits): -``` -[0x11][0x02 0x00][0x40 0x00] -``` +## 7. ACK / ERROR / SUCCESS -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. +## 7.1 ACK (`0x11`) Payload: + ```c -uint8_t type; // 0x00 = Datei, 0x01 = Verzeichnis (buzz_fs_entry_type) -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 +uint16_t credits; // LE ``` -`type`-Werte: -| Wert | Bedeutung | -|--------|---------------| -| `0x00` | Datei (FILE) | -| `0x01` | Verzeichnis (DIR) | +Wichtig: Es gibt zwei Semantiken je nach Richtung. -### 8.3 `LS_END` (`0x42`) — Device → Host -Signalisiert das Ende des Streams. +- Download (`LS`, `FILE_GET`, `TAGS_GET`): + 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: + ```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 -- 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). -- Der Host sollte einen eigenen Watchdog implementieren; empfohlener Timeout: 3 s ohne empfangenen Frame. +| Code | Name | Bedeutung | +|---|---|---| +| `1` | `EPERM` | fehlende Rechte | +| `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: -``` + +```text 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`: `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`: `REQUEST` -- `03 00`: `payload_length = 3` -- `40`: `data_type = LS` -- `2F 61`: Pfad `/a` -Antwort (Sequenz): -``` -40 00 00 // LS_START, keine Payload -// Host sendet ACK mit Credits -11 02 00 40 00 // ACK, 64 Credits -// 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) -// ... weitere Eintraege ... -42 04 00 01 00 00 00 // LS_END, total_entries = 1 -``` \ No newline at end of file +Interpretation: +- `00`: REQUEST +- `03 00`: Payload 3 Byte +- `40`: data_type LS +- `2F 61`: `/a` + +## 11. Implementierungsnotizen + +- 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. \ No newline at end of file diff --git a/webpage/src/components/FileMenuOverlay.svelte b/webpage/src/components/FileMenuOverlay.svelte index 68f43cd..046e2de 100644 --- a/webpage/src/components/FileMenuOverlay.svelte +++ b/webpage/src/components/FileMenuOverlay.svelte @@ -8,13 +8,8 @@ import { refreshLocal } from "../lib/sync"; import { - GearIcon, - CloudArrowUpIcon, - ArrowClockwiseIcon, - DotsThreeVerticalIcon, CheckSquareOffsetIcon, SquareIcon, - DownloadIcon, TrashIcon, FingerprintIcon, } from "phosphor-svelte"; diff --git a/webpage/src/lib/settings.ts b/webpage/src/lib/settings.ts index 5dfa63a..45bd681 100644 --- a/webpage/src/lib/settings.ts +++ b/webpage/src/lib/settings.ts @@ -8,8 +8,9 @@ export const SETTINGS = { }, ui: { toastDurationMs: 5000, - transferUpdateIntervalMs: 1000, - kbpsCalculationWindowMs: 1000, + transferUpdateIntervalMs: 200, + speedSmoothingSamples: 50, // Anzahl der Messwerte für den gleitenden ETA-Durchschnitt transferOverlayPersistMs: 4000, + estimatedInterFileGapMs: 700, // Initialer Schätzwert für die Pause zwischen zwei Dateien }, }; \ No newline at end of file diff --git a/webpage/src/lib/store.ts b/webpage/src/lib/store.ts index d06aa97..6fc86c1 100644 --- a/webpage/src/lib/store.ts +++ b/webpage/src/lib/store.ts @@ -123,6 +123,7 @@ export const storageUsage = derived( // Für die Anzeige der Transferdetails (Dateiname, Fortschritt, Geschwindigkeit, ETA) export const transferStats = writable({ + isActive: false, currentFileName: '', pendingFileName: '', bytesDone: 0, @@ -130,11 +131,13 @@ export const transferStats = writable({ overallDone: 0, overallTotal: 0, bulkStartTime: 0, - fileStartTime: 0 + fileStartTime: 0, + filesRemaining: 0 }); export const resetTransferStats = () => { transferStats.set({ + isActive: false, currentFileName: '', pendingFileName: '', bytesDone: 0, @@ -142,41 +145,111 @@ export const resetTransferStats = () => { overallDone: 0, overallTotal: 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) => { 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) { - 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 }; } - speedHistory.push({ bytes: $s.overallDone, time: now }); - speedHistory = speedHistory.filter(p => now - p.time < SETTINGS.ui.kbpsCalculationWindowMs); + const isActivelyTransferring = $s.bytesDone > 0 && $s.bytesDone < $s.bytesTotal; - let speedKbs = 0; - if (speedHistory.length > 1) { - const first = speedHistory[0]; - const last = speedHistory[speedHistory.length - 1]; - const timeDiff = (last.time - first.time) / 1000; - const bytesDiff = last.bytes - first.bytes; - if (timeDiff > 0) speedKbs = (bytesDiff / 1024) / timeDiff; + // --- 1. Gap Calculation (Realer Overhead zwischen den Dateien) --- + if (!isActivelyTransferring) { + if (wasActivelyTransferring || gapStartTime === 0) { + gapStartTime = now; + } + } else { + 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 { filePercent: Math.round(($s.bytesTotal > 0 ? $s.bytesDone / $s.bytesTotal : 0) * 100), totalPercent: Math.round(($s.overallTotal > 0 ? $s.overallDone / $s.overallTotal : 0) * 100), - speedKbs: parseFloat(speedKbs.toFixed(2)), - // Wenn Speed zu gering, direkt Infinity für das ∞ Symbol - fileEta: speedBytesPerSec > 100 ? ($s.bytesTotal - $s.bytesDone) / speedBytesPerSec : Infinity, - totalEta: speedBytesPerSec > 100 ? ($s.overallTotal - $s.overallDone) / speedBytesPerSec : Infinity + speedKbs: parseFloat(lastCalculatedSpeedKbs.toFixed(2)), + fileEta, + totalEta }; }); diff --git a/webpage/src/lib/sync.ts b/webpage/src/lib/sync.ts index fe8ea04..fe8420b 100644 --- a/webpage/src/lib/sync.ts +++ b/webpage/src/lib/sync.ts @@ -111,9 +111,11 @@ export async function downloadSelectedFiles() { transferStats.update(s => ({ ...s, + isActive: true, overallTotal: totalBytes, overallDone: 0, - bulkStartTime: bulkStart + bulkStartTime: bulkStart, + filesRemaining: files.length })); isTransferingRemote.set(true); @@ -122,10 +124,17 @@ export async function downloadSelectedFiles() { for (const file of files) { 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}`; - await new Promise(r => setTimeout(r, SETTINGS.ui.transferUpdateIntervalMs)); // Kurze Verzögerung für UI-Update await getFile(fullPath); } @@ -140,6 +149,7 @@ export async function downloadSelectedFiles() { } finally { transferStats.update(s => ({ ...s, + isActive: false, // UI Overlay verstecken overallDone: s.overallTotal, })); isTransferingRemote.set(false); @@ -208,18 +218,28 @@ export async function uploadSelectedFiles() { transferStats.update(s => ({ ...s, + isActive: true, overallTotal: totalBytes, 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); try { for (const file of files) { 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); if (!dbRecord || !dbRecord.blob) { @@ -227,7 +247,6 @@ export async function uploadSelectedFiles() { } const fullPath = `${pathPrefix}/${file.name}`; - await new Promise(r => setTimeout(r, SETTINGS.ui.transferUpdateIntervalMs)); await putFile(dbRecord.blob, fullPath, file.name); } @@ -245,6 +264,7 @@ export async function uploadSelectedFiles() { } finally { transferStats.update(s => ({ ...s, + isActive: false, overallDone: s.overallTotal, // Schließt den Ladebalken visuell sauber ab })); isTransferingRemote.set(false); diff --git a/webpage/src/lib/transport.ts b/webpage/src/lib/transport.ts index 05aa7d9..23e1bec 100644 --- a/webpage/src/lib/transport.ts +++ b/webpage/src/lib/transport.ts @@ -210,9 +210,9 @@ export async function putFile(fileBlob: Blob, remotePath: string, fileNameForUI: uploadState.credits--; offset += chunkLen; - // UI gedrosselt updaten (z.B. alle 100ms) + // UI gedrosselt updaten (gemäß Settings) const now = performance.now(); - if (now - lastUiUpdate > 100) { + if (now - lastUiUpdate > SETTINGS.ui.transferUpdateIntervalMs) { transferStats.update(s => ({ ...s, bytesDone: offset, @@ -223,7 +223,11 @@ export async function putFile(fileBlob: Blob, remotePath: string, fileNameForUI: } // 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 const endBuffer = new ArrayBuffer(3 + 4);