Add BleFileTransferActivity for handling BLE file transfers

This commit is contained in:
altsysrq 2025-12-30 21:26:07 -06:00
parent 1f2380be56
commit 2c5c5503a5
4 changed files with 371 additions and 12 deletions

View File

@ -0,0 +1,199 @@
#include "BleFileTransferActivity.h"
#include <GfxRenderer.h>
#include <qrcode.h>
#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<BleFileTransferActivity*>(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();
}

View File

@ -0,0 +1,49 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional>
#include <memory>
#include <string>
#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<void()> onGoBack;
// BLE service - owned by this activity
std::unique_ptr<BleFileTransfer> 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<void()>& 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
};

View File

@ -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());
}
}

View File

@ -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() {