vor ble umbau

This commit is contained in:
2026-03-12 07:07:00 +01:00
parent 96aed70fc6
commit 5bb0d345da
45 changed files with 3681 additions and 48 deletions

View File

@@ -1,3 +1,6 @@
{ {
"svelte.plugin.svelte.format.config.printWidth": 300 "svelte.plugin.svelte.format.config.printWidth": 300,
"nrf-connect.applications": [
"${workspaceFolder}/firmware"
]
} }

1
firmware/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
build*/

9
firmware/CMakeLists.txt Normal file
View File

@@ -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)

View File

@@ -0,0 +1,8 @@
/{
chosen {
nordic,pm-ext-flash = &mx25r64;
};
aliases {
qspi-flash = &mx25r64;
};
};

View File

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

3
firmware/libs/Kconfig Normal file
View File

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

View File

@@ -0,0 +1,5 @@
if(CONFIG_BLE_MGMT)
zephyr_library()
zephyr_library_sources(src/ble_mgmt.c)
zephyr_include_directories(include)
endif()

View File

@@ -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

View File

@@ -0,0 +1,38 @@
#ifndef BLE_MGMT_H
#define BLE_MGMT_H
#include <zephyr/types.h>
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

View File

@@ -0,0 +1,258 @@
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/hci.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/uuid.h>
#include <zephyr/bluetooth/gatt.h>
#include <zephyr/logging/log.h>
#include <zephyr/bluetooth/gatt.h>
#include <string.h>
#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,
};

View File

@@ -0,0 +1,5 @@
if(CONFIG_BUZZ_PROTO)
zephyr_library()
zephyr_library_sources(src/buzz_proto.c)
zephyr_include_directories(include)
endif()

View File

@@ -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

View File

@@ -0,0 +1,145 @@
#ifndef BUZZ_PROTO_H
#define BUZZ_PROTO_H
#include <stdint.h>
#include <stddef.h>
#include <zephyr/sys/byteorder.h>
#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 */

View File

@@ -0,0 +1,619 @@
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/logging/log_ctrl.h>
#include <zephyr/fs/fs.h>
#include <zephyr/sys/crc.h>
#include <errno.h>
#include <stdlib.h>
#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);

View File

@@ -0,0 +1,17 @@
if(CONFIG_FS_MGMT)
zephyr_library()
zephyr_library_sources(src/fs_mgmt.c)
zephyr_include_directories(include)
if(CONFIG_FILE_SYSTEM_LITTLEFS)
if(DEFINED ZEPHYR_LITTLEFS_MODULE_DIR)
zephyr_include_directories(${ZEPHYR_LITTLEFS_MODULE_DIR})
elseif(DEFINED WEST_TOPDIR)
zephyr_include_directories(${WEST_TOPDIR}/modules/fs/littlefs)
endif()
if(DEFINED ZEPHYR_BASE)
zephyr_include_directories(${ZEPHYR_BASE}/modules/littlefs)
endif()
endif()
endif()

View File

@@ -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

View File

@@ -0,0 +1,117 @@
#ifndef FS_MGMT_H
#define FS_MGMT_H
#include <zephyr/fs/fs.h>
#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 */

View File

@@ -0,0 +1,373 @@
#include <zephyr/fs/littlefs.h>
#include <zephyr/fs/fs.h>
#include <zephyr/logging/log.h>
#include <zephyr/pm/device.h>
#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;
}

View File

@@ -0,0 +1,4 @@
name: libs
build:
cmake: .
kconfig: Kconfig

View File

@@ -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

35
firmware/prj.conf Normal file
View File

@@ -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

66
firmware/src/main.c Normal file
View File

@@ -0,0 +1,66 @@
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <string.h>
#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);
}

313
protocol.md Normal file
View File

