mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-06 23:57:39 +03:00
Add BleFileTransferActivity for handling BLE file transfers
This commit is contained in:
parent
1f2380be56
commit
2c5c5503a5
199
src/activities/bluetooth/BleFileTransferActivity.cpp
Normal file
199
src/activities/bluetooth/BleFileTransferActivity.cpp
Normal 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();
|
||||||
|
}
|
||||||
49
src/activities/bluetooth/BleFileTransferActivity.h
Normal file
49
src/activities/bluetooth/BleFileTransferActivity.h
Normal 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
|
||||||
|
};
|
||||||
@ -132,32 +132,135 @@ uint32_t BleFileTransfer::getConnectedCount() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::string BleFileTransfer::getFileList() {
|
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"
|
// 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
|
FsFile root;
|
||||||
return "example1.epub,example2.epub,example3.epub";
|
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) {
|
void BleFileTransfer::handleControlCommand(const std::string& command) {
|
||||||
Serial.printf("[%lu] [BLE] Control command: %s\n", millis(), command.c_str());
|
Serial.printf("[%lu] [BLE] Control command: %s\n", millis(), command.c_str());
|
||||||
|
|
||||||
// Parse and handle commands
|
// 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") {
|
if (command == "LIST") {
|
||||||
// Refresh file list
|
// Refresh file list - client should read FILE_LIST characteristic after this
|
||||||
Serial.printf("[%lu] [BLE] Refreshing file list\n", millis());
|
Serial.printf("[%lu] [BLE] File list refresh requested\n", millis());
|
||||||
} else if (command.rfind("GET:", 0) == 0) {
|
} else if (command.rfind("GET:", 0) == 0) {
|
||||||
std::string filename = command.substr(4);
|
std::string filename = command.substr(4);
|
||||||
Serial.printf("[%lu] [BLE] Request to download: %s\n", millis(), filename.c_str());
|
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) {
|
} else if (command.rfind("PUT:", 0) == 0) {
|
||||||
std::string filename = command.substr(4);
|
std::string filename = command.substr(4);
|
||||||
Serial.printf("[%lu] [BLE] Request to upload: %s\n", millis(), filename.c_str());
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
10
src/main.cpp
10
src/main.cpp
@ -11,6 +11,7 @@
|
|||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "CrossPointState.h"
|
#include "CrossPointState.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
|
#include "activities/bluetooth/BleFileTransferActivity.h"
|
||||||
#include "activities/boot_sleep/BootActivity.h"
|
#include "activities/boot_sleep/BootActivity.h"
|
||||||
#include "activities/boot_sleep/SleepActivity.h"
|
#include "activities/boot_sleep/SleepActivity.h"
|
||||||
#include "activities/home/HomeActivity.h"
|
#include "activities/home/HomeActivity.h"
|
||||||
@ -214,7 +215,14 @@ void onContinueReading() { onGoToReader(APP_STATE.openEpubPath); }
|
|||||||
|
|
||||||
void onGoToFileTransfer() {
|
void onGoToFileTransfer() {
|
||||||
exitActivity();
|
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() {
|
void onGoToSettings() {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user