diff --git a/.vscode/settings.json b/.vscode/settings.json index 08d9527..61edc8e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,6 @@ { - "svelte.plugin.svelte.format.config.printWidth": 300 + "svelte.plugin.svelte.format.config.printWidth": 300, + "nrf-connect.applications": [ + "${workspaceFolder}/firmware" + ] } \ No newline at end of file diff --git a/firmware/.gitignore b/firmware/.gitignore new file mode 100644 index 0000000..a5309e6 --- /dev/null +++ b/firmware/.gitignore @@ -0,0 +1 @@ +build*/ diff --git a/firmware/CMakeLists.txt b/firmware/CMakeLists.txt new file mode 100644 index 0000000..a7ddef2 --- /dev/null +++ b/firmware/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.20.0) + +list(APPEND ZEPHYR_EXTRA_MODULES ${CMAKE_CURRENT_SOURCE_DIR}/libs) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) + +project(firmware) + +target_sources(app PRIVATE src/main.c) diff --git a/firmware/boards/nrf52840dk_nrf52840.overlay b/firmware/boards/nrf52840dk_nrf52840.overlay new file mode 100644 index 0000000..79d2c24 --- /dev/null +++ b/firmware/boards/nrf52840dk_nrf52840.overlay @@ -0,0 +1,8 @@ +/{ + chosen { + nordic,pm-ext-flash = &mx25r64; + }; + aliases { + qspi-flash = &mx25r64; + }; +}; diff --git a/firmware/libs/CMakeLists.txt b/firmware/libs/CMakeLists.txt new file mode 100644 index 0000000..0ffea08 --- /dev/null +++ b/firmware/libs/CMakeLists.txt @@ -0,0 +1,3 @@ +add_subdirectory(fs_mgmt) +add_subdirectory(ble_mgmt) +add_subdirectory(buzz_proto) \ No newline at end of file diff --git a/firmware/libs/Kconfig b/firmware/libs/Kconfig new file mode 100644 index 0000000..b0f20d6 --- /dev/null +++ b/firmware/libs/Kconfig @@ -0,0 +1,3 @@ +rsource "fs_mgmt/Kconfig" +rsource "ble_mgmt/Kconfig" +rsource "buzz_proto/Kconfig" \ No newline at end of file diff --git a/firmware/libs/ble_mgmt/CMakeLists.txt b/firmware/libs/ble_mgmt/CMakeLists.txt new file mode 100644 index 0000000..fa095c1 --- /dev/null +++ b/firmware/libs/ble_mgmt/CMakeLists.txt @@ -0,0 +1,5 @@ +if(CONFIG_BLE_MGMT) + zephyr_library() + zephyr_library_sources(src/ble_mgmt.c) + zephyr_include_directories(include) +endif() \ No newline at end of file diff --git a/firmware/libs/ble_mgmt/Kconfig b/firmware/libs/ble_mgmt/Kconfig new file mode 100644 index 0000000..133d919 --- /dev/null +++ b/firmware/libs/ble_mgmt/Kconfig @@ -0,0 +1,62 @@ +menuconfig BLE_MGMT + bool "Bluetooth Management" + select BT + select BT_PERIPHERAL + select BT_LOG_LEVEL_WARN + select BT_DEVICE_NAME_DYNAMIC + help + Library for initializing and managing Bluetooth functionality. + +if BLE_MGMT + config BLE_MGMT_DEFAULT_DEVICE_NAME + string "Default Bluetooth Device Name" + default "Edis Buzzer" + config BLE_MGMT_ADV_INT_MIN + int "Minimum Advertising Interval (in 0.625 ms units)" + default 160 + help + Minimal advertising interval. 160 equals to 100ms. + config BLE_MGMT_ADV_INT_MAX + int "Maximum Advertising Interval (ms)" + default 160 + help + Maximal advertising interval. 160 equals to 100ms. + +# 1. MTU und Data Length (Maximale Paketgrößen) + config BT_L2CAP_TX_MTU + default 247 + config BT_BUF_ACL_RX_SIZE + default 251 + config BT_BUF_ACL_TX_SIZE + default 251 + config BT_CTLR_DATA_LENGTH_MAX + default 251 + config BT_USER_DATA_LEN_UPDATE + default y + + # 2. Physical Layer (Erlaubt 2M PHY) + config BT_USER_PHY_UPDATE + default y + + # 3. Flow-Control und Queues (High Throughput, Host + SDC Controller synchronisiert) + config BT_HCI_ACL_FLOW_CONTROL + default y + config BT_BUF_EVT_RX_COUNT + default 22 + config BT_BUF_ACL_TX_COUNT + default 20 + config BT_L2CAP_TX_BUF_COUNT + default 20 + config BT_CONN_TX_MAX + default 20 + + # 4. SDC Controller Buffering (an Host-Tiefen angeglichen) + config BT_CTLR_SDC_TX_PACKET_COUNT + default 20 + config BT_CTLR_SDC_RX_PACKET_COUNT + default 20 + + module = BLE_MGMT + module-str = ble_mgmt + source "subsys/logging/Kconfig.template.log_config" +endif # BLE_MGMT \ No newline at end of file diff --git a/firmware/libs/ble_mgmt/include/ble_mgmt.h b/firmware/libs/ble_mgmt/include/ble_mgmt.h new file mode 100644 index 0000000..55f8572 --- /dev/null +++ b/firmware/libs/ble_mgmt/include/ble_mgmt.h @@ -0,0 +1,38 @@ +#ifndef BLE_MGMT_H +#define BLE_MGMT_H + +#include + +typedef void (*ble_mgmt_rx_cb_t)(const uint8_t *data, uint16_t len); + +/** + * Initializes the BLE management module, sets up the GATT service and starts advertising. + * @param rx_cb Callback function to handle received data from the central device. + * @param device_name Optional custom device name for advertising. If NULL, a default name is used. + * @return 0 on success, or a negative error code on failure. + */ +int ble_mgmt_init(ble_mgmt_rx_cb_t rx_cb, const char *device_name); + +/** + * Sends data to the connected central device via a GATT characteristic. + * @param data Pointer to the data buffer to send. + * @param len Length of the data in bytes. + * @return 0 on success, -EACCES if notifications are not enabled, or a negative error code on failure. + */ +int ble_mgmt_send(const uint8_t *data, uint16_t len); + +/** + * Updates the advertised device name and restarts advertising with the new name. + * @param new_name The new device name to advertise. + * @return 0 on success, or a negative error code on failure. + */ +int ble_mgmt_update_adv_name(const char *new_name); + +/** + * Retrieves the maximum payload size that can be sent in a single notification. + * This is determined by the current ATT MTU size minus the GATT header overhead. + * @return The maximum payload size in bytes. + */ +uint16_t ble_mgmt_get_max_payload(void); + +#endif // BLE_MGMT_H \ No newline at end of file diff --git a/firmware/libs/ble_mgmt/src/ble_mgmt.c b/firmware/libs/ble_mgmt/src/ble_mgmt.c new file mode 100644 index 0000000..bad33c1 --- /dev/null +++ b/firmware/libs/ble_mgmt/src/ble_mgmt.c @@ -0,0 +1,258 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ble_mgmt.h" + +LOG_MODULE_REGISTER(ble_mgmt, CONFIG_BLE_MGMT_LOG_LEVEL); + +#define BUZZ_SERVICE_UUID_VAL \ + BT_UUID_128_ENCODE(0xe517d988, 0xbab5, 0x4574, 0x8479, 0x97c6cb115ca0) +#define BUZZ_RX_UUID_VAL \ + BT_UUID_128_ENCODE(0xe517d988, 0xbab5, 0x4574, 0x8479, 0x97c6cb115ca1) +#define BUZZ_TX_UUID_VAL \ + BT_UUID_128_ENCODE(0xe517d988, 0xbab5, 0x4574, 0x8479, 0x97c6cb115ca2) + +static struct bt_uuid_128 buzz_service_uuid = BT_UUID_INIT_128(BUZZ_SERVICE_UUID_VAL); +static struct bt_uuid_128 buzz_rx_uuid = BT_UUID_INIT_128(BUZZ_RX_UUID_VAL); +static struct bt_uuid_128 buzz_tx_uuid = BT_UUID_INIT_128(BUZZ_TX_UUID_VAL); + +static ble_mgmt_rx_cb_t app_rx_cb = NULL; +static bool notify_enabled = false; +static uint16_t current_tx_mtu = 23; + +#define MAX_ADV_NAME_LEN 29 +static char current_device_name[MAX_ADV_NAME_LEN + 1]; + +static const struct bt_data ad[] = { + BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)), + BT_DATA_BYTES(BT_DATA_UUID128_ALL, BUZZ_SERVICE_UUID_VAL), +}; + +static struct bt_data sd[] = { + BT_DATA(BT_DATA_NAME_COMPLETE, current_device_name, 0), +}; + +static struct bt_le_adv_param adv_param = { + .id = BT_ID_DEFAULT, + .sid = 0, + .secondary_max_skip = 0, + .options = BT_LE_ADV_OPT_CONN, + .interval_min = CONFIG_BLE_MGMT_ADV_INT_MIN, + .interval_max = CONFIG_BLE_MGMT_ADV_INT_MAX, + .peer = NULL, +}; + +static void att_mtu_updated(struct bt_conn *conn, uint16_t tx, uint16_t rx) +{ + LOG_INF("MTU exchanged: TX %u bytes, RX %u bytes", tx, rx); + current_tx_mtu = tx; +} + +static ssize_t rx_cb(struct bt_conn *conn, const struct bt_gatt_attr *attr, + const void *buf, uint16_t len, uint16_t offset, uint8_t flags) +{ + LOG_DBG("Received %u bytes", len); + LOG_HEXDUMP_DBG(buf, len, "Data:"); + + if (app_rx_cb) { + app_rx_cb((const uint8_t *)buf, len); + } + return len; +} + +static void tx_ccc_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value) +{ + notify_enabled = (value == BT_GATT_CCC_NOTIFY); + LOG_DBG("Notifications %s", notify_enabled ? "enabled" : "disabled"); +} + +BT_GATT_SERVICE_DEFINE(ble_mgmt_svc, + BT_GATT_PRIMARY_SERVICE(&buzz_service_uuid), + BT_GATT_CHARACTERISTIC(&buzz_rx_uuid.uuid, BT_GATT_CHRC_WRITE_WITHOUT_RESP, + BT_GATT_PERM_WRITE, NULL, rx_cb, NULL), + BT_GATT_CHARACTERISTIC(&buzz_tx_uuid.uuid, BT_GATT_CHRC_NOTIFY, + BT_GATT_PERM_NONE, NULL, NULL, NULL), + BT_GATT_CCC(tx_ccc_cfg_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE) +); + +uint16_t ble_mgmt_get_max_payload(void) +{ + /* Kappe die verhandelte MTU auf die hart konfigurierte Zephyr-Puffergrenze */ + uint16_t effective_mtu = current_tx_mtu; + +#ifdef CONFIG_BT_L2CAP_TX_MTU + if (effective_mtu > CONFIG_BT_L2CAP_TX_MTU) { + effective_mtu = CONFIG_BT_L2CAP_TX_MTU; + } +#endif + + /* 3 Bytes abziehen für den GATT Notification Overhead */ + return (effective_mtu > 3) ? (effective_mtu - 3) : 20; +} + +int ble_mgmt_send(const uint8_t *data, uint16_t len) +{ + if (!notify_enabled) { + return -EACCES; + } + int rc; + + do { + rc = bt_gatt_notify(NULL, &ble_mgmt_svc.attrs[4], data, len); + if (rc == -ENOMEM) { + k_sleep(K_MSEC(5)); // Thread pausieren, bis TX-Buffer frei wird + } + } while (rc == -ENOMEM); + + if (rc) { + LOG_ERR("Failed to send notification (err %d)", rc); + return rc; + } + return rc; +} + +/* Interne Hilfsfunktion zur Zuweisung des Namens */ +static void set_device_name(const char *name) +{ + if (!name) { + return; + } + + strncpy(current_device_name, name, MAX_ADV_NAME_LEN); + current_device_name[MAX_ADV_NAME_LEN] = '\0'; + + /* Längen-Update im Scan-Response Array */ + sd[0].data_len = strlen(current_device_name); + +#ifdef CONFIG_BT_DEVICE_NAME_DYNAMIC + /* Setzt den Namen parallel im Zephyr GAP-Service (wichtig für macOS) */ + bt_set_name(current_device_name); +#endif +} + +int ble_mgmt_update_adv_name(const char *new_name) +{ + int rc; + + bt_le_adv_stop(); + set_device_name(new_name); + + rc = bt_le_adv_start(&adv_param, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd)); + if (rc) { + LOG_ERR("Advertising failed to restart after name update (err %d)", rc); + return rc; + } + + LOG_INF("Advertising updated. New Name: %s", current_device_name); + return 0; +} + +int ble_mgmt_init(ble_mgmt_rx_cb_t rx_cb, const char *device_name) +{ + int rc; + + app_rx_cb = rx_cb; + + static struct bt_gatt_cb gatt_callbacks = { + .att_mtu_updated = att_mtu_updated, + }; + + bt_gatt_cb_register(&gatt_callbacks); + + rc = bt_enable(NULL); + if (rc) { + LOG_ERR("Bluetooth init failed (err %d)", rc); + return rc; + } + + const char *name_to_use = (device_name != NULL) ? device_name : CONFIG_BLE_MGMT_DEFAULT_DEVICE_NAME; + set_device_name(name_to_use); + + rc = bt_le_adv_start(&adv_param, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd)); + if (rc) { + LOG_ERR("Advertising failed to start (err %d)", rc); + return rc; + } + + LOG_INF("Bluetooth initialized. Adv-Name: %s", current_device_name); + return 0; +} + +static void connected(struct bt_conn *conn, uint8_t err) +{ + if (err) { + LOG_ERR("Connection failed (err 0x%02x)", err); + return; + } + + char addr_str[BT_ADDR_LE_STR_LEN]; + struct bt_conn_info info; + + int rc = bt_conn_get_info(conn, &info); + if (rc == 0) { + bt_addr_le_to_str(info.le.dst, addr_str, sizeof(addr_str)); + LOG_INF("Connected to %s", addr_str); + + /* Nur noch die Rolle ausgeben, da Timing-Parameter hier deprecated sind */ + LOG_DBG("Role: %s", info.role == BT_CONN_ROLE_CENTRAL ? "Central" : "Peripheral"); + } else { + LOG_INF("Connected (info retrieval failed)"); + } + struct bt_conn_le_phy_param phy_param = { + .options = BT_CONN_LE_PHY_OPT_NONE, + .pref_tx_phy = BT_GAP_LE_PHY_2M, + .pref_rx_phy = BT_GAP_LE_PHY_2M, + }; + rc = bt_conn_le_phy_update(conn, &phy_param); + if (rc) { + LOG_WRN("PHY update failed (err %d)", rc); + } + struct bt_le_conn_param *param = BT_LE_CONN_PARAM(12, 24, 0, 400); + rc = bt_conn_le_param_update(conn, param); + if (rc) { + LOG_WRN("Connection update failed (err %d)", rc); + } +} + +static void disconnected(struct bt_conn *conn, uint8_t reason) +{ + LOG_DBG("Disconnected (reason 0x%02x)", reason); + + /* Startet Advertising mit dem global definierten Setup neu */ + int rc = bt_le_adv_start(&adv_param, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd)); + if (rc) { + LOG_ERR("Advertising failed to restart (err %d)", rc); + } else { + LOG_DBG("Advertising successfully restarted"); + } +} + +static void le_phy_updated(struct bt_conn *conn, struct bt_conn_le_phy_info *param) +{ + const char *tx_phy_str = (param->tx_phy == BT_GAP_LE_PHY_2M) ? "2M" : + (param->tx_phy == BT_GAP_LE_PHY_1M) ? "1M" : "Coded/Unknown"; + const char *rx_phy_str = (param->rx_phy == BT_GAP_LE_PHY_2M) ? "2M" : + (param->rx_phy == BT_GAP_LE_PHY_1M) ? "1M" : "Coded/Unknown"; + + LOG_INF("LE PHY updated: TX PHY %s, RX PHY %s", tx_phy_str, rx_phy_str); +} + +static void le_param_updated(struct bt_conn *conn, uint16_t interval, + uint16_t latency, uint16_t timeout) +{ + LOG_INF("Connection parameters updated: Interval: %u, Latency: %u, Timeout: %u", + interval, latency, timeout); +} + +BT_CONN_CB_DEFINE(conn_callbacks) = { + .connected = connected, + .disconnected = disconnected, + .le_param_updated = le_param_updated, + .le_phy_updated = le_phy_updated, +}; diff --git a/firmware/libs/buzz_proto/CMakeLists.txt b/firmware/libs/buzz_proto/CMakeLists.txt new file mode 100644 index 0000000..c93f88f --- /dev/null +++ b/firmware/libs/buzz_proto/CMakeLists.txt @@ -0,0 +1,5 @@ +if(CONFIG_BUZZ_PROTO) + zephyr_library() + zephyr_library_sources(src/buzz_proto.c) + zephyr_include_directories(include) +endif() \ No newline at end of file diff --git a/firmware/libs/buzz_proto/Kconfig b/firmware/libs/buzz_proto/Kconfig new file mode 100644 index 0000000..7d8e38b --- /dev/null +++ b/firmware/libs/buzz_proto/Kconfig @@ -0,0 +1,41 @@ +menuconfig BUZZ_PROTO + bool "Buzzer Protocol" + select CRC + help + Library for initializing and managing the buzzer protocol. + + config BUZZ_PROTO_SLAB_SIZE + int "Slab Size" + default 256 + help + Size of the memory slabs used for message buffers. Must be large enough to hold the largest expected message. + + config BUZZ_PROTO_SLAB_COUNT + int "Slab Count" + default 64 + help + Number of memory slabs to allocate for message buffers. More slabs allow for more concurrent messages but use more RAM. + + config BUZZ_PROTO_MSGQ_SIZE + int "Message Queue Size" + default 16 + help + Number of messages that can be queued for processing. Adjust based on expected message burstiness. + + config BUZZ_PROT_THREAD_STACK_SIZE + int "Thread Stack Size" + default 2048 + help + Stack size for the buzzer protocol thread. Adjust based on the expected workload and function call depth. + + config BUZZ_PROTO_THREAD_PRIORITY + int "Thread Priority" + default 7 + help + Priority for the buzzer protocol thread. Lower numbers indicate higher priority. + +if BUZZ_PROTO + module = BUZZ_PROTO + module-str = buzz_proto + source "subsys/logging/Kconfig.template.log_config" +endif # BUZZ_PROTO \ No newline at end of file diff --git a/firmware/libs/buzz_proto/include/buzz_proto.h b/firmware/libs/buzz_proto/include/buzz_proto.h new file mode 100644 index 0000000..47e8b3f --- /dev/null +++ b/firmware/libs/buzz_proto/include/buzz_proto.h @@ -0,0 +1,145 @@ +#ifndef BUZZ_PROTO_H +#define BUZZ_PROTO_H + +#include +#include +#include + +#define BUZZ_PROTO_VERSION 1 + +/* --- Enums für Protokoll-Typen --- */ +enum buzz_frame_type +{ + BUZZ_FRAME_REQUEST = 0x00, + + BUZZ_FRAME_RESPONSE = 0x10, + BUZZ_FRAME_ACK = 0x11, + BUZZ_FRAME_ERROR = 0x12, + + BUZZ_FRAME_FILE_START = 0x20, + BUZZ_FRAME_FILE_CHUNK = 0x21, + BUZZ_FRAME_FILE_END = 0x22, + + BUZZ_FRAME_FW_START = 0x30, + BUZZ_FRAME_FW_CHUNK = 0x31, + BUZZ_FRAME_FW_END = 0x32, + + BUZZ_FRAME_LS_START = 0x40, + BUZZ_FRAME_LS_ENTRY = 0x41, + BUZZ_FRAME_LS_END = 0x42, +}; + +enum buzz_data_type +{ + BUZZ_DATA_PROTO_INFO = 0x01, + BUZZ_DATA_DEVICE_INFO = 0x02, + BUZZ_DATA_FS_INFO = 0x03, + + BUZZ_DATA_FILE_GET = 0x20, + BUZZ_DATA_FILE_PUT = 0x21, + + BUZZ_DATA_LS = 0x40, +}; + +enum buzz_fs_entry_type +{ + BUZZ_FS_ENTRY_FILE = 0x00, + BUZZ_FS_ENTRY_DIR = 0x01, +}; + +/* --- Wire Protocol Structs (Packed) --- */ + +/* Generischer Header für alle Frames */ +struct __attribute__((packed)) buzz_proto_header +{ + uint8_t frame_type; /* Nutzt enum buzz_frame_type */ + uint16_t payload_length; /* Länge der folgenden Daten (Little Endian) */ +}; + +/* Payload für einen Error-Frame */ +struct __attribute__((packed)) buzz_resp_error +{ + uint16_t error_code; /* Bis 0xFF reserviert für Standard-Fehler, 0x100+ für spezifische Fehler */ +}; + +/* Payload für eine Standard-Anfrage (Request) */ +struct __attribute__((packed)) buzz_request_payload +{ + uint8_t data_type; /* Nutzt enum buzz_data_type */ +}; + +/* Payload für die Protokollversions-Antwort */ +struct __attribute__((packed)) buzz_resp_proto_version +{ + uint8_t data_type; /* BUZZ_DATA_PROTO_INFO */ + uint16_t version; /* Little Endian */ + uint16_t max_chunk_size; /* Little Endian */ +}; + +/* Payload für die Dateisystem-Informationen */ +struct __attribute__((packed)) buzz_resp_fs_info +{ + uint8_t data_type; /* BUZZ_DATA_FS_INFO */ + uint32_t total_size; /* Little Endian */ + uint32_t free_size; /* Little Endian */ + uint8_t max_path_length; /* Maximale Pfadlänge (z.B. 32) */ + uint8_t sys_path_length; /* Länge des System-Ordners (z.B. 2 für "/s") */ + uint8_t audio_path_length; /* Länge des Audio-Ordners (z.B. 2 für "/a") */ + uint8_t data[]; /* Pfadnamen */ +}; + +/* Payload für das Credit-System (ACK) */ +struct __attribute__((packed)) buzz_ack_payload +{ + uint16_t credits; /* Little Endian */ +}; + +/* Payload für einen einzelnen Verzeichniseintrag */ +struct __attribute__((packed)) buzz_ls_entry_payload +{ + uint8_t type; /* enum buzz_fs_entry_type */ + uint32_t size; /* Little Endian */ + uint8_t name_length; + char name[]; /* Variabler String ohne Null-Terminierung */ +}; + +/* Payload für das Ende der Liste */ +struct __attribute__((packed)) buzz_ls_end_payload +{ + uint32_t total_entries; /* Little Endian */ +}; + +/* Payload für FILE_START */ +struct __attribute__((packed)) buzz_file_start_payload +{ + uint32_t total_size; /* Little Endian */ +}; + +/* Payload für FILE_END */ +struct __attribute__((packed)) buzz_file_end_payload +{ + uint32_t crc32; /* Little Endian */ +}; + +/* --- System API --- */ + +/* Callback-Signatur für den Transport-Layer (BLE/UART) */ +typedef int (*buzz_transport_reply_fn)(const uint8_t *data, uint16_t len); + +/* Struktur für die interne Message Queue */ +struct buzz_frame_msg +{ + uint8_t *data_ptr; + uint16_t length; + buzz_transport_reply_fn reply_cb; + uint16_t max_payload; /* NEU: Maximales Limit für ausgehende Frames dieses Transports */ +}; + +/* Allokation und Freigabe von Memory Slabs */ +int buzz_proto_buf_alloc(uint8_t **buf); +void buzz_proto_buf_free(uint8_t **buf); + +/* Übergabe eines empfangenen Frames an den Protokoll-Thread */ +int buzz_proto_submit_frame(struct buzz_frame_msg *msg); + +#endif /* BUZZ_PROTO_H */ \ No newline at end of file diff --git a/firmware/libs/buzz_proto/src/buzz_proto.c b/firmware/libs/buzz_proto/src/buzz_proto.c new file mode 100644 index 0000000..ea8b9c9 --- /dev/null +++ b/firmware/libs/buzz_proto/src/buzz_proto.c @@ -0,0 +1,619 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "buzz_proto.h" +#include "fs_mgmt.h" + +LOG_MODULE_REGISTER(buzz_proto, CONFIG_BUZZ_PROTO_LOG_LEVEL); +K_MEM_SLAB_DEFINE(buzz_proto_slabs, CONFIG_BUZZ_PROTO_SLAB_SIZE, CONFIG_BUZZ_PROTO_SLAB_COUNT, 4); +K_MSGQ_DEFINE(buzz_proto_msgq, sizeof(struct buzz_frame_msg), CONFIG_BUZZ_PROTO_MSGQ_SIZE, 4); + +struct ls_state_t +{ + bool active; + int credits; + uint32_t entries_sent; + uint32_t retry_counter; + struct fs_dir_t dir; + struct fs_dirent entry; + buzz_transport_reply_fn reply_cb; +}; + +static struct ls_state_t ls_state = { + .active = false, + .credits = 0, + .entries_sent = 0, + .retry_counter = 0, + .reply_cb = NULL, +}; + +struct get_file_state_t +{ + bool active; + int credits; + uint32_t offset; + uint32_t retry_counter; + uint32_t crc32; + struct fs_file_t file; + buzz_transport_reply_fn reply_cb; + uint16_t max_payload; +}; + +static struct get_file_state_t get_file_state = { + .active = false, + .credits = 0, + .offset = 0, + .retry_counter = 0, + .crc32 = 0, + .reply_cb = NULL, +}; +enum stream_state_t +{ + STREAM_IDLE, + STREAM_LS, + STREAM_FILE_PUT, + STREAM_FILE_GET, + STREAM_FW_UPDATE, +}; + +static enum stream_state_t current_stream = STREAM_IDLE; + +static char src_path[FS_MGMT_MAX_PATH_LENGTH], dst_path[FS_MGMT_MAX_PATH_LENGTH]; + +int buzz_proto_buf_alloc(uint8_t **buf) +{ + return k_mem_slab_alloc(&buzz_proto_slabs, (void **)buf, K_NO_WAIT); +} + +void buzz_proto_buf_free(uint8_t **buf) +{ + if (buf && *buf) + { + k_mem_slab_free(&buzz_proto_slabs, (void **)*buf); + *buf = NULL; + } +} + +int buzz_proto_submit_frame(struct buzz_frame_msg *msg) +{ + return k_msgq_put(&buzz_proto_msgq, msg, K_NO_WAIT); +} + +static void send_error_frame(struct buzz_frame_msg *msg, uint16_t error_code) +{ + struct buzz_proto_header *hdr = (struct buzz_proto_header *)msg->data_ptr; + struct buzz_resp_error *err = (struct buzz_resp_error *)(msg->data_ptr + sizeof(*hdr)); + + hdr->frame_type = BUZZ_FRAME_ERROR; + hdr->payload_length = sys_cpu_to_le16(sizeof(struct buzz_resp_error)); + err->error_code = sys_cpu_to_le16(error_code); + + if (msg->reply_cb) + { + msg->reply_cb(msg->data_ptr, sizeof(*hdr) + sizeof(*err)); + } +} + +static void handle_proto_version_request(struct buzz_frame_msg *msg) +{ + struct buzz_proto_header *hdr = (struct buzz_proto_header *)msg->data_ptr; + + hdr->frame_type = BUZZ_FRAME_RESPONSE; + + struct buzz_resp_proto_version *resp_data = (struct buzz_resp_proto_version *)(msg->data_ptr + sizeof(*hdr)); + + resp_data->data_type = BUZZ_DATA_PROTO_INFO; + resp_data->version = sys_cpu_to_le16(BUZZ_PROTO_VERSION); + + resp_data->max_chunk_size = sys_cpu_to_le16(CONFIG_BUZZ_PROTO_SLAB_SIZE - sizeof(struct buzz_proto_header)); + + hdr->payload_length = sys_cpu_to_le16(sizeof(struct buzz_resp_proto_version)); + + uint16_t total_len = sizeof(struct buzz_proto_header) + sizeof(struct buzz_resp_proto_version); + + if (msg->reply_cb) + { + msg->reply_cb(msg->data_ptr, total_len); + } +} + +void handle_fs_info_request(struct buzz_frame_msg *msg) +{ + struct buzz_proto_header *hdr = (struct buzz_proto_header *)msg->data_ptr; + struct fs_statvfs stat; + int rc = fs_mgmt_pm_statvfs(FS_AUDIO_PATH, &stat); + if (rc != 0) + { + LOG_ERR("Failed to statvfs audio path"); + send_error_frame(msg, abs(rc)); + return; + } + + hdr->frame_type = BUZZ_FRAME_RESPONSE; + + struct buzz_resp_fs_info *resp_data = (struct buzz_resp_fs_info *)(msg->data_ptr + sizeof(*hdr)); + + uint32_t block_size = stat.f_frsize; + uint32_t total_size = stat.f_blocks * block_size; + uint32_t free_size = stat.f_bfree * block_size; + + LOG_DBG("FS Info: block_size=%u, total_size=%u, free_size=%u", block_size, total_size, free_size); + + resp_data->data_type = BUZZ_DATA_FS_INFO; + resp_data->total_size = sys_cpu_to_le32(total_size); + resp_data->free_size = sys_cpu_to_le32(free_size); + resp_data->max_path_length = FS_MGMT_MAX_PATH_LENGTH; + resp_data->sys_path_length = strlen(FS_SYSTEM_PATH); + resp_data->audio_path_length = strlen(FS_AUDIO_PATH); + memcpy(resp_data->data, FS_SYSTEM_PATH, resp_data->sys_path_length); + memcpy(resp_data->data + resp_data->sys_path_length, FS_AUDIO_PATH, resp_data->audio_path_length); + + uint16_t payload_length = sizeof(struct buzz_resp_fs_info) + resp_data->sys_path_length + resp_data->audio_path_length; + hdr->payload_length = sys_cpu_to_le16(payload_length); + + uint16_t total_len = sizeof(struct buzz_proto_header) + payload_length; + + if (msg->reply_cb) + { + msg->reply_cb(msg->data_ptr, total_len); + } +} + +static void handle_ls_request(struct buzz_frame_msg *msg) +{ + struct buzz_proto_header *hdr = (struct buzz_proto_header *)msg->data_ptr; + uint16_t payload_len = sys_le16_to_cpu(hdr->payload_length); + + if (current_stream != STREAM_IDLE) + { + LOG_WRN("Stream active, rejecting LS request"); + send_error_frame(msg, EBUSY); + return; + } + + uint16_t path_len = payload_len - 1; + + if (path_len >= sizeof(src_path)) + { + LOG_ERR("Path too long for LS request"); + send_error_frame(msg, ENAMETOOLONG); + return; + } + + memcpy(src_path, msg->data_ptr + sizeof(*hdr) + 1, path_len); + src_path[path_len] = '\0'; + + int rc = fs_mgmt_pm_opendir(&ls_state.dir, src_path); + if (rc != 0) + { + LOG_ERR("Failed to open dir: %d", rc); + send_error_frame(msg, abs(rc)); + return; + } + + current_stream = STREAM_LS; + ls_state.active = true; + ls_state.credits = 0; + ls_state.entries_sent = 0; + ls_state.retry_counter = 0; + ls_state.reply_cb = msg->reply_cb; + + LOG_DBG("Started LS stream for path '%s'", src_path); + + hdr->frame_type = BUZZ_FRAME_LS_START; + hdr->payload_length = 0; + + if (msg->reply_cb) + { + msg->reply_cb(msg->data_ptr, sizeof(*hdr)); + } +} + +static void handle_file_get_request(struct buzz_frame_msg *msg) +{ + struct buzz_proto_header *hdr = (struct buzz_proto_header *)msg->data_ptr; + uint16_t payload_len = sys_le16_to_cpu(hdr->payload_length); + + if (current_stream != STREAM_IDLE) + { + LOG_WRN("Stream active, rejecting FILE_GET request"); + send_error_frame(msg, EBUSY); + return; + } + + uint16_t path_len = payload_len - 1; // 1 Byte für data_type abziehen + if (path_len >= sizeof(src_path)) + { + LOG_ERR("Path too long for FILE_GET request"); + send_error_frame(msg, ENAMETOOLONG); + return; + } + + memcpy(src_path, msg->data_ptr + sizeof(*hdr) + 1, path_len); + src_path[path_len] = '\0'; + + // 1. Datei-Größe ermitteln + struct fs_dirent entry; + if (fs_mgmt_pm_stat(src_path, &entry) != 0) + { + LOG_ERR("File not found: %s", src_path); + send_error_frame(msg, ENOENT); + return; + } + + // 2. Datei öffnen + fs_file_t_init(&get_file_state.file); + int rc = fs_mgmt_pm_open(&get_file_state.file, src_path, FS_O_READ); + if (rc != 0) + { + LOG_ERR("Failed to open file: %d", rc); + send_error_frame(msg, abs(rc)); + return; + } + + // 3. State initialisieren + current_stream = STREAM_FILE_GET; + get_file_state.active = true; + get_file_state.credits = 0; + get_file_state.offset = 0; + get_file_state.crc32 = 0; // IEEE CRC32 Startwert + get_file_state.retry_counter = 0; + get_file_state.reply_cb = msg->reply_cb; + get_file_state.max_payload = msg->max_payload; + + LOG_INF("Started FILE_GET stream for '%s' (%u bytes)", src_path, entry.size); + + // 4. FILE_START Frame senden + hdr->frame_type = BUZZ_FRAME_FILE_START; + hdr->payload_length = sys_cpu_to_le16(sizeof(struct buzz_file_start_payload)); + + struct buzz_file_start_payload *start_pl = (struct buzz_file_start_payload *)(msg->data_ptr + sizeof(*hdr)); + start_pl->total_size = sys_cpu_to_le32(entry.size); + + if (msg->reply_cb) + { + int send_rc = msg->reply_cb(msg->data_ptr, sizeof(*hdr) + sizeof(*start_pl)); + if (send_rc) + { + LOG_ERR("Failed to send FILE_START (err %d)", send_rc); + fs_mgmt_pm_close(&get_file_state.file); + get_file_state.active = false; + current_stream = STREAM_IDLE; + return; + } + } +} + +static void process_file_get_stream(void) +{ + uint8_t *buf = NULL; + if (buzz_proto_buf_alloc(&buf) != 0) + { + return; // Puffer voll, im nächsten Zyklus nochmal probieren + } + + struct buzz_proto_header *hdr = (struct buzz_proto_header *)buf; + uint8_t *payload_ptr = buf + sizeof(*hdr); + + if (get_file_state.max_payload <= sizeof(*hdr)) + { + struct buzz_frame_msg err_msg = { + .data_ptr = buf, + .reply_cb = get_file_state.reply_cb, + .max_payload = get_file_state.max_payload, + }; + send_error_frame(&err_msg, EMSGSIZE); + fs_mgmt_pm_close(&get_file_state.file); + get_file_state.active = false; + current_stream = STREAM_IDLE; + buzz_proto_buf_free(&buf); + LOG_ERR("Invalid max payload for FILE_GET: %u", get_file_state.max_payload); + return; + } + + // Chunk Size berechnen + uint16_t max_chunk_size = MIN( + get_file_state.max_payload - sizeof(*hdr), + CONFIG_BUZZ_PROTO_SLAB_SIZE - sizeof(struct buzz_proto_header)); + + ssize_t read_len = fs_read(&get_file_state.file, payload_ptr, max_chunk_size); + + if (read_len < 0) + { + // Lesefehler + struct buzz_frame_msg err_msg = {.data_ptr = buf, .reply_cb = get_file_state.reply_cb, .max_payload = get_file_state.max_payload}; + send_error_frame(&err_msg, EIO); + + fs_mgmt_pm_close(&get_file_state.file); + get_file_state.active = false; + current_stream = STREAM_IDLE; + buzz_proto_buf_free(&buf); + LOG_ERR("Error reading file: %d", (int)read_len); + return; + } + + if (read_len == 0) + { + // EOF erreicht -> FILE_END senden + hdr->frame_type = BUZZ_FRAME_FILE_END; + hdr->payload_length = sys_cpu_to_le16(sizeof(struct buzz_file_end_payload)); + + struct buzz_file_end_payload *end_pl = (struct buzz_file_end_payload *)payload_ptr; + end_pl->crc32 = sys_cpu_to_le32(get_file_state.crc32); + + if (get_file_state.reply_cb) + { + int send_rc = get_file_state.reply_cb(buf, sizeof(*hdr) + sizeof(*end_pl)); + if (send_rc) + { + LOG_WRN("Failed to send FILE_END (err %d)", send_rc); + } + } + + fs_mgmt_pm_close(&get_file_state.file); + get_file_state.active = false; + current_stream = STREAM_IDLE; + buzz_proto_buf_free(&buf); + LOG_INF("FILE_GET stream ended. CRC32: 0x%08X", get_file_state.crc32); + return; + } + + // Daten gelesen -> CRC aktualisieren und Chunk senden + get_file_state.crc32 = crc32_ieee_update(get_file_state.crc32, payload_ptr, read_len); + get_file_state.offset += read_len; + + hdr->frame_type = BUZZ_FRAME_FILE_CHUNK; + hdr->payload_length = sys_cpu_to_le16(read_len); + + if (get_file_state.reply_cb) + { + int send_rc = get_file_state.reply_cb(buf, sizeof(*hdr) + read_len); + if (send_rc) + { + LOG_ERR("Failed to send FILE_CHUNK (err %d)", send_rc); + fs_mgmt_pm_close(&get_file_state.file); + get_file_state.active = false; + current_stream = STREAM_IDLE; + buzz_proto_buf_free(&buf); + return; + } + } + + get_file_state.credits--; + get_file_state.retry_counter = 0; + buzz_proto_buf_free(&buf); +} + +static void handle_request(struct buzz_frame_msg *msg) +{ + struct buzz_proto_header *hdr = (struct buzz_proto_header *)msg->data_ptr; + + uint16_t payload_len = sys_le16_to_cpu(hdr->payload_length); + if (payload_len < sizeof(struct buzz_request_payload)) + { + LOG_WRN("Invalid request length"); + send_error_frame(msg, EINVAL); + return; + } + + struct buzz_request_payload *req_data = (struct buzz_request_payload *)(msg->data_ptr + sizeof(*hdr)); + + switch (req_data->data_type) + { + case BUZZ_DATA_PROTO_INFO: + LOG_DBG("Received Proto Version Request"); + handle_proto_version_request(msg); + break; + case BUZZ_DATA_FS_INFO: + LOG_DBG("Received FS Info Request"); + handle_fs_info_request(msg); + break; + case BUZZ_DATA_LS: + LOG_DBG("Received LS Request"); + handle_ls_request(msg); + break; + case BUZZ_DATA_FILE_GET: + LOG_DBG("Received FILE_GET Request"); + handle_file_get_request(msg); + break; + default: + LOG_WRN("Unknown request data_type: 0x%02x", req_data->data_type); + send_error_frame(msg, EINVAL); + break; + } +} + +static void process_ls_stream(void) +{ + uint8_t *buf = NULL; + if (buzz_proto_buf_alloc(&buf) != 0) + { + return; // Nächster Versuch im nächsten Zyklus + } + + int rc = fs_readdir(&ls_state.dir, &ls_state.entry); + if (rc < 0) + { + struct buzz_frame_msg err_msg = {.data_ptr = buf, .reply_cb = ls_state.reply_cb}; + send_error_frame(&err_msg, abs(rc)); + fs_mgmt_pm_closedir(&ls_state.dir); + ls_state.active = false; + current_stream = STREAM_IDLE; + buzz_proto_buf_free(&buf); + LOG_ERR("Error reading directory: %d", rc); + return; + } + + struct buzz_proto_header *hdr = (struct buzz_proto_header *)buf; + + if (ls_state.entry.name[0] == 0) + { + hdr->frame_type = BUZZ_FRAME_LS_END; + hdr->payload_length = sys_cpu_to_le16(sizeof(struct buzz_ls_end_payload)); + struct buzz_ls_end_payload *end_pl = (struct buzz_ls_end_payload *)(buf + sizeof(*hdr)); + end_pl->total_entries = sys_cpu_to_le32(ls_state.entries_sent); + + if (ls_state.reply_cb) + { + ls_state.reply_cb(buf, sizeof(*hdr) + sizeof(*end_pl)); + } + + fs_mgmt_pm_closedir(&ls_state.dir); + ls_state.active = false; + current_stream = STREAM_IDLE; + buzz_proto_buf_free(&buf); + LOG_DBG("LS stream ended. Total entries sent: %u", ls_state.entries_sent); + return; + } + + hdr->frame_type = BUZZ_FRAME_LS_ENTRY; + struct buzz_ls_entry_payload *entry_pl = (struct buzz_ls_entry_payload *)(buf + sizeof(*hdr)); + entry_pl->type = (ls_state.entry.type == FS_DIR_ENTRY_DIR) ? BUZZ_FS_ENTRY_DIR : BUZZ_FS_ENTRY_FILE; + entry_pl->size = sys_cpu_to_le32(ls_state.entry.size); + + size_t name_len = strlen(ls_state.entry.name); + entry_pl->name_length = (uint8_t)name_len; + memcpy(entry_pl->name, ls_state.entry.name, name_len); + + uint16_t payload_len = sizeof(*entry_pl) + name_len; + hdr->payload_length = sys_cpu_to_le16(payload_len); + + if (ls_state.reply_cb) + { + ls_state.reply_cb(buf, sizeof(*hdr) + payload_len); + } + + ls_state.credits--; + ls_state.entries_sent++; + ls_state.retry_counter = 0; + buzz_proto_buf_free(&buf); +} + +static void buzz_proto_thread_fn(void *p1, void *p2, void *p3) +{ + struct buzz_frame_msg msg; + struct buzz_proto_header *hdr; + + LOG_INF("Buzz Protocol Thread started"); + + while (1) + { + k_timeout_t wait_time = K_FOREVER; + + if ((current_stream == STREAM_LS && ls_state.active && ls_state.credits > 0) || + (current_stream == STREAM_FILE_GET && get_file_state.active && get_file_state.credits > 0)) + { + wait_time = K_NO_WAIT; + } + else if (current_stream != STREAM_IDLE) + { + wait_time = K_MSEC(500); // Watchdog Timeout + } + + int q_res = k_msgq_get(&buzz_proto_msgq, &msg, wait_time); + + /* 1. Eingehende Nachrichten verarbeiten */ + if (q_res == 0) + { + if (msg.length < sizeof(struct buzz_proto_header)) + { + LOG_WRN("Received frame too short"); + buzz_proto_buf_free(&msg.data_ptr); + continue; + } + + hdr = (struct buzz_proto_header *)msg.data_ptr; + + switch (hdr->frame_type) + { + case BUZZ_FRAME_REQUEST: + handle_request(&msg); + buzz_proto_buf_free(&msg.data_ptr); + break; + + case BUZZ_FRAME_ACK: + if (current_stream == STREAM_LS && msg.length >= sizeof(*hdr) + sizeof(struct buzz_ack_payload)) + { + struct buzz_ack_payload *ack = (struct buzz_ack_payload *)(msg.data_ptr + sizeof(*hdr)); + // Absolute Credits übernehmen, wie von dir vorgeschlagen + ls_state.credits = sys_le16_to_cpu(ack->credits); + ls_state.retry_counter = 0; + LOG_DBG("Got %u credits", ls_state.credits); + } + else if (current_stream == STREAM_FILE_GET && msg.length >= sizeof(*hdr) + sizeof(struct buzz_ack_payload)) + { + struct buzz_ack_payload *ack = (struct buzz_ack_payload *)(msg.data_ptr + sizeof(*hdr)); + get_file_state.credits = sys_le16_to_cpu(ack->credits); + get_file_state.retry_counter = 0; + } + buzz_proto_buf_free(&msg.data_ptr); + break; + + case BUZZ_FRAME_FILE_CHUNK: + send_error_frame(&msg, ENOSYS); + buzz_proto_buf_free(&msg.data_ptr); + break; + + case BUZZ_FRAME_FW_CHUNK: + send_error_frame(&msg, ENOSYS); + buzz_proto_buf_free(&msg.data_ptr); + break; + + case BUZZ_FRAME_LS_ENTRY: + send_error_frame(&msg, ENOSYS); + buzz_proto_buf_free(&msg.data_ptr); + break; + + default: + LOG_WRN("Unhandled frame type: 0x%02x", hdr->frame_type); + send_error_frame(&msg, EPROTO); + buzz_proto_buf_free(&msg.data_ptr); + break; + } + } + if (current_stream == STREAM_LS && ls_state.active) + { + if (ls_state.credits > 0) + { + process_ls_stream(); + } + else if (q_res == -EAGAIN) + { + // Watchdog: Queue hat 500ms blockiert, weil keine Credits (ACK) kamen + ls_state.retry_counter++; + if (ls_state.retry_counter > 5) + { + LOG_WRN("LS timeout waiting for ACK"); + fs_mgmt_pm_closedir(&ls_state.dir); + ls_state.active = false; + current_stream = STREAM_IDLE; + } + } + } + else if (current_stream == STREAM_FILE_GET && get_file_state.active) + { + if (get_file_state.credits > 0) + { + process_file_get_stream(); + } + else if (q_res == -EAGAIN) + { + get_file_state.retry_counter++; + if (get_file_state.retry_counter > 5) + { + LOG_WRN("FILE_GET timeout waiting for ACK"); + fs_close(&get_file_state.file); + get_file_state.active = false; + current_stream = STREAM_IDLE; + } + } + } + } +} + +K_THREAD_DEFINE(buzz_proto_thread, CONFIG_BUZZ_PROT_THREAD_STACK_SIZE, buzz_proto_thread_fn, NULL, NULL, NULL, CONFIG_BUZZ_PROTO_THREAD_PRIORITY, 0, 0); \ No newline at end of file diff --git a/firmware/libs/fs_mgmt/CMakeLists.txt b/firmware/libs/fs_mgmt/CMakeLists.txt new file mode 100644 index 0000000..d4c5abb --- /dev/null +++ b/firmware/libs/fs_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/fs_mgmt/Kconfig b/firmware/libs/fs_mgmt/Kconfig new file mode 100644 index 0000000..84e26bf --- /dev/null +++ b/firmware/libs/fs_mgmt/Kconfig @@ -0,0 +1,40 @@ +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". + + 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/libs/fs_mgmt/include/fs_mgmt.h b/firmware/libs/fs_mgmt/include/fs_mgmt.h new file mode 100644 index 0000000..c51c633 --- /dev/null +++ b/firmware/libs/fs_mgmt/include/fs_mgmt.h @@ -0,0 +1,117 @@ +#ifndef FS_MGMT_H +#define FS_MGMT_H + +#include + +#define FS_MGMT_MAX_PATH_LENGTH 32 +#define FS_AUDIO_PATH CONFIG_FS_MGMT_MOUNT_POINT "/a" +#define FS_SYSTEM_PATH CONFIG_FS_MGMT_MOUNT_POINT "/sys" + +/** + * @brief Initializes the filesystem management module. + */ +int fs_mgmt_init(void); + +// /** +// * @brief Puts the QSPI flash into deep sleep mode to save power +// */ +// int fs_pm_flash_suspend(void); + +// /** +// * @brief Resumes the QSPI flash from deep sleep mode +// */ +// int fs_pm_flash_resume(void); + +/** + * @brief Wrapper around fs_open that handles power management for the flash + * Resumes the flash before opening and suspends it if opening fails + * @param file Pointer to fs_file_t structure to be initialized + * @param path Path to the file to open + * @param mode Open flags (e.g. FS_O_READ, FS_O_WRITE) + * @return 0 on success, negative error code on failure + */ +int fs_mgmt_pm_open(struct fs_file_t *file, const char *path, fs_mode_t mode); + +/** + * @brief Wrapper around fs_close that handles power management for the flash + * Resumes the flash after closing and suspends it if closing fails + * @param file Pointer to fs_file_t structure to be closed + * @return 0 on success, negative error code on failure + */ +int fs_mgmt_pm_close(struct fs_file_t *file); + +/** + * @brief Wrapper around fs_opendir that handles power management for the flash + * Resumes the flash before opening and suspends it if opening fails + * @param dirp Pointer to fs_dir_t structure to be initialized + * @param path Path to the directory to open + * @return 0 on success, negative error code on failure + */ +int fs_mgmt_pm_opendir(struct fs_dir_t *dirp, const char *path); + +/** + * @brief Wrapper around fs_closedir that handles power management for the flash + * Resumes the flash after closing and suspends it if closing fails + * @param dirp Pointer to fs_dir_t structure to be closed + * @return 0 on success, negative error code on failure + */ +int fs_mgmt_pm_closedir(struct fs_dir_t *dirp); + +/** + * @brief Unlinks (deletes) a file, ensuring the flash is active during the operation + * @param path Path to the file to unlink + * @return 0 on success, negative error code on failure + */ +int fs_mgmt_pm_unlink(const char *path); + +/** + * @brief Wrapper around fs_statvfs that handles power management for the flash + * Resumes the flash before getting stats and suspends it afterwards + * @param path Path to the filesystem to get stats for + * @param stat Pointer to fs_statvfs structure to be filled with stats + * @return 0 on success, negative error code on failure + */ +int fs_mgmt_pm_statvfs(const char *path, struct fs_statvfs *stat); + +/** + * @brief Wrapper around fs_stat that handles power management for the flash + * Resumes the flash before stat and suspends it afterwards + * @param path Path to file or directory + * @param entry Pointer to fs_dirent structure to receive metadata + * @return 0 on success, negative error code on failure + */ +int fs_mgmt_pm_stat(const char *path, struct fs_dirent *entry); + +/** + * @brief Wrapper around fs_mkdir that handles power management for the flash + * Resumes the flash before creating the directory and suspends it afterwards + * @param path Path to the directory to create + * @return 0 on success, negative error code on failure + */ +int fs_mgmt_pm_mkdir(const char *path); + +/** + * @brief Wrapper around fs_rename that handles power management for the flash + * Resumes the flash before renaming and suspends it afterwards + * @param old_path Current path of the file or directory + * @param new_path New path for the file or directory + * @return 0 on success, negative error code on failure + */ +int fs_mgmt_pm_rename(const char *old_path, const char *new_path); + +/** + * @brief Recursively creates directories for the given path, ensuring the flash is active during the operation + * @param path Path to the directory to create (can include multiple levels, e.g. "/dir1/dir2/dir3") + * @return 0 on success, negative error code on failure + */ +int fs_mgmt_pm_mkdir_recursive(char *path); + +/** + * @brief Recursively removes a directory and all its contents, ensuring the flash is active during the operation + * @param path Path to the directory to remove + * @param max_len Maximum length of the path buffer + * @return 0 on success, negative error code on failure + */ +int fs_mgmt_pm_rm_recursive(char *path, size_t max_len); + +#endif /* FS_MGMT_H */ \ No newline at end of file diff --git a/firmware/libs/fs_mgmt/src/fs_mgmt.c b/firmware/libs/fs_mgmt/src/fs_mgmt.c new file mode 100644 index 0000000..66ba478 --- /dev/null +++ b/firmware/libs/fs_mgmt/src/fs_mgmt.c @@ -0,0 +1,373 @@ +#include +#include +#include +#include + +#include "fs_mgmt.h" + +LOG_MODULE_REGISTER(fs_mgmt, CONFIG_FS_MGMT_LOG_LEVEL); + +#define FS_PARTITION_ID FLASH_AREA_ID(littlefs_storage) +FS_LITTLEFS_DECLARE_DEFAULT_CONFIG(fs_storage_data); + +#define QSPI_FLASH_NODE DT_ALIAS(qspi_flash) +static const struct device *flash_dev = DEVICE_DT_GET(QSPI_FLASH_NODE); + +static struct fs_mount_t fs_storage_mnt = { + .type = FS_LITTLEFS, + .fs_data = &fs_storage_data, + .storage_dev = (void *)FS_PARTITION_ID, + .mnt_point = CONFIG_FS_MGMT_MOUNT_POINT, +}; + +static int open_count = 0; +static struct k_mutex flash_pm_lock; + +/** + * @brief Puts the QSPI flash into deep sleep mode to save power + * Decrements the open count and suspends the flash if no more users are active + * @return 0 on success, negative error code on failure + */ +static int fs_mgmt_pm_flash_suspend(void) +{ +#if IS_ENABLED(CONFIG_PM_DEVICE) + if (!device_is_ready(flash_dev)) + { + return -ENODEV; + } + + k_mutex_lock(&flash_pm_lock, K_FOREVER); + + if (open_count > 0) + { + open_count--; + if (open_count == 0) + { + int rc = pm_device_action_run(flash_dev, PM_DEVICE_ACTION_SUSPEND); + if (rc < 0) + { + LOG_WRN("Could not suspend flash: %d", rc); + } + else + { + LOG_DBG("Flash entered deep power-down"); + } + } + } + + k_mutex_unlock(&flash_pm_lock); +#endif /* CONFIG_PM_DEVICE */ + return 0; +} + +/** + * @brief Resumes the QSPI flash from deep sleep mode + * Increments the open count and resumes the flash if it was previously suspended + * @return 0 on success, negative error code on failure + */ +static int fs_mgmt_pm_flash_resume(void) +{ +#if IS_ENABLED(CONFIG_PM_DEVICE) + if (!device_is_ready(flash_dev)) + return -ENODEV; + + k_mutex_lock(&flash_pm_lock, K_FOREVER); + + if (open_count == 0) + { + int rc = pm_device_action_run(flash_dev, PM_DEVICE_ACTION_RESUME); + if (rc == 0) + { + k_busy_wait(50); // t-exit-dpd + LOG_DBG("Flash resumed"); + } + } + open_count++; + + k_mutex_unlock(&flash_pm_lock); +#endif /* CONFIG_PM_DEVICE */ + return 0; +} + +int fs_mgmt_pm_open(struct fs_file_t *file, const char *path, fs_mode_t mode) +{ + LOG_DBG("PM Opening file '%s' with mode 0x%02x", path, mode); + fs_mgmt_pm_flash_resume(); + int rc = fs_open(file, path, mode); + if (rc < 0) + { + fs_mgmt_pm_flash_suspend(); + } + return rc; +} + +int fs_mgmt_pm_close(struct fs_file_t *file) +{ + LOG_DBG("PM Closing file"); + int rc = fs_close(file); + fs_mgmt_pm_flash_suspend(); + return rc; +} + +int fs_mgmt_pm_opendir(struct fs_dir_t *dirp, const char *path) +{ + LOG_DBG("PM Opening directory '%s'", path); + fs_mgmt_pm_flash_resume(); + int rc = fs_opendir(dirp, path); + if (rc < 0) + { + fs_mgmt_pm_flash_suspend(); + } + return rc; +} + +int fs_mgmt_pm_closedir(struct fs_dir_t *dirp) +{ + LOG_DBG("PM Closing directory"); + int rc = fs_closedir(dirp); + fs_mgmt_pm_flash_suspend(); + return rc; +} + +int fs_mgmt_pm_unlink(const char *path) +{ + LOG_DBG("PM Unlinking file '%s'", path); + fs_mgmt_pm_flash_resume(); + int rc = fs_unlink(path); + fs_mgmt_pm_flash_suspend(); + return rc; +} + +int fs_mgmt_pm_statvfs(const char *path, struct fs_statvfs *stat) +{ + LOG_DBG("PM Getting filesystem stats for '%s'", path); + fs_mgmt_pm_flash_resume(); + int rc = fs_statvfs(path, stat); + fs_mgmt_pm_flash_suspend(); + return rc; +} + +int fs_mgmt_pm_stat(const char *path, struct fs_dirent *entry) +{ + LOG_DBG("PM Getting stat for '%s'", path); + fs_mgmt_pm_flash_resume(); + int rc = fs_stat(path, entry); + fs_mgmt_pm_flash_suspend(); + return rc; +} + +int fs_mgmt_pm_mkdir(const char *path) +{ + LOG_DBG("PM Creating directory '%s'", path); + fs_mgmt_pm_flash_resume(); + int rc = fs_mkdir(path); + fs_mgmt_pm_flash_suspend(); + return rc; +} + +int fs_mgmt_pm_rename(const char *old_path, const char *new_path) +{ + LOG_DBG("PM Renaming '%s' to '%s'", old_path, new_path); + fs_mgmt_pm_flash_resume(); + int rc = fs_rename(old_path, new_path); + fs_mgmt_pm_flash_suspend(); + return rc; +} + +int fs_mgmt_pm_rm_recursive(char *path_buf, size_t max_len) +{ + struct fs_dirent entry; + struct fs_dir_t dir; + int rc; + + fs_mgmt_pm_flash_resume(); + + /* 1. Stat prüfen: Ist es eine Datei? */ + rc = fs_stat(path_buf, &entry); + if (rc != 0) + { + fs_mgmt_pm_flash_suspend(); + return rc; + } + + /* Wenn es eine Datei ist, direkt löschen und beenden */ + if (entry.type == FS_DIR_ENTRY_FILE) + { + rc = fs_unlink(path_buf); + fs_mgmt_pm_flash_suspend(); + return rc; + } + + /* 2. Es ist ein Verzeichnis. Schleife bis es leer ist. */ + size_t orig_len = strlen(path_buf); + + while (1) + { + fs_dir_t_init(&dir); + rc = fs_opendir(&dir, path_buf); + if (rc != 0) + { + break; + } + + bool found_something = false; + + /* Genau EINEN löschbaren Eintrag suchen */ + while (1) + { + rc = fs_readdir(&dir, &entry); + if (rc != 0 || entry.name[0] == '\0') + { + break; /* Ende oder Fehler */ + } + if (strcmp(entry.name, ".") == 0 || strcmp(entry.name, "..") == 0) + { + continue; /* Ignorieren */ + } + + found_something = true; + break; /* Treffer! Schleife abbrechen. */ + } + + /* WICHTIG: Das Verzeichnis SOFORT schließen, BEVOR wir rekurieren! + * Damit geben wir das File-Handle (NUM_DIRS) an Zephyr zurück. */ + fs_closedir(&dir); + + if (!found_something || rc != 0) + { + break; /* Verzeichnis ist nun restlos leer */ + } + + size_t name_len = strlen(entry.name); + if (orig_len + 1 + name_len >= max_len) + { + rc = -ENAMETOOLONG; + break; + } + + /* Pfad für das gefundene Kindelement bauen */ + path_buf[orig_len] = '/'; + strcpy(&path_buf[orig_len + 1], entry.name); + + /* Rekursiver Aufruf für das Kind */ + rc = fs_mgmt_pm_rm_recursive(path_buf, max_len); + + /* Puffer sofort wieder auf unser Verzeichnis zurückschneiden */ + path_buf[orig_len] = '\0'; + + if (rc != 0) + { + break; /* Abbruch, falls beim Löschen des Kindes ein Fehler auftrat */ + } + } + + /* 3. Das nun restlos leere Verzeichnis selbst löschen */ + if (rc == 0) + { + rc = fs_unlink(path_buf); + } + + fs_mgmt_pm_flash_suspend(); + return rc; +} + +int fs_mgmt_pm_mkdir_recursive(char *path) +{ + int rc = 0; + struct fs_dirent entry; + char *p = path; + + /* Führenden Slash überspringen, falls vorhanden (z. B. bei "/lfs") */ + if (*p == '/') + { + p++; + } + + /* Flash für den gesamten Durchlauf aktivieren */ + fs_mgmt_pm_flash_resume(); + + while (*p != '\0') + { + if (*p == '/') + { + *p = '\0'; /* String temporär am aktuellen Slash terminieren */ + + /* Prüfen, ob dieser Pfadabschnitt bereits existiert */ + rc = fs_stat(path, &entry); + + if (rc == -ENOENT) + { + /* Existiert nicht -> anlegen */ + rc = fs_mkdir(path); + if (rc != 0) + { + *p = '/'; /* Bei Fehler Slash wiederherstellen und abbrechen */ + break; + } + } + else if (rc == 0) + { + /* Existiert -> prüfen, ob es ein Verzeichnis ist */ + if (entry.type != FS_DIR_ENTRY_DIR) + { + rc = -ENOTDIR; + *p = '/'; + break; + } + } + else + { + /* Anderer Dateisystemfehler */ + *p = '/'; + break; + } + + *p = '/'; /* Slash für den nächsten Schleifendurchlauf wiederherstellen */ + } + p++; + } + + /* Letztes Element verarbeiten, falls der Pfad nicht mit '/' endet */ + if (rc == 0 && p > path && *(p - 1) != '/') + { + rc = fs_stat(path, &entry); + if (rc == -ENOENT) + { + rc = fs_mkdir(path); + } + else if (rc == 0) + { + if (entry.type != FS_DIR_ENTRY_DIR) + { + rc = -ENOTDIR; + } + } + } + + /* Flash am Ende wieder in den Suspend schicken */ + fs_mgmt_pm_flash_suspend(); + return rc; +} + +int fs_mgmt_init(void) +{ + k_mutex_init(&flash_pm_lock); + + if (!device_is_ready(flash_dev)) { + LOG_ERR("Flash device not ready!"); + return -ENODEV; + } + + fs_mgmt_pm_flash_resume(); + + int rc = fs_mount(&fs_storage_mnt); + + if (rc < 0) + { + LOG_ERR("Error mounting filesystem: %d", rc); + return rc; + } + fs_mgmt_pm_flash_suspend(); + LOG_DBG("Filesystem mounted successfully"); + return 0; +} \ No newline at end of file diff --git a/firmware/libs/zephyr/module.yml b/firmware/libs/zephyr/module.yml new file mode 100644 index 0000000..577b9ad --- /dev/null +++ b/firmware/libs/zephyr/module.yml @@ -0,0 +1,4 @@ +name: libs +build: + cmake: . + kconfig: Kconfig \ No newline at end of file diff --git a/firmware/pm_static_nrf52840dk_nrf52840.yml b/firmware/pm_static_nrf52840dk_nrf52840.yml new file mode 100644 index 0000000..d5e6f3b --- /dev/null +++ b/firmware/pm_static_nrf52840dk_nrf52840.yml @@ -0,0 +1,39 @@ +# mcuboot: +# address: 0x0 +# size: 0xC000 +# region: flash_primary + +# # Primary Slot: Start bleibt 0xC000, Größe 200KB (0x32000) +# mcuboot_primary: +# address: 0xC000 +# size: 0x32000 +# region: flash_primary + +# mcuboot_pad: +# address: 0xC000 +# size: 0x200 +# region: flash_primary + +# # Die App startet nach dem Padding des Primary Slots +# app: +# address: 0xC200 +# size: 0x31E00 # (0x32000 - 0x200) +# region: flash_primary + +# # Secondary Slot: Startet jetzt bei 0xC000 + 0x32000 = 0x3E000 +# mcuboot_secondary: +# address: 0x3E000 +# size: 0x32000 +# region: flash_primary + +# # NVS storage am Ende des Flashs, 16KB (0x4000) +# settings_storage: +# address: 0xFC000 +# size: 0x4000 +# region: flash_primary + +# External Flash +littlefs_storage: + address: 0x0 + size: 0x800000 # 8MB + region: external_flash \ No newline at end of file diff --git a/firmware/prj.conf b/firmware/prj.conf new file mode 100644 index 0000000..9c957b1 --- /dev/null +++ b/firmware/prj.conf @@ -0,0 +1,35 @@ +### Logging +CONFIG_LOG=y + +### File System +CONFIG_FS_MGMT=y +CONFIG_FS_MGMT_LOG_LEVEL_DBG=y +CONFIG_FS_LOG_LEVEL_WRN=y + +### Bluetooth +CONFIG_BLE_MGMT=y +# CONFIG_BLE_MGMT_LOG_LEVEL_DBG=y + +# Explicit throughput tuning in project config (wins over competing defaults) +CONFIG_BT_HCI_ACL_FLOW_CONTROL=y +CONFIG_BT_BUF_CMD_TX_COUNT=24 +CONFIG_BT_BUF_ACL_TX_COUNT=20 +CONFIG_BT_L2CAP_TX_BUF_COUNT=20 +CONFIG_BT_CONN_TX_MAX=20 +CONFIG_BT_CTLR_SDC_TX_PACKET_COUNT=20 +CONFIG_BT_CTLR_SDC_RX_PACKET_COUNT=20 + +# Advertising 500ms - 1s +CONFIG_BLE_MGMT_ADV_INT_MIN=160 +CONFIG_BLE_MGMT_ADV_INT_MAX=320 + +## Buzzer protocol +CONFIG_BUZZ_PROTO=y +CONFIG_BUZZ_PROTO_LOG_LEVEL_DBG=y + +## Power management +CONFIG_PM_DEVICE=y + +## Shell +# CONFIG_SHELL=y +# CONFIG_FILE_SYSTEM_SHELL=y \ No newline at end of file diff --git a/firmware/src/main.c b/firmware/src/main.c new file mode 100644 index 0000000..81eb2b5 --- /dev/null +++ b/firmware/src/main.c @@ -0,0 +1,66 @@ +#include +#include +#include + +#include "fs_mgmt.h" +#include "ble_mgmt.h" +#include "buzz_proto.h" + +LOG_MODULE_REGISTER(main); + +void ble_rx_cb(const uint8_t *data, uint16_t len) +{ + uint8_t *buf; + + /* 1. Länge prüfen (darf SLAB_BLOCK_SIZE = 256 nicht überschreiten) */ + if (len > 256) { + LOG_ERR("Received data too large for proto buf (%u bytes)", len); + return; + } + + /* 2. Speicher aus dem Protokoll-Slab-Pool anfordern (Zero-Wait) */ + if (buzz_proto_buf_alloc(&buf) != 0) { + LOG_ERR("No free memory slabs for incoming BLE frame!"); + return; + } + + /* 3. Daten in den allokierten Slab kopieren */ + memcpy(buf, data, len); + + /* 4. Nachrichten-Struktur für den Protokoll-Thread füllen */ + struct buzz_frame_msg msg = { + .data_ptr = buf, + .length = len, + .reply_cb = ble_mgmt_send, + .max_payload = ble_mgmt_get_max_payload(), + }; + + /* 5. Frame asynchron an den Protokoll-Thread übergeben */ + if (buzz_proto_submit_frame(&msg) != 0) { + LOG_ERR("Failed to submit frame to proto thread (Queue full)"); + buzz_proto_buf_free(&buf); /* Speicher bei Fehler sofort wieder freigeben */ + } +} + +int main(void) +{ + LOG_INF("Starting app on %s (SOC: %s)", CONFIG_BOARD, CONFIG_SOC); + + int rc; + + rc = fs_mgmt_init(); + if (rc < 0) { + LOG_ERR("Failed to initialize file system management: %d", rc); + return rc; + } + + /* BLE-Subsystem initialisieren und RX-Callback registrieren */ + rc = ble_mgmt_init(ble_rx_cb, CONFIG_BLE_MGMT_DEFAULT_DEVICE_NAME); + if (rc < 0) { + LOG_ERR("Failed to initialize BLE management: %d", rc); + return rc; + } + + LOG_INF("Init complete"); + k_sleep(K_FOREVER); +} \ No newline at end of file diff --git a/protocol.md b/protocol.md new file mode 100644 index 0000000..d166ee6 --- /dev/null +++ b/protocol.md @@ -0,0 +1,313 @@ +# 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. + +Das Protokoll spezifiziert: +- Frame-Struktur (Header + Payload) +- Frametypen +- Datentypen fuer Request/Response +- Semantik fuer Stream-Transfers (Verzeichnisliste, Datei, Firmware) + +## 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. + +## 3. Frame-Format + +### 3.1 Header (3 Byte) +```c +uint8_t frame_type +uint16_t payload_length // Little Endian +``` + +### 3.2 Gesamtframe +``` ++------------------+-------------------------+ +| Header (3 Byte) | Payload (optional) | +| frame_type (1B) | payload_length Byte | +| payload_len (2B) | | ++------------------+-------------------------+ +``` + +## 4. Frametypen + +### 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 | + +### 4.2 Datei-Transfer (reserviert, noch nicht implementiert) +| Wert | Name | +|--------|--------------| +| `0x20` | `FILE_START` | +| `0x21` | `FILE_CHUNK` | +| `0x22` | `FILE_END` | + +### 4.3 Firmware-Transfer (reserviert, noch nicht implementiert) +| Wert | Name | +|--------|------------| +| `0x30` | `FW_START` | +| `0x31` | `FW_CHUNK` | +| `0x32` | `FW_END` | + +### 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 | + +## 5. Request/Response-Schema + +### 5.1 Request (`frame_type = 0x00`) +Payload-Mindestformat: +```c +uint8_t data_type // Nutzt enum buzz_data_type +// optional: datentypspezifische Parameter +``` + +Wire-Format: +``` +[0x00][payload_length LE][data_type][optional parameters] +``` + +### 5.2 Response (`frame_type = 0x10`) +Payload-Mindestformat: +```c +uint8_t data_type // Echo des angefragten data_type +// danach: datentypspezifische Response-Daten +``` + +Wire-Format: +``` +[0x10][payload_length LE][data_type][response payload] +``` + +## 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 | +| `0x40` | `LS` | Verzeichnisliste starten | + +### 6.1 `PROTO_INFO` (`0x01`) +Request-Parameter: keine + +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) +``` + +Hinweis: `max_chunk_size` ergibt sich aus der internen Slab-Konfiguration (`CONFIG_BUZZ_PROTO_SLAB_SIZE - 3`). + +### 6.2 `DEVICE_INFO` (`0x02`) +TBD + +### 6.3 `FS_INFO` (`0x03`) +Request-Parameter: keine + +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 +``` + +### 6.4 `LS` (`0x40`) — Verzeichnisliste anfordern +Startet einen LS-Stream fuer den angegebenen Pfad. + +Request-Payload: +```c +uint8_t data_type; // 0x40 +char path[]; // Pfad ohne 0-Terminator, Laenge ergibt sich aus payload_length - 1 +``` + +Wire-Format (Beispiel fuer Pfad `/a`): +``` +[0x00][0x03 0x00][0x40][0x2F 0x61] +``` + +Das Device antwortet mit dem LS-Stream (siehe Abschnitt 8). + +## 7. ACK- und ERROR-Frames + +### 7.1 ACK (`frame_type = 0x11`) — Host → Device +Wird waehrend eines laufenden LS-Streams gesendet, um dem Device Credits (Sendeerlaubnisse) zu erteilen. + +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 | + +## 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: +```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 +``` + +`type`-Werte: +| Wert | Bedeutung | +|--------|---------------| +| `0x00` | Datei (FILE) | +| `0x01` | Verzeichnis (DIR) | + +### 8.3 `LS_END` (`0x42`) — Device → Host +Signalisiert das Ende des Streams. + +Payload: +```c +uint32_t total_entries; // Gesamtzahl gesendeter Eintraege (LE) +``` + +Der Host kann `total_entries` mit der empfangenen Anzahl von `LS_ENTRY`-Frames vergleichen, um Vollstaendigkeit zu pruefen. + +### 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. + +## 9. Beispiele + +### 9.1 PROTO_INFO abfragen + +Request: +``` +00 01 00 01 +``` +- `00`: `REQUEST` +- `01 00`: `payload_length = 1` +- `01`: `data_type = PROTO_INFO` + +Response (Beispielwerte): +``` +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 + +Request: +``` +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 diff --git a/webpage/.prettierrc b/webpage/.prettierrc index 8d2c733..6bfa08a 100644 --- a/webpage/.prettierrc +++ b/webpage/.prettierrc @@ -6,7 +6,7 @@ { "files": ["*.svelte", "*.astro"], "options": { - "printWidth": 1000 + "printWidth": 100 } } ] diff --git a/webpage/package-lock.json b/webpage/package-lock.json index 69a900f..7ac3fd7 100644 --- a/webpage/package-lock.json +++ b/webpage/package-lock.json @@ -17,6 +17,7 @@ "typescript": "^5.9.3" }, "devDependencies": { + "phosphor-svelte": "^3.1.0", "prettier": "^3.8.1", "prettier-plugin-astro": "^0.14.1" } @@ -4490,6 +4491,26 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/phosphor-svelte": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/phosphor-svelte/-/phosphor-svelte-3.1.0.tgz", + "integrity": "sha512-nldtxx+XCgNREvrb7O5xgDsefytXpSkPTx8Rnu3f2qQCUZLDV1rLxYSd2Jcwckuo9lZB1qKMqGR17P4UDC0PrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^3.0.3", + "magic-string": "^0.30.13" + }, + "peerDependencies": { + "svelte": "^5.0.0 || ^5.0.0-next.96", + "vite": ">=5" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, "node_modules/piccolore": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", diff --git a/webpage/package.json b/webpage/package.json index 6f515ce..99e2225 100644 --- a/webpage/package.json +++ b/webpage/package.json @@ -18,6 +18,7 @@ "typescript": "^5.9.3" }, "devDependencies": { + "phosphor-svelte": "^3.1.0", "prettier": "^3.8.1", "prettier-plugin-astro": "^0.14.1" } diff --git a/webpage/src/components/AppGuard.svelte b/webpage/src/components/AppGuard.svelte new file mode 100644 index 0000000..94ac96b --- /dev/null +++ b/webpage/src/components/AppGuard.svelte @@ -0,0 +1,61 @@ + + +{#if $isInitializing} +
+