@@ -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
```

View File

@@ -6,7 +6,7 @@
{ {
"files": ["*.svelte", "*.astro"], "files": ["*.svelte", "*.astro"],
"options": { "options": {
"printWidth": 1000 "printWidth": 100
} }
} }
] ]

View File

@@ -17,6 +17,7 @@
"typescript": "^5.9.3" "typescript": "^5.9.3"
}, },
"devDependencies": { "devDependencies": {
"phosphor-svelte": "^3.1.0",
"prettier": "^3.8.1", "prettier": "^3.8.1",
"prettier-plugin-astro": "^0.14.1" "prettier-plugin-astro": "^0.14.1"
} }
@@ -4490,6 +4491,26 @@
"url": "https://github.com/inikulin/parse5?sponsor=1" "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": { "node_modules/piccolore": {
"version": "0.1.3", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz",

View File

@@ -18,6 +18,7 @@
"typescript": "^5.9.3" "typescript": "^5.9.3"
}, },
"devDependencies": { "devDependencies": {
"phosphor-svelte": "^3.1.0",
"prettier": "^3.8.1", "prettier": "^3.8.1",
"prettier-plugin-astro": "^0.14.1" "prettier-plugin-astro": "^0.14.1"
} }

View File

@@ -0,0 +1,61 @@
<script lang="ts">
import { onMount } from "svelte";
import { isInitializing, isBluetoothSupported, isSerialSupported } from "../lib/store";
import { performHardwareCheck, getBrowserName } from "../lib/init";
import ToastContainer from "./ToastContainer.svelte";
import { injectDummyDevices } from "../lib/store";
let browserName = "";
onMount(() => {
browserName = getBrowserName();
performHardwareCheck();
injectDummyDevices(); // Fügt Dummy-Geräte für Testzwecke hinzu
});
</script>
{#if $isInitializing}
<div class="fixed inset-0 bg-slate-50 flex items-center justify-center z-[100]">
<p class="text-slate-600 font-mono animate-pulse">SYSTEM_CHECK_RUNNING...</p>
</div>
{:else if !$isBluetoothSupported && !$isSerialSupported}
<div class="min-h-[60vh] flex items-center justify-center p-4">
<div class="max-w-md w-full bg-white border-2 border-red-600 shadow-2xl rounded-sm p-8 pb-4">
<h1 class="text-2xl font-black text-red-600 mb-4 uppercase italic">Inkompatibler Browser</h1>
<p class="text-slate-800 mb-6">
Du nutzt aktuell <strong>{browserName}</strong>
. Dieser Browser unterstützt weder Bluetooth noch serielle USB-Verbindungen.
</p>
<div class="space-y-2 mb-4 text-sm font-mono">
<div class="flex justify-between border-b border-slate-100 pb-1">
<span>Web Bluetooth:</span>
<span class="text-red-600 font-bold">NICHT UNTERSTÜTZT</span>
</div>
<div class="flex justify-between border-b border-slate-100 pb-1">
<span>Web Serial:</span>
<span class="text-red-600 font-bold">NICHT UNTERSTÜTZT</span>
</div>
</div>
<p class="text-xs text-slate-500 mb-6 italic">
(Info: Firefox und Safari blockieren diese Hardware-Schnittstellen aus Prinzip.)
</p>
<a
href="https://www.google.com/chrome/"
class="block w-full text-center bg-blue-600 text-white py-3 font-bold hover:bg-blue-700 transition uppercase tracking-widest text-sm mb-4"
>
Googles Glanzeisen installieren
</a>
<p class="text-xs text-slate-500 mb-6 italic">
Gerüchten zufolge soll <b>Winzigweichs Kante</b>
-Browser diese Technologien auch unterstützen. Aber wer nutzt schon diese Weichware?
</p>
</div>
</div>
{:else}
<ToastContainer client:load />
<slot />
{/if}

View File

@@ -0,0 +1,22 @@
<script lang="ts">
import BLEListItem from "./BLEListItem.svelte";
import { pairedDevices } from "../lib/store";
</script>
<div class="bg-white shadow shadow-slate-300 rounded-lg border border-slate-100 overflow-hidden">
<div class="p-6 pb-4 border-b border-slate-100">
<h2 class="text-xl font-bold text-slate-800">Verfügbare Geräte</h2>
</div>
<div class="flex flex-col">
{#if $pairedDevices.length > 0}
{#each $pairedDevices as device (device.id)}
<BLEListItem {device} />
{/each}
{:else}
<div class="px-6 py-8 text-center text-slate-500 text-sm">
Keine gepairten Geräte gefunden. Bitte pairen Sie zunächst ein Gerät.
</div>
{/if}
</div>
</div>

View File

@@ -0,0 +1,55 @@
<script lang="ts">
import { BluetoothIcon, BluetoothSlashIcon, BluetoothXIcon } from "phosphor-svelte";
import { connectBuzzer, forgetDevice } from "../lib/bluetooth";
import { availableDevices, activeDeviceId, isConnected, targetDeviceId } from "../lib/store"; // targetDeviceId importiert
export let device: BluetoothDevice;
$: isAvailable = $availableDevices.has(device.id);
$: isActive = $activeDeviceId === device.id;
$: isTarget = $targetDeviceId === device.id;
$: showBlueBorder = isActive || (isTarget && !$isConnected);
</script>
<div
class="flex items-center border-b border-slate-200/50 last:border-b-0 transition-colors border-l-4
{showBlueBorder ? 'border-l-blue-600' : 'border-l-transparent'}
{isActive ? 'bg-blue-100' : isAvailable ? 'bg-blue-50 hover:bg-blue-100' : 'bg-white hover:bg-slate-50'}"
>
<div
class="flex-1 px-6 py-3 flex items-center cursor-pointer {isAvailable || isActive ? 'opacity-100' : 'opacity-50'}"
on:click={() => { if (isAvailable && !isActive) connectBuzzer(device); }}
>
{#if isAvailable || isActive}
<BluetoothIcon class="text-blue-600 mr-3 w-5 h-5" />
{:else}
<BluetoothSlashIcon class="text-slate-400 mr-3 w-5 h-5" />
{/if}
<div class="flex flex-col">
<span
class="font-medium {isActive
? 'text-blue-900'
: isAvailable
? 'text-blue-800'
: 'text-slate-500'}"
>
{device.name || "Unbekanntes Gerät"}
</span>
{#if isActive}
<span class="text-xs text-blue-700 font-semibold">Verbunden</span>
{:else if !isAvailable}
<span class="text-xs text-slate-400">Nicht in Reichweite</span>
{/if}
</div>
</div>
<button
class="px-4 h-full flex items-center text-slate-300 hover:text-red-500 transition-colors"
on:click|stopPropagation={() => forgetDevice(device)}
title="Gerät entkoppeln"
>
<BluetoothXIcon class="w-5 h-5" />
</button>
</div>

View File

@@ -0,0 +1,137 @@
<script lang="ts">
import { onMount } from "svelte";
import { pairBuzzer, connectBuzzer, disconnectBuzzer, restoreSession } from "../lib/bluetooth";
import {
isConnected,
isPaired,
isConnecting,
protocolInfo,
fsInfo,
loadConnectionState,
availableDevices,
} from "../lib/store";
import { refreshRemote } from "../lib/sync";
import { fetchFileThroughputTest } from "../lib/transport";
onMount(() => {
restoreSession();
});
// Automatischer Datenabruf, sobald der Transport-Layer isConnected auf true setzt
$: if ($isConnected) {
refreshRemote();
}
$: lastDeviceId = loadConnectionState()?.deviceId;
$: canQuickConnect = lastDeviceId ? $availableDevices.has(lastDeviceId) : false;
</script>
<div class="bg-white shadow shadow-slate-300 rounded-lg p-6 border border-slate-100">
<h2 class="text-xl font-bold mb-4 text-slate-800">Geräte-Status</h2>
<div
class="flex items-center gap-2 font-semibold mb-5 {$isConnected
? 'text-green-600'
: 'text-slate-500'}"
>
<div
class="w-2.5 h-2.5 rounded-full {$isConnected
? 'bg-green-500 animate-pulse'
: 'bg-slate-400'}"
></div>
{$isConnected ? "Bluetooth Verbunden" : "Bluetooth Getrennt"}
</div>
<div
class="bg-slate-50 border border-slate-200 p-4 rounded text-sm font-mono text-slate-600 mb-4 flex flex-col justify-center"
>
<div class="flex justify-between mb-1">
<span>Protokoll Version:</span>
<span class="font-semibold text-slate-800">
{$protocolInfo ? "v" + $protocolInfo.version : "unbekannt"}
</span>
</div>
<div class="flex justify-between">
<span>Max Chunk Size:</span>
<span class="font-semibold text-slate-800">
{$protocolInfo ? $protocolInfo.maxChunkSize + " B" : "unbekannt"}
</span>
</div>
<div class="flex justify-between">
<span>Flash-Grösse:</span>
<span class="font-semibold text-slate-800">
{$fsInfo ? $fsInfo.totalSize.toFixed(2) + " MB" : "unbekannt"}
</span>
</div>
<div class="flex justify-between">
<span>Freier Speicher:</span>
<span class="font-semibold text-slate-800">
{$fsInfo ? $fsInfo.freeSize.toFixed(2) + " MB" : "unbekannt"}
</span>
</div>
<div class="flex justify-between">
<span>Max. Pfadlänge:</span>
<span class="font-semibold text-slate-800">
{$fsInfo ? $fsInfo.maxPathLength : "unbekannt"}
</span>
</div>
<div class="flex justify-between">
<span>Systempfad:</span>
<span class="font-semibold text-slate-800">
{$fsInfo ? $fsInfo.sysPath : "unbekannt"}
</span>
</div>
<div class="flex justify-between">
<span>Audiopfad:</span>
<span class="font-semibold text-slate-800">
{$fsInfo ? $fsInfo.audioPath : "unbekannt"}
</span>
</div>
</div>
<div class="flex gap-3 mt-4">
{#if !$isConnected}
<button
class="bg-blue-600 text-white px-4 py-2.5 rounded shadow-sm hover:bg-blue-700 hover:shadow transition text-sm font-medium flex-1 disabled:opacity-50 disabled:cursor-not-allowed"
on:click={() => pairBuzzer()}
disabled={$isConnecting}
>
Pairen
</button>
<button
class="px-4 py-2.5 rounded transition text-sm font-medium flex-1
{$isPaired && canQuickConnect
? 'bg-emerald-600 text-white shadow-sm hover:bg-emerald-700 hover:shadow'
: 'bg-slate-50 text-slate-400 cursor-not-allowed opacity-70'}
{$isConnecting ? 'animate-pulse' : ''}"
on:click={() => connectBuzzer()}
disabled={!$isPaired || !canQuickConnect || $isConnecting}
>
{$isConnecting ? "Verbinde..." : "Verbinden"}
</button>
{:else}
<button
class="bg-slate-100 text-slate-700 px-4 py-2.5 rounded hover:bg-slate-200 transition text-sm font-medium flex-1"
on:click={() => refreshRemote()}
>
Werte neu laden
</button>
<button
class="bg-red-600 text-white px-4 py-2.5 rounded shadow-sm hover:bg-red-700 hover:shadow transition text-sm font-medium flex-1"
on:click={() => disconnectBuzzer()}
>
Trennen
</button>
{/if}
</div>
<button
class="mt-4 w-full text-left text-xs text-slate-500 hover:text-slate-700 transition"
on:click={() => {
fetchFileThroughputTest("/lfs/a/countdown");
}}
>
Durchsatztest mit /lfs/a/countdown
</button>
</div>

View File

@@ -0,0 +1,27 @@
<script lang="ts">
import { storageUsage } from "../lib/store";
</script>
<div class="mt-4 w-full h-3 bg-slate-100 rounded-sm overflow-hidden flex shadow-inner">
<div
class="h-full bg-slate-400 transition-all duration-500"
style="width: {$storageUsage?.systemPercent ?? 0}%"
></div>
<div class="mt-1 text-xs text-slate-400 flex justify-between uppercase tracking-tighter font-medium">
{#if $storageUsage}
<div class="flex gap-4">
<div>
<span class="font-semibold text-slate-400">
Rate: {($storageUsage.systemBytes / 1048576).toFixed(2)} MB
</span>
</div>
<div>
<span class="font-semibold text-slate-400">
{($storageUsage.freeBytes / 1048576).toFixed(2)} Sekunden
</span>
</div>
{:else}
<div class="text-slate-400">Kein Transfer aktiv</div>
{/if}
</div>

View File

@@ -0,0 +1,42 @@
<script lang="ts">
import { storageUsage } from "../lib/store";
</script>
<div class="mt-4 w-full h-3 bg-slate-100 rounded-sm overflow-hidden flex shadow-inner">
<div
class="h-full bg-slate-400 transition-all duration-500"
style="width: {$storageUsage?.systemPercent ?? 0}%"
></div>
<div
class="h-full bg-indigo-500 transition-all duration-500"
style="width: {$storageUsage?.audioPercent ?? 0}%"
></div>
<div
class="h-full bg-emerald-500 transition-all duration-500"
style="width: {$storageUsage?.freePercent ?? 0}%"
></div>
</div>
<div class="mt-1 text-xs text-slate-400 flex justify-between uppercase tracking-tighter font-medium">
{#if $storageUsage}
<div class="flex gap-4">
<div>
<span class="font-semibold text-slate-400">System:
{($storageUsage.systemBytes / 1048576).toFixed(2)} MB</span>
</div>
<div>
<span class="font-semibold text-indigo-500">Audio:
{($storageUsage.audioBytes / 1048576).toFixed(2)} MB</span>
</div>
</div>
<div>
<span class="font-semibold text-emerald-500">Frei:
{($storageUsage.freeBytes / 1048576).toFixed(2)} MB</span>
</div>
{:else}
<div class="text-slate-400">Speicherdaten nicht verfügbar</div>
{/if}
</div>

View File

@@ -0,0 +1,33 @@
<script lang="ts">
import { toasts, removeToast } from "../lib/toast";
import { XIcon } from "phosphor-svelte";
import { fly } from "svelte/transition"; // Import für DOM-Animationen hinzugefügt
// Debug-Ausgabe: Zeigt in der Konsole an, sobald ein Toast getriggert wird
$: console.debug("Aktuelle Toasts im Store:", $toasts);
</script>
<div class="fixed bottom-20 right-6 z-[9999] flex flex-col gap-3 pointer-events-none">
{#each $toasts as toast (toast.id)}
<div
in:fly={{ y: 20, duration: 300 }}
out:fly={{ y: -20, duration: 300 }}
class="pointer-events-auto flex items-center justify-between px-5 py-3 rounded-lg border-l-4 shadow-xl min-w-[280px]
{toast.type === 'success' ? 'bg-green-100/50 border-green-500 text-green-800' : ''}
{toast.type === 'info' ? 'bg-blue-100/50 border-blue-500 text-blue-800' : ''}
{toast.type === 'warning' ? 'bg-amber-100/50 border-amber-500 text-amber-800' : ''}
{toast.type === 'error' ? 'bg-red-100/50 border-red-500 text-red-800' : ''}
"
>
<span class="text-sm font-medium">{@html toast.message}</span>
{#if toast.dismissible}
<button
on:click={() => removeToast(toast.id)}
class="ml-4 opacity-50 hover:opacity-100 transition cursor-pointer"
>
<XIcon class="w-5 h-5" />
</button>
{/if}
</div>
{/each}
</div>

View File

@@ -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 <b>${device.name}</b>`, "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 <b>" + device.name + "</b> 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);
}

