vor ble umbau
This commit is contained in:
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -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
1
firmware/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
build*/
|
||||
9
firmware/CMakeLists.txt
Normal file
9
firmware/CMakeLists.txt
Normal 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)
|
||||
8
firmware/boards/nrf52840dk_nrf52840.overlay
Normal file
8
firmware/boards/nrf52840dk_nrf52840.overlay
Normal file
@@ -0,0 +1,8 @@
|
||||
/{
|
||||
chosen {
|
||||
nordic,pm-ext-flash = &mx25r64;
|
||||
};
|
||||
aliases {
|
||||
qspi-flash = &mx25r64;
|
||||
};
|
||||
};
|
||||
3
firmware/libs/CMakeLists.txt
Normal file
3
firmware/libs/CMakeLists.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
add_subdirectory(fs_mgmt)
|
||||
add_subdirectory(ble_mgmt)
|
||||
add_subdirectory(buzz_proto)
|
||||
3
firmware/libs/Kconfig
Normal file
3
firmware/libs/Kconfig
Normal file
@@ -0,0 +1,3 @@
|
||||
rsource "fs_mgmt/Kconfig"
|
||||
rsource "ble_mgmt/Kconfig"
|
||||
rsource "buzz_proto/Kconfig"
|
||||
5
firmware/libs/ble_mgmt/CMakeLists.txt
Normal file
5
firmware/libs/ble_mgmt/CMakeLists.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
if(CONFIG_BLE_MGMT)
|
||||
zephyr_library()
|
||||
zephyr_library_sources(src/ble_mgmt.c)
|
||||
zephyr_include_directories(include)
|
||||
endif()
|
||||
62
firmware/libs/ble_mgmt/Kconfig
Normal file
62
firmware/libs/ble_mgmt/Kconfig
Normal 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
|
||||
38
firmware/libs/ble_mgmt/include/ble_mgmt.h
Normal file
38
firmware/libs/ble_mgmt/include/ble_mgmt.h
Normal 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
|
||||
258
firmware/libs/ble_mgmt/src/ble_mgmt.c
Normal file
258
firmware/libs/ble_mgmt/src/ble_mgmt.c
Normal 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,
|
||||
};
|
||||
5
firmware/libs/buzz_proto/CMakeLists.txt
Normal file
5
firmware/libs/buzz_proto/CMakeLists.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
if(CONFIG_BUZZ_PROTO)
|
||||
zephyr_library()
|
||||
zephyr_library_sources(src/buzz_proto.c)
|
||||
zephyr_include_directories(include)
|
||||
endif()
|
||||
41
firmware/libs/buzz_proto/Kconfig
Normal file
41
firmware/libs/buzz_proto/Kconfig
Normal 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
|
||||
145
firmware/libs/buzz_proto/include/buzz_proto.h
Normal file
145
firmware/libs/buzz_proto/include/buzz_proto.h
Normal 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 */
|
||||
619
firmware/libs/buzz_proto/src/buzz_proto.c
Normal file
619
firmware/libs/buzz_proto/src/buzz_proto.c
Normal 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);
|
||||
17
firmware/libs/fs_mgmt/CMakeLists.txt
Normal file
17
firmware/libs/fs_mgmt/CMakeLists.txt
Normal file
@@ -0,0 +1,17 @@
|
||||
if(CONFIG_FS_MGMT)
|
||||
zephyr_library()
|
||||
zephyr_library_sources(src/fs_mgmt.c)
|
||||
zephyr_include_directories(include)
|
||||
|
||||
if(CONFIG_FILE_SYSTEM_LITTLEFS)
|
||||
if(DEFINED ZEPHYR_LITTLEFS_MODULE_DIR)
|
||||
zephyr_include_directories(${ZEPHYR_LITTLEFS_MODULE_DIR})
|
||||
elseif(DEFINED WEST_TOPDIR)
|
||||
zephyr_include_directories(${WEST_TOPDIR}/modules/fs/littlefs)
|
||||
endif()
|
||||
|
||||
if(DEFINED ZEPHYR_BASE)
|
||||
zephyr_include_directories(${ZEPHYR_BASE}/modules/littlefs)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
40
firmware/libs/fs_mgmt/Kconfig
Normal file
40
firmware/libs/fs_mgmt/Kconfig
Normal 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
|
||||
117
firmware/libs/fs_mgmt/include/fs_mgmt.h
Normal file
117
firmware/libs/fs_mgmt/include/fs_mgmt.h
Normal 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 */
|
||||
373
firmware/libs/fs_mgmt/src/fs_mgmt.c
Normal file
373
firmware/libs/fs_mgmt/src/fs_mgmt.c
Normal 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;
|
||||
}
|
||||
4
firmware/libs/zephyr/module.yml
Normal file
4
firmware/libs/zephyr/module.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
name: libs
|
||||
build:
|
||||
cmake: .
|
||||
kconfig: Kconfig
|
||||
39
firmware/pm_static_nrf52840dk_nrf52840.yml
Normal file
39
firmware/pm_static_nrf52840dk_nrf52840.yml
Normal 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
35
firmware/prj.conf
Normal 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
66
firmware/src/main.c
Normal 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
313
protocol.md
Normal 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
|
||||
```
|
||||
@@ -6,7 +6,7 @@
|
||||
{
|
||||
"files": ["*.svelte", "*.astro"],
|
||||
"options": {
|
||||
"printWidth": 1000
|
||||
"printWidth": 100
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
21
webpage/package-lock.json
generated
21
webpage/package-lock.json
generated
@@ -17,6 +17,7 @@
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"phosphor-svelte": "^3.1.0",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-astro": "^0.14.1"
|
||||
}
|
||||
@@ -4490,6 +4491,26 @@
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/phosphor-svelte": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/phosphor-svelte/-/phosphor-svelte-3.1.0.tgz",
|
||||
"integrity": "sha512-nldtxx+XCgNREvrb7O5xgDsefytXpSkPTx8Rnu3f2qQCUZLDV1rLxYSd2Jcwckuo9lZB1qKMqGR17P4UDC0PrA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^0.30.13"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^5.0.0 || ^5.0.0-next.96",
|
||||
"vite": ">=5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"vite": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/piccolore": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"phosphor-svelte": "^3.1.0",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-astro": "^0.14.1"
|
||||
}
|
||||
|
||||
61
webpage/src/components/AppGuard.svelte
Normal file
61
webpage/src/components/AppGuard.svelte
Normal 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}
|
||||
22
webpage/src/components/BLEList.svelte
Normal file
22
webpage/src/components/BLEList.svelte
Normal 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>
|
||||
55
webpage/src/components/BLEListItem.svelte
Normal file
55
webpage/src/components/BLEListItem.svelte
Normal 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>
|
||||
137
webpage/src/components/BuzzerControl.svelte
Normal file
137
webpage/src/components/BuzzerControl.svelte
Normal 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>
|
||||
27
webpage/src/components/FileTransfer.svelte
Normal file
27
webpage/src/components/FileTransfer.svelte
Normal 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>
|
||||
42
webpage/src/components/FlashUsage.svelte
Normal file
42
webpage/src/components/FlashUsage.svelte
Normal 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>
|
||||
33
webpage/src/components/ToastContainer.svelte
Normal file
33
webpage/src/components/ToastContainer.svelte
Normal 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>
|
||||
247
webpage/src/lib/bluetooth.ts
Normal file
247
webpage/src/lib/bluetooth.ts
Normal 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
24
webpage/src/lib/init.ts
Normal 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);
|
||||
}
|
||||
56
webpage/src/lib/protocol/constants.ts
Normal file
56
webpage/src/lib/protocol/constants.ts
Normal 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" }
|
||||
};
|
||||
2
webpage/src/lib/protocol/index.ts
Normal file
2
webpage/src/lib/protocol/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './constants';
|
||||
export * from './parser';
|
||||
309
webpage/src/lib/protocol/parser.ts
Normal file
309
webpage/src/lib/protocol/parser.ts
Normal 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;
|
||||
}
|
||||
11
webpage/src/lib/settings.ts
Normal file
11
webpage/src/lib/settings.ts
Normal 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
182
webpage/src/lib/store.ts
Normal 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
59
webpage/src/lib/sync.ts
Normal 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
31
webpage/src/lib/toast.ts
Normal 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([]);
|
||||
}
|
||||
92
webpage/src/lib/transport.ts
Normal file
92
webpage/src/lib/transport.ts
Normal 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
29
webpage/src/lib/types.ts
Normal 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;
|
||||
}
|
||||
@@ -1,53 +1,41 @@
|
||||
---
|
||||
import MainLayout from "../layouts/MainLayout.astro";
|
||||
import BuzzerControl from "../components/BuzzerControl.svelte";
|
||||
import BLEList from "../components/BLEList.svelte";
|
||||
import AppGuard from "../components/AppGuard.svelte";
|
||||
import FlashUsage from "../components/FlashUsage.svelte";
|
||||
---
|
||||
|
||||
<MainLayout>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<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>
|
||||
</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.
|
||||
<AppGuard client:load>
|
||||
<div class="max-w-4xl mx-auto mt-4">
|
||||
<header class="mb-12 text-center">
|
||||
<h1 class="text-3xl md:text-4xl font-extrabold text-slate-800 tracking-tight mb-3">
|
||||
Buzzer Management
|
||||
</h1>
|
||||
<p class="text-slate-500 text-lg max-w-2xl mx-auto">
|
||||
Verbinde dich mit dem nRF52840 Buzzer, um Audio-Dateien zu übertragen und
|
||||
Systemparameter auszulesen.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<p>
|
||||
It va esser tam simplic quam Occidental in fact, it va esser
|
||||
Occidental. A un Angleso it va semblar un simplificat Angles,
|
||||
quam un skeptic Cambridge amico dit me que Occidental es. Li
|
||||
Europan lingues es membres del sam familie. Lor separat
|
||||
existentie es un myth. Por scientie, musica, sport etc, litot
|
||||
Europa usa li sam vocabular. Li lingues differe solmen in li
|
||||
grammatica, li pronunciation e li plu commun vocabules. Omnicos
|
||||
directe al desirabilite de un nov lingua franca: On refusa
|
||||
continuar payar custosi traductores. At solmen va esser necessi
|
||||
far uniform grammatica, pronunciation e plu sommun paroles.
|
||||
</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<BuzzerControl client:load />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<BLEList client:load />
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user