SYSTEM_CHECK_RUNNING...

+
+{:else if !$isBluetoothSupported && !$isSerialSupported} +
+
+

Inkompatibler Browser

+ +

+ Du nutzt aktuell {browserName} + . Dieser Browser unterstützt weder Bluetooth noch serielle USB-Verbindungen. +

+ +
+
+ Web Bluetooth: + NICHT UNTERSTÜTZT +
+
+ Web Serial: + NICHT UNTERSTÜTZT +
+
+ +

+ (Info: Firefox und Safari blockieren diese Hardware-Schnittstellen aus Prinzip.) +

+ + + Googles Glanzeisen installieren + + +

+ Gerüchten zufolge soll Winzigweichs Kante + -Browser diese Technologien auch unterstützen. Aber wer nutzt schon diese Weichware? +

+
+
+{:else} + + +{/if} diff --git a/webpage/src/components/BLEList.svelte b/webpage/src/components/BLEList.svelte new file mode 100644 index 0000000..60f90dd --- /dev/null +++ b/webpage/src/components/BLEList.svelte @@ -0,0 +1,22 @@ + + +
+
+

Verfügbare Geräte

+
+ +
+ {#if $pairedDevices.length > 0} + {#each $pairedDevices as device (device.id)} + + {/each} + {:else} +
+ Keine gepairten Geräte gefunden. Bitte pairen Sie zunächst ein Gerät. +
+ {/if} +
+
\ No newline at end of file diff --git a/webpage/src/components/BLEListItem.svelte b/webpage/src/components/BLEListItem.svelte new file mode 100644 index 0000000..b30ef7f --- /dev/null +++ b/webpage/src/components/BLEListItem.svelte @@ -0,0 +1,55 @@ + + +
+
{ if (isAvailable && !isActive) connectBuzzer(device); }} + > + {#if isAvailable || isActive} + + {:else} + + {/if} + +
+ + {device.name || "Unbekanntes Gerät"} + + {#if isActive} + Verbunden + {:else if !isAvailable} + Nicht in Reichweite + {/if} +
+
+ + +
diff --git a/webpage/src/components/BuzzerControl.svelte b/webpage/src/components/BuzzerControl.svelte new file mode 100644 index 0000000..e4ff4e4 --- /dev/null +++ b/webpage/src/components/BuzzerControl.svelte @@ -0,0 +1,137 @@ + + +
+

Geräte-Status

+ +
+
+ {$isConnected ? "Bluetooth Verbunden" : "Bluetooth Getrennt"} +
+ +
+
+ Protokoll Version: + + {$protocolInfo ? "v" + $protocolInfo.version : "unbekannt"} + +
+
+ Max Chunk Size: + + {$protocolInfo ? $protocolInfo.maxChunkSize + " B" : "unbekannt"} + +
+
+ Flash-Grösse: + + {$fsInfo ? $fsInfo.totalSize.toFixed(2) + " MB" : "unbekannt"} + +
+
+ Freier Speicher: + + {$fsInfo ? $fsInfo.freeSize.toFixed(2) + " MB" : "unbekannt"} + +
+
+ Max. Pfadlänge: + + {$fsInfo ? $fsInfo.maxPathLength : "unbekannt"} + +
+
+ Systempfad: + + {$fsInfo ? $fsInfo.sysPath : "unbekannt"} + +
+
+ Audiopfad: + + {$fsInfo ? $fsInfo.audioPath : "unbekannt"} + +
+
+ +
+ {#if !$isConnected} + + + + {:else} + + + + {/if} +
+ +
diff --git a/webpage/src/components/FileTransfer.svelte b/webpage/src/components/FileTransfer.svelte new file mode 100644 index 0000000..047674d --- /dev/null +++ b/webpage/src/components/FileTransfer.svelte @@ -0,0 +1,27 @@ + + +
+
+ +
+ {#if $storageUsage} +
+
+ + Rate: {($storageUsage.systemBytes / 1048576).toFixed(2)} MB + +
+
+ + {($storageUsage.freeBytes / 1048576).toFixed(2)} Sekunden + +
+ {:else} +
Kein Transfer aktiv
+ {/if} +
\ No newline at end of file diff --git a/webpage/src/components/FlashUsage.svelte b/webpage/src/components/FlashUsage.svelte new file mode 100644 index 0000000..d230b4e --- /dev/null +++ b/webpage/src/components/FlashUsage.svelte @@ -0,0 +1,42 @@ + + +
+
+ +
+ +
+
+ +
+ {#if $storageUsage} +
+
+ System: + {($storageUsage.systemBytes / 1048576).toFixed(2)} MB +
+
+ Audio: + {($storageUsage.audioBytes / 1048576).toFixed(2)} MB +
+
+ +
+ Frei: + {($storageUsage.freeBytes / 1048576).toFixed(2)} MB +
+ {:else} +
Speicherdaten nicht verfügbar
+ {/if} +
\ No newline at end of file diff --git a/webpage/src/components/ToastContainer.svelte b/webpage/src/components/ToastContainer.svelte new file mode 100644 index 0000000..525818c --- /dev/null +++ b/webpage/src/components/ToastContainer.svelte @@ -0,0 +1,33 @@ + + +
+ {#each $toasts as toast (toast.id)} +
+ {@html toast.message} + {#if toast.dismissible} + + {/if} +
+ {/each} +
diff --git a/webpage/src/lib/bluetooth.ts b/webpage/src/lib/bluetooth.ts new file mode 100644 index 0000000..7a20cbd --- /dev/null +++ b/webpage/src/lib/bluetooth.ts @@ -0,0 +1,247 @@ +import { get } from 'svelte/store'; +import { injectDummyDevices, isConnected, isPaired, isConnecting, pairedDevices, availableDevices, activeDeviceId, saveConnectionState, loadConnectionState, targetDeviceId, resetLocal, resetRemote } from './store'; +import { BLE } from './protocol/constants'; +import { parseIncomingFrame } from './protocol'; +import { registerTransport, handleTransportDisconnect, handleTransportConnect } from './transport'; +import { addToast, clearAllToasts } from './toast'; +import { SETTINGS } from './settings'; + +let rxCharacteristic: BluetoothRemoteGATTCharacteristic | null = null; +let txCharacteristic: BluetoothRemoteGATTCharacteristic | null = null; +let device: BluetoothDevice | null = null; + +export async function restoreSession() { + try { + const devices = await getPairedDevices(); + if (devices.length > 0) { + isPaired.set(true); + startScanningAdvertisements(devices); + + const savedState = loadConnectionState(); + if (savedState && savedState.autoConnect && savedState.transport === 'ble') { + const targetDev = devices.find(d => d.id === savedState.deviceId); + if (targetDev) { + addToast("Versuche automatische Wiederverbindung...", "info"); + await connectBuzzer(targetDev); + } + } else if (savedState) { + targetDeviceId.set(savedState.deviceId); + device = devices.find(d => d.id === savedState.deviceId) || devices[0]; + } else { + device = devices[0]; + } + } + } catch (error) { + console.error("Session-Wiederherstellung fehlgeschlagen:", error); + } +} + +async function startScanningAdvertisements(devices: BluetoothDevice[]) { + for (const dev of devices) { + // Sicherheits-Check für Mock-Objekte + if (typeof dev.addEventListener !== 'function') continue; + + dev.addEventListener('advertisementreceived', () => { + availableDevices.update(set => { + const newSet = new Set(set); + newSet.add(dev.id); + return newSet; + }); + }); + + try { + // Auch hier vorher prüfen + if (typeof dev.watchAdvertisements === 'function') { + await dev.watchAdvertisements(); + } + } catch (e) { + console.warn("Scanning für Gerät nicht möglich:", dev.name); + } + } +} + +export async function pairBuzzer() { + try { + const newDevice = await navigator.bluetooth.requestDevice({ + filters: [{ services: [BLE.SERVICE_UUID] }] + }); + + isPaired.set(true); + + const devices = await getPairedDevices(); + startScanningAdvertisements(devices); + + await connectBuzzer(newDevice); + + } catch (error) { + console.error("Pairing abgebrochen oder fehlgeschlagen", error); + } +} + +export async function connectBuzzer(targetDevice?: BluetoothDevice | Event) { + console.debug("connectBuzzer aufgerufen"); + if (targetDevice instanceof Event) { + targetDevice = undefined; + } + + if (get(isConnecting)) return; + + if (targetDevice) { + if (device && device.gatt?.connected && device.id !== (targetDevice as BluetoothDevice).id) { + device.gatt.disconnect(); + } + device = targetDevice as BluetoothDevice; + } + + clearAllToasts(); + + if (!device) return; + + device.removeEventListener('gattserverdisconnected', handleDisconnect); + device.addEventListener('gattserverdisconnected', handleDisconnect); + + isConnecting.set(true); + console.debug("connectBuzzer: Verbindungsversuch mit", device.name); + + const connectionTimeout = setTimeout(() => { + isConnecting.set(false); + addToast("Verbindungsaufbau fehlgeschlagen (Timeout).", "error", true); + handleTransportDisconnect(); + }, SETTINGS.bluetooth.connectionTimeoutMs); + + try { + const server = await device.gatt?.connect(); + if (!server) throw new Error("GATT Server nicht gefunden"); + + const service = await server.getPrimaryService(BLE.SERVICE_UUID); + rxCharacteristic = await service.getCharacteristic(BLE.RX_UUID); + txCharacteristic = await service.getCharacteristic(BLE.TX_UUID); + + await txCharacteristic.startNotifications(); + txCharacteristic.addEventListener('characteristicvaluechanged', handleIncomingData); + + clearTimeout(connectionTimeout); + + // Hardware-Setup ist fertig -> Übergabe an den Transport-Layer + activeDeviceId.set(device.id); + saveConnectionState({ transport: 'ble', deviceId: device.id, autoConnect: true }); + targetDeviceId.set(device.id); + + addToast(`Verbunden mit ${device.name}`, "success"); + + // Führt die Requests aus und setzt $isConnected = true + await handleTransportConnect(sendBleFrame); + + } catch (error) { + clearTimeout(connectionTimeout); + const errMsg = error instanceof Error ? error.message : String(error); + console.error("Bluetooth Fehler während connectBuzzer:", errMsg); + + if (errMsg.includes("no longer in range")) { + addToast("Geräte-Referenz veraltet (Out of Range). Bitte über den 'Pairen'-Button neu autorisieren.", "error", true); + + if (device) { + const deadId = device.id; + pairedDevices.update(devices => devices.filter(d => d.id !== deadId)); + availableDevices.update(set => { + const newSet = new Set(set); + newSet.delete(deadId); + return newSet; + }); + } + } else { + addToast("Verbindungsfehler: " + errMsg, "error"); + } + + handleTransportDisconnect(); + } finally { + isConnecting.set(false); + } +} + +export function disconnectBuzzer() { + console.debug("disconnectBuzer aufgerufen"); + + if (device) { + saveConnectionState({ + transport: 'ble', + deviceId: device.id, + autoConnect: false + }); + + isConnected.set(false); + + if (device.gatt?.connected) { + device.gatt.disconnect(); + } + } + resetRemote(); + addToast("Verbindung mit " + device.name + " getrennt", "info"); +} + +export async function forgetDevice(targetDevice: BluetoothDevice) { + console.debug("forgetDevice aufgerufen"); + try { + if (targetDevice.gatt?.connected) { + targetDevice.gatt.disconnect(); + } + await targetDevice.forget(); + addToast(`Gerät ${targetDevice.name} vergessen`, "success"); + + const devices = await getPairedDevices(); + if (devices.length === 0) { + isPaired.set(false); + } + } catch (error) { + console.error("Fehler beim Löschen des Geräts:", error); + addToast("Konnte Gerät nicht entfernen", "error", true); + } +} + +export async function getPairedDevices() { + let rawDevices: BluetoothDevice[] = []; + + // 1. Physische Geräte abrufen, falls die API verfügbar ist + if ('bluetooth' in navigator && 'getDevices' in navigator.bluetooth) { + try { + rawDevices = await navigator.bluetooth.getDevices(); + } catch (error) { + console.error("Fehler beim Abrufen der gekoppelten Geräte:", error); + } + } + + // 2. Physische Geräte in den Store schreiben + pairedDevices.set(rawDevices); + + // 3. Testdaten anfügen + injectDummyDevices(); + + // 4. Den aktualisierten Store-Inhalt (inkl. Dummies) für die weiterverarbeitenden Funktionen zurückgeben + return get(pairedDevices); +} + +function handleDisconnect() { + console.debug("handleDisconnect aufgerufen"); + + if (get(isConnected)) { + addToast("Verbindung zu Buzzer verloren", "warning"); + } + + resetRemote(); + registerTransport(null); + rxCharacteristic = null; + txCharacteristic = null; +} + +function handleIncomingData(event: Event) { + const target = event.target as BluetoothRemoteGATTCharacteristic; + if (target.value) { + parseIncomingFrame(target.value, (buffer) => sendBleFrame(buffer)); + } +} + +export async function sendBleFrame(buffer: ArrayBuffer) { + // TODO: MTU Check einfügen! + if (!rxCharacteristic) return; + await rxCharacteristic.writeValueWithoutResponse(buffer); +} diff --git a/webpage/src/lib/init.ts b/webpage/src/lib/init.ts new file mode 100644 index 0000000..ca3e0c5 --- /dev/null +++ b/webpage/src/lib/init.ts @@ -0,0 +1,24 @@ +// src/lib/init.ts +import { isBluetoothSupported, isSerialSupported, isInitializing } from './store'; + +export function getBrowserName(): string { + const ua = navigator.userAgent; + if (ua.includes("Firefox")) return "Feuerfuchs"; + if (ua.includes("Safari") && !ua.includes("Chrome")) return "Apfel-Rundreise"; + if (ua.includes("Edg")) return "Winzigweich Kante"; + if (ua.includes("Chrome")) return "Google Glanzeisen"; + return "dein Browser"; +} + +export function performHardwareCheck() { + if (typeof navigator === 'undefined') return; + + // Web Bluetooth Check + const hasBT = 'bluetooth' in navigator; + // Web Serial Check + const hasSerial = 'serial' in navigator; + + isBluetoothSupported.set(hasBT); + isSerialSupported.set(hasSerial); + isInitializing.set(false); +} \ No newline at end of file diff --git a/webpage/src/lib/protocol/constants.ts b/webpage/src/lib/protocol/constants.ts new file mode 100644 index 0000000..bc98f46 --- /dev/null +++ b/webpage/src/lib/protocol/constants.ts @@ -0,0 +1,56 @@ +export const BLE = { + SERVICE_UUID: "e517d988-bab5-4574-8479-97c6cb115ca0", + RX_UUID: "e517d988-bab5-4574-8479-97c6cb115ca1", + TX_UUID: "e517d988-bab5-4574-8479-97c6cb115ca2" +}; + +export const FRAME = { + REQUEST: 0x00, + + RESPONSE: 0x10, + ACK: 0x11, + ERROR: 0x12, + + FILE_START: 0x20, + FILE_CHUNK: 0x21, + FILE_END: 0x22, + + LS_START: 0x40, + LS_ENTRY: 0x41, + LS_END: 0x42, +}; + +export const DATA = { + PROTO_INFO: 0x01, + FS_INFO: 0x03, + + FILE_GET: 0x20, + FILE_PUT: 0x21, + + + LS: 0x40 +}; + +export const FS_ENTRY_TYPE = { + FILE: 0x00, + DIR: 0x01, +} + +export interface ZephyrError { + text: string; + zephyr: string; +} + +export const ZEPHYR_ERRORS: Record = { + 1: { text: "Fehlende Berechtigung", zephyr: "EPERM" }, + 2: { text: "Datei oder Verzeichnis nicht gefunden", zephyr: "ENOENT" }, + 5: { text: "Ein-/Ausgabefehler auf dem Flash", zephyr: "EIO" }, + 12: { text: "Nicht genügend Speicher frei", zephyr: "ENOMEM" }, + 16: { text: "Gerät oder Ressource belegt", zephyr: "EBUSY" }, + 22: { text: "Ungültiges Argument oder Parameter", zephyr: "EINVAL" }, + 24: { text: "Zu viele offene Dateien", zephyr: "EMFILE" }, + 28: { text: "Kein freier Speicherplatz mehr", zephyr: "ENOSPC" }, + 36: { text: "Dateiname oder Pfad zu lang", zephyr: "ENAMETOOLONG" }, + 88: { text: "Funktion im Buzzer nicht implementiert", zephyr: "ENOSYS" }, + 134: { text: "Operation nicht unterstützt", zephyr: "ENOTSUP" } +}; \ No newline at end of file diff --git a/webpage/src/lib/protocol/index.ts b/webpage/src/lib/protocol/index.ts new file mode 100644 index 0000000..5fcd2d9 --- /dev/null +++ b/webpage/src/lib/protocol/index.ts @@ -0,0 +1,2 @@ +export * from './constants'; +export * from './parser'; \ No newline at end of file diff --git a/webpage/src/lib/protocol/parser.ts b/webpage/src/lib/protocol/parser.ts new file mode 100644 index 0000000..bec5f37 --- /dev/null +++ b/webpage/src/lib/protocol/parser.ts @@ -0,0 +1,309 @@ +import { FRAME, DATA, ZEPHYR_ERRORS } from './constants'; +import { protocolInfo, fsInfo } from '../store'; +import { addToast } from '../toast'; + +export type FrameSender = (buffer: ArrayBuffer) => Promise; + +let lsBuffer: any[] = []; +let lsTimeout: ReturnType | null = null; +let lsResolve: ((data: any[]) => void) | null = null; +let lsReject: ((error: Error) => void) | null = null; +let fileGetResolve: ((success: boolean) => void) | null = null; +let fileGetReject: ((error: Error) => void) | null = null; + +export function showErrorToast(errorCode: number) { + const errorInfo = ZEPHYR_ERRORS[errorCode]; + const hexCode = errorCode.toString(16).padStart(2, '0'); + + if (errorInfo) { + // Variante mit HTML-Tags (erfordert {@html ...} im Svelte-Template) + const htmlMessage = `Buzzer: ${errorInfo.zephyr} (0x${hexCode}): ${errorInfo.text} `; + addToast(htmlMessage, 'error'); + + /* Alternativ als reiner Text, falls HTML im Toast nicht unterstützt wird: + const textMessage = `Buzzer: [${errorInfo.zephyr}] ${errorInfo.text} (0x${hexCode})`; + addToast(textMessage, 'error'); + */ + } else { + addToast(`Der Buzzer meldet einen unbekannten Fehler: 0x${hexCode}`, 'error'); + } +} + +export function parseIncomingFrame(view: DataView, sender: FrameSender) { + if (view.byteLength < 3) return; + + const frameType = view.getUint8(0); + const payloadLength = view.getUint16(1, true); + + switch (frameType) { + case FRAME.RESPONSE: + const dataType = view.getUint8(3); + + if (dataType === DATA.PROTO_INFO && payloadLength >= 5) { + const version = view.getUint16(4, true); + const maxChunkSize = view.getUint16(6, true); + protocolInfo.set({ version, maxChunkSize }); + } else if (dataType === DATA.FS_INFO && payloadLength >= 14) { + const totalSizeBytes = view.getUint32(4, true); + const freeSizeBytes = view.getUint32(8, true); + const maxPathLength = view.getUint8(12); + const sysPathLength = view.getUint8(13); + const audioPathLength = view.getUint8(14); + const sysPath = new TextDecoder().decode(new Uint8Array(view.buffer, 15, sysPathLength)); + const audioPath = new TextDecoder().decode(new Uint8Array(view.buffer, 15 + sysPathLength, audioPathLength)); + fsInfo.set({ totalSize: totalSizeBytes / 1024 / 1024, freeSize: freeSizeBytes / 1024 / 1024, maxPathLength, sysPath, audioPath }); + } + break; + + case FRAME.LS_START: + lsBuffer = []; + resetLsWatchdog(); + handleLsStart(sender); + break; + + case FRAME.LS_ENTRY: + resetLsWatchdog(); + if (payloadLength >= 6) { + const type = view.getUint8(3); + const size = view.getUint32(4, true); + const nameLen = view.getUint8(8); + const name = new TextDecoder().decode(new Uint8Array(view.buffer, 9, nameLen)); + + lsBuffer.push({ type, size, name }); + + // TODO: Mehr credits senden, wenn diese knapp werden + } + break; + + case FRAME.LS_END: + if (lsTimeout) clearTimeout(lsTimeout); + const total = view.getUint32(3, true); + console.debug(`LS Stream beendet. Erwartete Einträge: ${total}, empfangen: ${lsBuffer.length}`, lsBuffer); + if (total !== lsBuffer.length) { + console.warn(`LS Stream: Erwartete Anzahl Einträge laut Header: ${total}, tatsächlich empfangen: ${lsBuffer.length}`); + addToast(`Warnung: LS Stream erwartete ${total} Einträge, aber nur ${lsBuffer.length} empfangen.`, 'warning'); + } else if (lsResolve) { + lsResolve([...lsBuffer]); + lsResolve = null; + lsReject = null; + } + break; + +case FRAME.FILE_START: + fileTransfer.totalBytes = view.getUint32(3, true); + fileTransfer.receivedBytes = 0; + fileTransfer.lastReceivedBytes = 0; + fileTransfer.stalledSeconds = 0; + fileTransfer.active = true; + fileTransfer.startTime = performance.now(); + + console.log(`[FILE_GET] Stream gestartet. Erwartete Größe: ${fileTransfer.totalBytes} Bytes.`); + + fileTransfer.metricsTimer = setInterval(() => { + if (!fileTransfer.active) return; + + // Watchdog-Logik: Prüfen ob seit der letzten Sekunde Daten kamen + if (fileTransfer.receivedBytes === fileTransfer.lastReceivedBytes) { + fileTransfer.stalledSeconds++; + + if (fileTransfer.stalledSeconds >= 5) { // 5 Sekunden Timeout + console.warn("[FILE_GET] Übertragung abgebrochen: Timeout (Keine Daten empfangen)."); + + if (fileTransfer.metricsTimer) clearInterval(fileTransfer.metricsTimer); + fileTransfer.active = false; + + // Hier optional einen Toast anzeigen lassen, falls importiert: + // addToast("Dateitransfer abgebrochen (Timeout)", "error"); + + if (fileGetReject) { + fileGetReject(new Error("Timeout beim Dateitransfer")); + fileGetResolve = null; + fileGetReject = null; + } + return; + } + } else { + // Daten fließen -> Watchdog zurücksetzen + fileTransfer.stalledSeconds = 0; + fileTransfer.lastReceivedBytes = fileTransfer.receivedBytes; + } + + const elapsedSec = (performance.now() - fileTransfer.startTime) / 1000; + const speedKB = (fileTransfer.receivedBytes / 1024) / elapsedSec; + const percent = fileTransfer.totalBytes > 0 + ? ((fileTransfer.receivedBytes / fileTransfer.totalBytes) * 100).toFixed(1) + : "0.0"; + + console.log(`[FILE_GET] Fortschritt: ${percent}% | Speed: ${speedKB.toFixed(2)} KB/s`); + }, 1000); + + // Initiale Credits (z.B. 64) + fileTransfer.credits = 128; + sendCredits(fileTransfer.credits, sender); + break; + + case FRAME.FILE_CHUNK: + if (!fileTransfer.active) break; + + fileTransfer.receivedBytes += payloadLength; + fileTransfer.credits--; + + // Nachladen, sobald die Credits auf 32 fallen (Dein Vorschlag) + if (fileTransfer.credits <= 64) { + fileTransfer.credits = 128; + sendCredits(fileTransfer.credits, sender); + + } + break; + + case FRAME.FILE_END: + if (fileTransfer.metricsTimer) { + clearInterval(fileTransfer.metricsTimer); + fileTransfer.metricsTimer = null; + } + fileTransfer.active = false; + + const crc32 = view.getUint32(3, true); + const totalElapsed = (performance.now() - fileTransfer.startTime) / 1000; + const avgSpeed = (fileTransfer.receivedBytes / 1024) / totalElapsed; + + console.log(`[FILE_GET] Stream beendet.`); + console.log(`[FILE_GET] Empfangen: ${fileTransfer.receivedBytes} Bytes in ${totalElapsed.toFixed(2)}s.`); + console.log(`[FILE_GET] Durchschnitt: ${avgSpeed.toFixed(2)} KB/s`); + console.log(`[FILE_GET] Zephyr CRC32: 0x${crc32.toString(16).toUpperCase().padStart(8, '0')}`); + + if (fileGetResolve) { + fileGetResolve(true); + fileGetResolve = null; + fileGetReject = null; + } + break; + + case FRAME.ERROR: + const errorCode = view.getUint16(3, true); + console.error(`Received error frame with code: 0x${errorCode.toString(16)}`); + showErrorToast(errorCode); + if (lsReject) { + lsReject(new Error(`Buzzer Error 0x${errorCode.toString(16)}`)); + lsResolve = null; + lsReject = null; + } + if (fileGetReject && fileTransfer.active) { + if (fileTransfer.metricsTimer) clearInterval(fileTransfer.metricsTimer); + fileTransfer.active = false; + fileGetReject(new Error(`Buzzer Error 0x${errorCode.toString(16)}`)); + fileGetResolve = null; + fileGetReject = null; + } + break; + + default: + console.error(`Unknown frame type received: 0x${frameType.toString(16)}`); + addToast(`Unbekannter Frame-Typ empfangen: 0x${frameType.toString(16)}`, 'error'); + } +} + +export function buildProtocolInfoRequest(): ArrayBuffer { + const buffer = new ArrayBuffer(4); + const view = new DataView(buffer); + + view.setUint8(0, FRAME.REQUEST); + view.setUint16(1, 1, true); + view.setUint8(3, DATA.PROTO_INFO); + + return buffer; +} + +export function buildFSInfoRequest(): ArrayBuffer { + const buffer = new ArrayBuffer(4); + const view = new DataView(buffer); + + view.setUint8(0, FRAME.REQUEST); + view.setUint16(1, 1, true); + view.setUint8(3, DATA.FS_INFO); + + return buffer; +} + +export function buildLSRequest(path: string): ArrayBuffer { + const encoder = new TextEncoder(); + const pathBytes = encoder.encode(path); + const buffer = new ArrayBuffer(4 + pathBytes.length); + const view = new DataView(buffer); + + view.setUint8(0, FRAME.REQUEST); + view.setUint16(1, 1 + pathBytes.length, true); // Payload: DataType(1) + String + view.setUint8(3, DATA.LS); + + const uint8Buffer = new Uint8Array(buffer); + uint8Buffer.set(pathBytes, 4); + + return buffer; +} + +async function handleLsStart(send: FrameSender) { + lsBuffer = []; + await sendCredits(64, send); +} + +async function sendCredits(count: number, send: FrameSender) { + const buffer = new ArrayBuffer(5); + const view = new DataView(buffer); + + console.debug(`Sende ${count} Credits für Stream...`); + view.setUint8(0, FRAME.ACK); + view.setUint16(1, 2, true); + view.setUint16(3, count, true); + + await send(buffer); +} + +function resetLsWatchdog() { + if (lsTimeout) clearTimeout(lsTimeout); + lsTimeout = setTimeout(() => { + addToast("Verzeichnis-Streaming abgebrochen (Timeout)", "warning"); + lsBuffer = []; + if (lsReject) { + lsReject(new Error("Timeout beim Lesen des Verzeichnisses")); + lsResolve = null; + lsReject = null; + } + }, 3000); +} + +export function setLsResolver(resolve: (data: any[]) => void, reject: (error: Error) => void) { + lsResolve = resolve; + lsReject = reject; +} + +const fileTransfer = { + active: false, + startTime: 0, + totalBytes: 0, + receivedBytes: 0, + lastReceivedBytes: 0, // NEU: Für die Timeout-Berechnung + stalledSeconds: 0, // NEU: Zähler für Stillstand + credits: 0, + metricsTimer: null as ReturnType | null +}; + +export function setFileGetResolver(resolve: (success: boolean) => void, reject: (error: Error) => void) { + fileGetResolve = resolve; + fileGetReject = reject; +} + +export function buildFileGetRequest(path: string): ArrayBuffer { + const encoder = new TextEncoder(); + const pathBytes = encoder.encode(path); + const buffer = new ArrayBuffer(4 + pathBytes.length); + const view = new DataView(buffer); + + view.setUint8(0, FRAME.REQUEST); + view.setUint16(1, 1 + pathBytes.length, true); + view.setUint8(3, DATA.FILE_GET); + + const uint8Buffer = new Uint8Array(buffer); + uint8Buffer.set(pathBytes, 4); + + return buffer; +} \ No newline at end of file diff --git a/webpage/src/lib/settings.ts b/webpage/src/lib/settings.ts new file mode 100644 index 0000000..d30cace --- /dev/null +++ b/webpage/src/lib/settings.ts @@ -0,0 +1,11 @@ +export const SETTINGS = { + storage: { + connectionKey: 'buzzer_connection_state' + }, + bluetooth: { + connectionTimeoutMs: 10000 // Timeout für den Verbindungsaufbau + }, + ui: { + toastDurationMs: 5000 + } +}; \ No newline at end of file diff --git a/webpage/src/lib/store.ts b/webpage/src/lib/store.ts new file mode 100644 index 0000000..da1bc58 --- /dev/null +++ b/webpage/src/lib/store.ts @@ -0,0 +1,182 @@ +import { writable, derived } from 'svelte/store'; +import type { BuzzerFile } from './types'; + +// Fallback-Typ fuer Build-Umgebungen ohne DOM-Library. +interface BluetoothDevice { + id: string; + name?: string | null; + gatt?: { + connected?: boolean; + disconnect?: () => void; + }; + forget?: () => Promise; + addEventListener?: (...args: unknown[]) => void; + removeEventListener?: (...args: unknown[]) => void; + watchAdvertisements?: () => Promise; +} + +type TransportType = 'ble' | 'serial'; + +export interface ConnectionState { + transport: TransportType; + deviceId: string; + autoConnect: boolean; +} + +export interface ProtocolInfo { + version: number; + maxChunkSize: number; +} + +export interface FsInfo { + totalSize: number; + freeSize: number; + maxPathLength: number; + sysPath: string; + audioPath: string; +} + +export interface StorageUsage { + totalBytes: number; + freeBytes: number; + audioBytes: number; + systemBytes: number; + audioPercent: number; + systemPercent: number; + freePercent: number; +} + +const CONNECTION_STATE_KEY = 'buzzer_connection_state'; + +// App-Status: Initialisierung und Feature-Support +export const isInitializing = writable(true); +export const isBluetoothSupported = writable(null); +export const isSerialSupported = writable(null); + +// Verbindungszustand +export const isConnected = writable(false); +export const isConnecting = writable(false); +export const isPaired = writable(false); +export const activeDeviceId = writable(null); +export const targetDeviceId = writable(null); + +// Geräteverwaltung (Pairing + Discovery) +export const pairedDevices = writable([]); +export const availableDevices = writable>(new Set()); // IDs der derzeit advertisierten Geräte + +// Protokoll- und Dateisystem-Metadaten aus dem Device +export const protocolInfo = writable(null); +export const fsInfo = writable(null); + +// Dateilisten +export const buzzerAudioFiles = writable([]); +export const buzzerSysFiles = writable([]); +export const localAudioFiles = writable([]); + +// Ladezustände getrennt nach Quelle +export const isFetchingRemote = writable(false); +export const isFetchingLocal = writable(false); + +// Persistenz des letzten Verbindungsziels (nur im Browser nutzbar) +export function saveConnectionState(state: ConnectionState): void { + if (typeof window === 'undefined' || !window.localStorage) { + return; + } + + localStorage.setItem(CONNECTION_STATE_KEY, JSON.stringify(state)); +} + +export function loadConnectionState(): ConnectionState | null { + if (typeof window === 'undefined' || !window.localStorage) { + return null; + } + + const data = localStorage.getItem(CONNECTION_STATE_KEY); + return data ? (JSON.parse(data) as ConnectionState) : null; +} + +// Abgeleitete Berechnung für die Speicherbalken in der UI +export const storageUsage = derived( + [fsInfo, buzzerAudioFiles], + ([$fsInfo, $buzzerAudioFiles]): StorageUsage | null => { + if (!$fsInfo) { + return null; + } + + const totalBytes = $fsInfo.totalSize * 1024 * 1024; + const freeBytes = $fsInfo.freeSize * 1024 * 1024; + const audioBytes = $buzzerAudioFiles.reduce((sum, file) => sum + file.size, 0); + const systemBytes = Math.max(0, totalBytes - freeBytes - audioBytes); + + return { + totalBytes, + freeBytes, + audioBytes, + systemBytes, + audioPercent: totalBytes === 0 ? 0 : (audioBytes / totalBytes) * 100, + systemPercent: totalBytes === 0 ? 0 : (systemBytes / totalBytes) * 100, + freePercent: totalBytes === 0 ? 0 : (freeBytes / totalBytes) * 100, + }; + }, +); + +// Nur für Entwicklungszwecke: lokale Dummy-Geräte für UI-Tests +export function injectDummyDevices(): void { + const dummy1 = { + id: 'dummy-1', + name: 'Dev Buzzer (Erreichbar)', + forget: async () => { + console.log('Forget dummy-1'); + }, + addEventListener: () => {}, + removeEventListener: () => {}, + watchAdvertisements: async () => {}, + gatt: { connected: false, disconnect: () => {} }, + } as unknown as BluetoothDevice; + + const dummy2 = { + id: 'dummy-2', + name: 'Dev Buzzer (Offline)', + forget: async () => { + console.log('Forget dummy-2'); + }, + addEventListener: () => {}, + removeEventListener: () => {}, + watchAdvertisements: async () => {}, + gatt: { connected: false, disconnect: () => {} }, + } as unknown as BluetoothDevice; + + pairedDevices.update((devices) => { + if (!devices.find((d) => d.id === 'dummy-1')) { + return [...devices, dummy1, dummy2]; + } + return devices; + }); + + availableDevices.update((set) => { + const newSet = new Set(set); + newSet.add('dummy-1'); + return newSet; + }); +} + +export function resetRemote(): void { + isConnected.set(false); + isConnecting.set(false); + protocolInfo.set(null); + fsInfo.set(null); + activeDeviceId.set(null); + buzzerAudioFiles.set([]); + buzzerSysFiles.set([]); + isFetchingRemote.set(false); +} + +export function resetLocal(): void { + localAudioFiles.set([]); + isFetchingLocal.set(false); +} + +export function resetAll(): void { + resetRemote(); + resetLocal(); +} \ No newline at end of file diff --git a/webpage/src/lib/sync.ts b/webpage/src/lib/sync.ts new file mode 100644 index 0000000..e8ba464 --- /dev/null +++ b/webpage/src/lib/sync.ts @@ -0,0 +1,59 @@ +import { get } from 'svelte/store'; +import { isConnected, fsInfo, buzzerAudioFiles, buzzerSysFiles, isFetchingRemote, isFetchingLocal, storageUsage} from './store'; +import { requestProtocolInfo, requestFSInfo, fetchDirectory } from './transport'; +import type { BuzzerFile } from './types'; + +function mapToBuzzerFile(rawFile: any): BuzzerFile { + return { + name: rawFile.name, + size: rawFile.size, + type: rawFile.type, + tagsLoaded: false, + sysTags: { format: null, crc32: null }, + metaTags: {} + }; +} + +export async function refreshRemote() { + if (!get(isConnected)) return; + + isFetchingRemote.set(true); + try { + await requestProtocolInfo(); + await requestFSInfo(); + + // Kurze Verzögerung für Store-Propagation + await new Promise(r => setTimeout(r, 100)); + + const currentFsInfo = get(fsInfo); + + // Sequenzielle Abfrage via Transport-Layer + const sysFiles = await fetchDirectory(currentFsInfo?.sysPath || "/lfs/sys"); + buzzerSysFiles.set(sysFiles.map(mapToBuzzerFile)); + + const audioFiles = await fetchDirectory(currentFsInfo?.audioPath || "/lfs/a"); + buzzerAudioFiles.set(audioFiles.map(mapToBuzzerFile)); + + console.log("Audiodatein: ", audioFiles); + console.log("Systemdatein: ", sysFiles); + console.log("Aktuelle FS-Info: ", currentFsInfo); + console.log("Storage Usage: ", get(storageUsage)); + } catch (error) { + console.error("Fehler beim Aktualisieren der Buzzer-Daten:", error); + } finally { + isFetchingRemote.set(false); + } +} + +export async function refreshLocal() { + isFetchingLocal.set(true); + try { + // TODO: Implementierung lokaler Dateisystem-Zugriff (z.B. File System Access API) + // const files = await readLocalDirectory(); + // localAudioFiles.set(files.map(mapToBuzzerFile)); + } catch (error) { + console.error("Fehler beim Aktualisieren der lokalen Daten:", error); + } finally { + isFetchingLocal.set(false); + } +} \ No newline at end of file diff --git a/webpage/src/lib/toast.ts b/webpage/src/lib/toast.ts new file mode 100644 index 0000000..113e421 --- /dev/null +++ b/webpage/src/lib/toast.ts @@ -0,0 +1,31 @@ +import { writable } from 'svelte/store'; +import { SETTINGS } from './settings'; + +export type ToastType = 'success' | 'info' | 'warning' | 'error'; + +export interface ToastMessage { + id: string; + type: ToastType; + message: string; + dismissible: boolean; +} + +export const toasts = writable([]); + +export function addToast(message: string, type: ToastType = 'info', dismissible: boolean = false) { + const id = Math.random().toString(36).substring(2, 9); + + toasts.update(all => [...all, { id, type, message, dismissible }]); + + if (!dismissible) { + setTimeout(() => removeToast(id), SETTINGS.ui.toastDurationMs); + } +} + +export function removeToast(id: string) { + toasts.update(all => all.filter(t => t.id !== id)); +} + +export function clearAllToasts() { + toasts.set([]); +} \ No newline at end of file diff --git a/webpage/src/lib/transport.ts b/webpage/src/lib/transport.ts new file mode 100644 index 0000000..35b8cc1 --- /dev/null +++ b/webpage/src/lib/transport.ts @@ -0,0 +1,92 @@ +import { buildLSRequest, buildProtocolInfoRequest, buildFSInfoRequest, setLsResolver } from './protocol/parser'; +import { buildFileGetRequest, setFileGetResolver } from './protocol/parser'; +import { isConnected, resetRemote } from './store'; + +export type FrameSender = (buffer: ArrayBuffer) => Promise; +let currentSender: FrameSender | null = null; + +export function registerTransport(sender: FrameSender | null) { + currentSender = sender; +} + +// NEU: Wird von bluetooth.ts oder serial.ts nach dem physischen Connect gerufen +export async function handleTransportConnect(sender: FrameSender) { + registerTransport(sender); + + try { + // Basis-Informationen zwingend vorab laden + await requestProtocolInfo(); + await requestFSInfo(); + + // Erst wenn diese Basisdaten da sind, wird die UI freigeschaltet + isConnected.set(true); + } catch (error) { + console.error("Transport-Initialisierung fehlgeschlagen:", error); + handleTransportDisconnect(); + } +} + +export async function sendFrame(buffer: ArrayBuffer) { + if (!currentSender) throw new Error("Kein Transportweg registriert."); + await currentSender(buffer); +} + +export async function requestProtocolInfo() { + await sendFrame(buildProtocolInfoRequest()); +} + +export async function requestFSInfo() { + await sendFrame(buildFSInfoRequest()); +} + +let isListing = false; + +export async function fetchDirectory(path: string): Promise { + if (isListing) { + throw new Error("Ein Verzeichnis-Stream läuft bereits. Bitte warten."); + } + isListing = true; + + return new Promise(async (resolve, reject) => { + // Dem Parser sagen, wen er bei Erfolg/Fehler anrufen soll + setLsResolver( + (data) => { isListing = false; resolve(data); }, + (err) => { isListing = false; reject(err); } + ); + + try { + await sendFrame(buildLSRequest(path)); + } catch (e) { + isListing = false; + reject(e); + } + }); +} + +export function handleTransportDisconnect() { + registerTransport(null); + resetRemote(); +} + +let isFileTransferring = false; + +export async function fetchFileThroughputTest(path: string): Promise { + if (isFileTransferring) { + throw new Error("Ein Dateitransfer läuft bereits."); + } + isFileTransferring = true; + + return new Promise(async (resolve, reject) => { + setFileGetResolver( + (success) => { isFileTransferring = false; resolve(success); }, + (err) => { isFileTransferring = false; reject(err); } + ); + + try { + await sendFrame(buildFileGetRequest(path)); + } catch (e) { + isFileTransferring = false; + reject(e); + } + }); +} \ No newline at end of file diff --git a/webpage/src/lib/types.ts b/webpage/src/lib/types.ts new file mode 100644 index 0000000..441afc2 --- /dev/null +++ b/webpage/src/lib/types.ts @@ -0,0 +1,29 @@ +export interface AudioFormat { + codec: number; + bitDepth: number; + sampleRate: number; +} + +export interface SystemTags { + format: AudioFormat | null; + crc32: number | null; +} + +export interface MetadataTags { + title?: string; // "t" + author?: string; // "a" + remarks?: string; // "r" + categories?: string[];// "c" + dateCreated?: string; // "dc" + dateSaved?: string; // "ds" + [key: string]: any; +} + +export interface BuzzerFile { + name: string; + size: number; // in Bytes + type: number; // 0 = File, 1 = Dir + tagsLoaded: boolean; + sysTags: SystemTags; + metaTags: MetadataTags; +} \ No newline at end of file diff --git a/webpage/src/pages/index.astro b/webpage/src/pages/index.astro index 9ed8033..00b3c9b 100644 --- a/webpage/src/pages/index.astro +++ b/webpage/src/pages/index.astro @@ -1,53 +1,41 @@ --- import MainLayout from "../layouts/MainLayout.astro"; +import BuzzerControl from "../components/BuzzerControl.svelte"; +import BLEList from "../components/BLEList.svelte"; +import AppGuard from "../components/AppGuard.svelte"; +import FlashUsage from "../components/FlashUsage.svelte"; --- -
-
-

Titel

-

- Li Europan lingues es membres del sam familie. Lor separat - existentie es un myth. Por scientie, musica, sport etc, litot - Europa usa li sam vocabular. Li lingues differe solmen in li - grammatica, li pronunciation e li plu commun vocabules. Omnicos - directe al desirabilite de un nov lingua franca: On refusa - continuar payar custosi traductores. At solmen va esser necessi - far uniform grammatica, pronunciation e plu sommun paroles. Ma - quande lingues coalesce, li grammatica del resultant lingue es - plu simplic e regulari quam ti del coalescent lingues. Li nov - lingua franca va esser plu simplic e regulari quam li existent - Europan lingues. -

-
-
-

Titel

-

- Li Europan lingues es membres del sam familie. Lor separat - existentie es un myth. Por scientie, musica, sport etc, litot - Europa usa li sam vocabular. Li lingues differe solmen in li - grammatica, li pronunciation e li plu commun vocabules. Omnicos - directe al desirabilite de un nov lingua franca: On refusa - continuar payar custosi traductores. At solmen va esser necessi - far uniform grammatica, pronunciation e plu sommun paroles. Ma - quande lingues coalesce, li grammatica del resultant lingue es - plu simplic e regulari quam ti del coalescent lingues. Li nov - lingua franca va esser plu simplic e regulari quam li existent - Europan lingues. -

+ +
+
+

+ Buzzer Management +

+

+ Verbinde dich mit dem nRF52840 Buzzer, um Audio-Dateien zu übertragen und + Systemparameter auszulesen. +

+
-

- It va esser tam simplic quam Occidental in fact, it va esser - Occidental. A un Angleso it va semblar un simplificat Angles, - quam un skeptic Cambridge amico dit me que Occidental es. Li - Europan lingues es membres del sam familie. Lor separat - existentie es un myth. Por scientie, musica, sport etc, litot - Europa usa li sam vocabular. Li lingues differe solmen in li - grammatica, li pronunciation e li plu commun vocabules. Omnicos - directe al desirabilite de un nov lingua franca: On refusa - continuar payar custosi traductores. At solmen va esser necessi - far uniform grammatica, pronunciation e plu sommun paroles. -

-
-
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+