24
webpage/src/lib/init.ts Normal file
View File

@@ -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);
}

View File

@@ -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<number, ZephyrError> = {
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" }
};

View File

@@ -0,0 +1,2 @@
export * from './constants';
export * from './parser';

View File

@@ -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<void>;
let lsBuffer: any[] = [];
let lsTimeout: ReturnType<typeof setTimeout> | 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: <span class="font-mono font-bold text-xs">${errorInfo.zephyr} (0x${hexCode}):</span> ${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: <span class="font-mono">0x${hexCode}</span>`, '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: <span class="font-mono">0x${frameType.toString(16)}</span>`, '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<typeof setInterval> | 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;
}

View File

@@ -0,0 +1,11 @@
export const SETTINGS = {
storage: {
connectionKey: 'buzzer_connection_state'
},
bluetooth: {
connectionTimeoutMs: 10000 // Timeout für den Verbindungsaufbau
},
ui: {
toastDurationMs: 5000
}
};

182
webpage/src/lib/store.ts Normal file
View File

@@ -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<void>;
addEventListener?: (...args: unknown[]) => void;
removeEventListener?: (...args: unknown[]) => void;
watchAdvertisements?: () => Promise<void>;
}
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<boolean>(true);
export const isBluetoothSupported = writable<boolean | null>(null);
export const isSerialSupported = writable<boolean | null>(null);
// Verbindungszustand
export const isConnected = writable<boolean>(false);
export const isConnecting = writable<boolean>(false);
export const isPaired = writable<boolean>(false);
export const activeDeviceId = writable<string | null>(null);
export const targetDeviceId = writable<string | null>(null);
// Geräteverwaltung (Pairing + Discovery)
export const pairedDevices = writable<BluetoothDevice[]>([]);
export const availableDevices = writable<Set<string>>(new Set()); // IDs der derzeit advertisierten Geräte
// Protokoll- und Dateisystem-Metadaten aus dem Device
export const protocolInfo = writable<ProtocolInfo | null>(null);
export const fsInfo = writable<FsInfo | null>(null);
// Dateilisten
export const buzzerAudioFiles = writable<BuzzerFile[]>([]);
export const buzzerSysFiles = writable<BuzzerFile[]>([]);
export const localAudioFiles = writable<BuzzerFile[]>([]);
// Ladezustände getrennt nach Quelle
export const isFetchingRemote = writable<boolean>(false);
export const isFetchingLocal = writable<boolean>(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();
}

