zwischenstand
@@ -28,34 +28,34 @@ if BLE_MGMT
|
|||||||
help
|
help
|
||||||
Maximal advertising interval. 160 equals to 100ms.
|
Maximal advertising interval. 160 equals to 100ms.
|
||||||
|
|
||||||
config BT_L2CAP_TX_MTU
|
# config BT_L2CAP_TX_MTU
|
||||||
default 247
|
# default 247
|
||||||
config BT_BUF_ACL_RX_SIZE
|
# config BT_BUF_ACL_RX_SIZE
|
||||||
default 251
|
# default 251
|
||||||
config BT_BUF_ACL_TX_SIZE
|
# config BT_BUF_ACL_TX_SIZE
|
||||||
default 251
|
# default 251
|
||||||
config BT_CTLR_DATA_LENGTH_MAX
|
# config BT_CTLR_DATA_LENGTH_MAX
|
||||||
default 251
|
# default 251
|
||||||
config BT_USER_DATA_LEN_UPDATE
|
# config BT_USER_DATA_LEN_UPDATE
|
||||||
default y
|
# default y
|
||||||
config BT_USER_PHY_UPDATE
|
# config BT_USER_PHY_UPDATE
|
||||||
default y
|
# default y
|
||||||
config BT_HCI_ACL_FLOW_CONTROL
|
# config BT_HCI_ACL_FLOW_CONTROL
|
||||||
default y
|
# default y
|
||||||
config BT_BUF_CMD_TX_COUNT
|
# config BT_BUF_CMD_TX_COUNT
|
||||||
default 24
|
# default 24
|
||||||
config BT_BUF_EVT_RX_COUNT
|
# config BT_BUF_EVT_RX_COUNT
|
||||||
default 22
|
# default 22
|
||||||
config BT_BUF_ACL_TX_COUNT
|
# config BT_BUF_ACL_TX_COUNT
|
||||||
default 20
|
# default 20
|
||||||
config BT_L2CAP_TX_BUF_COUNT
|
# config BT_L2CAP_TX_BUF_COUNT
|
||||||
default 20
|
# default 20
|
||||||
config BT_CONN_TX_MAX
|
# config BT_CONN_TX_MAX
|
||||||
default 20
|
# default 20
|
||||||
config BT_CTLR_SDC_TX_PACKET_COUNT
|
# config BT_CTLR_SDC_TX_PACKET_COUNT
|
||||||
default 20
|
# default 20
|
||||||
config BT_CTLR_SDC_RX_PACKET_COUNT
|
# config BT_CTLR_SDC_RX_PACKET_COUNT
|
||||||
default 20
|
# default 20
|
||||||
config BT_MAX_CONN
|
config BT_MAX_CONN
|
||||||
default 2
|
default 2
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
#include <zephyr/bluetooth/gatt.h>
|
#include <zephyr/bluetooth/gatt.h>
|
||||||
#include <zephyr/logging/log.h>
|
#include <zephyr/logging/log.h>
|
||||||
#include <zephyr/bluetooth/gatt.h>
|
#include <zephyr/bluetooth/gatt.h>
|
||||||
|
#include <zephyr/bluetooth/conn.h>
|
||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#include "ble_mgmt.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 ble_mgmt_rx_cb_t app_rx_cb = NULL;
|
||||||
static bool notify_enabled = false;
|
static bool notify_enabled = false;
|
||||||
|
|
||||||
static uint16_t current_tx_mtu = 23;
|
static uint16_t current_tx_mtu = 23;
|
||||||
|
static uint16_t current_rx_mtu = 23;
|
||||||
|
|
||||||
#define MAX_ADV_NAME_LEN 29
|
#define MAX_ADV_NAME_LEN 29
|
||||||
static char current_device_name[MAX_ADV_NAME_LEN + 1];
|
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);
|
LOG_INF("MTU exchanged: TX %u bytes, RX %u bytes", tx, rx);
|
||||||
current_tx_mtu = tx;
|
current_tx_mtu = tx;
|
||||||
|
current_rx_mtu = rx;
|
||||||
}
|
}
|
||||||
|
|
||||||
static ssize_t rx_cb(struct bt_conn *conn, const struct bt_gatt_attr *attr,
|
static ssize_t rx_cb(struct bt_conn *conn, const struct bt_gatt_attr *attr,
|
||||||
@@ -60,7 +65,8 @@ static ssize_t rx_cb(struct bt_conn *conn, const struct bt_gatt_attr *attr,
|
|||||||
LOG_DBG("Received %u bytes", len);
|
LOG_DBG("Received %u bytes", len);
|
||||||
LOG_HEXDUMP_DBG(buf, len, "Data:");
|
LOG_HEXDUMP_DBG(buf, len, "Data:");
|
||||||
|
|
||||||
if (app_rx_cb) {
|
if (app_rx_cb)
|
||||||
|
{
|
||||||
app_rx_cb((const uint8_t *)buf, len);
|
app_rx_cb((const uint8_t *)buf, len);
|
||||||
}
|
}
|
||||||
return 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)
|
static void tx_ccc_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value)
|
||||||
{
|
{
|
||||||
notify_enabled = (value == BT_GATT_CCC_NOTIFY);
|
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_SERVICE_DEFINE(ble_mgmt_svc,
|
||||||
BT_GATT_PRIMARY_SERVICE(&buzz_service_uuid),
|
BT_GATT_PRIMARY_SERVICE(&buzz_service_uuid),
|
||||||
BT_GATT_CHARACTERISTIC(&buzz_rx_uuid.uuid, BT_GATT_CHRC_WRITE_WITHOUT_RESP,
|
BT_GATT_CHARACTERISTIC(&buzz_rx_uuid.uuid, BT_GATT_CHRC_WRITE_WITHOUT_RESP,
|
||||||
BT_GATT_PERM_WRITE, NULL, rx_cb, NULL),
|
BT_GATT_PERM_WRITE, NULL, rx_cb, NULL),
|
||||||
BT_GATT_CHARACTERISTIC(&buzz_tx_uuid.uuid, BT_GATT_CHRC_NOTIFY,
|
BT_GATT_CHARACTERISTIC(&buzz_tx_uuid.uuid, BT_GATT_CHRC_NOTIFY,
|
||||||
BT_GATT_PERM_NONE, NULL, NULL, NULL),
|
BT_GATT_PERM_NONE, NULL, NULL, NULL),
|
||||||
BT_GATT_CCC(tx_ccc_cfg_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE)
|
BT_GATT_CCC(tx_ccc_cfg_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE));
|
||||||
);
|
|
||||||
|
|
||||||
uint16_t ble_mgmt_get_max_payload(void)
|
uint16_t ble_mgmt_get_max_payload(void)
|
||||||
{
|
{
|
||||||
/* Kappe die verhandelte MTU auf die hart konfigurierte Zephyr-Puffergrenze */
|
/* 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
|
#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;
|
effective_mtu = CONFIG_BT_L2CAP_TX_MTU;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -98,19 +104,23 @@ uint16_t ble_mgmt_get_max_payload(void)
|
|||||||
|
|
||||||
int ble_mgmt_send(const uint8_t *data, uint16_t len)
|
int ble_mgmt_send(const uint8_t *data, uint16_t len)
|
||||||
{
|
{
|
||||||
if (!notify_enabled) {
|
if (!notify_enabled)
|
||||||
|
{
|
||||||
return -EACCES;
|
return -EACCES;
|
||||||
}
|
}
|
||||||
int rc;
|
int rc;
|
||||||
|
|
||||||
do {
|
do
|
||||||
|
{
|
||||||
rc = bt_gatt_notify(NULL, &ble_mgmt_svc.attrs[4], data, len);
|
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
|
k_sleep(K_MSEC(5)); // Thread pausieren, bis TX-Buffer frei wird
|
||||||
}
|
}
|
||||||
} while (rc == -ENOMEM);
|
} while (rc == -ENOMEM);
|
||||||
|
|
||||||
if (rc) {
|
if (rc)
|
||||||
|
{
|
||||||
LOG_ERR("Failed to send notification (err %d)", rc);
|
LOG_ERR("Failed to send notification (err %d)", rc);
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
@@ -120,7 +130,8 @@ int ble_mgmt_send(const uint8_t *data, uint16_t len)
|
|||||||
/* Interne Hilfsfunktion zur Zuweisung des Namens */
|
/* Interne Hilfsfunktion zur Zuweisung des Namens */
|
||||||
static void set_device_name(const char *name)
|
static void set_device_name(const char *name)
|
||||||
{
|
{
|
||||||
if (!name) {
|
if (!name)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +155,8 @@ int ble_mgmt_update_adv_name(const char *new_name)
|
|||||||
set_device_name(new_name);
|
set_device_name(new_name);
|
||||||
|
|
||||||
rc = bt_le_adv_start(&adv_param, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd));
|
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);
|
LOG_ERR("Advertising failed to restart after name update (err %d)", rc);
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
@@ -153,40 +165,10 @@ int ble_mgmt_update_adv_name(const char *new_name)
|
|||||||
return 0;
|
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)
|
static void connected(struct bt_conn *conn, uint8_t err)
|
||||||
{
|
{
|
||||||
if (err) {
|
if (err)
|
||||||
|
{
|
||||||
LOG_ERR("Connection failed (err 0x%02x)", err);
|
LOG_ERR("Connection failed (err 0x%02x)", err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -195,50 +177,38 @@ static void connected(struct bt_conn *conn, uint8_t err)
|
|||||||
struct bt_conn_info info;
|
struct bt_conn_info info;
|
||||||
|
|
||||||
int rc = bt_conn_get_info(conn, &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));
|
bt_addr_le_to_str(info.le.dst, addr_str, sizeof(addr_str));
|
||||||
LOG_INF("Connected to %s", addr_str);
|
LOG_INF("Connected to %s", addr_str);
|
||||||
|
LOG_INF("Role: %s", info.role == BT_CONN_ROLE_CENTRAL ? "Central" : "Peripheral");
|
||||||
/* Nur noch die Rolle ausgeben, da Timing-Parameter hier deprecated sind */
|
}
|
||||||
LOG_DBG("Role: %s", info.role == BT_CONN_ROLE_CENTRAL ? "Central" : "Peripheral");
|
else
|
||||||
} else {
|
{
|
||||||
LOG_INF("Connected (info retrieval failed)");
|
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)
|
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 */
|
/* Startet Advertising mit dem global definierten Setup neu */
|
||||||
int rc = bt_le_adv_start(&adv_param, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd));
|
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);
|
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)
|
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" :
|
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";
|
||||||
(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 *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);
|
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_param_updated = le_param_updated,
|
||||||
.le_phy_updated = le_phy_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
|
config BUZZ_PROTO_SLAB_SIZE
|
||||||
int "Slab Size"
|
int "Slab Size"
|
||||||
default 256
|
default 512
|
||||||
help
|
help
|
||||||
Size of the memory slabs used for message buffers. Must be large enough to hold the largest expected message.
|
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
|
config BUZZ_PROTO_MSGQ_SIZE
|
||||||
int "Message Queue Size"
|
int "Message Queue Size"
|
||||||
default 16
|
default 64
|
||||||
help
|
help
|
||||||
Number of messages that can be queued for processing. Adjust based on expected message burstiness.
|
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 int open_count = 0;
|
||||||
static struct k_mutex flash_pm_lock;
|
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))
|
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);
|
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_IDLE,
|
||||||
FS_STATE_RECEIVING_FILE,
|
FS_STATE_RECEIVING_FILE,
|
||||||
FS_STATE_RECEIVING_TAGS,
|
FS_STATE_RECEIVING_TAGS,
|
||||||
@@ -509,7 +512,8 @@ static void fs_thread_entry(void *p1, void *p2, void *p3)
|
|||||||
if (rc == -EAGAIN)
|
if (rc == -EAGAIN)
|
||||||
{
|
{
|
||||||
LOG_WRN("Write timeout! Aborting transfer.");
|
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_close(&write_ctx.file);
|
||||||
fs_mgmt_pm_unlink(write_ctx.filename);
|
fs_mgmt_pm_unlink(write_ctx.filename);
|
||||||
}
|
}
|
||||||
@@ -522,7 +526,8 @@ static void fs_thread_entry(void *p1, void *p2, void *p3)
|
|||||||
case FS_STATE_IDLE:
|
case FS_STATE_IDLE:
|
||||||
if (msg.op == FS_WRITE_OP_FILE_START)
|
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");
|
LOG_ERR("Filename too long");
|
||||||
buzz_proto_send_error_reusing_slab(msg.reply_cb, ENAMETOOLONG, msg.slab_ptr);
|
buzz_proto_send_error_reusing_slab(msg.reply_cb, ENAMETOOLONG, msg.slab_ptr);
|
||||||
break;
|
break;
|
||||||
@@ -534,23 +539,27 @@ static void fs_thread_entry(void *p1, void *p2, void *p3)
|
|||||||
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);
|
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.state = FS_STATE_RECEIVING_FILE;
|
||||||
write_ctx.crc32 = 0;
|
write_ctx.crc32 = 0;
|
||||||
write_ctx.unacked_chunks = 0;
|
write_ctx.unacked_chunks = 0;
|
||||||
LOG_INF("File transfer started: %s (Expected: %u bytes)", write_ctx.filename, msg.metadata);
|
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_buf_free(&msg.slab_ptr);
|
||||||
buzz_proto_send_ack(msg.reply_cb, credits);
|
buzz_proto_send_ack(msg.reply_cb, credits);
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
LOG_ERR("Failed to open %s: %d", write_ctx.filename, rc);
|
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);
|
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)
|
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);
|
buzz_proto_send_error_reusing_slab(msg.reply_cb, ENAMETOOLONG, msg.slab_ptr);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -561,10 +570,12 @@ static void fs_thread_entry(void *p1, void *p2, void *p3)
|
|||||||
/* Datei öffnen: Nur Lese- und Schreibrechte, Datei muss bereits existieren */
|
/* Datei öffnen: Nur Lese- und Schreibrechte, Datei muss bereits existieren */
|
||||||
int rc = fs_mgmt_pm_open(&write_ctx.file, write_ctx.filename, FS_O_READ | FS_O_WRITE);
|
int rc = fs_mgmt_pm_open(&write_ctx.file, write_ctx.filename, FS_O_READ | FS_O_WRITE);
|
||||||
|
|
||||||
if (rc == 0) {
|
if (rc == 0)
|
||||||
|
{
|
||||||
ssize_t audio_len = fs_get_audio_data_len(&write_ctx.file);
|
ssize_t audio_len = fs_get_audio_data_len(&write_ctx.file);
|
||||||
|
|
||||||
if (audio_len < 0) {
|
if (audio_len < 0)
|
||||||
|
{
|
||||||
LOG_ERR("Failed to get audio length: %d", (int)audio_len);
|
LOG_ERR("Failed to get audio length: %d", (int)audio_len);
|
||||||
fs_mgmt_pm_close(&write_ctx.file);
|
fs_mgmt_pm_close(&write_ctx.file);
|
||||||
buzz_proto_send_error_reusing_slab(msg.reply_cb, EIO, msg.slab_ptr);
|
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) */
|
/* Datei ab dem Ende der Audiodaten abschneiden (alte Tags entfernen) */
|
||||||
rc = fs_truncate(&write_ctx.file, audio_len);
|
rc = fs_truncate(&write_ctx.file, audio_len);
|
||||||
if (rc != 0) {
|
if (rc != 0)
|
||||||
|
{
|
||||||
LOG_ERR("Failed to truncate file: %d", rc);
|
LOG_ERR("Failed to truncate file: %d", rc);
|
||||||
fs_mgmt_pm_close(&write_ctx.file);
|
fs_mgmt_pm_close(&write_ctx.file);
|
||||||
buzz_proto_send_error_reusing_slab(msg.reply_cb, abs(rc), msg.slab_ptr);
|
buzz_proto_send_error_reusing_slab(msg.reply_cb, abs(rc), msg.slab_ptr);
|
||||||
@@ -593,11 +605,14 @@ static void fs_thread_entry(void *p1, void *p2, void *p3)
|
|||||||
uint16_t credits = buzz_proto_get_free_rx_slabs();
|
uint16_t credits = buzz_proto_get_free_rx_slabs();
|
||||||
buzz_proto_buf_free(&msg.slab_ptr);
|
buzz_proto_buf_free(&msg.slab_ptr);
|
||||||
buzz_proto_send_ack(msg.reply_cb, credits);
|
buzz_proto_send_ack(msg.reply_cb, credits);
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
LOG_ERR("Failed to open %s for tags: %d", write_ctx.filename, rc);
|
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);
|
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");
|
LOG_WRN("Operation not yet fully implemented in FS state machine");
|
||||||
buzz_proto_send_error_reusing_slab(msg.reply_cb, ENOSYS, msg.slab_ptr);
|
buzz_proto_send_error_reusing_slab(msg.reply_cb, ENOSYS, msg.slab_ptr);
|
||||||
@@ -609,19 +624,24 @@ static void fs_thread_entry(void *p1, void *p2, void *p3)
|
|||||||
{
|
{
|
||||||
ssize_t written = fs_write(&write_ctx.file, msg.slab_ptr + msg.data_offset, msg.data_len);
|
ssize_t written = fs_write(&write_ctx.file, msg.slab_ptr + msg.data_offset, msg.data_len);
|
||||||
|
|
||||||
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);
|
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);
|
buzz_proto_buf_free(&msg.slab_ptr);
|
||||||
write_ctx.unacked_chunks++;
|
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 free_slabs = buzz_proto_get_free_rx_slabs();
|
||||||
uint16_t credits_to_send = MIN(free_slabs, write_ctx.unacked_chunks);
|
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);
|
buzz_proto_send_ack(msg.reply_cb, credits_to_send);
|
||||||
write_ctx.unacked_chunks -= credits_to_send;
|
write_ctx.unacked_chunks -= credits_to_send;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
LOG_ERR("Flash write failed!");
|
LOG_ERR("Flash write failed!");
|
||||||
write_ctx.state = FS_STATE_IDLE;
|
write_ctx.state = FS_STATE_IDLE;
|
||||||
buzz_proto_send_error_reusing_slab(msg.reply_cb, EIO, msg.slab_ptr);
|
buzz_proto_send_error_reusing_slab(msg.reply_cb, EIO, msg.slab_ptr);
|
||||||
@@ -632,10 +652,13 @@ static void fs_thread_entry(void *p1, void *p2, void *p3)
|
|||||||
fs_mgmt_pm_close(&write_ctx.file);
|
fs_mgmt_pm_close(&write_ctx.file);
|
||||||
write_ctx.state = FS_STATE_IDLE;
|
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);
|
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);
|
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);
|
LOG_ERR("CRC Mismatch! Expected: 0x%08X, Got: 0x%08X", msg.metadata, write_ctx.crc32);
|
||||||
fs_mgmt_pm_unlink(write_ctx.filename);
|
fs_mgmt_pm_unlink(write_ctx.filename);
|
||||||
buzz_proto_send_error_reusing_slab(msg.reply_cb, EBADMSG, msg.slab_ptr);
|
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_close(&write_ctx.file);
|
||||||
fs_mgmt_pm_unlink(write_ctx.filename);
|
fs_mgmt_pm_unlink(write_ctx.filename);
|
||||||
write_ctx.state = FS_STATE_IDLE;
|
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;
|
break;
|
||||||
|
|
||||||
case FS_STATE_RECEIVING_TAGS:
|
case FS_STATE_RECEIVING_TAGS:
|
||||||
if (msg.op == FS_WRITE_OP_FILE_CHUNK && msg.slab_ptr)
|
if (msg.op == FS_WRITE_OP_FILE_CHUNK && msg.slab_ptr)
|
||||||
{
|
{
|
||||||
ssize_t written = fs_write(&write_ctx.file, msg.slab_ptr + msg.data_offset, msg.data_len);
|
ssize_t written = fs_write(&write_ctx.file, msg.slab_ptr + msg.data_offset, msg.data_len);
|
||||||
|
|
||||||
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);
|
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);
|
buzz_proto_buf_free(&msg.slab_ptr);
|
||||||
|
|
||||||
write_ctx.unacked_chunks++;
|
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 free_slabs = buzz_proto_get_free_rx_slabs();
|
||||||
uint16_t credits_to_send = MIN(free_slabs, write_ctx.unacked_chunks);
|
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);
|
buzz_proto_send_ack(msg.reply_cb, credits_to_send);
|
||||||
write_ctx.unacked_chunks -= credits_to_send;
|
write_ctx.unacked_chunks -= credits_to_send;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
LOG_ERR("Flash write failed during tags transfer!");
|
LOG_ERR("Flash write failed during tags transfer!");
|
||||||
fs_truncate(&write_ctx.file, write_ctx.audio_len); /* Rollback */
|
fs_truncate(&write_ctx.file, write_ctx.audio_len); /* Rollback */
|
||||||
fs_mgmt_pm_close(&write_ctx.file);
|
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)
|
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);
|
LOG_INF("Tags transfer finished. CRC valid: 0x%08X", write_ctx.crc32);
|
||||||
fs_mgmt_pm_close(&write_ctx.file);
|
fs_mgmt_pm_close(&write_ctx.file);
|
||||||
buzz_proto_send_success_reusing_slab(msg.reply_cb, BUZZ_DATA_TAGS_PUT, msg.slab_ptr);
|
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);
|
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_truncate(&write_ctx.file, write_ctx.audio_len); /* Rollback */
|
||||||
fs_mgmt_pm_close(&write_ctx.file);
|
fs_mgmt_pm_close(&write_ctx.file);
|
||||||
@@ -695,7 +727,8 @@ static void fs_thread_entry(void *p1, void *p2, void *p3)
|
|||||||
fs_truncate(&write_ctx.file, write_ctx.audio_len); /* Rollback */
|
fs_truncate(&write_ctx.file, write_ctx.audio_len); /* Rollback */
|
||||||
fs_mgmt_pm_close(&write_ctx.file);
|
fs_mgmt_pm_close(&write_ctx.file);
|
||||||
write_ctx.state = FS_STATE_IDLE;
|
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;
|
break;
|
||||||
|
|
||||||
@@ -704,7 +737,8 @@ static void fs_thread_entry(void *p1, void *p2, void *p3)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Garbage Collection: Ungültige Operationen im falschen State abfangen */
|
/* 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);
|
buzz_proto_buf_free(&msg.slab_ptr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ CONFIG_FS_LOG_LEVEL_WRN=y
|
|||||||
|
|
||||||
### Bluetooth
|
### Bluetooth
|
||||||
CONFIG_BLE_MGMT=y
|
CONFIG_BLE_MGMT=y
|
||||||
# CONFIG_BLE_MGMT_LOG_LEVEL_DBG=y
|
|
||||||
|
|
||||||
# Advertising 500ms - 1s
|
# Advertising 500ms - 1s
|
||||||
CONFIG_BLE_MGMT_ADV_INT_MIN=160
|
CONFIG_BLE_MGMT_ADV_INT_MIN=160
|
||||||
@@ -24,3 +23,35 @@ CONFIG_PM_DEVICE=y
|
|||||||
## Shell
|
## Shell
|
||||||
# CONFIG_SHELL=y
|
# 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;
|
uint8_t *buf;
|
||||||
|
|
||||||
/* 1. Länge prüfen (darf SLAB_BLOCK_SIZE = 256 nicht überschreiten) */
|
/* 1. Länge prüfen (darf SLAB_BLOCK_SIZE = 256 nicht überschreiten) */
|
||||||
if (len > 256) {
|
if (len > CONFIG_BUZZ_PROTO_SLAB_SIZE) {
|
||||||
LOG_ERR("Received data too large for proto buf (%u bytes)", len);
|
LOG_ERR("Received data too large for proto buf (%u > %u)", len, CONFIG_BUZZ_PROTO_SLAB_SIZE);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
72
protocol.md
@@ -39,15 +39,16 @@ uint16_t payload_length // Little Endian
|
|||||||
|--------|------------|----------------|---------------------------------------|
|
|--------|------------|----------------|---------------------------------------|
|
||||||
| `0x00` | `REQUEST` | Host → Device | Abfrage eines Datentyps |
|
| `0x00` | `REQUEST` | Host → Device | Abfrage eines Datentyps |
|
||||||
| `0x10` | `RESPONSE` | Device → Host | Antwort auf `REQUEST` |
|
| `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 |
|
| `0x12` | `ERROR` | Device → Host | Fehlerantwort mit Fehlercode |
|
||||||
|
| `0x13` | `SUCCESS` | Device → Host | Bestaetigung einer Operation |
|
||||||
|
|
||||||
### 4.2 Datei-Transfer (reserviert, noch nicht implementiert)
|
### 4.2 Datei-Transfer
|
||||||
| Wert | Name |
|
| Wert | Name | Richtung | Beschreibung |
|
||||||
|--------|--------------|
|
|--------|--------------|----------------|---------------------------------------------|
|
||||||
| `0x20` | `FILE_START` |
|
| `0x20` | `FILE_START` | Host ↔ Device | Beginn eines Dateitransfers |
|
||||||
| `0x21` | `FILE_CHUNK` |
|
| `0x21` | `FILE_CHUNK` | Host ↔ Device | Ein Datenblock des Dateitransfers |
|
||||||
| `0x22` | `FILE_END` |
|
| `0x22` | `FILE_END` | Host ↔ Device | Abschluss des Dateitransfers inkl. CRC32 |
|
||||||
|
|
||||||
### 4.3 Firmware-Transfer (reserviert, noch nicht implementiert)
|
### 4.3 Firmware-Transfer (reserviert, noch nicht implementiert)
|
||||||
| Wert | Name |
|
| Wert | Name |
|
||||||
@@ -97,6 +98,13 @@ Definierte `data_type`-Werte:
|
|||||||
| `0x01` | `PROTO_INFO` | Protokollversion und Chunk-Groesse |
|
| `0x01` | `PROTO_INFO` | Protokollversion und Chunk-Groesse |
|
||||||
| `0x02` | `DEVICE_INFO` | Geraeteinformationen (TBD) |
|
| `0x02` | `DEVICE_INFO` | Geraeteinformationen (TBD) |
|
||||||
| `0x03` | `FS_INFO` | Dateisystem-Statistik und Pfadnamen |
|
| `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 |
|
| `0x40` | `LS` | Verzeichnisliste starten |
|
||||||
|
|
||||||
### 6.1 `PROTO_INFO` (`0x01`)
|
### 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).
|
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
|
### 6.6 `RENAME_FILE` (`0x25`) — Datei umbenennen
|
||||||
Wird waehrend eines laufenden LS-Streams gesendet, um dem Device Credits (Sendeerlaubnisse) zu erteilen.
|
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:
|
Format:
|
||||||
```c
|
```c
|
||||||
@@ -204,6 +238,24 @@ Fehlercode-Tabelle (Zephyr errno, positiver Wert):
|
|||||||
| 88 | `ENOSYS` | Funktion nicht implementiert |
|
| 88 | `ENOSYS` | Funktion nicht implementiert |
|
||||||
| 134 | `ENOTSUP` | Operation nicht unterstuetzt |
|
| 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)
|
## 8. LS-Stream (Verzeichnisliste)
|
||||||
|
|
||||||
Der LS-Stream wird durch einen `REQUEST` mit `data_type = 0x40` ausgeloest und laeuft wie folgt ab:
|
Der LS-Stream wird durch einen `REQUEST` mit `data_type = 0x40` ausgeloest und laeuft wie folgt ab:
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
import { defineConfig } from 'astro/config';
|
import { defineConfig } from 'astro/config';
|
||||||
|
|
||||||
import svelte from '@astrojs/svelte';
|
import svelte from '@astrojs/svelte';
|
||||||
|
|
||||||
import tailwindcss from '@tailwindcss/vite';
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
|
||||||
|
const isProd = process.env.NODE_ENV === 'production';
|
||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
base: isProd ? '/buzzer/' : '/',
|
||||||
|
site: 'https://home.iten.pro',
|
||||||
|
|
||||||
integrations: [svelte()],
|
integrations: [svelte()],
|
||||||
|
|
||||||
vite: {
|
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">
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="1000" height="1000"><style>
|
||||||
<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" />
|
#light-icon {
|
||||||
<style>
|
display: inline;
|
||||||
path { fill: #000; }
|
}
|
||||||
@media (prefers-color-scheme: dark) {
|
#dark-icon {
|
||||||
path { fill: #FFF; }
|
display: none;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
</svg>
|
@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 |
@@ -22,9 +22,11 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else if !$isBluetoothSupported}
|
{:else if !$isBluetoothSupported}
|
||||||
<div
|
<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
|
<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>
|
<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>
|
<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
|
Leider unterstützt dein Browser die benötigten Bluetooth-Funktionen nicht. Bitte versuche
|
||||||
es mit einem aktuellen <span class="font-semibold">Chrome</span>
|
es mit einem aktuellen <span class="font-semibold">Chrome</span>
|
||||||
oder einem andern Chromium-basierten Browser.
|
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>
|
||||||
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,17 +15,22 @@
|
|||||||
WarningCircleIcon,
|
WarningCircleIcon,
|
||||||
} from "phosphor-svelte";
|
} from "phosphor-svelte";
|
||||||
import {
|
import {
|
||||||
isFetchingRemote,
|
isTransferingRemote,
|
||||||
transferStats,
|
transferStats,
|
||||||
transferDetails,
|
transferDetails,
|
||||||
buzzerAudioFiles,
|
buzzerAudioFiles,
|
||||||
localAudioFiles,
|
localAudioFiles,
|
||||||
syncStateMap,
|
syncStateMap,
|
||||||
|
fsInfo,
|
||||||
} from "../lib/store";
|
} from "../lib/store";
|
||||||
|
|
||||||
import { SETTINGS } from "../lib/settings";
|
import { SETTINGS } from "../lib/settings";
|
||||||
import { tagEditorState } from "../lib/store";
|
import { tagEditorState } from "../lib/store";
|
||||||
import { tooltip } from "../lib/actions/tooltip";
|
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 file: BuzzerFile;
|
||||||
export let type: "local" | "buzzer" = "buzzer";
|
export let type: "local" | "buzzer" = "buzzer";
|
||||||
@@ -37,7 +42,7 @@
|
|||||||
$: myIndex = selectedFiles.findIndex((f) => f.name === file.name);
|
$: myIndex = selectedFiles.findIndex((f) => f.name === file.name);
|
||||||
|
|
||||||
$: state = (() => {
|
$: state = (() => {
|
||||||
if (!file.selected || !$isFetchingRemote) return "default";
|
if (!file.selected || !$isTransferingRemote) return "default";
|
||||||
if (file.name === $transferStats.currentFileName) return "active";
|
if (file.name === $transferStats.currentFileName) return "active";
|
||||||
if (myIndex < currentIndex) return "done";
|
if (myIndex < currentIndex) return "done";
|
||||||
if (myIndex > currentIndex) return "pending";
|
if (myIndex > currentIndex) return "pending";
|
||||||
@@ -93,7 +98,7 @@
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
function toggleSelection() {
|
function toggleSelection() {
|
||||||
if ($isFetchingRemote) return;
|
if ($isTransferingRemote) return;
|
||||||
|
|
||||||
if (type === "buzzer") {
|
if (type === "buzzer") {
|
||||||
buzzerAudioFiles.update((files) =>
|
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>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:click={() => (menuOpen = false)} />
|
<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
|
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 ? 'border-l-blue-600' : 'border-l-transparent'}
|
||||||
{file.selected && state !== 'active' ? 'bg-blue-50' : ''}
|
{file.selected && state !== 'active' ? 'bg-blue-50' : ''}
|
||||||
{!$isFetchingRemote && file.selected ? 'hover:bg-blue-100 cursor-pointer' : ''}
|
{!$isTransferingRemote && file.selected ? 'hover:bg-blue-100 cursor-pointer' : ''}
|
||||||
{!$isFetchingRemote && !file.selected
|
{!$isTransferingRemote && !file.selected
|
||||||
? 'hover:bg-slate-100 hover:border-l-blue-200 cursor-pointer'
|
? 'hover:bg-slate-100 hover:border-l-blue-200 cursor-pointer'
|
||||||
: ''}
|
: ''}
|
||||||
{$isFetchingRemote ? 'cursor-default' : ''}
|
{$isTransferingRemote ? 'cursor-default' : ''}
|
||||||
{state === 'pending' ? 'grayscale opacity-80' : ''}"
|
{state === 'pending' ? 'grayscale opacity-80' : ''}"
|
||||||
on:click={toggleSelection}
|
on:click={toggleSelection}
|
||||||
disabled={$isFetchingRemote}
|
disabled={$isTransferingRemote}
|
||||||
>
|
>
|
||||||
<MusicNotesIcon weight="fill" class="mr-3 w-5 h-5 shrink-0" />
|
<MusicNotesIcon weight="fill" class="mr-3 w-5 h-5 shrink-0" />
|
||||||
|
|
||||||
@@ -198,10 +232,7 @@
|
|||||||
<button
|
<button
|
||||||
class="menu-btn danger"
|
class="menu-btn danger"
|
||||||
title="Löschen"
|
title="Löschen"
|
||||||
on:click|stopPropagation={() => {
|
on:click|stopPropagation={handleDeleteClick}
|
||||||
console.log("Delete", file.name);
|
|
||||||
menuOpen = false;
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<TrashIcon class="list-menu-icon" />
|
<TrashIcon class="list-menu-icon" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { fade, slide } from "svelte/transition";
|
import { fade, slide } from "svelte/transition";
|
||||||
import { localAudioFiles, buzzerAudioFiles } from "../lib/store";
|
import { localAudioFiles, buzzerAudioFiles } from "../lib/store";
|
||||||
import { deleteSelectedLocalFiles } from "../lib/sync";
|
import { deleteSelectedLocalFiles, deleteSelectedRemoteFiles } from "../lib/sync";
|
||||||
import { addToast } from "../lib/toast";
|
import { addToast } from "../lib/toast";
|
||||||
import { tooltip } from "../lib/actions/tooltip";
|
import { tooltip } from "../lib/actions/tooltip";
|
||||||
import { updateLocalAudioCrc } from "../lib/tagHandler";
|
import { updateLocalAudioCrc } from "../lib/tagHandler";
|
||||||
@@ -143,12 +143,11 @@
|
|||||||
class="menu-btn danger"
|
class="menu-btn danger"
|
||||||
disabled={selectedFileCount === 0}
|
disabled={selectedFileCount === 0}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
if (type === "buzzer")
|
if (type === "buzzer") {
|
||||||
addToast(
|
deleteSelectedRemoteFiles();
|
||||||
"Löschen von Dateien auf dem Buzzer wird derzeit nicht unterstützt.",
|
} else {
|
||||||
"error",
|
deleteSelectedLocalFiles();
|
||||||
);
|
}
|
||||||
else deleteSelectedLocalFiles();
|
|
||||||
closeMenu();
|
closeMenu();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,8 +2,13 @@
|
|||||||
import { updateFile } from "../lib/tagHandler";
|
import { updateFile } from "../lib/tagHandler";
|
||||||
import { refreshLocal, refreshRemote } from "../lib/sync";
|
import { refreshLocal, refreshRemote } from "../lib/sync";
|
||||||
import { fade, slide } from "svelte/transition";
|
import { fade, slide } from "svelte/transition";
|
||||||
import { localAudioFiles, buzzerAudioFiles, fsInfo } from "../lib/store";
|
import {
|
||||||
import type { MetadataTags } from "../lib/types";
|
localAudioFiles,
|
||||||
|
buzzerAudioFiles,
|
||||||
|
fsInfo,
|
||||||
|
syncStateMap,
|
||||||
|
} from "../lib/store";
|
||||||
|
import { type MetadataTags, SyncState } from "../lib/types";
|
||||||
import {
|
import {
|
||||||
XIcon,
|
XIcon,
|
||||||
CaretLeftIcon,
|
CaretLeftIcon,
|
||||||
@@ -15,6 +20,7 @@
|
|||||||
PencilIcon,
|
PencilIcon,
|
||||||
} from "phosphor-svelte";
|
} from "phosphor-svelte";
|
||||||
import { addToast } from "../lib/toast";
|
import { addToast } from "../lib/toast";
|
||||||
|
import { tooltip } from "../lib/actions/tooltip";
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
export let type: "local" | "buzzer" = "buzzer";
|
export let type: "local" | "buzzer" = "buzzer";
|
||||||
@@ -29,7 +35,7 @@
|
|||||||
|
|
||||||
$: autoApplyIcon = applyToBoth ? CheckSquareIcon : SquareIcon;
|
$: autoApplyIcon = applyToBoth ? CheckSquareIcon : SquareIcon;
|
||||||
$: activeStore = type === "local" ? localAudioFiles : buzzerAudioFiles;
|
$: activeStore = type === "local" ? localAudioFiles : buzzerAudioFiles;
|
||||||
$: fileList = $activeStore;
|
$: fileList = $activeStore || [];
|
||||||
|
|
||||||
$: if (show && initialFileName !== lastOpenedName) {
|
$: if (show && initialFileName !== lastOpenedName) {
|
||||||
if (initialFileName) {
|
if (initialFileName) {
|
||||||
@@ -44,7 +50,7 @@
|
|||||||
lastOpenedName = null;
|
lastOpenedName = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$: currentIndex = fileList.findIndex((f) => f.name === currentFileName);
|
$: currentIndex = (fileList || []).findIndex((f) => f.name === currentFileName);
|
||||||
$: currentFile = fileList[currentIndex];
|
$: currentFile = fileList[currentIndex];
|
||||||
$: hasDraft = currentFile ? drafts[currentFile.name] !== undefined : false;
|
$: hasDraft = currentFile ? drafts[currentFile.name] !== undefined : false;
|
||||||
$: hasAnyDrafts = Object.keys(drafts).length > 0;
|
$: hasAnyDrafts = Object.keys(drafts).length > 0;
|
||||||
@@ -53,6 +59,13 @@
|
|||||||
$: activeTags = activeDraft ? activeDraft.tags : currentFile?.metaTags || {};
|
$: activeTags = activeDraft ? activeDraft.tags : currentFile?.metaTags || {};
|
||||||
$: activeName = activeDraft ? activeDraft.newName : currentFile?.name || "";
|
$: 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;
|
$: maxFilenameLength = $fsInfo ? $fsInfo.maxPathLength - $fsInfo.audioPath.length - 2 : 30;
|
||||||
|
|
||||||
function closeEditor() {
|
function closeEditor() {
|
||||||
@@ -120,13 +133,21 @@
|
|||||||
const newName = draft.newName;
|
const newName = draft.newName;
|
||||||
|
|
||||||
try {
|
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");
|
addToast(`Datei ${newName} gespeichert.`, "success");
|
||||||
delete drafts[oldName];
|
delete drafts[oldName];
|
||||||
drafts = drafts;
|
drafts = drafts;
|
||||||
if (oldName !== newName) currentFileName = newName;
|
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) {
|
} catch (error) {
|
||||||
addToast("Fehler beim Speichern.", "error");
|
addToast("Fehler beim Speichern.", "error");
|
||||||
}
|
}
|
||||||
@@ -138,14 +159,20 @@
|
|||||||
for (const [oldName, draft] of Object.entries(drafts)) {
|
for (const [oldName, draft] of Object.entries(drafts)) {
|
||||||
const file = fileList.find((f) => f.name === oldName);
|
const file = fileList.find((f) => f.name === oldName);
|
||||||
if (file) {
|
if (file) {
|
||||||
await updateFile(oldName, draft.newName, file.sysTags, draft.tags, type);
|
await updateFile(oldName, draft.newName, file.sysTags, draft.tags, type, applyToBoth);
|
||||||
savedCount++;
|
savedCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
drafts = {};
|
drafts = {};
|
||||||
addToast(`${savedCount} Dateien gespeichert.`, "success");
|
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) {
|
} catch (error) {
|
||||||
addToast("Fehler beim Speichern.", "error");
|
addToast("Fehler beim Speichern.", "error");
|
||||||
}
|
}
|
||||||
@@ -398,15 +425,27 @@
|
|||||||
<FloppyDiskIcon class="btn-icon" /> Speichern
|
<FloppyDiskIcon class="btn-icon" /> Speichern
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div
|
||||||
class="menu-btn bg-slate-50 hover:bg-slate-100 !justify-start text-slate-700"
|
use:tooltip={{
|
||||||
on:click={() => (applyToBoth = !applyToBoth)}
|
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
|
<button
|
||||||
this={autoApplyIcon}
|
class="menu-btn bg-slate-50 hover:bg-slate-100 !justify-start text-slate-700 w-full"
|
||||||
class="btn-icon {applyToBoth ? 'text-blue-600' : 'text-slate-400'}"
|
on:click={() => (applyToBoth = !applyToBoth)}
|
||||||
/> Auch {type === "buzzer" ? "lokal" : "auf dem Buzzer"} anwenden
|
disabled={isDuplicate}
|
||||||
</button>
|
>
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import FileList from "./FileList.svelte";
|
import FileList from "./FileList.svelte";
|
||||||
import DeviceInfo from "./DeviceInfo.svelte";
|
import DeviceInfo from "./DeviceInfo.svelte";
|
||||||
import { refreshRemote } from "../lib/sync";
|
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 { SETTINGS } from "../lib/settings";
|
||||||
import TransferProgress from "./TransferProgress.svelte";
|
import TransferProgress from "./TransferProgress.svelte";
|
||||||
import FileMenuOverlay from "./FileMenuOverlay.svelte";
|
import FileMenuOverlay from "./FileMenuOverlay.svelte";
|
||||||
@@ -25,12 +25,12 @@
|
|||||||
refreshRemote();
|
refreshRemote();
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if ($isFetchingRemote && $transferStats.overallTotal > 0) {
|
$: if ($isTransferingRemote && $transferStats.overallTotal > 0) {
|
||||||
// Transfer startet oder läuft
|
// Transfer startet oder läuft
|
||||||
showOverlay = true;
|
showOverlay = true;
|
||||||
isTransferFinished = false;
|
isTransferFinished = false;
|
||||||
clearTimeout(overlayTimeout);
|
clearTimeout(overlayTimeout);
|
||||||
} else if (showOverlay && !$isFetchingRemote && $transferStats.overallDone > 0) {
|
} else if (showOverlay && !$isTransferingRemote && $transferStats.overallDone > 0) {
|
||||||
// Transfer wurde soeben abgeschlossen
|
// Transfer wurde soeben abgeschlossen
|
||||||
isTransferFinished = true;
|
isTransferFinished = true;
|
||||||
overlayTimeout = setTimeout(closeOverlay, SETTINGS.ui.transferOverlayPersistMs);
|
overlayTimeout = setTimeout(closeOverlay, SETTINGS.ui.transferOverlayPersistMs);
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import { XIcon } from "phosphor-svelte";
|
import { XIcon } from "phosphor-svelte";
|
||||||
import { isFetchingRemote, transferStats, transferDetails } from '../lib/store';
|
import { isTransferingRemote, transferStats, transferDetails } from '../lib/store';
|
||||||
import { SETTINGS } from '../lib/settings';
|
import { SETTINGS } from '../lib/settings';
|
||||||
|
|
||||||
let showOverlay = false;
|
let showOverlay = false;
|
||||||
let isTransferFinished = false;
|
let isTransferFinished = false;
|
||||||
let overlayTimeout: ReturnType<typeof setTimeout>;
|
let overlayTimeout: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
$: if ($isFetchingRemote && $transferStats.overallTotal > 0) {
|
$: if ($isTransferingRemote && $transferStats.overallTotal > 0) {
|
||||||
showOverlay = true;
|
showOverlay = true;
|
||||||
isTransferFinished = false;
|
isTransferFinished = false;
|
||||||
clearTimeout(overlayTimeout);
|
clearTimeout(overlayTimeout);
|
||||||
} else if (showOverlay && !$isFetchingRemote && $transferStats.overallDone > 0) {
|
} else if (showOverlay && !$isTransferingRemote && $transferStats.overallDone > 0) {
|
||||||
isTransferFinished = true;
|
isTransferFinished = true;
|
||||||
overlayTimeout = setTimeout(closeOverlay, SETTINGS.ui.transferOverlayPersistMs);
|
overlayTimeout = setTimeout(closeOverlay, SETTINGS.ui.transferOverlayPersistMs);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,30 @@
|
|||||||
<!-- MainLayout.astro -->
|
|
||||||
---
|
---
|
||||||
import "../styles/app.css";
|
import "../styles/app.css";
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<!-- MainLayout.astro -->
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<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>
|
<title>Edis Buzzer</title>
|
||||||
</head>
|
</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 />
|
<slot />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -3,12 +3,13 @@ export const SETTINGS = {
|
|||||||
connectionKey: 'buzzer_connection_state'
|
connectionKey: 'buzzer_connection_state'
|
||||||
},
|
},
|
||||||
bluetooth: {
|
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: {
|
ui: {
|
||||||
toastDurationMs: 5000,
|
toastDurationMs: 5000,
|
||||||
transferUpdateIntervalMs: 300,
|
transferUpdateIntervalMs: 1000,
|
||||||
kbpsCalculationWindowMs: 5500,
|
kbpsCalculationWindowMs: 1000,
|
||||||
transferOverlayPersistMs: 4000,
|
transferOverlayPersistMs: 4000,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
@@ -75,7 +75,7 @@ export const buzzerSysFiles = writable<BuzzerFile[]>([]);
|
|||||||
export const localAudioFiles = writable<BuzzerFile[]>([]);
|
export const localAudioFiles = writable<BuzzerFile[]>([]);
|
||||||
|
|
||||||
// Ladezustände getrennt nach Quelle
|
// Ladezustände getrennt nach Quelle
|
||||||
export const isFetchingRemote = writable<boolean>(false);
|
export const isTransferingRemote = writable<boolean>(false);
|
||||||
export const isFetchingLocal = writable<boolean>(false);
|
export const isFetchingLocal = writable<boolean>(false);
|
||||||
|
|
||||||
// Persistenz des letzten Verbindungsziels (nur im Browser nutzbar)
|
// Persistenz des letzten Verbindungsziels (nur im Browser nutzbar)
|
||||||
@@ -189,7 +189,7 @@ export function resetRemote(): void {
|
|||||||
activeDeviceId.set(null);
|
activeDeviceId.set(null);
|
||||||
buzzerAudioFiles.set([]);
|
buzzerAudioFiles.set([]);
|
||||||
buzzerSysFiles.set([]);
|
buzzerSysFiles.set([]);
|
||||||
isFetchingRemote.set(false);
|
isTransferingRemote.set(false);
|
||||||
resetTransferStats();
|
resetTransferStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { get } from 'svelte/store';
|
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 { requestProtocolInfo, requestFSInfo, fetchDirectory } from './transport';
|
||||||
import type { BuzzerFile } from './types';
|
import type { BuzzerFile } from './types';
|
||||||
import { getFile, putFile } from './transport';
|
import { getFile, putFile, deleteRemoteFile } from './transport';
|
||||||
import { addToast } from './toast';
|
import { addToast } from './toast';
|
||||||
import { getLocalFiles, deleteLocalFile, getLocalFile } from './db';
|
import { getLocalFiles, deleteLocalFile, getLocalFile } from './db';
|
||||||
import { parseAudioFileTags } from './tagHandler';
|
import { parseAudioFileTags } from './tagHandler';
|
||||||
@@ -24,7 +24,7 @@ function mapToBuzzerFile(rawFile: any): BuzzerFile {
|
|||||||
export async function refreshRemote() {
|
export async function refreshRemote() {
|
||||||
if (!get(isConnected)) return;
|
if (!get(isConnected)) return;
|
||||||
|
|
||||||
isFetchingRemote.set(true);
|
isTransferingRemote.set(true);
|
||||||
try {
|
try {
|
||||||
await requestProtocolInfo();
|
await requestProtocolInfo();
|
||||||
await requestFSInfo();
|
await requestFSInfo();
|
||||||
@@ -64,7 +64,7 @@ export async function refreshRemote() {
|
|||||||
console.error("Fehler beim Aktualisieren der Buzzer-Daten:", error);
|
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");
|
addToast("Fehler beim Laden der Daten vom Buzzer: " + (error instanceof Error ? error.message : "Unbekannter Fehler"), "error");
|
||||||
} finally {
|
} finally {
|
||||||
isFetchingRemote.set(false);
|
isTransferingRemote.set(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@ export async function downloadSelectedFiles() {
|
|||||||
bulkStartTime: bulkStart
|
bulkStartTime: bulkStart
|
||||||
}));
|
}));
|
||||||
|
|
||||||
isFetchingRemote.set(true);
|
isTransferingRemote.set(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
@@ -142,7 +142,7 @@ export async function downloadSelectedFiles() {
|
|||||||
...s,
|
...s,
|
||||||
overallDone: s.overallTotal,
|
overallDone: s.overallTotal,
|
||||||
}));
|
}));
|
||||||
isFetchingRemote.set(false);
|
isTransferingRemote.set(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +152,10 @@ export async function deleteSelectedLocalFiles() {
|
|||||||
|
|
||||||
if (selectedFiles.length === 0) return;
|
if (selectedFiles.length === 0) return;
|
||||||
|
|
||||||
|
if (!confirm(`Möchten Sie wirklich ${selectedFiles.length} lokale Datei(en) löschen?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (const file of selectedFiles) {
|
for (const file of selectedFiles) {
|
||||||
await deleteLocalFile(file.name);
|
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() {
|
export async function uploadSelectedFiles() {
|
||||||
const files = get(localAudioFiles).filter(f => f.selected);
|
const files = get(localAudioFiles).filter(f => f.selected);
|
||||||
const pathPrefix = get(fsInfo)?.audioPath || "/lfs/a";
|
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?
|
// Wir nutzen isFetchingRemote als generischen "Transfer aktiv"-Trigger für das UI TODO: Namensänderung in isTransferring?
|
||||||
isFetchingRemote.set(true);
|
isTransferingRemote.set(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
@@ -216,6 +247,6 @@ export async function uploadSelectedFiles() {
|
|||||||
...s,
|
...s,
|
||||||
overallDone: s.overallTotal, // Schließt den Ladebalken visuell sauber ab
|
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 type { SystemTags, MetadataTags } from './types';
|
||||||
import { addToast } from './toast';
|
import { addToast } from './toast';
|
||||||
import { crc32 } from './protocol/crc32';
|
import { crc32 } from './protocol/crc32';
|
||||||
import { getTags, putTags } from './transport';
|
import { getTags, putTags, renameRemoteFile } from './transport';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { fsInfo } from './store';
|
import { fsInfo, syncStateMap } from './store';
|
||||||
|
|
||||||
function getEmptyTags() {
|
function getEmptyTags() {
|
||||||
return {
|
return {
|
||||||
@@ -206,25 +206,24 @@ export async function updateRemoteFile(
|
|||||||
sysTags: SystemTags,
|
sysTags: SystemTags,
|
||||||
newMetaTags: MetadataTags
|
newMetaTags: MetadataTags
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Da das Binärprotokoll noch kein echtes "Rename"-Kommando hat,
|
const currentFsInfo = get(fsInfo);
|
||||||
// blockieren wir Dateinamen-Änderungen für den Buzzer vorerst.
|
const basePath = currentFsInfo?.audioPath || "/lfs/a";
|
||||||
|
const oldFullPath = `${basePath}/${oldName}`;
|
||||||
|
const newFullPath = `${basePath}/${newName}`;
|
||||||
|
|
||||||
if (oldName !== 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
|
// 1. Den binären TLV-Block inkl. Footer bauen
|
||||||
const newTagsBuffer = buildTagBlock(sysTags, newMetaTags);
|
const newTagsBuffer = buildTagBlock(sysTags, newMetaTags);
|
||||||
const tagsBlob = new Blob([newTagsBuffer]);
|
const tagsBlob = new Blob([newTagsBuffer]);
|
||||||
|
|
||||||
// 2. Zielpfad ermitteln
|
console.log(`Sende modifizierte Tags (${tagsBlob.size} Bytes) an ${newFullPath}...`);
|
||||||
const currentFsInfo = get(fsInfo);
|
|
||||||
const basePath = currentFsInfo?.audioPath || "/lfs/a";
|
|
||||||
const fullPath = `${basePath}/${oldName}`;
|
|
||||||
|
|
||||||
console.log(`Sende modifizierte Tags (${tagsBlob.size} Bytes) an ${fullPath}...`);
|
|
||||||
|
|
||||||
// 3. Über das Protokoll an den Buzzer senden
|
// 3. Über das Protokoll an den Buzzer senden
|
||||||
await putTags(tagsBlob, fullPath);
|
await putTags(tagsBlob, newFullPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateFile(
|
export async function updateFile(
|
||||||
@@ -232,12 +231,43 @@ export async function updateFile(
|
|||||||
newName: string,
|
newName: string,
|
||||||
sysTags: SystemTags,
|
sysTags: SystemTags,
|
||||||
newMetaTags: MetadataTags,
|
newMetaTags: MetadataTags,
|
||||||
type: "local" | "buzzer"
|
type: "local" | "buzzer",
|
||||||
|
applyToBoth: boolean = false
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (type === "local") {
|
if (type === "local") {
|
||||||
await updateLocalFile(oldName, newName, sysTags, newMetaTags);
|
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 {
|
} else {
|
||||||
await updateRemoteFile(oldName, newName, sysTags, newMetaTags);
|
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 { buildLSRequest, buildProtocolInfoRequest, buildFSInfoRequest, setLsResolver, buildFileGetRequest, setFileGetResolver, buildTagsGetRequest, uploadState } from './protocol/parser';
|
||||||
import { crc32 } from './protocol/crc32';
|
import { crc32 } from './protocol/crc32';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { protocolInfo, transferStats } from './store';
|
import { protocolInfo, transferStats, } from './store';
|
||||||
import { DATA, FRAME } from './protocol/constants';
|
import { DATA, FRAME } from './protocol/constants';
|
||||||
import { isConnected, resetRemote } from './store';
|
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>;
|
export type FrameSender = (buffer: ArrayBuffer) => Promise<void>;
|
||||||
let currentSender: FrameSender | null = null;
|
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 chunkLen = Math.min(maxChunkSize, fileData.length - offset);
|
||||||
const chunkData = fileData.subarray(offset, offset + chunkLen);
|
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> {
|
export async function putTags(tagsBlob: Blob, remotePath: string): Promise<void> {
|
||||||
if (isFileTransferring) {
|
if (isFileTransferring) {
|
||||||
throw new Error("Ein Dateitransfer läuft bereits.");
|
throw new Error("Ein Dateitransfer läuft bereits.");
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
<!-- index.astro -->
|
|
||||||
---
|
---
|
||||||
import AppGuard from "../components/AppGuard.svelte";
|
import AppGuard from "../components/AppGuard.svelte";
|
||||||
import MainLayout from "../layouts/MainLayout.astro";
|
import MainLayout from "../layouts/MainLayout.astro";
|
||||||
import Header from "../components/Header.svelte";
|
import Header from "../components/Header.svelte";
|
||||||
import MainGrid from "../components/MainGrid.svelte";
|
import MainGrid from "../components/MainGrid.svelte";
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<!-- index.astro -->
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<AppGuard client:load>
|
<AppGuard client:only="svelte">
|
||||||
<Header client:load/>
|
<Header client:only="svelte" />
|
||||||
<MainGrid client:load/>
|
<MainGrid client:only="svelte" />
|
||||||
</AppGuard>
|
</AppGuard>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card-title {
|
.card-title {
|
||||||
@apply font-light font-features-['smcp'] tracking-tight;
|
@apply font-light font-features-['smcp'] tracking-tighter;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* .card-body {
|
/* .card-body {
|
||||||
|
|||||||