zwischenstand
@@ -28,34 +28,34 @@ if BLE_MGMT
|
||||
help
|
||||
Maximal advertising interval. 160 equals to 100ms.
|
||||
|
||||
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
|
||||
config BT_USER_PHY_UPDATE
|
||||
default y
|
||||
config BT_HCI_ACL_FLOW_CONTROL
|
||||
default y
|
||||
config BT_BUF_CMD_TX_COUNT
|
||||
default 24
|
||||
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
|
||||
config BT_CTLR_SDC_TX_PACKET_COUNT
|
||||
default 20
|
||||
config BT_CTLR_SDC_RX_PACKET_COUNT
|
||||
default 20
|
||||
# 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
|
||||
# config BT_USER_PHY_UPDATE
|
||||
# default y
|
||||
# config BT_HCI_ACL_FLOW_CONTROL
|
||||
# default y
|
||||
# config BT_BUF_CMD_TX_COUNT
|
||||
# default 24
|
||||
# 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
|
||||
# config BT_CTLR_SDC_TX_PACKET_COUNT
|
||||
# default 20
|
||||
# config BT_CTLR_SDC_RX_PACKET_COUNT
|
||||
# default 20
|
||||
config BT_MAX_CONN
|
||||
default 2
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <zephyr/bluetooth/gatt.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <zephyr/bluetooth/gatt.h>
|
||||
#include <zephyr/bluetooth/conn.h>
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "ble_mgmt.h"
|
||||
@@ -24,7 +26,9 @@ 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;
|
||||
static uint16_t current_rx_mtu = 23;
|
||||
|
||||
#define MAX_ADV_NAME_LEN 29
|
||||
static char current_device_name[MAX_ADV_NAME_LEN + 1];
|
||||
@@ -52,6 +56,7 @@ 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;
|
||||
current_rx_mtu = rx;
|
||||
}
|
||||
|
||||
static ssize_t rx_cb(struct bt_conn *conn, const struct bt_gatt_attr *attr,
|
||||
@@ -59,8 +64,9 @@ static ssize_t rx_cb(struct bt_conn *conn, const struct bt_gatt_attr *attr,
|
||||
{
|
||||
LOG_DBG("Received %u bytes", len);
|
||||
LOG_HEXDUMP_DBG(buf, len, "Data:");
|
||||
|
||||
if (app_rx_cb) {
|
||||
|
||||
if (app_rx_cb)
|
||||
{
|
||||
app_rx_cb((const uint8_t *)buf, len);
|
||||
}
|
||||
return len;
|
||||
@@ -69,25 +75,25 @@ static ssize_t rx_cb(struct bt_conn *conn, const struct bt_gatt_attr *attr,
|
||||
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");
|
||||
LOG_INF("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)
|
||||
);
|
||||
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;
|
||||
|
||||
uint16_t effective_mtu = MIN(current_tx_mtu, current_rx_mtu);
|
||||
|
||||
#ifdef CONFIG_BT_L2CAP_TX_MTU
|
||||
if (effective_mtu > CONFIG_BT_L2CAP_TX_MTU) {
|
||||
if (effective_mtu > CONFIG_BT_L2CAP_TX_MTU)
|
||||
{
|
||||
effective_mtu = CONFIG_BT_L2CAP_TX_MTU;
|
||||
}
|
||||
#endif
|
||||
@@ -98,19 +104,23 @@ uint16_t ble_mgmt_get_max_payload(void)
|
||||
|
||||
int ble_mgmt_send(const uint8_t *data, uint16_t len)
|
||||
{
|
||||
if (!notify_enabled) {
|
||||
if (!notify_enabled)
|
||||
{
|
||||
return -EACCES;
|
||||
}
|
||||
int rc;
|
||||
|
||||
do {
|
||||
do
|
||||
{
|
||||
rc = bt_gatt_notify(NULL, &ble_mgmt_svc.attrs[4], data, len);
|
||||
if (rc == -ENOMEM) {
|
||||
if (rc == -ENOMEM)
|
||||
{
|
||||
k_sleep(K_MSEC(5)); // Thread pausieren, bis TX-Buffer frei wird
|
||||
}
|
||||
} while (rc == -ENOMEM);
|
||||
|
||||
if (rc) {
|
||||
if (rc)
|
||||
{
|
||||
LOG_ERR("Failed to send notification (err %d)", rc);
|
||||
return rc;
|
||||
}
|
||||
@@ -120,13 +130,14 @@ int ble_mgmt_send(const uint8_t *data, uint16_t len)
|
||||
/* Interne Hilfsfunktion zur Zuweisung des Namens */
|
||||
static void set_device_name(const char *name)
|
||||
{
|
||||
if (!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);
|
||||
|
||||
@@ -144,7 +155,8 @@ int ble_mgmt_update_adv_name(const char *new_name)
|
||||
set_device_name(new_name);
|
||||
|
||||
rc = bt_le_adv_start(&adv_param, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd));
|
||||
if (rc) {
|
||||
if (rc)
|
||||
{
|
||||
LOG_ERR("Advertising failed to restart after name update (err %d)", rc);
|
||||
return rc;
|
||||
}
|
||||
@@ -153,40 +165,10 @@ int ble_mgmt_update_adv_name(const char *new_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) {
|
||||
if (err)
|
||||
{
|
||||
LOG_ERR("Connection failed (err 0x%02x)", err);
|
||||
return;
|
||||
}
|
||||
@@ -195,50 +177,38 @@ static void connected(struct bt_conn *conn, uint8_t err)
|
||||
struct bt_conn_info info;
|
||||
|
||||
int rc = bt_conn_get_info(conn, &info);
|
||||
if (rc == 0) {
|
||||
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("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);
|
||||
LOG_INF("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) {
|
||||
if (rc)
|
||||
{
|
||||
LOG_ERR("Advertising failed to restart (err %d)", rc);
|
||||
} else {
|
||||
LOG_DBG("Advertising successfully restarted");
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_INF("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";
|
||||
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);
|
||||
}
|
||||
@@ -256,3 +226,35 @@ BT_CONN_CB_DEFINE(conn_callbacks) = {
|
||||
.le_param_updated = le_param_updated,
|
||||
.le_phy_updated = le_phy_updated,
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -6,7 +6,7 @@ menuconfig BUZZ_PROTO
|
||||
|
||||
config BUZZ_PROTO_SLAB_SIZE
|
||||
int "Slab Size"
|
||||
default 256
|
||||
default 512
|
||||
help
|
||||
Size of the memory slabs used for message buffers. Must be large enough to hold the largest expected message.
|
||||
|
||||
@@ -18,7 +18,7 @@ menuconfig BUZZ_PROTO
|
||||
|
||||
config BUZZ_PROTO_MSGQ_SIZE
|
||||
int "Message Queue Size"
|
||||
default 16
|
||||
default 64
|
||||
help
|
||||
Number of messages that can be queued for processing. Adjust based on expected message burstiness.
|
||||
|
||||
|
||||
@@ -29,7 +29,9 @@ static struct fs_mount_t fs_storage_mnt = {
|
||||
static int open_count = 0;
|
||||
static struct k_mutex flash_pm_lock;
|
||||
|
||||
#define ACK_WATERMARK (CONFIG_BUZZ_PROTO_SLAB_COUNT / 4)
|
||||
// #define ACK_WATERMARK (CONFIG_BUZZ_PROTO_SLAB_COUNT / 4)
|
||||
#define INITIAL_CREDITS CONFIG_BUZZ_PROTO_SLAB_COUNT
|
||||
#define ACK_WATERMARK (MAX(2, MIN(8, CONFIG_BUZZ_PROTO_SLAB_COUNT / 4)))
|
||||
|
||||
typedef struct __attribute__((packed))
|
||||
{
|
||||
@@ -40,7 +42,8 @@ typedef struct __attribute__((packed))
|
||||
|
||||
K_MSGQ_DEFINE(fs_write_msgq, sizeof(struct fs_write_msg), CONFIG_BUZZ_PROTO_SLAB_COUNT, 4);
|
||||
|
||||
typedef enum {
|
||||
typedef enum
|
||||
{
|
||||
FS_STATE_IDLE,
|
||||
FS_STATE_RECEIVING_FILE,
|
||||
FS_STATE_RECEIVING_TAGS,
|
||||
@@ -509,7 +512,8 @@ static void fs_thread_entry(void *p1, void *p2, void *p3)
|
||||
if (rc == -EAGAIN)
|
||||
{
|
||||
LOG_WRN("Write timeout! Aborting transfer.");
|
||||
if (write_ctx.state == FS_STATE_RECEIVING_FILE) {
|
||||
if (write_ctx.state == FS_STATE_RECEIVING_FILE)
|
||||
{
|
||||
fs_mgmt_pm_close(&write_ctx.file);
|
||||
fs_mgmt_pm_unlink(write_ctx.filename);
|
||||
}
|
||||
@@ -522,49 +526,56 @@ static void fs_thread_entry(void *p1, void *p2, void *p3)
|
||||
case FS_STATE_IDLE:
|
||||
if (msg.op == FS_WRITE_OP_FILE_START)
|
||||
{
|
||||
if (msg.data_len >= sizeof(write_ctx.filename)) {
|
||||
if (msg.data_len >= sizeof(write_ctx.filename))
|
||||
{
|
||||
LOG_ERR("Filename too long");
|
||||
buzz_proto_send_error_reusing_slab(msg.reply_cb, ENAMETOOLONG, msg.slab_ptr);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
memcpy(write_ctx.filename, msg.slab_ptr + msg.data_offset, msg.data_len);
|
||||
write_ctx.filename[msg.data_len] = '\0';
|
||||
|
||||
fs_mgmt_pm_unlink(write_ctx.filename);
|
||||
|
||||
fs_mgmt_pm_unlink(write_ctx.filename);
|
||||
rc = fs_mgmt_pm_open(&write_ctx.file, write_ctx.filename, FS_O_CREATE | FS_O_WRITE);
|
||||
|
||||
if (rc == 0) {
|
||||
|
||||
if (rc == 0)
|
||||
{
|
||||
write_ctx.state = FS_STATE_RECEIVING_FILE;
|
||||
write_ctx.crc32 = 0;
|
||||
write_ctx.unacked_chunks = 0;
|
||||
LOG_INF("File transfer started: %s (Expected: %u bytes)", write_ctx.filename, msg.metadata);
|
||||
|
||||
uint16_t credits = buzz_proto_get_free_rx_slabs();
|
||||
|
||||
uint16_t credits = MIN(INITIAL_CREDITS, buzz_proto_get_free_rx_slabs());
|
||||
buzz_proto_buf_free(&msg.slab_ptr);
|
||||
buzz_proto_send_ack(msg.reply_cb, credits);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_ERR("Failed to open %s: %d", write_ctx.filename, rc);
|
||||
buzz_proto_send_error_reusing_slab(msg.reply_cb, abs(rc), msg.slab_ptr);
|
||||
}
|
||||
}/* Innerhalb von case FS_STATE_IDLE: */
|
||||
} /* Innerhalb von case FS_STATE_IDLE: */
|
||||
else if (msg.op == FS_WRITE_OP_TAGS_START)
|
||||
{
|
||||
if (msg.data_len >= sizeof(write_ctx.filename)) {
|
||||
if (msg.data_len >= sizeof(write_ctx.filename))
|
||||
{
|
||||
buzz_proto_send_error_reusing_slab(msg.reply_cb, ENAMETOOLONG, msg.slab_ptr);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
memcpy(write_ctx.filename, msg.slab_ptr + msg.data_offset, msg.data_len);
|
||||
write_ctx.filename[msg.data_len] = '\0';
|
||||
|
||||
|
||||
/* Datei öffnen: Nur Lese- und Schreibrechte, Datei muss bereits existieren */
|
||||
int rc = fs_mgmt_pm_open(&write_ctx.file, write_ctx.filename, FS_O_READ | FS_O_WRITE);
|
||||
|
||||
if (rc == 0) {
|
||||
|
||||
if (rc == 0)
|
||||
{
|
||||
ssize_t audio_len = fs_get_audio_data_len(&write_ctx.file);
|
||||
|
||||
if (audio_len < 0) {
|
||||
|
||||
if (audio_len < 0)
|
||||
{
|
||||
LOG_ERR("Failed to get audio length: %d", (int)audio_len);
|
||||
fs_mgmt_pm_close(&write_ctx.file);
|
||||
buzz_proto_send_error_reusing_slab(msg.reply_cb, EIO, msg.slab_ptr);
|
||||
@@ -573,7 +584,8 @@ static void fs_thread_entry(void *p1, void *p2, void *p3)
|
||||
|
||||
/* Datei ab dem Ende der Audiodaten abschneiden (alte Tags entfernen) */
|
||||
rc = fs_truncate(&write_ctx.file, audio_len);
|
||||
if (rc != 0) {
|
||||
if (rc != 0)
|
||||
{
|
||||
LOG_ERR("Failed to truncate file: %d", rc);
|
||||
fs_mgmt_pm_close(&write_ctx.file);
|
||||
buzz_proto_send_error_reusing_slab(msg.reply_cb, abs(rc), msg.slab_ptr);
|
||||
@@ -587,17 +599,20 @@ static void fs_thread_entry(void *p1, void *p2, void *p3)
|
||||
write_ctx.crc32 = 0;
|
||||
write_ctx.unacked_chunks = 0;
|
||||
write_ctx.audio_len = audio_len;
|
||||
|
||||
|
||||
LOG_INF("Tags transfer started: %s (Expected tags: %u bytes)", write_ctx.filename, msg.metadata);
|
||||
|
||||
|
||||
uint16_t credits = buzz_proto_get_free_rx_slabs();
|
||||
buzz_proto_buf_free(&msg.slab_ptr);
|
||||
buzz_proto_send_ack(msg.reply_cb, credits);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_ERR("Failed to open %s for tags: %d", write_ctx.filename, rc);
|
||||
buzz_proto_send_error_reusing_slab(msg.reply_cb, abs(rc), msg.slab_ptr);
|
||||
}
|
||||
} else if ( msg.op == FS_WRITE_OP_FW_START)
|
||||
}
|
||||
else if (msg.op == FS_WRITE_OP_FW_START)
|
||||
{
|
||||
LOG_WRN("Operation not yet fully implemented in FS state machine");
|
||||
buzz_proto_send_error_reusing_slab(msg.reply_cb, ENOSYS, msg.slab_ptr);
|
||||
@@ -608,20 +623,25 @@ static void fs_thread_entry(void *p1, void *p2, void *p3)
|
||||
if (msg.op == FS_WRITE_OP_FILE_CHUNK && msg.slab_ptr)
|
||||
{
|
||||
ssize_t written = fs_write(&write_ctx.file, msg.slab_ptr + msg.data_offset, msg.data_len);
|
||||
|
||||
if (written == msg.data_len) {
|
||||
|
||||
if (written == msg.data_len)
|
||||
{
|
||||
write_ctx.crc32 = crc32_ieee_update(write_ctx.crc32, msg.slab_ptr + msg.data_offset, msg.data_len);
|
||||
buzz_proto_buf_free(&msg.slab_ptr);
|
||||
write_ctx.unacked_chunks++;
|
||||
if (write_ctx.unacked_chunks >= ACK_WATERMARK) {
|
||||
if (write_ctx.unacked_chunks >= ACK_WATERMARK)
|
||||
{
|
||||
uint16_t free_slabs = buzz_proto_get_free_rx_slabs();
|
||||
uint16_t credits_to_send = MIN(free_slabs, write_ctx.unacked_chunks);
|
||||
if (credits_to_send > 0) {
|
||||
if (credits_to_send > 0)
|
||||
{
|
||||
buzz_proto_send_ack(msg.reply_cb, credits_to_send);
|
||||
write_ctx.unacked_chunks -= credits_to_send;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_ERR("Flash write failed!");
|
||||
write_ctx.state = FS_STATE_IDLE;
|
||||
buzz_proto_send_error_reusing_slab(msg.reply_cb, EIO, msg.slab_ptr);
|
||||
@@ -631,11 +651,14 @@ static void fs_thread_entry(void *p1, void *p2, void *p3)
|
||||
{
|
||||
fs_mgmt_pm_close(&write_ctx.file);
|
||||
write_ctx.state = FS_STATE_IDLE;
|
||||
|
||||
if (write_ctx.crc32 == msg.metadata) {
|
||||
|
||||
if (write_ctx.crc32 == msg.metadata)
|
||||
{
|
||||
LOG_INF("File transfer finished. CRC valid: 0x%08X", write_ctx.crc32);
|
||||
buzz_proto_send_success_reusing_slab(msg.reply_cb, BUZZ_DATA_FILE_PUT, msg.slab_ptr);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_ERR("CRC Mismatch! Expected: 0x%08X, Got: 0x%08X", msg.metadata, write_ctx.crc32);
|
||||
fs_mgmt_pm_unlink(write_ctx.filename);
|
||||
buzz_proto_send_error_reusing_slab(msg.reply_cb, EBADMSG, msg.slab_ptr);
|
||||
@@ -646,29 +669,35 @@ static void fs_thread_entry(void *p1, void *p2, void *p3)
|
||||
fs_mgmt_pm_close(&write_ctx.file);
|
||||
fs_mgmt_pm_unlink(write_ctx.filename);
|
||||
write_ctx.state = FS_STATE_IDLE;
|
||||
if (msg.slab_ptr) buzz_proto_buf_free(&msg.slab_ptr);
|
||||
if (msg.slab_ptr)
|
||||
buzz_proto_buf_free(&msg.slab_ptr);
|
||||
}
|
||||
break;
|
||||
|
||||
case FS_STATE_RECEIVING_TAGS:
|
||||
|
||||
case FS_STATE_RECEIVING_TAGS:
|
||||
if (msg.op == FS_WRITE_OP_FILE_CHUNK && msg.slab_ptr)
|
||||
{
|
||||
ssize_t written = fs_write(&write_ctx.file, msg.slab_ptr + msg.data_offset, msg.data_len);
|
||||
|
||||
if (written == msg.data_len) {
|
||||
|
||||
if (written == msg.data_len)
|
||||
{
|
||||
write_ctx.crc32 = crc32_ieee_update(write_ctx.crc32, msg.slab_ptr + msg.data_offset, msg.data_len);
|
||||
buzz_proto_buf_free(&msg.slab_ptr);
|
||||
|
||||
|
||||
write_ctx.unacked_chunks++;
|
||||
if (write_ctx.unacked_chunks >= ACK_WATERMARK) {
|
||||
if (write_ctx.unacked_chunks >= ACK_WATERMARK)
|
||||
{
|
||||
uint16_t free_slabs = buzz_proto_get_free_rx_slabs();
|
||||
uint16_t credits_to_send = MIN(free_slabs, write_ctx.unacked_chunks);
|
||||
if (credits_to_send > 0) {
|
||||
if (credits_to_send > 0)
|
||||
{
|
||||
buzz_proto_send_ack(msg.reply_cb, credits_to_send);
|
||||
write_ctx.unacked_chunks -= credits_to_send;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_ERR("Flash write failed during tags transfer!");
|
||||
fs_truncate(&write_ctx.file, write_ctx.audio_len); /* Rollback */
|
||||
fs_mgmt_pm_close(&write_ctx.file);
|
||||
@@ -678,11 +707,14 @@ static void fs_thread_entry(void *p1, void *p2, void *p3)
|
||||
}
|
||||
else if (msg.op == FS_WRITE_OP_FILE_END)
|
||||
{
|
||||
if (write_ctx.crc32 == msg.metadata) {
|
||||
if (write_ctx.crc32 == msg.metadata)
|
||||
{
|
||||
LOG_INF("Tags transfer finished. CRC valid: 0x%08X", write_ctx.crc32);
|
||||
fs_mgmt_pm_close(&write_ctx.file);
|
||||
buzz_proto_send_success_reusing_slab(msg.reply_cb, BUZZ_DATA_TAGS_PUT, msg.slab_ptr);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_ERR("Tags CRC Mismatch! Expected: 0x%08X, Got: 0x%08X", msg.metadata, write_ctx.crc32);
|
||||
fs_truncate(&write_ctx.file, write_ctx.audio_len); /* Rollback */
|
||||
fs_mgmt_pm_close(&write_ctx.file);
|
||||
@@ -695,20 +727,22 @@ static void fs_thread_entry(void *p1, void *p2, void *p3)
|
||||
fs_truncate(&write_ctx.file, write_ctx.audio_len); /* Rollback */
|
||||
fs_mgmt_pm_close(&write_ctx.file);
|
||||
write_ctx.state = FS_STATE_IDLE;
|
||||
if (msg.slab_ptr) buzz_proto_buf_free(&msg.slab_ptr);
|
||||
if (msg.slab_ptr)
|
||||
buzz_proto_buf_free(&msg.slab_ptr);
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case FS_STATE_RECEIVING_FIRMWARE:
|
||||
break;
|
||||
}
|
||||
|
||||
/* Garbage Collection: Ungültige Operationen im falschen State abfangen */
|
||||
if (write_ctx.state == FS_STATE_IDLE && msg.op == FS_WRITE_OP_FILE_CHUNK && msg.slab_ptr) {
|
||||
if (write_ctx.state == FS_STATE_IDLE && msg.op == FS_WRITE_OP_FILE_CHUNK && msg.slab_ptr)
|
||||
{
|
||||
buzz_proto_buf_free(&msg.slab_ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
K_THREAD_DEFINE(fs_thread, CONFIG_FS_MGMT_THREAD_STACK_SIZE, fs_thread_entry,
|
||||
K_THREAD_DEFINE(fs_thread, CONFIG_FS_MGMT_THREAD_STACK_SIZE, fs_thread_entry,
|
||||
NULL, NULL, NULL, CONFIG_FS_MGMT_THREAD_PRIORITY, 0, 0);
|
||||
@@ -8,7 +8,6 @@ CONFIG_FS_LOG_LEVEL_WRN=y
|
||||
|
||||
### Bluetooth
|
||||
CONFIG_BLE_MGMT=y
|
||||
# CONFIG_BLE_MGMT_LOG_LEVEL_DBG=y
|
||||
|
||||
# Advertising 500ms - 1s
|
||||
CONFIG_BLE_MGMT_ADV_INT_MIN=160
|
||||
@@ -23,4 +22,36 @@ CONFIG_PM_DEVICE=y
|
||||
|
||||
## Shell
|
||||
# CONFIG_SHELL=y
|
||||
# CONFIG_FILE_SYSTEM_SHELL=y
|
||||
# CONFIG_FILE_SYSTEM_SHELL=y
|
||||
|
||||
# Airtime-Maximierung
|
||||
CONFIG_BT_CTLR_SDC_MAX_CONN_EVENT_LEN_DEFAULT=4000000
|
||||
|
||||
# MTU-Setup
|
||||
CONFIG_BT_BUF_ACL_RX_SIZE=502
|
||||
CONFIG_BT_BUF_ACL_TX_SIZE=502
|
||||
CONFIG_BT_L2CAP_TX_MTU=498
|
||||
CONFIG_BT_CTLR_DATA_LENGTH_MAX=251
|
||||
|
||||
# Puffer-Konfiguration (TX = 15, EVT = 16)
|
||||
CONFIG_BT_BUF_ACL_TX_COUNT=15
|
||||
CONFIG_BT_L2CAP_TX_BUF_COUNT=15
|
||||
CONFIG_BT_CONN_TX_MAX=15
|
||||
CONFIG_BT_CTLR_SDC_TX_PACKET_COUNT=15
|
||||
CONFIG_BT_CTLR_SDC_RX_PACKET_COUNT=15
|
||||
CONFIG_BT_BUF_EVT_RX_COUNT=16
|
||||
|
||||
# WICHTIG: Diese Flags aktivieren die Callbacks in der bt_conn_cb Struktur
|
||||
CONFIG_BT_USER_PHY_UPDATE=y
|
||||
CONFIG_BT_USER_DATA_LEN_UPDATE=y
|
||||
|
||||
# Automatische Updates im Hintergrund aktivieren
|
||||
CONFIG_BT_AUTO_PHY_UPDATE=y
|
||||
CONFIG_BT_AUTO_DATA_LEN_UPDATE=y
|
||||
CONFIG_BT_GAP_AUTO_UPDATE_CONN_PARAMS=y
|
||||
|
||||
# Bevorzugte Parameter für das Auto-Update definieren (entspricht BT_LE_CONN_PARAM(12, 36, 0, 400))
|
||||
CONFIG_BT_PERIPHERAL_PREF_MIN_INT=12
|
||||
CONFIG_BT_PERIPHERAL_PREF_MAX_INT=40
|
||||
CONFIG_BT_PERIPHERAL_PREF_LATENCY=0
|
||||
CONFIG_BT_PERIPHERAL_PREF_TIMEOUT=400
|
||||
@@ -13,8 +13,8 @@ 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);
|
||||
if (len > CONFIG_BUZZ_PROTO_SLAB_SIZE) {
|
||||
LOG_ERR("Received data too large for proto buf (%u > %u)", len, CONFIG_BUZZ_PROTO_SLAB_SIZE);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
72
protocol.md
@@ -39,15 +39,16 @@ uint16_t payload_length // Little Endian
|
||||
|--------|------------|----------------|---------------------------------------|
|
||||
| `0x00` | `REQUEST` | Host → Device | Abfrage eines Datentyps |
|
||||
| `0x10` | `RESPONSE` | Device → Host | Antwort auf `REQUEST` |
|
||||
| `0x11` | `ACK` | Host → Device | Flusskontrolle bei Stream-Transfers |
|
||||
| `0x11` | `ACK` | Host ↔ Device | Flusskontrolle bei Stream-Transfers |
|
||||
| `0x12` | `ERROR` | Device → Host | Fehlerantwort mit Fehlercode |
|
||||
| `0x13` | `SUCCESS` | Device → Host | Bestaetigung einer Operation |
|
||||
|
||||
### 4.2 Datei-Transfer (reserviert, noch nicht implementiert)
|
||||
| Wert | Name |
|
||||
|--------|--------------|
|
||||
| `0x20` | `FILE_START` |
|
||||
| `0x21` | `FILE_CHUNK` |
|
||||
| `0x22` | `FILE_END` |
|
||||
### 4.2 Datei-Transfer
|
||||
| Wert | Name | Richtung | Beschreibung |
|
||||
|--------|--------------|----------------|---------------------------------------------|
|
||||
| `0x20` | `FILE_START` | Host ↔ Device | Beginn eines Dateitransfers |
|
||||
| `0x21` | `FILE_CHUNK` | Host ↔ Device | Ein Datenblock des Dateitransfers |
|
||||
| `0x22` | `FILE_END` | Host ↔ Device | Abschluss des Dateitransfers inkl. CRC32 |
|
||||
|
||||
### 4.3 Firmware-Transfer (reserviert, noch nicht implementiert)
|
||||
| Wert | Name |
|
||||
@@ -97,6 +98,13 @@ Definierte `data_type`-Werte:
|
||||
| `0x01` | `PROTO_INFO` | Protokollversion und Chunk-Groesse |
|
||||
| `0x02` | `DEVICE_INFO` | Geraeteinformationen (TBD) |
|
||||
| `0x03` | `FS_INFO` | Dateisystem-Statistik und Pfadnamen |
|
||||
| `0x20` | `FILE_GET` | Datei vom Device anfordern |
|
||||
| `0x21` | `FILE_PUT` | Datei auf das Device hochladen |
|
||||
| `0x22` | `TAGS_GET` | Metadaten-Tags anfordern |
|
||||
| `0x23` | `TAGS_PUT` | Metadaten-Tags schreiben |
|
||||
| `0x24` | `RM_FILE` | Datei loeschen |
|
||||
| `0x25` | `RENAME_FILE` | Datei umbenennen |
|
||||
| `0x30` | `FW_UPDATE` | Firmware-Update starten |
|
||||
| `0x40` | `LS` | Verzeichnisliste starten |
|
||||
|
||||
### 6.1 `PROTO_INFO` (`0x01`)
|
||||
@@ -144,10 +152,36 @@ Wire-Format (Beispiel fuer Pfad `/a`):
|
||||
|
||||
Das Device antwortet mit dem LS-Stream (siehe Abschnitt 8).
|
||||
|
||||
## 7. ACK- und ERROR-Frames
|
||||
### 6.5 `RM_FILE` (`0x24`) — Datei loeschen
|
||||
Request-Payload:
|
||||
```c
|
||||
uint8_t data_type; // 0x24
|
||||
uint8_t path_length; // Laenge des Pfads
|
||||
char path[]; // Pfad ohne 0-Terminator
|
||||
```
|
||||
|
||||
### 7.1 ACK (`frame_type = 0x11`) — Host → Device
|
||||
Wird waehrend eines laufenden LS-Streams gesendet, um dem Device Credits (Sendeerlaubnisse) zu erteilen.
|
||||
### 6.6 `RENAME_FILE` (`0x25`) — Datei umbenennen
|
||||
Request-Payload:
|
||||
```c
|
||||
uint8_t data_type; // 0x25
|
||||
uint8_t old_path_length; // Laenge des alten Pfads
|
||||
uint8_t new_path_length; // Laenge des neuen Pfads
|
||||
char paths[]; // Alter Pfad, direkt gefolgt vom neuen Pfad (beide ohne 0-Terminator)
|
||||
```
|
||||
|
||||
### 6.7 `FILE_PUT` (`0x21`) / `TAGS_PUT` (`0x23`) — Upload initiieren
|
||||
Request-Payload:
|
||||
```c
|
||||
uint8_t data_type; // 0x21 (Datei) oder 0x23 (Tags)
|
||||
uint32_t total_size; // Dateigroesse in Bytes (LE)
|
||||
char path[]; // Zielpfad ohne 0-Terminator
|
||||
```
|
||||
|
||||
## 7. ACK-, ERROR- und SUCCESS-Frames
|
||||
|
||||
### 7.1 ACK (`frame_type = 0x11`) — Host ↔ Device
|
||||
Wird waehrend eines laufenden Stream-Transfers gesendet, um der sendenden Seite Credits (Sendeerlaubnisse) zu erteilen.
|
||||
Bei einem Download (`LS` oder `FILE_GET`) sendet der Host das ACK. Bei einem Upload (`FILE_PUT` oder `TAGS_PUT`) sendet das Device das ACK.
|
||||
|
||||
Format:
|
||||
```c
|
||||
@@ -204,6 +238,24 @@ Fehlercode-Tabelle (Zephyr errno, positiver Wert):
|
||||
| 88 | `ENOSYS` | Funktion nicht implementiert |
|
||||
| 134 | `ENOTSUP` | Operation nicht unterstuetzt |
|
||||
|
||||
### 7.3 SUCCESS (`frame_type = 0x13`) — Device → Host
|
||||
Bestaetigt den erfolgreichen Abschluss einer Operation, z. B. nach Beendigung eines Uploads oder einer Dateioperation (Loeschen, Umbenennen).
|
||||
|
||||
Format:
|
||||
```c
|
||||
// Header:
|
||||
uint8_t frame_type; // 0x13
|
||||
uint16_t payload_length; // 0x0001
|
||||
|
||||
// Payload:
|
||||
uint8_t data_type; // Der Befehl, der erfolgreich war (z.B. 0x21 fuer FILE_PUT)
|
||||
```
|
||||
|
||||
Wire-Format (Beispiel: Erfolg bei RM_FILE):
|
||||
```
|
||||
[0x13][0x01 0x00][0x24]
|
||||
```
|
||||
|
||||
## 8. LS-Stream (Verzeichnisliste)
|
||||
|
||||
Der LS-Stream wird durch einen `REQUEST` mit `data_type = 0x40` ausgeloest und laeuft wie folgt ab:
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
|
||||
import svelte from '@astrojs/svelte';
|
||||
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
base: isProd ? '/buzzer/' : '/',
|
||||
site: 'https://home.iten.pro',
|
||||
|
||||
integrations: [svelte()],
|
||||
|
||||
vite: {
|
||||
|
||||
BIN
webpage/public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
webpage/public/favicon-96x96.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 655 B After Width: | Height: | Size: 15 KiB |
@@ -1,9 +1,17 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||
<style>
|
||||
path { fill: #000; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
path { fill: #FFF; }
|
||||
}
|
||||
</style>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="1000" height="1000"><style>
|
||||
#light-icon {
|
||||
display: inline;
|
||||
}
|
||||
#dark-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
#light-icon {
|
||||
display: none;
|
||||
}
|
||||
#dark-icon {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
</style><g id="light-icon"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="1000" height="1000"><g><g transform="matrix(31.25,0,0,31.25,0,0)"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="32" height="32" fill="#000000" viewBox="0 0 256 256" id="svg1" sodipodi:docname="siren.svg" inkscape:version="1.4 (86a8ad7, 2024-10-11)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"><defs id="defs1"></defs><sodipodi:namedview id="namedview1" pagecolor="#ffffff" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:zoom="36.46875" inkscape:cx="16" inkscape:cy="16" inkscape:window-width="3440" inkscape:window-height="1369" inkscape:window-x="1672" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="svg1"></sodipodi:namedview><path d="m 232,176 v 24 a 16,16 0 0 1 -16,16 H 40 A 16,16 0 0 1 24,200 V 176 A 16,16 0 0 1 40,160 V 128 A 88,88 0 0 1 128.67,40 C 176.82,40.36 216,80.29 216,129 v 31 a 16,16 0 0 1 16,16 z" id="path5"></path><path d="M 216,200 V 176 H 40 v 24 z" id="path7" style="fill:#b3b3b3"></path><path d="M 56,160 H 200 V 129 C 200,89 167.95,56.29 128.55,56 H 128 a 72,72 0 0 0 -72,72 z" id="path6" style="fill:#ff0000"></path><path d="M 137.34,72.11 A 8,8 0 1 0 134.7,87.89 C 153.67,91.08 168,108.32 168,128 a 8,8 0 0 0 16,0 c 0,-27.4 -20.07,-51.43 -46.68,-55.89 z" id="path4"></path><path d="M 50.34,45.66 A 8.0044488,8.0044488 0 0 0 61.66,34.34 l -8,-8 A 8.0044488,8.0044488 0 0 0 42.34,37.66 Z" id="path3"></path><path d="m 200,48 a 8,8 0 0 0 5.66,-2.34 l 8,-8 A 8.0044488,8.0044488 0 0 0 202.34,26.34 l -8,8 A 8,8 0 0 0 200,48 Z" id="path2"></path><path d="M 120,16 V 8 a 8,8 0 0 1 16,0 v 8 a 8,8 0 0 1 -16,0 z" id="path1"></path></svg></g></g></svg></g><g id="dark-icon"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="1000" height="1000"><g><g transform="matrix(31.25,0,0,31.25,0,0)"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="32" height="32" fill="#000000" viewBox="0 0 256 256" id="svg1" sodipodi:docname="siren_dark.svg" inkscape:version="1.4 (86a8ad7, 2024-10-11)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"><defs id="defs1"></defs><sodipodi:namedview id="namedview1" pagecolor="#ffffff" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:zoom="36.46875" inkscape:cx="16" inkscape:cy="16" inkscape:window-width="3440" inkscape:window-height="1369" inkscape:window-x="1672" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="svg1"></sodipodi:namedview><path d="m 232,176 v 24 a 16,16 0 0 1 -16,16 H 40 A 16,16 0 0 1 24,200 V 176 A 16,16 0 0 1 40,160 V 128 A 88,88 0 0 1 128.67,40 C 176.82,40.36 216,80.29 216,129 v 31 a 16,16 0 0 1 16,16 z" id="path5" style="fill:#ffffff"></path><path d="M 216,200 V 176 H 40 v 24 z" id="path7" style="fill:#b3b3b3"></path><path d="M 56,160 H 200 V 129 C 200,89 167.95,56.29 128.55,56 H 128 a 72,72 0 0 0 -72,72 z" id="path6" style="fill:#ff0000"></path><path d="M 137.34,72.11 A 8,8 0 1 0 134.7,87.89 C 153.67,91.08 168,108.32 168,128 a 8,8 0 0 0 16,0 c 0,-27.4 -20.07,-51.43 -46.68,-55.89 z" id="path4" style="fill:#ffffff"></path><path d="M 50.34,45.66 A 8.0044488,8.0044488 0 0 0 61.66,34.34 l -8,-8 A 8.0044488,8.0044488 0 0 0 42.34,37.66 Z" id="path3" style="fill:#ffffff"></path><path d="m 200,48 a 8,8 0 0 0 5.66,-2.34 l 8,-8 A 8.0044488,8.0044488 0 0 0 202.34,26.34 l -8,8 A 8,8 0 0 0 200,48 Z" id="path2" style="fill:#ffffff"></path><path d="M 120,16 V 8 a 8,8 0 0 1 16,0 v 8 a 8,8 0 0 1 -16,0 z" id="path1" style="fill:#ffffff"></path></svg></g></g></svg></g></svg>
|
||||
|
Before Width: | Height: | Size: 749 B After Width: | Height: | Size: 4.4 KiB |
21
webpage/public/site.webmanifest
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "Edis Buzzer",
|
||||
"short_name": "Buzzer",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
BIN
webpage/public/web-app-manifest-192x192.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
webpage/public/web-app-manifest-512x512.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
@@ -6,7 +6,7 @@
|
||||
|
||||
onMount(async () => {
|
||||
performHardwareCheck();
|
||||
|
||||
|
||||
if ($isBluetoothSupported) {
|
||||
const { restoreSession } = await import("../lib/bluetooth");
|
||||
await restoreSession();
|
||||
@@ -22,9 +22,11 @@
|
||||
</div>
|
||||
{:else if !$isBluetoothSupported}
|
||||
<div
|
||||
class="fixed lg:h-screen inset-0 flex flex-col items-center justify-center p-0 lg:p-4 z-[100] bg-white lg:bg-transparent" style="hyphens:auto;">
|
||||
class="fixed lg:h-screen inset-0 flex flex-col items-center justify-center p-0 lg:p-4 z-[100] bg-white lg:bg-transparent"
|
||||
style="hyphens:auto;"
|
||||
>
|
||||
<div
|
||||
class="w-full h-full lg:h-auto lg:max-w-md bg-red-50 lg:border border-red-600 lg:shadow-xl lg:rounded-lg p-6 lg:p-8 text-red-600 flex flex-col justify-center"
|
||||
class="w-full h-full lg:h-auto lg:max-w-md bg-red-50 shadow-red-500/30 lg:border border-red-600 lg:shadow-xl lg:rounded-lg p-6 lg:p-8 text-red-600 flex flex-col justify-center"
|
||||
>
|
||||
<h1 class="text-2xl font-bold mb-2 text-center">Dein Browser ist... suboptimal</h1>
|
||||
<div class="text-center text-7xl md:text-9xl font-bold mb-4">🥺</div>
|
||||
@@ -34,10 +36,12 @@
|
||||
Leider unterstützt dein Browser die benötigten Bluetooth-Funktionen nicht. Bitte versuche
|
||||
es mit einem aktuellen <span class="font-semibold">Chrome</span>
|
||||
oder einem andern Chromium-basierten Browser.
|
||||
<span class="font-semibold">Winzigweich Kante</span> soll gerüchteweise auch Chromium-basiert sein...
|
||||
<span class="font-semibold">Winzigweich Kante</span>
|
||||
soll gerüchteweise auch Chromium-basiert sein...
|
||||
</p>
|
||||
<p>
|
||||
Rundreise auf iOS unterstützt Bluetooth leider nicht, aber du kannst es mit einem vernünftigen Gerät oder Browser versuchen.
|
||||
Rundreise auf iOS unterstützt Bluetooth leider nicht, aber du kannst es mit einem
|
||||
vernünftigen Gerät oder Browser versuchen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,17 +15,22 @@
|
||||
WarningCircleIcon,
|
||||
} from "phosphor-svelte";
|
||||
import {
|
||||
isFetchingRemote,
|
||||
isTransferingRemote,
|
||||
transferStats,
|
||||
transferDetails,
|
||||
buzzerAudioFiles,
|
||||
localAudioFiles,
|
||||
syncStateMap,
|
||||
fsInfo,
|
||||
} from "../lib/store";
|
||||
|
||||
import { SETTINGS } from "../lib/settings";
|
||||
import { tagEditorState } from "../lib/store";
|
||||
import { tooltip } from "../lib/actions/tooltip";
|
||||
import { deleteRemoteFile } from "../lib/transport";
|
||||
import { deleteLocalFile } from "../lib/db";
|
||||
import { refreshRemote, refreshLocal } from "../lib/sync";
|
||||
import { addToast } from "../lib/toast";
|
||||
|
||||
export let file: BuzzerFile;
|
||||
export let type: "local" | "buzzer" = "buzzer";
|
||||
@@ -37,7 +42,7 @@
|
||||
$: myIndex = selectedFiles.findIndex((f) => f.name === file.name);
|
||||
|
||||
$: state = (() => {
|
||||
if (!file.selected || !$isFetchingRemote) return "default";
|
||||
if (!file.selected || !$isTransferingRemote) return "default";
|
||||
if (file.name === $transferStats.currentFileName) return "active";
|
||||
if (myIndex < currentIndex) return "done";
|
||||
if (myIndex > currentIndex) return "pending";
|
||||
@@ -93,7 +98,7 @@
|
||||
})();
|
||||
|
||||
function toggleSelection() {
|
||||
if ($isFetchingRemote) return;
|
||||
if ($isTransferingRemote) return;
|
||||
|
||||
if (type === "buzzer") {
|
||||
buzzerAudioFiles.update((files) =>
|
||||
@@ -110,6 +115,35 @@
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async function handleDeleteClick() {
|
||||
if (!confirm(`Möchten Sie die Datei "${file.name}" wirklich löschen?`)) {
|
||||
menuOpen = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "buzzer") {
|
||||
try {
|
||||
const basePath = $fsInfo?.audioPath || "/lfs/a";
|
||||
const fullPath = `${basePath}/${file.name}`;
|
||||
await deleteRemoteFile(fullPath);
|
||||
addToast(`Datei ${file.name} erfolgreich vom Buzzer gelöscht.`, "success");
|
||||
await refreshRemote();
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Löschen:", error);
|
||||
addToast("Fehler beim Löschen der Datei auf dem Buzzer.", "error");
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await deleteLocalFile(file.name);
|
||||
addToast(`Lokale Datei ${file.name} gelöscht.`, "success");
|
||||
await refreshLocal();
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Löschen:", error);
|
||||
}
|
||||
}
|
||||
menuOpen = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:click={() => (menuOpen = false)} />
|
||||
@@ -128,14 +162,14 @@
|
||||
class="relative z-10 w-full text-left flex-1 px-3 py-1 pr-16 flex items-center border-l-4 transition-colors border-b border-b-border-card
|
||||
{file.selected ? 'border-l-blue-600' : 'border-l-transparent'}
|
||||
{file.selected && state !== 'active' ? 'bg-blue-50' : ''}
|
||||
{!$isFetchingRemote && file.selected ? 'hover:bg-blue-100 cursor-pointer' : ''}
|
||||
{!$isFetchingRemote && !file.selected
|
||||
{!$isTransferingRemote && file.selected ? 'hover:bg-blue-100 cursor-pointer' : ''}
|
||||
{!$isTransferingRemote && !file.selected
|
||||
? 'hover:bg-slate-100 hover:border-l-blue-200 cursor-pointer'
|
||||
: ''}
|
||||
{$isFetchingRemote ? 'cursor-default' : ''}
|
||||
{$isTransferingRemote ? 'cursor-default' : ''}
|
||||
{state === 'pending' ? 'grayscale opacity-80' : ''}"
|
||||
on:click={toggleSelection}
|
||||
disabled={$isFetchingRemote}
|
||||
disabled={$isTransferingRemote}
|
||||
>
|
||||
<MusicNotesIcon weight="fill" class="mr-3 w-5 h-5 shrink-0" />
|
||||
|
||||
@@ -198,10 +232,7 @@
|
||||
<button
|
||||
class="menu-btn danger"
|
||||
title="Löschen"
|
||||
on:click|stopPropagation={() => {
|
||||
console.log("Delete", file.name);
|
||||
menuOpen = false;
|
||||
}}
|
||||
on:click|stopPropagation={handleDeleteClick}
|
||||
>
|
||||
<TrashIcon class="list-menu-icon" />
|
||||
</button>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { fade, slide } from "svelte/transition";
|
||||
import { localAudioFiles, buzzerAudioFiles } from "../lib/store";
|
||||
import { deleteSelectedLocalFiles } from "../lib/sync";
|
||||
import { deleteSelectedLocalFiles, deleteSelectedRemoteFiles } from "../lib/sync";
|
||||
import { addToast } from "../lib/toast";
|
||||
import { tooltip } from "../lib/actions/tooltip";
|
||||
import { updateLocalAudioCrc } from "../lib/tagHandler";
|
||||
@@ -143,12 +143,11 @@
|
||||
class="menu-btn danger"
|
||||
disabled={selectedFileCount === 0}
|
||||
on:click={() => {
|
||||
if (type === "buzzer")
|
||||
addToast(
|
||||
"Löschen von Dateien auf dem Buzzer wird derzeit nicht unterstützt.",
|
||||
"error",
|
||||
);
|
||||
else deleteSelectedLocalFiles();
|
||||
if (type === "buzzer") {
|
||||
deleteSelectedRemoteFiles();
|
||||
} else {
|
||||
deleteSelectedLocalFiles();
|
||||
}
|
||||
closeMenu();
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -2,8 +2,13 @@
|
||||
import { updateFile } from "../lib/tagHandler";
|
||||
import { refreshLocal, refreshRemote } from "../lib/sync";
|
||||
import { fade, slide } from "svelte/transition";
|
||||
import { localAudioFiles, buzzerAudioFiles, fsInfo } from "../lib/store";
|
||||
import type { MetadataTags } from "../lib/types";
|
||||
import {
|
||||
localAudioFiles,
|
||||
buzzerAudioFiles,
|
||||
fsInfo,
|
||||
syncStateMap,
|
||||
} from "../lib/store";
|
||||
import { type MetadataTags, SyncState } from "../lib/types";
|
||||
import {
|
||||
XIcon,
|
||||
CaretLeftIcon,
|
||||
@@ -15,6 +20,7 @@
|
||||
PencilIcon,
|
||||
} from "phosphor-svelte";
|
||||
import { addToast } from "../lib/toast";
|
||||
import { tooltip } from "../lib/actions/tooltip";
|
||||
|
||||
export let show = false;
|
||||
export let type: "local" | "buzzer" = "buzzer";
|
||||
@@ -29,7 +35,7 @@
|
||||
|
||||
$: autoApplyIcon = applyToBoth ? CheckSquareIcon : SquareIcon;
|
||||
$: activeStore = type === "local" ? localAudioFiles : buzzerAudioFiles;
|
||||
$: fileList = $activeStore;
|
||||
$: fileList = $activeStore || [];
|
||||
|
||||
$: if (show && initialFileName !== lastOpenedName) {
|
||||
if (initialFileName) {
|
||||
@@ -44,7 +50,7 @@
|
||||
lastOpenedName = null;
|
||||
}
|
||||
|
||||
$: currentIndex = fileList.findIndex((f) => f.name === currentFileName);
|
||||
$: currentIndex = (fileList || []).findIndex((f) => f.name === currentFileName);
|
||||
$: currentFile = fileList[currentIndex];
|
||||
$: hasDraft = currentFile ? drafts[currentFile.name] !== undefined : false;
|
||||
$: hasAnyDrafts = Object.keys(drafts).length > 0;
|
||||
@@ -53,6 +59,13 @@
|
||||
$: activeTags = activeDraft ? activeDraft.tags : currentFile?.metaTags || {};
|
||||
$: activeName = activeDraft ? activeDraft.newName : currentFile?.name || "";
|
||||
|
||||
$: syncStatus = (currentFileName && $syncStateMap[type]?.[currentFileName]) || { state: SyncState.UNKNOWN, linkedFiles: [] };
|
||||
$: isDuplicate = syncStatus.state === SyncState.DUPLICATE;
|
||||
|
||||
$: if (isDuplicate) {
|
||||
applyToBoth = false;
|
||||
}
|
||||
|
||||
$: maxFilenameLength = $fsInfo ? $fsInfo.maxPathLength - $fsInfo.audioPath.length - 2 : 30;
|
||||
|
||||
function closeEditor() {
|
||||
@@ -120,13 +133,21 @@
|
||||
const newName = draft.newName;
|
||||
|
||||
try {
|
||||
await updateFile(oldName, newName, currentFile.sysTags, draft.tags, type);
|
||||
await updateFile(oldName, newName, currentFile.sysTags, draft.tags, type, applyToBoth);
|
||||
addToast(`Datei ${newName} gespeichert.`, "success");
|
||||
delete drafts[oldName];
|
||||
drafts = drafts;
|
||||
if (oldName !== newName) currentFileName = newName;
|
||||
if (type === "local") await refreshLocal();
|
||||
if (type === "buzzer") await refreshRemote();
|
||||
|
||||
if (applyToBoth) {
|
||||
// Wenn auf beide angewendet, beide Seiten neu laden
|
||||
await refreshLocal();
|
||||
await refreshRemote();
|
||||
} else {
|
||||
// Ansonsten nur die aktive Seite
|
||||
if (type === "local") await refreshLocal();
|
||||
if (type === "buzzer") await refreshRemote();
|
||||
}
|
||||
} catch (error) {
|
||||
addToast("Fehler beim Speichern.", "error");
|
||||
}
|
||||
@@ -138,14 +159,20 @@
|
||||
for (const [oldName, draft] of Object.entries(drafts)) {
|
||||
const file = fileList.find((f) => f.name === oldName);
|
||||
if (file) {
|
||||
await updateFile(oldName, draft.newName, file.sysTags, draft.tags, type);
|
||||
await updateFile(oldName, draft.newName, file.sysTags, draft.tags, type, applyToBoth);
|
||||
savedCount++;
|
||||
}
|
||||
}
|
||||
drafts = {};
|
||||
addToast(`${savedCount} Dateien gespeichert.`, "success");
|
||||
if (type === "local") await refreshLocal();
|
||||
if (type === "buzzer") await refreshRemote();
|
||||
|
||||
if (applyToBoth) {
|
||||
await refreshLocal();
|
||||
await refreshRemote();
|
||||
} else {
|
||||
if (type === "local") await refreshLocal();
|
||||
if (type === "buzzer") await refreshRemote();
|
||||
}
|
||||
} catch (error) {
|
||||
addToast("Fehler beim Speichern.", "error");
|
||||
}
|
||||
@@ -398,15 +425,27 @@
|
||||
<FloppyDiskIcon class="btn-icon" /> Speichern
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
class="menu-btn bg-slate-50 hover:bg-slate-100 !justify-start text-slate-700"
|
||||
on:click={() => (applyToBoth = !applyToBoth)}
|
||||
<div
|
||||
use:tooltip={{
|
||||
text: "Diese Option ist deaktiviert, da ein Duplikat-Konflikt vorliegt. Lösen Sie den Konflikt, um Änderungen auf beiden Seiten anwenden zu können.",
|
||||
pos: "top",
|
||||
variant: "danger",
|
||||
disabled: !isDuplicate,
|
||||
}}
|
||||
class:grayscale={isDuplicate}
|
||||
class:cursor-not-allowed={isDuplicate}
|
||||
>
|
||||
<svelte:component
|
||||
this={autoApplyIcon}
|
||||
class="btn-icon {applyToBoth ? 'text-blue-600' : 'text-slate-400'}"
|
||||
/> Auch {type === "buzzer" ? "lokal" : "auf dem Buzzer"} anwenden
|
||||
</button>
|
||||
<button
|
||||
class="menu-btn bg-slate-50 hover:bg-slate-100 !justify-start text-slate-700 w-full"
|
||||
on:click={() => (applyToBoth = !applyToBoth)}
|
||||
disabled={isDuplicate}
|
||||
>
|
||||
<svelte:component
|
||||
this={autoApplyIcon}
|
||||
class="btn-icon {applyToBoth ? 'text-blue-600' : 'text-slate-400'}"
|
||||
/> Auch {type === "buzzer" ? "lokal" : "auf dem Buzzer"} anwenden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import FileList from "./FileList.svelte";
|
||||
import DeviceInfo from "./DeviceInfo.svelte";
|
||||
import { refreshRemote } from "../lib/sync";
|
||||
import { transferStats, isFetchingRemote, pairedDevices, activeDeviceId } from "../lib/store";
|
||||
import { transferStats, isTransferingRemote, pairedDevices, activeDeviceId } from "../lib/store";
|
||||
import { SETTINGS } from "../lib/settings";
|
||||
import TransferProgress from "./TransferProgress.svelte";
|
||||
import FileMenuOverlay from "./FileMenuOverlay.svelte";
|
||||
@@ -25,12 +25,12 @@
|
||||
refreshRemote();
|
||||
}
|
||||
|
||||
$: if ($isFetchingRemote && $transferStats.overallTotal > 0) {
|
||||
$: if ($isTransferingRemote && $transferStats.overallTotal > 0) {
|
||||
// Transfer startet oder läuft
|
||||
showOverlay = true;
|
||||
isTransferFinished = false;
|
||||
clearTimeout(overlayTimeout);
|
||||
} else if (showOverlay && !$isFetchingRemote && $transferStats.overallDone > 0) {
|
||||
} else if (showOverlay && !$isTransferingRemote && $transferStats.overallDone > 0) {
|
||||
// Transfer wurde soeben abgeschlossen
|
||||
isTransferFinished = true;
|
||||
overlayTimeout = setTimeout(closeOverlay, SETTINGS.ui.transferOverlayPersistMs);
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
import { XIcon } from "phosphor-svelte";
|
||||
import { isFetchingRemote, transferStats, transferDetails } from '../lib/store';
|
||||
import { isTransferingRemote, transferStats, transferDetails } from '../lib/store';
|
||||
import { SETTINGS } from '../lib/settings';
|
||||
|
||||
let showOverlay = false;
|
||||
let isTransferFinished = false;
|
||||
let overlayTimeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
$: if ($isFetchingRemote && $transferStats.overallTotal > 0) {
|
||||
$: if ($isTransferingRemote && $transferStats.overallTotal > 0) {
|
||||
showOverlay = true;
|
||||
isTransferFinished = false;
|
||||
clearTimeout(overlayTimeout);
|
||||
} else if (showOverlay && !$isFetchingRemote && $transferStats.overallDone > 0) {
|
||||
} else if (showOverlay && !$isTransferingRemote && $transferStats.overallDone > 0) {
|
||||
isTransferFinished = true;
|
||||
overlayTimeout = setTimeout(closeOverlay, SETTINGS.ui.transferOverlayPersistMs);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,30 @@
|
||||
<!-- MainLayout.astro -->
|
||||
---
|
||||
import "../styles/app.css";
|
||||
---
|
||||
|
||||
<!-- MainLayout.astro -->
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
href={`${import.meta.env.BASE_URL}favicon-96x96.png`}
|
||||
sizes="96x96"
|
||||
/>
|
||||
<link rel="icon" type="image/svg+xml" href={`${import.meta.env.BASE_URL}favicon.svg`} />
|
||||
<link rel="shortcut icon" href={`${import.meta.env.BASE_URL}favicon.ico`} />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href={`${import.meta.env.BASE_URL}apple-touch-icon.png`}
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-title" content="Edis Buzzer" />
|
||||
<link rel="manifest" href={`${import.meta.env.BASE_URL}site.webmanifest`} />
|
||||
<title>Edis Buzzer</title>
|
||||
</head>
|
||||
<body class="bg-surface text-on-surface subpixel-antialiased transition-colors duration-300">
|
||||
<body class="bg-surface text-on-surface antialiased transition-colors duration-300">
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -3,12 +3,13 @@ export const SETTINGS = {
|
||||
connectionKey: 'buzzer_connection_state'
|
||||
},
|
||||
bluetooth: {
|
||||
connectionTimeoutMs: 10000 // Timeout für den Verbindungsaufbau
|
||||
connectionTimeoutMs: 3000, // Timeout für den Verbindungsaufbau
|
||||
appleMaxInflight: 15, // iOS erlaubt nur wenige unbestätigte Nachrichten, daher begrenzen wir die Anzahl der gleichzeitig gesendeten Frames
|
||||
},
|
||||
ui: {
|
||||
toastDurationMs: 5000,
|
||||
transferUpdateIntervalMs: 300,
|
||||
kbpsCalculationWindowMs: 5500,
|
||||
transferUpdateIntervalMs: 1000,
|
||||
kbpsCalculationWindowMs: 1000,
|
||||
transferOverlayPersistMs: 4000,
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -75,7 +75,7 @@ export const buzzerSysFiles = writable<BuzzerFile[]>([]);
|
||||
export const localAudioFiles = writable<BuzzerFile[]>([]);
|
||||
|
||||
// Ladezustände getrennt nach Quelle
|
||||
export const isFetchingRemote = writable<boolean>(false);
|
||||
export const isTransferingRemote = writable<boolean>(false);
|
||||
export const isFetchingLocal = writable<boolean>(false);
|
||||
|
||||
// Persistenz des letzten Verbindungsziels (nur im Browser nutzbar)
|
||||
@@ -189,7 +189,7 @@ export function resetRemote(): void {
|
||||
activeDeviceId.set(null);
|
||||
buzzerAudioFiles.set([]);
|
||||
buzzerSysFiles.set([]);
|
||||
isFetchingRemote.set(false);
|
||||
isTransferingRemote.set(false);
|
||||
resetTransferStats();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { get } from 'svelte/store';
|
||||
import { isConnected, fsInfo, buzzerAudioFiles, buzzerSysFiles, isFetchingRemote, isFetchingLocal, storageUsage, localAudioFiles, transferStats } from './store';
|
||||
import { isConnected, fsInfo, buzzerAudioFiles, buzzerSysFiles, isTransferingRemote, isFetchingLocal, storageUsage, localAudioFiles, transferStats } from './store';
|
||||
import { requestProtocolInfo, requestFSInfo, fetchDirectory } from './transport';
|
||||
import type { BuzzerFile } from './types';
|
||||
import { getFile, putFile } from './transport';
|
||||
import { getFile, putFile, deleteRemoteFile } from './transport';
|
||||
import { addToast } from './toast';
|
||||
import { getLocalFiles, deleteLocalFile, getLocalFile } from './db';
|
||||
import { parseAudioFileTags } from './tagHandler';
|
||||
@@ -24,7 +24,7 @@ function mapToBuzzerFile(rawFile: any): BuzzerFile {
|
||||
export async function refreshRemote() {
|
||||
if (!get(isConnected)) return;
|
||||
|
||||
isFetchingRemote.set(true);
|
||||
isTransferingRemote.set(true);
|
||||
try {
|
||||
await requestProtocolInfo();
|
||||
await requestFSInfo();
|
||||
@@ -64,7 +64,7 @@ export async function refreshRemote() {
|
||||
console.error("Fehler beim Aktualisieren der Buzzer-Daten:", error);
|
||||
addToast("Fehler beim Laden der Daten vom Buzzer: " + (error instanceof Error ? error.message : "Unbekannter Fehler"), "error");
|
||||
} finally {
|
||||
isFetchingRemote.set(false);
|
||||
isTransferingRemote.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ export async function downloadSelectedFiles() {
|
||||
bulkStartTime: bulkStart
|
||||
}));
|
||||
|
||||
isFetchingRemote.set(true);
|
||||
isTransferingRemote.set(true);
|
||||
|
||||
try {
|
||||
for (const file of files) {
|
||||
@@ -142,7 +142,7 @@ export async function downloadSelectedFiles() {
|
||||
...s,
|
||||
overallDone: s.overallTotal,
|
||||
}));
|
||||
isFetchingRemote.set(false);
|
||||
isTransferingRemote.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,6 +152,10 @@ export async function deleteSelectedLocalFiles() {
|
||||
|
||||
if (selectedFiles.length === 0) return;
|
||||
|
||||
if (!confirm(`Möchten Sie wirklich ${selectedFiles.length} lokale Datei(en) löschen?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
for (const file of selectedFiles) {
|
||||
await deleteLocalFile(file.name);
|
||||
@@ -163,6 +167,33 @@ export async function deleteSelectedLocalFiles() {
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteSelectedRemoteFiles() {
|
||||
const files = get(buzzerAudioFiles).filter(f => f.selected);
|
||||
const pathPrefix = get(fsInfo)?.audioPath || "/lfs/a";
|
||||
|
||||
if (files.length === 0) return;
|
||||
|
||||
if (!confirm(`Möchten Sie wirklich ${files.length} Datei(en) auf dem Buzzer löschen?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
isTransferingRemote.set(true);
|
||||
try {
|
||||
for (const file of files) {
|
||||
const fullPath = `${pathPrefix}/${file.name}`;
|
||||
console.debug(`Lösche Datei auf dem Buzzer: ${fullPath}`);
|
||||
await deleteRemoteFile(fullPath);
|
||||
}
|
||||
addToast(`${files.length} Datei(en) auf dem Buzzer gelöscht.`, "success");
|
||||
await refreshRemote();
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Löschen auf dem Buzzer:", error);
|
||||
addToast("Fehler beim Löschen: " + (error instanceof Error ? error.message : "Unbekannt"), "error");
|
||||
} finally {
|
||||
isTransferingRemote.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
export async function uploadSelectedFiles() {
|
||||
const files = get(localAudioFiles).filter(f => f.selected);
|
||||
const pathPrefix = get(fsInfo)?.audioPath || "/lfs/a";
|
||||
@@ -183,7 +214,7 @@ export async function uploadSelectedFiles() {
|
||||
}));
|
||||
|
||||
// Wir nutzen isFetchingRemote als generischen "Transfer aktiv"-Trigger für das UI TODO: Namensänderung in isTransferring?
|
||||
isFetchingRemote.set(true);
|
||||
isTransferingRemote.set(true);
|
||||
|
||||
try {
|
||||
for (const file of files) {
|
||||
@@ -216,6 +247,6 @@ export async function uploadSelectedFiles() {
|
||||
...s,
|
||||
overallDone: s.overallTotal, // Schließt den Ladebalken visuell sauber ab
|
||||
}));
|
||||
isFetchingRemote.set(false);
|
||||
isTransferingRemote.set(false);
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,9 @@ import { getLocalFiles, saveLocalFile, deleteLocalFile } from './db';
|
||||
import type { SystemTags, MetadataTags } from './types';
|
||||
import { addToast } from './toast';
|
||||
import { crc32 } from './protocol/crc32';
|
||||
import { getTags, putTags } from './transport';
|
||||
import { getTags, putTags, renameRemoteFile } from './transport';
|
||||
import { get } from 'svelte/store';
|
||||
import { fsInfo } from './store';
|
||||
import { fsInfo, syncStateMap } from './store';
|
||||
|
||||
function getEmptyTags() {
|
||||
return {
|
||||
@@ -206,25 +206,24 @@ export async function updateRemoteFile(
|
||||
sysTags: SystemTags,
|
||||
newMetaTags: MetadataTags
|
||||
): Promise<void> {
|
||||
// Da das Binärprotokoll noch kein echtes "Rename"-Kommando hat,
|
||||
// blockieren wir Dateinamen-Änderungen für den Buzzer vorerst.
|
||||
const currentFsInfo = get(fsInfo);
|
||||
const basePath = currentFsInfo?.audioPath || "/lfs/a";
|
||||
const oldFullPath = `${basePath}/${oldName}`;
|
||||
const newFullPath = `${basePath}/${newName}`;
|
||||
|
||||
if (oldName !== newName) {
|
||||
throw new Error("Das Umbenennen von Dateien direkt auf dem Buzzer wird noch nicht unterstützt. Bitte speichere nur die Tags.");
|
||||
console.log(`TagHandler: Benenne Datei auf dem Buzzer um (${oldFullPath} -> ${newFullPath})`);
|
||||
await renameRemoteFile(oldFullPath, newFullPath);
|
||||
}
|
||||
|
||||
// 1. Den binären TLV-Block inkl. Footer bauen
|
||||
const newTagsBuffer = buildTagBlock(sysTags, newMetaTags);
|
||||
const tagsBlob = new Blob([newTagsBuffer]);
|
||||
|
||||
// 2. Zielpfad ermitteln
|
||||
const currentFsInfo = get(fsInfo);
|
||||
const basePath = currentFsInfo?.audioPath || "/lfs/a";
|
||||
const fullPath = `${basePath}/${oldName}`;
|
||||
|
||||
console.log(`Sende modifizierte Tags (${tagsBlob.size} Bytes) an ${fullPath}...`);
|
||||
console.log(`Sende modifizierte Tags (${tagsBlob.size} Bytes) an ${newFullPath}...`);
|
||||
|
||||
// 3. Über das Protokoll an den Buzzer senden
|
||||
await putTags(tagsBlob, fullPath);
|
||||
await putTags(tagsBlob, newFullPath);
|
||||
}
|
||||
|
||||
export async function updateFile(
|
||||
@@ -232,12 +231,43 @@ export async function updateFile(
|
||||
newName: string,
|
||||
sysTags: SystemTags,
|
||||
newMetaTags: MetadataTags,
|
||||
type: "local" | "buzzer"
|
||||
type: "local" | "buzzer",
|
||||
applyToBoth: boolean = false
|
||||
): Promise<void> {
|
||||
if (type === "local") {
|
||||
await updateLocalFile(oldName, newName, sysTags, newMetaTags);
|
||||
if (applyToBoth) {
|
||||
try {
|
||||
const sMap = get(syncStateMap);
|
||||
const syncStatus = sMap.local[oldName];
|
||||
if (syncStatus && syncStatus.linkedFiles.length > 0) {
|
||||
const remoteOldName = syncStatus.linkedFiles[0];
|
||||
await updateRemoteFile(remoteOldName, newName, sysTags, newMetaTags);
|
||||
} else {
|
||||
console.warn(`Keine verknüpfte Remote-Datei für '${oldName}' gefunden. Überspringe Remote-Update.`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Fehler beim synchronen Aktualisieren der Buzzer-Datei:", e);
|
||||
addToast("Fehler beim Update der Buzzer-Datei: " + (e instanceof Error ? e.message : "Unbekannt"), "error");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await updateRemoteFile(oldName, newName, sysTags, newMetaTags);
|
||||
if (applyToBoth) {
|
||||
try {
|
||||
const sMap = get(syncStateMap);
|
||||
const syncStatus = sMap.buzzer[oldName];
|
||||
if (syncStatus && syncStatus.linkedFiles.length > 0) {
|
||||
const localOldName = syncStatus.linkedFiles[0];
|
||||
await updateLocalFile(localOldName, newName, sysTags, newMetaTags);
|
||||
} else {
|
||||
throw new Error(`Keine verknüpfte lokale Datei für '${oldName}' gefunden.`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Fehler beim synchronen Aktualisieren der lokalen Datei:", e);
|
||||
addToast("Fehler beim Update der lokalen Datei: " + (e instanceof Error ? e.message : "Unbekannt"), "error");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { buildLSRequest, buildProtocolInfoRequest, buildFSInfoRequest, setLsResolver, buildFileGetRequest, setFileGetResolver, buildTagsGetRequest, uploadState } from './protocol/parser';
|
||||
import { crc32 } from './protocol/crc32';
|
||||
import { get } from 'svelte/store';
|
||||
import { protocolInfo, transferStats } from './store';
|
||||
import { protocolInfo, transferStats, } from './store';
|
||||
import { DATA, FRAME } from './protocol/constants';
|
||||
import { isConnected, resetRemote } from './store';
|
||||
import { SETTINGS } from './settings';
|
||||
|
||||
const isMac = navigator.userAgent.includes('Macintosh') || navigator.userAgent.includes('Mac OS X');
|
||||
const MAX_INFLIGHT = isMac ? SETTINGS.bluetooth.appleMaxInflight : Infinity; // iOS erlaubt nur wenige unbestätigte Nachrichten
|
||||
|
||||
console.log("Transport: Max Inflight Frames =", MAX_INFLIGHT);
|
||||
|
||||
export type FrameSender = (buffer: ArrayBuffer) => Promise<void>;
|
||||
let currentSender: FrameSender | null = null;
|
||||
@@ -183,6 +189,10 @@ export async function putFile(fileBlob: Blob, remotePath: string, fileNameForUI:
|
||||
});
|
||||
}
|
||||
|
||||
if (uploadState.credits > MAX_INFLIGHT) {
|
||||
uploadState.credits = MAX_INFLIGHT;
|
||||
}
|
||||
|
||||
const chunkLen = Math.min(maxChunkSize, fileData.length - offset);
|
||||
const chunkData = fileData.subarray(offset, offset + chunkLen);
|
||||
|
||||
@@ -252,6 +262,51 @@ export async function putFile(fileBlob: Blob, remotePath: string, fileNameForUI:
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteRemoteFile(fullPath: string): Promise<void> {
|
||||
const pathBytes = new TextEncoder().encode(fullPath);
|
||||
const payloadLength = 1 + 1 + pathBytes.length; // data_type(1) + path_length(1) + path
|
||||
|
||||
const buffer = new ArrayBuffer(3 + payloadLength);
|
||||
const view = new DataView(buffer);
|
||||
const uint8View = new Uint8Array(buffer);
|
||||
|
||||
view.setUint8(0, FRAME.REQUEST);
|
||||
view.setUint16(1, payloadLength, true);
|
||||
|
||||
view.setUint8(3, 0x24); // BUZZ_DATA_RM_FILE
|
||||
view.setUint8(4, pathBytes.length);
|
||||
uint8View.set(pathBytes, 5);
|
||||
|
||||
await sendFrame(buffer);
|
||||
// Kurze Wartezeit, bis der Parser SUCCESS verarbeitet und der Flash fertig ist
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
}
|
||||
|
||||
export async function renameRemoteFile(oldFullPath: string, newFullPath: string): Promise<void> {
|
||||
const oldBytes = new TextEncoder().encode(oldFullPath);
|
||||
const newBytes = new TextEncoder().encode(newFullPath);
|
||||
|
||||
const payloadLength = 1 + 1 + 1 + oldBytes.length + newBytes.length; // data_type + 2x len + 2x string
|
||||
|
||||
const buffer = new ArrayBuffer(3 + payloadLength);
|
||||
const view = new DataView(buffer);
|
||||
const uint8View = new Uint8Array(buffer);
|
||||
|
||||
view.setUint8(0, FRAME.REQUEST);
|
||||
view.setUint16(1, payloadLength, true);
|
||||
|
||||
view.setUint8(3, 0x25); // BUZZ_DATA_RENAME_FILE
|
||||
view.setUint8(4, oldBytes.length);
|
||||
view.setUint8(5, newBytes.length);
|
||||
|
||||
uint8View.set(oldBytes, 6);
|
||||
uint8View.set(newBytes, 6 + oldBytes.length);
|
||||
|
||||
await sendFrame(buffer);
|
||||
// Kurze Wartezeit, bis der Parser SUCCESS verarbeitet und der Flash fertig ist
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
}
|
||||
|
||||
export async function putTags(tagsBlob: Blob, remotePath: string): Promise<void> {
|
||||
if (isFileTransferring) {
|
||||
throw new Error("Ein Dateitransfer läuft bereits.");
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<!-- index.astro -->
|
||||
---
|
||||
import AppGuard from "../components/AppGuard.svelte";
|
||||
import MainLayout from "../layouts/MainLayout.astro";
|
||||
import Header from "../components/Header.svelte";
|
||||
import MainGrid from "../components/MainGrid.svelte";
|
||||
---
|
||||
|
||||
<!-- index.astro -->
|
||||
<MainLayout>
|
||||
<AppGuard client:load>
|
||||
<Header client:load/>
|
||||
<MainGrid client:load/>
|
||||
<AppGuard client:only="svelte">
|
||||
<Header client:only="svelte" />
|
||||
<MainGrid client:only="svelte" />
|
||||
</AppGuard>
|
||||
</MainLayout>
|
||||
</MainLayout>
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
}
|
||||
|
||||
.card-title {
|
||||
@apply font-light font-features-['smcp'] tracking-tight;
|
||||
@apply font-light font-features-['smcp'] tracking-tighter;
|
||||
}
|
||||
|
||||
/* .card-body {
|
||||
|
||||