59
webpage/src/lib/sync.ts Normal file
View File

@@ -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);
}
}

31
webpage/src/lib/toast.ts Normal file
View File

@@ -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<ToastMessage[]>([]);
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([]);
}

View File

@@ -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<void>;
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<any[]> {
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<boolean> {
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);
}
});
}

29
webpage/src/lib/types.ts Normal file
View File

@@ -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;
}

View File

@@ -1,53 +1,41 @@
--- ---
import MainLayout from "../layouts/MainLayout.astro"; 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";
--- ---
<MainLayout> <MainLayout>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6"> <AppGuard client:load>
<div class="bg-white shadow shadow-slate-300 rounded-lg p-6"> <div class="max-w-4xl mx-auto mt-4">
<h1>Titel</h1> <header class="mb-12 text-center">
<p> <h1 class="text-3xl md:text-4xl font-extrabold text-slate-800 tracking-tight mb-3">
Li Europan lingues es membres del sam familie. Lor separat Buzzer Management
existentie es un myth. Por scientie, musica, sport etc, litot </h1>
Europa usa li sam vocabular. Li lingues differe solmen in li <p class="text-slate-500 text-lg max-w-2xl mx-auto">
grammatica, li pronunciation e li plu commun vocabules. Omnicos Verbinde dich mit dem nRF52840 Buzzer, um Audio-Dateien zu übertragen und
directe al desirabilite de un nov lingua franca: On refusa Systemparameter auszulesen.
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.
</p>
</div>
<div class="bg-white shadow shadow-slate-300 rounded-lg p-6">
<h1>Titel</h1>
<p>
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.
</p> </p>
</header>
<p> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
It va esser tam simplic quam Occidental in fact, it va esser <div>
Occidental. A un Angleso it va semblar un simplificat Angles, <BuzzerControl client:load />
quam un skeptic Cambridge amico dit me que Occidental es. Li </div>
Europan lingues es membres del sam familie. Lor separat
existentie es un myth. Por scientie, musica, sport etc, litot <div>
Europa usa li sam vocabular. Li lingues differe solmen in li <BLEList client:load />
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.
</p>
</div> </div>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<FlashUsage client:load />
</div>
<div>
</div>
</div>
</div>
</AppGuard>
</MainLayout> </MainLayout>