diff --git a/Firmware/components/backend-esp/src/time_sync_service.cpp b/Firmware/components/backend-esp/src/time_sync_service.cpp index a1e0c69..c2ea0bb 100644 --- a/Firmware/components/backend-esp/src/time_sync_service.cpp +++ b/Firmware/components/backend-esp/src/time_sync_service.cpp @@ -13,6 +13,7 @@ #include "host/ble_gap.h" #include "host/ble_gatt.h" #include "host/ble_hs.h" +#include "host/ble_att.h" #include "host/ble_hs_mbuf.h" #include "host/util/util.h" #include "nimble/nimble_port.h" @@ -43,6 +44,19 @@ namespace { constexpr char kLogTag[] = "TimeSyncBLE"; constexpr char kDeviceName[] = "Cardboy"; +constexpr std::uint16_t kPreferredConnIntervalMin = BLE_GAP_CONN_ITVL_MS(80); // 80 ms +constexpr std::uint16_t kPreferredConnIntervalMax = BLE_GAP_CONN_ITVL_MS(150); // 150 ms +constexpr std::uint16_t kPreferredConnLatency = 2; +constexpr std::uint16_t kPreferredSupervisionTimeout = BLE_GAP_SUPERVISION_TIMEOUT_MS(5000); // 5 s + +constexpr float connIntervalUnitsToMs(std::uint16_t units) { + return static_cast(units) * 1.25f; +} + +constexpr float supervisionUnitsToMs(std::uint16_t units) { + return static_cast(units) * 10.0f; +} + // 128-bit UUIDs (little-endian order for NimBLE macros) static const ble_uuid128_t kTimeServiceUuid = BLE_UUID128_INIT(0x30, 0xF2, 0xD3, 0xF4, 0xC3, 0x10, 0xA6, 0xB5, 0xFD, 0x4E, 0x7B, 0xCA, 0x01, 0x00, 0x00, 0x00); @@ -75,6 +89,7 @@ struct ResponseMessage { uint8_t status; uint16_t length; uint8_t* data; + bool streamDownload; }; static QueueHandle_t g_responseQueue = nullptr; @@ -95,6 +110,7 @@ struct FileDownloadContext { FILE* file = nullptr; std::size_t remaining = 0; bool active = false; + bool chunkScheduled = false; }; static FileUploadContext g_uploadCtx{}; @@ -142,6 +158,8 @@ void handleRename(const uint8_t* payload, std::size_t length); bool enqueueFileResponse(uint8_t opcode, uint8_t status, const uint8_t* data, std::size_t length); bool sendFileResponseNow(const ResponseMessage& msg); void notificationTask(void* param); +bool scheduleDownloadChunk(); +void processDownloadChunk(); static const ble_gatt_chr_def kTimeServiceCharacteristics[] = { { @@ -345,6 +363,29 @@ bool sendFileResponse(uint8_t opcode, uint8_t status, const uint8_t* data, std:: return enqueueFileResponse(opcode, status, data, length); } +bool scheduleDownloadChunk() { + if (!g_downloadCtx.active || !g_responseQueue || g_activeConnHandle == BLE_HS_CONN_HANDLE_NONE) + return false; + + if (g_downloadCtx.chunkScheduled) + return true; + + ResponseMessage msg{}; + msg.opcode = static_cast(FileCommandCode::DownloadRequest); + msg.status = 0; + msg.length = 0; + msg.data = nullptr; + msg.streamDownload = true; + + if (xQueueSend(g_responseQueue, &msg, pdMS_TO_TICKS(20)) != pdPASS) { + ESP_LOGW(kLogTag, "Failed to schedule download chunk; response queue full"); + return false; + } + + g_downloadCtx.chunkScheduled = true; + return true; +} + bool sendFileError(uint8_t opcode, int err, const char* message) { const uint8_t status = static_cast(std::min(err, 0x7F)) | kResponseFlagComplete; if (message && *message != '\0') { @@ -371,6 +412,78 @@ void resetDownloadContext() { } g_downloadCtx.remaining = 0; g_downloadCtx.active = false; + g_downloadCtx.chunkScheduled = false; +} + +void processDownloadChunk() { + if (!g_downloadCtx.active || !g_downloadCtx.file) { + return; + } + + if (g_activeConnHandle == BLE_HS_CONN_HANDLE_NONE) { + resetDownloadContext(); + return; + } + + constexpr std::size_t kMaxChunkBuffer = 244; + std::array buffer{}; + + std::size_t maxPayload = buffer.size(); + if (g_activeConnHandle != BLE_HS_CONN_HANDLE_NONE) { + const uint16_t mtu = ble_att_mtu(g_activeConnHandle); + if (mtu > sizeof(PacketHeader)) { + maxPayload = std::min(buffer.size(), static_cast(mtu - sizeof(PacketHeader))); + } else { + maxPayload = std::min(buffer.size(), static_cast(20)); + } + } + + const std::size_t toRead = std::min(maxPayload, g_downloadCtx.remaining); + if (toRead == 0) { + resetDownloadContext(); + return; + } + + const std::size_t read = std::fread(buffer.data(), 1, toRead, g_downloadCtx.file); + if (read == 0) { + const int err = ferror(g_downloadCtx.file) ? errno : EIO; + resetDownloadContext(); + sendFileError(static_cast(FileCommandCode::DownloadRequest), err, "Read failed"); + return; + } + + g_downloadCtx.remaining -= read; + + ResponseMessage chunk{}; + chunk.opcode = static_cast(FileCommandCode::DownloadRequest); + chunk.status = (g_downloadCtx.remaining == 0) ? kResponseFlagComplete : 0; + chunk.length = static_cast(read); + chunk.data = buffer.data(); + + bool sent = false; + while (g_activeConnHandle != BLE_HS_CONN_HANDLE_NONE) { + if (sendFileResponseNow(chunk)) { + sent = true; + break; + } + vTaskDelay(pdMS_TO_TICKS(100)); + } + if (!sent) { + resetDownloadContext(); + sendFileError(static_cast(FileCommandCode::DownloadRequest), EIO, "Notify failed"); + return; + } + + if (g_downloadCtx.remaining == 0) { + resetDownloadContext(); + return; + } + + vTaskDelay(pdMS_TO_TICKS(3)); + if (!scheduleDownloadChunk()) { + resetDownloadContext(); + sendFileError(static_cast(FileCommandCode::DownloadRequest), EAGAIN, "Queue busy"); + } } void handleListDirectory(const uint8_t* payload, std::size_t length) { @@ -597,28 +710,10 @@ void handleDownloadRequest(const uint8_t* payload, std::size_t length) { return; } - std::array chunk{}; - while (g_downloadCtx.remaining > 0) { - const std::size_t toRead = std::min(chunk.size(), g_downloadCtx.remaining); - const std::size_t read = std::fread(chunk.data(), 1, toRead, g_downloadCtx.file); - if (read == 0) { - const int err = ferror(g_downloadCtx.file) ? errno : EIO; - resetDownloadContext(); - sendFileError(opcode, err, "Read failed"); - return; - } - - g_downloadCtx.remaining -= read; - const uint8_t status = (g_downloadCtx.remaining == 0) ? kResponseFlagComplete : 0; - if (!sendFileResponse(opcode, status, chunk.data(), read)) { - resetDownloadContext(); - return; - } - if (g_downloadCtx.remaining > 0) - vTaskDelay(pdMS_TO_TICKS(5)); + if (!scheduleDownloadChunk()) { + sendFileError(opcode, EAGAIN, "Queue busy"); + resetDownloadContext(); } - - resetDownloadContext(); } void handleDeletePath(const uint8_t* payload, std::size_t length, bool directory) { @@ -829,12 +924,20 @@ int timeSyncWriteAccess(uint16_t /*conn_handle*/, uint16_t /*attr_handle*/, ble_ void notificationTask(void* /*param*/) { ResponseMessage msg{}; while (xQueueReceive(g_responseQueue, &msg, portMAX_DELAY) == pdTRUE) { - if (msg.opcode == kResponseOpcodeShutdown && msg.length == 0) { + if (msg.opcode == kResponseOpcodeShutdown && msg.length == 0 && !msg.streamDownload) { if (msg.data) vPortFree(msg.data); break; } + if (msg.streamDownload) { + g_downloadCtx.chunkScheduled = false; + processDownloadChunk(); + if (msg.data) + vPortFree(msg.data); + continue; + } + bool sent = false; while (g_activeConnHandle != BLE_HS_CONN_HANDLE_NONE) { if (sendFileResponseNow(msg)) { @@ -856,6 +959,48 @@ void notificationTask(void* /*param*/) { vTaskDelete(nullptr); } +void logConnectionParams(uint16_t connHandle, const char* context) { + ble_gap_conn_desc desc{}; + if (ble_gap_conn_find(connHandle, &desc) != 0) { + ESP_LOGW(kLogTag, "%s: unable to read conn params for handle=%u", context, static_cast(connHandle)); + return; + } + + const float intervalMs = connIntervalUnitsToMs(desc.conn_itvl); + const float timeoutMs = supervisionUnitsToMs(desc.supervision_timeout); + + ESP_LOGI(kLogTag, + "%s params: interval=%.1f ms latency=%u supervision=%.0f ms", + context, + intervalMs, + static_cast(desc.conn_latency), + timeoutMs); +} + +void applyPreferredConnectionParams(uint16_t connHandle) { + ble_gap_upd_params params{ + .itvl_min = kPreferredConnIntervalMin, + .itvl_max = kPreferredConnIntervalMax, + .latency = kPreferredConnLatency, + .supervision_timeout = kPreferredSupervisionTimeout, + .min_ce_len = 0, + .max_ce_len = 0, + }; + + const int rc = ble_gap_update_params(connHandle, ¶ms); + if (rc != 0) { + ESP_LOGW(kLogTag, "Requesting preferred conn params failed (rc=%d)", rc); + return; + } + + ESP_LOGI(kLogTag, + "Requested conn params: interval=%.0f-%.0f ms latency=%u supervision=%.0f ms", + connIntervalUnitsToMs(kPreferredConnIntervalMin), + connIntervalUnitsToMs(kPreferredConnIntervalMax), + kPreferredConnLatency, + supervisionUnitsToMs(kPreferredSupervisionTimeout)); +} + void startAdvertising() { ble_hs_adv_fields fields{}; std::memset(&fields, 0, sizeof(fields)); @@ -940,6 +1085,8 @@ int gapEventHandler(struct ble_gap_event* event, void* /*arg*/) { if (event->connect.status == 0) { ESP_LOGI(kLogTag, "Connected; handle=%d", event->connect.conn_handle); g_activeConnHandle = event->connect.conn_handle; + logConnectionParams(event->connect.conn_handle, "Initial"); + applyPreferredConnectionParams(event->connect.conn_handle); } else { ESP_LOGW(kLogTag, "Connection attempt failed; status=%d", event->connect.status); startAdvertising(); @@ -961,6 +1108,40 @@ int gapEventHandler(struct ble_gap_event* event, void* /*arg*/) { startAdvertising(); break; + case BLE_GAP_EVENT_CONN_UPDATE: + if (event->conn_update.status == 0) { + logConnectionParams(event->conn_update.conn_handle, "Updated"); + } else { + ESP_LOGW(kLogTag, + "Connection update failed; status=%d handle=%u", + event->conn_update.status, + static_cast(event->conn_update.conn_handle)); + } + break; + + case BLE_GAP_EVENT_CONN_UPDATE_REQ: + if (event->conn_update_req.self_params) { + auto& params = *event->conn_update_req.self_params; + if (params.itvl_max > kPreferredConnIntervalMax) + params.itvl_max = kPreferredConnIntervalMax; + if (params.itvl_min > params.itvl_max) + params.itvl_min = params.itvl_max; + if (params.latency > kPreferredConnLatency) + params.latency = kPreferredConnLatency; + if (params.supervision_timeout > kPreferredSupervisionTimeout) + params.supervision_timeout = kPreferredSupervisionTimeout; + params.min_ce_len = 0; + params.max_ce_len = 0; + + ESP_LOGI(kLogTag, + "Peer update request -> interval %.1f-%.1f ms latency %u timeout %.0f ms", + connIntervalUnitsToMs(params.itvl_min), + connIntervalUnitsToMs(params.itvl_max), + params.latency, + supervisionUnitsToMs(params.supervision_timeout)); + } + break; + default: break; } diff --git a/Firmware/main/src/app_main.cpp b/Firmware/main/src/app_main.cpp index 5859895..bb27541 100644 --- a/Firmware/main/src/app_main.cpp +++ b/Firmware/main/src/app_main.cpp @@ -2,8 +2,8 @@ #include "cardboy/apps/clock_app.hpp" #include "cardboy/apps/gameboy_app.hpp" -#include "cardboy/apps/menu_app.hpp" #include "cardboy/apps/lockscreen_app.hpp" +#include "cardboy/apps/menu_app.hpp" #include "cardboy/apps/settings_app.hpp" #include "cardboy/apps/snake_app.hpp" #include "cardboy/apps/tetris_app.hpp" @@ -241,7 +241,7 @@ extern "C" void app_main() { system.registerApp(apps::createTetrisAppFactory()); system.registerApp(apps::createGameboyAppFactory()); - start_task_usage_monitor(); + // start_task_usage_monitor(); system.run(); }