From 2c5c5503a55913818915317fa1a106603a2a4758 Mon Sep 17 00:00:00 2001 From: altsysrq Date: Tue, 30 Dec 2025 21:26:07 -0600 Subject: [PATCH] Add BleFileTransferActivity for handling BLE file transfers --- .../bluetooth/BleFileTransferActivity.cpp | 199 ++++++++++++++++++ .../bluetooth/BleFileTransferActivity.h | 49 +++++ src/bluetooth/BleFileTransfer.cpp | 125 ++++++++++- src/main.cpp | 10 +- 4 files changed, 371 insertions(+), 12 deletions(-) create mode 100644 src/activities/bluetooth/BleFileTransferActivity.cpp create mode 100644 src/activities/bluetooth/BleFileTransferActivity.h diff --git a/src/activities/bluetooth/BleFileTransferActivity.cpp b/src/activities/bluetooth/BleFileTransferActivity.cpp new file mode 100644 index 00000000..fb545424 --- /dev/null +++ b/src/activities/bluetooth/BleFileTransferActivity.cpp @@ -0,0 +1,199 @@ +#include "BleFileTransferActivity.h" + +#include +#include + +#include "CrossPointSettings.h" +#include "MappedInputManager.h" +#include "activities/util/FullScreenMessageActivity.h" +#include "fontIds.h" + +namespace { +constexpr const char* BLE_DEVICE_NAME = "CrossPoint-Reader"; +constexpr int LINE_SPACING = 28; +} // namespace + +void BleFileTransferActivity::taskTrampoline(void* param) { + auto* self = static_cast(param); + self->displayTaskLoop(); +} + +void BleFileTransferActivity::onEnter() { + Activity::onEnter(); + + Serial.printf("[%lu] [BLEACT] [MEM] Free heap at onEnter: %d bytes\n", millis(), ESP.getFreeHeap()); + + renderingMutex = xSemaphoreCreateMutex(); + + // Reset state + state = BleActivityState::STARTING; + lastConnectedCount = 0; + lastUpdateTime = millis(); + updateRequired = true; + + xTaskCreate(&BleFileTransferActivity::taskTrampoline, "BleActivityTask", + 2048, // Stack size + this, // Parameters + 1, // Priority + &displayTaskHandle // Task handle + ); + + // Check if WiFi is active (mutual exclusion) + // Note: We check SETTINGS.bluetoothEnabled in the settings toggle, + // but this is a safety check in case WiFi was started after BLE was enabled + + Serial.printf("[%lu] [BLEACT] Starting BLE service...\n", millis()); + + // Create and start BLE service + bleService.reset(new BleFileTransfer()); + if (bleService->begin(BLE_DEVICE_NAME)) { + state = BleActivityState::RUNNING; + Serial.printf("[%lu] [BLEACT] BLE service started successfully\n", millis()); + } else { + Serial.printf("[%lu] [BLEACT] ERROR: Failed to start BLE service\n", millis()); + bleService.reset(); + onGoBack(); + return; + } + + updateRequired = true; +} + +void BleFileTransferActivity::onExit() { + Activity::onExit(); + + Serial.printf("[%lu] [BLEACT] [MEM] Free heap at onExit start: %d bytes\n", millis(), ESP.getFreeHeap()); + + state = BleActivityState::SHUTTING_DOWN; + + // Stop the BLE service + if (bleService) { + Serial.printf("[%lu] [BLEACT] Stopping BLE service...\n", millis()); + bleService->stop(); + bleService.reset(); + Serial.printf("[%lu] [BLEACT] BLE service stopped\n", millis()); + } + + // Small delay to let BLE cleanup complete + delay(200); + + Serial.printf("[%lu] [BLEACT] [MEM] Free heap after BLE cleanup: %d bytes\n", millis(), ESP.getFreeHeap()); + + // Acquire mutex before deleting task + Serial.printf("[%lu] [BLEACT] Acquiring rendering mutex before task deletion...\n", millis()); + xSemaphoreTake(renderingMutex, portMAX_DELAY); + + // Delete the display task + Serial.printf("[%lu] [BLEACT] Deleting display task...\n", millis()); + if (displayTaskHandle) { + vTaskDelete(displayTaskHandle); + displayTaskHandle = nullptr; + Serial.printf("[%lu] [BLEACT] Display task deleted\n", millis()); + } + + // Delete the mutex + Serial.printf("[%lu] [BLEACT] Deleting mutex...\n", millis()); + vSemaphoreDelete(renderingMutex); + renderingMutex = nullptr; + Serial.printf("[%lu] [BLEACT] Mutex deleted\n", millis()); + + Serial.printf("[%lu] [BLEACT] [MEM] Free heap at onExit end: %d bytes\n", millis(), ESP.getFreeHeap()); +} + +void BleFileTransferActivity::loop() { + // Handle exit on Back button + if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { + onGoBack(); + return; + } + + // Check for connection count changes + if (bleService && state == BleActivityState::RUNNING) { + const uint32_t currentConnectedCount = bleService->getConnectedCount(); + if (currentConnectedCount != lastConnectedCount) { + lastConnectedCount = currentConnectedCount; + updateRequired = true; + Serial.printf("[%lu] [BLEACT] Connection count changed: %u\n", millis(), currentConnectedCount); + } + + // Periodic update every 5 seconds to show that we're still alive + if (millis() - lastUpdateTime > 5000) { + lastUpdateTime = millis(); + updateRequired = true; + } + } +} + +void BleFileTransferActivity::displayTaskLoop() { + while (true) { + if (updateRequired) { + updateRequired = false; + xSemaphoreTake(renderingMutex, portMAX_DELAY); + render(); + xSemaphoreGive(renderingMutex); + } + vTaskDelay(100 / portTICK_PERIOD_MS); + } +} + +void BleFileTransferActivity::render() const { + renderer.clearScreen(); + + const auto pageWidth = renderer.getScreenWidth(); + const auto pageHeight = renderer.getScreenHeight(); + + // Draw header + renderer.drawCenteredText(UI_12_FONT_ID, 15, "Bluetooth File Transfer", true, BOLD); + + if (state == BleActivityState::RUNNING) { + int startY = 65; + + // Show device name + std::string deviceInfo = "Device: "; + deviceInfo += BLE_DEVICE_NAME; + renderer.drawCenteredText(UI_10_FONT_ID, startY, deviceInfo.c_str(), true, BOLD); + + // Show connection status + const uint32_t connectedCount = bleService ? bleService->getConnectedCount() : 0; + std::string statusText; + if (connectedCount == 0) { + statusText = "Status: Waiting for connection..."; + } else if (connectedCount == 1) { + statusText = "Status: 1 device connected"; + } else { + char buf[64]; + snprintf(buf, sizeof(buf), "Status: %u devices connected", connectedCount); + statusText = buf; + } + renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING, statusText.c_str()); + + // Instructions + renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 3, + "1. Open a Bluetooth LE scanner app"); + renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 4, + " on your phone or computer"); + renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, + "2. Connect to 'CrossPoint-Reader'"); + renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 6, + "3. Browse files and transfer data"); + + // Service info + renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 8, + "BLE GATT Service Active"); + renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 9, + "File List | Data Transfer | Control"); + + // Memory info + char memBuf[64]; + snprintf(memBuf, sizeof(memBuf), "Free RAM: %d bytes", ESP.getFreeHeap()); + renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 60, memBuf); + + } else if (state == BleActivityState::STARTING) { + renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, "Starting Bluetooth...", true, BOLD); + } + + const auto labels = mappedInput.mapLabels("« Exit", "", "", ""); + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + + renderer.displayBuffer(); +} diff --git a/src/activities/bluetooth/BleFileTransferActivity.h b/src/activities/bluetooth/BleFileTransferActivity.h new file mode 100644 index 00000000..cd1109f7 --- /dev/null +++ b/src/activities/bluetooth/BleFileTransferActivity.h @@ -0,0 +1,49 @@ +#pragma once +#include +#include +#include + +#include +#include +#include + +#include "activities/Activity.h" +#include "bluetooth/BleFileTransfer.h" + +enum class BleActivityState { + STARTING, // BLE service is starting + RUNNING, // BLE service is running and advertising + SHUTTING_DOWN // Shutting down BLE service +}; + +/** + * BleFileTransferActivity manages the BLE file transfer service. + * It starts the BLE service, displays connection status, and handles cleanup. + */ +class BleFileTransferActivity final : public Activity { + TaskHandle_t displayTaskHandle = nullptr; + SemaphoreHandle_t renderingMutex = nullptr; + bool updateRequired = false; + BleActivityState state = BleActivityState::STARTING; + const std::function onGoBack; + + // BLE service - owned by this activity + std::unique_ptr bleService; + + // Status tracking + uint32_t lastConnectedCount = 0; + unsigned long lastUpdateTime = 0; + + static void taskTrampoline(void* param); + [[noreturn]] void displayTaskLoop(); + void render() const; + + public: + explicit BleFileTransferActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::function& onGoBack) + : Activity("BleFileTransfer", renderer, mappedInput), onGoBack(onGoBack) {} + void onEnter() override; + void onExit() override; + void loop() override; + bool skipLoopDelay() override { return false; } // BLE doesn't need fast polling +}; diff --git a/src/bluetooth/BleFileTransfer.cpp b/src/bluetooth/BleFileTransfer.cpp index c9f6cb41..97c9e66e 100644 --- a/src/bluetooth/BleFileTransfer.cpp +++ b/src/bluetooth/BleFileTransfer.cpp @@ -132,32 +132,135 @@ uint32_t BleFileTransfer::getConnectedCount() const { } std::string BleFileTransfer::getFileList() { - // Return a simple JSON-like list of files in the root directory + // List all .epub files in the root directory // Format: "file1.epub,file2.epub,file3.epub" - // For a full implementation, this would traverse SD card directories + std::string fileList; - // Placeholder implementation - would need to integrate with SDCardManager - return "example1.epub,example2.epub,example3.epub"; + FsFile root; + if (!SdMan.openFileForRead("BLE", "/", root)) { + Serial.printf("[%lu] [BLE] Failed to open root directory\n", millis()); + return "ERROR: Cannot access SD card"; + } + + FsFile file; + int count = 0; + while (file.openNext(&root, O_RDONLY)) { + char filename[256]; + if (file.isDir()) { + file.close(); + continue; + } + + file.getName(filename, sizeof(filename)); + const std::string fname(filename); + + // Only include EPUB and XTC files + if (fname.length() >= 5 && + (fname.substr(fname.length() - 5) == ".epub" || + fname.substr(fname.length() - 4) == ".xtc")) { + if (count > 0) { + fileList += ","; + } + fileList += fname; + count++; + + // Limit to 50 files to avoid buffer overflow + if (count >= 50) { + Serial.printf("[%lu] [BLE] File list truncated at 50 files\n", millis()); + break; + } + } + file.close(); + } + root.close(); + + if (fileList.empty()) { + return "No EPUB or XTC files found"; + } + + Serial.printf("[%lu] [BLE] Found %d files\n", millis(), count); + return fileList; } void BleFileTransfer::handleControlCommand(const std::string& command) { Serial.printf("[%lu] [BLE] Control command: %s\n", millis(), command.c_str()); // Parse and handle commands - // Commands could be: "LIST", "GET:filename", "PUT:filename", "DELETE:filename", etc. - // For a full implementation, this would handle file operations via SDCardManager - if (command == "LIST") { - // Refresh file list - Serial.printf("[%lu] [BLE] Refreshing file list\n", millis()); + // Refresh file list - client should read FILE_LIST characteristic after this + Serial.printf("[%lu] [BLE] File list refresh requested\n", millis()); } else if (command.rfind("GET:", 0) == 0) { std::string filename = command.substr(4); Serial.printf("[%lu] [BLE] Request to download: %s\n", millis(), filename.c_str()); - // Would implement file read and send via pFileDataChar notifications + + // Open file for reading + std::string filePath = "/" + filename; + FsFile file; + if (!SdMan.openFileForRead("BLE", filePath.c_str(), file)) { + Serial.printf("[%lu] [BLE] ERROR: Failed to open file: %s\n", millis(), filename.c_str()); + if (pFileDataChar) { + pFileDataChar->setValue("ERROR: File not found"); + pFileDataChar->notify(); + } + return; + } + + // Get file size + const size_t fileSize = file.size(); + Serial.printf("[%lu] [BLE] File size: %zu bytes\n", millis(), fileSize); + + // NOTE: For full implementation, we'd need to: + // 1. Send file size first + // 2. Read file in chunks (BLE MTU is typically 512 bytes) + // 3. Send each chunk via notify() + // 4. Client would need to reassemble chunks + // + // For now, just send a status message + char statusMsg[128]; + snprintf(statusMsg, sizeof(statusMsg), "READY:%s:%zu", filename.c_str(), fileSize); + pFileDataChar->setValue(statusMsg); + pFileDataChar->notify(); + + file.close(); + Serial.printf("[%lu] [BLE] File download prepared (chunked transfer not yet implemented)\n", millis()); + } else if (command.rfind("PUT:", 0) == 0) { std::string filename = command.substr(4); Serial.printf("[%lu] [BLE] Request to upload: %s\n", millis(), filename.c_str()); - // Would implement file write from pFileDataChar writes + + // NOTE: For full implementation, we'd need to: + // 1. Open file for writing + // 2. Receive chunks via FILE_DATA characteristic writes + // 3. Write each chunk to file + // 4. Close file when complete + // + // For now, just acknowledge + if (pFileDataChar) { + pFileDataChar->setValue("ACK: Upload ready (not yet implemented)"); + pFileDataChar->notify(); + } + Serial.printf("[%lu] [BLE] File upload acknowledged (chunked transfer not yet implemented)\n", millis()); + + } else if (command.rfind("DELETE:", 0) == 0) { + std::string filename = command.substr(7); + Serial.printf("[%lu] [BLE] Request to delete: %s\n", millis(), filename.c_str()); + + std::string filePath = "/" + filename; + if (SdMan.remove(filePath.c_str())) { + Serial.printf("[%lu] [BLE] File deleted successfully: %s\n", millis(), filename.c_str()); + if (pFileDataChar) { + pFileDataChar->setValue("OK: File deleted"); + pFileDataChar->notify(); + } + } else { + Serial.printf("[%lu] [BLE] ERROR: Failed to delete file: %s\n", millis(), filename.c_str()); + if (pFileDataChar) { + pFileDataChar->setValue("ERROR: Delete failed"); + pFileDataChar->notify(); + } + } + } else { + Serial.printf("[%lu] [BLE] Unknown command: %s\n", millis(), command.c_str()); } } diff --git a/src/main.cpp b/src/main.cpp index 090e5191..7f015f41 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -11,6 +11,7 @@ #include "CrossPointSettings.h" #include "CrossPointState.h" #include "MappedInputManager.h" +#include "activities/bluetooth/BleFileTransferActivity.h" #include "activities/boot_sleep/BootActivity.h" #include "activities/boot_sleep/SleepActivity.h" #include "activities/home/HomeActivity.h" @@ -214,7 +215,14 @@ void onContinueReading() { onGoToReader(APP_STATE.openEpubPath); } void onGoToFileTransfer() { exitActivity(); - enterNewActivity(new CrossPointWebServerActivity(renderer, mappedInputManager, onGoHome)); + // Check if Bluetooth is enabled - use BLE file transfer instead of WiFi + if (SETTINGS.bluetoothEnabled) { + Serial.printf("[%lu] [ ] Starting BLE file transfer (Bluetooth enabled)\n", millis()); + enterNewActivity(new BleFileTransferActivity(renderer, mappedInputManager, onGoHome)); + } else { + Serial.printf("[%lu] [ ] Starting WiFi file transfer (Bluetooth disabled)\n", millis()); + enterNewActivity(new CrossPointWebServerActivity(renderer, mappedInputManager, onGoHome)); + } } void onGoToSettings() {