diff --git a/platformio.ini b/platformio.ini index ef27ffd5..11126687 100644 --- a/platformio.ini +++ b/platformio.ini @@ -30,6 +30,14 @@ build_flags = -std=c++2a # Enable UTF-8 long file names in SdFat -DUSE_UTF8_LONG_NAMES=1 +# BLE memory optimization flags + -DCONFIG_BT_ENABLED=1 + -DCONFIG_BT_NIMBLE_ENABLED=1 + -DCONFIG_NIMBLE_MAX_CONNECTIONS=1 + -DCONFIG_NIMBLE_MAX_BONDS=1 + -DCONFIG_NIMBLE_SVC_GAP_DEVICE_NAME_MAX_LEN=12 + -DCONFIG_NIMBLE_SVC_GAP_APPEARANCE=0x0 + -DCONFIG_NIMBLE_LOG_LEVEL=0 ; Board configuration board_build.flash_mode = dio @@ -48,6 +56,7 @@ lib_deps = bblanchon/ArduinoJson @ 7.4.2 ricmoo/QRCode @ 0.0.1 links2004/WebSockets @ 2.7.3 + h2zero/NimBLE-Arduino @ 2.1.0 [env:default] extends = base diff --git a/src/BluetoothManager.cpp b/src/BluetoothManager.cpp new file mode 100644 index 00000000..7271f072 --- /dev/null +++ b/src/BluetoothManager.cpp @@ -0,0 +1,186 @@ +#include "BluetoothManager.h" +#include + +// Static instance definition +BluetoothManager BluetoothManager::instance; + +bool BluetoothManager::initialize() { +#ifdef CONFIG_BT_ENABLED + // Prevent double initialization + if (initialized) { + return true; + } + + Serial.printf("[%lu] [BLE] Initializing Bluetooth\n", millis()); + + try { + // Initialize NimBLE device with minimal configuration + BLEDevice::init(DEVICE_NAME); + + // Create server if needed + if (!createServer()) { + Serial.printf("[%lu] [BLE] Failed to create server\n", millis()); + return false; + } + + // Setup advertising + setupAdvertising(); + + initialized = true; + Serial.printf("[%lu] [BLE] Bluetooth initialized successfully\n", millis()); + Serial.printf("[%lu] [BLE] Free heap after init: %d bytes\n", millis(), ESP.getFreeHeap()); + + return true; + + } catch (...) { + Serial.printf("[%lu] [BLE] Exception during initialization\n", millis()); + return false; + } +#else + Serial.printf("[%lu] [BLE] Bluetooth disabled in build\n", millis()); + return false; +#endif +} + +void BluetoothManager::shutdown() { +#ifdef CONFIG_BT_ENABLED + if (!initialized) { + return; + } + + Serial.printf("[%lu] [BLE] Shutting down Bluetooth\n", millis()); + + // Stop advertising + stopAdvertising(); + + // Deinitialize BLE device + BLEDevice::deinit(); + + // Clean up pointers + pServer = nullptr; + pAdvertising = nullptr; + + initialized = false; + advertising = false; + + Serial.printf("[%lu] [BLE] Bluetooth shutdown complete\n", millis()); + Serial.printf("[%lu] [BLE] Free heap after shutdown: %d bytes\n", millis(), ESP.getFreeHeap()); +#endif +} + +bool BluetoothManager::startAdvertising() { +#ifdef CONFIG_BT_ENABLED + if (!initialized || advertising) { + return advertising; + } + + if (pAdvertising && pAdvertising->start()) { + advertising = true; + Serial.printf("[%lu] [BLE] Advertising started\n", millis()); + return true; + } + + Serial.printf("[%lu] [BLE] Failed to start advertising\n", millis()); + return false; +#else + return false; +#endif +} + +void BluetoothManager::stopAdvertising() { +#ifdef CONFIG_BT_ENABLED + if (!advertising || !pAdvertising) { + return; + } + + pAdvertising->stop(); + advertising = false; + Serial.printf("[%lu] [BLE] Advertising stopped\n", millis()); +#endif +} + +size_t BluetoothManager::getMemoryUsage() const { +#ifdef CONFIG_BT_ENABLED + if (!initialized) { + return sizeof(*this); // Base object size (~20 bytes) + } + + // Estimate BLE stack memory usage + size_t baseUsage = sizeof(*this); + size_t stackUsage = 0; + + // NimBLE stack typically uses 12-15KB RAM + if (pServer) { + stackUsage += 12288; // Conservative estimate + } + + return baseUsage + stackUsage; +#else + return sizeof(*this); // Minimal usage when disabled +#endif +} + +void BluetoothManager::collectGarbage() { +#ifdef CONFIG_BT_ENABLED + if (!initialized) { + return; + } + + // Force garbage collection in NimBLE + NimBLEDevice::getScan()->clearResults(); + + Serial.printf("[%lu] [BLE] Garbage collection complete\n", millis()); +#endif +} + +#ifdef CONFIG_BT_ENABLED +bool BluetoothManager::createServer() { + try { + // Create BLE server with minimal configuration + pServer = BLEDevice::createServer(); + if (!pServer) { + return false; + } + + // Set callbacks with minimal overhead + pServer->setCallbacks(new ServerCallbacks()); + + return true; + + } catch (...) { + pServer = nullptr; + return false; + } +} + +void BluetoothManager::setupAdvertising() { + if (!pServer) { + return; + } + + pAdvertising = BLEDevice::getAdvertising(); + if (!pAdvertising) { + return; + } + + // Minimal advertising configuration + pAdvertising->addServiceUUID(BLEUUID((uint16_t)0x1800)); // Generic Access + pAdvertising->setScanResponse(false); // Save power and memory + pAdvertising->setMinPreferred(0x0); // No preferred connections + pAdvertising->setMaxPreferred(0x0); +} + +void BluetoothManager::ServerCallbacks::onConnect(BLEServer* pServer) { + Serial.printf("[%lu] [BLE] Device connected\n", millis()); + + // Restart advertising for more connections (though we only allow 1) + BLEDevice::getAdvertising()->start(); +} + +void BluetoothManager::ServerCallbacks::onDisconnect(BLEServer* pServer) { + Serial.printf("[%lu] [BLE] Device disconnected\n", millis()); + + // Restart advertising + BLEDevice::getAdvertising()->start(); +} +#endif \ No newline at end of file diff --git a/src/BluetoothManager.h b/src/BluetoothManager.h new file mode 100644 index 00000000..eff2e62e --- /dev/null +++ b/src/BluetoothManager.h @@ -0,0 +1,124 @@ +#pragma once + +#include +#include + +// Forward declarations to minimize includes when BLE is disabled +#ifdef CONFIG_BT_ENABLED +#include +#include +#endif + +/** + * Memory-efficient Bluetooth Manager for CrossPoint Reader + * + * Design principles: + * - Singleton pattern to minimize memory usage + * - Conditional compilation to avoid BLE overhead when disabled + * - Minimal RAM footprint (~2-3KB when disabled, ~15KB when enabled) + * - Lazy initialization only when needed + * - Clean shutdown to prevent memory leaks + */ +class BluetoothManager { +private: + // Private constructor for singleton + BluetoothManager() = default; + + // Static instance + static BluetoothManager instance; + + // State tracking (minimal memory usage) + bool initialized = false; + bool advertising = false; + +#ifdef CONFIG_BT_ENABLED + // BLE components (only allocated when BLE is enabled) + BLEServer* pServer = nullptr; + BLEAdvertising* pAdvertising = nullptr; + + // Device name (short to save memory) + static constexpr const char* DEVICE_NAME = "CrossPoint"; +#endif + +public: + // Delete copy constructor and assignment + BluetoothManager(const BluetoothManager&) = delete; + BluetoothManager& operator=(const BluetoothManager&) = delete; + + /** + * Get singleton instance + * @return Reference to BluetoothManager instance + */ + static BluetoothManager& getInstance() { return instance; } + + /** + * Initialize Bluetooth stack + * @return true if initialization successful, false otherwise + */ + bool initialize(); + + /** + * Shutdown Bluetooth stack to free memory + */ + void shutdown(); + + /** + * Start advertising device + * @return true if advertising started successfully + */ + bool startAdvertising(); + + /** + * Stop advertising to save power + */ + void stopAdvertising(); + + /** + * Check if Bluetooth is initialized + * @return true if initialized + */ + bool isInitialized() const { return initialized; } + + /** + * Check if currently advertising + * @return true if advertising + */ + bool isAdvertising() const { return advertising; } + + /** + * Get memory usage information + * @return Estimated RAM usage in bytes + */ + size_t getMemoryUsage() const; + + /** + * Force garbage collection to free unused memory + */ + void collectGarbage(); + +private: +#ifdef CONFIG_BT_ENABLED + /** + * Create BLE server with minimal services + * @return true if server created successfully + */ + bool createServer(); + + /** + * Setup advertising data with minimal payload + */ + void setupAdvertising(); + + /** + * BLE server callbacks (minimal implementation) + */ + class ServerCallbacks : public BLEServerCallbacks { + public: + void onConnect(BLEServer* pServer) override; + void onDisconnect(BLEServer* pServer) override; + }; +#endif +}; + +// Convenience macro for accessing the manager +#define BLUETOOTH_MANAGER BluetoothManager::getInstance() \ No newline at end of file diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 8ce32a2c..ba66d99c 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -58,6 +58,9 @@ class CrossPointSettings { // Hide battery percentage enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2 }; + // Bluetooth mode settings + enum BLUETOOTH_MODE { OFF = 0, ON = 1 }; + // Sleep screen settings uint8_t sleepScreen = DARK; // Sleep screen cover mode settings @@ -94,6 +97,8 @@ class CrossPointSettings { uint8_t hideBatteryPercentage = HIDE_NEVER; // Long-press chapter skip on side buttons uint8_t longPressChapterSkip = 1; + // Bluetooth enabled setting + uint8_t bluetoothEnabled = OFF; ~CrossPointSettings() = default; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 7907e50f..aad2d822 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -5,6 +5,7 @@ #include +#include "BluetoothManager.h" #include "CalibreSettingsActivity.h" #include "CrossPointSettings.h" #include "KOReaderSettingsActivity.h" @@ -14,7 +15,7 @@ // Define the static settings list namespace { -constexpr int settingsCount = 22; +constexpr int settingsCount = 23; const SettingInfo settingsList[settingsCount] = { // Should match with SLEEP_SCREEN_MODE SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}), @@ -43,6 +44,7 @@ const SettingInfo settingsList[settingsCount] = { {"1 min", "5 min", "10 min", "15 min", "30 min"}), SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency, {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}), + SettingInfo::Enum("Bluetooth", &CrossPointSettings::bluetoothEnabled, {"Off", "On"}), SettingInfo::Action("KOReader Sync"), SettingInfo::Action("Calibre Settings"), SettingInfo::Action("Check for updates")}; @@ -130,7 +132,28 @@ void SettingsActivity::toggleCurrentSetting() { SETTINGS.*(setting.valuePtr) = !currentValue; } else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) { const uint8_t currentValue = SETTINGS.*(setting.valuePtr); - SETTINGS.*(setting.valuePtr) = (currentValue + 1) % static_cast(setting.enumValues.size()); + const uint8_t newValue = (currentValue + 1) % static_cast(setting.enumValues.size()); + SETTINGS.*(setting.valuePtr) = newValue; + + // Handle Bluetooth toggle specifically + if (strcmp(setting.name, "Bluetooth") == 0) { + if (newValue == CrossPointSettings::BLUETOOTH_MODE::ON) { + // Enable Bluetooth + if (!BLUETOOTH_MANAGER.isInitialized()) { + if (BLUETOOTH_MANAGER.initialize()) { + BLUETOOTH_MANAGER.startAdvertising(); + } else { + // Failed to initialize, revert to OFF + SETTINGS.*(setting.valuePtr) = CrossPointSettings::BLUETOOTH_MODE::OFF; + } + } + } else { + // Disable Bluetooth + if (BLUETOOTH_MANAGER.isInitialized()) { + BLUETOOTH_MANAGER.shutdown(); + } + } + } } else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) { // Decreasing would also be nice for large ranges I think but oh well can't have everything const int8_t currentValue = SETTINGS.*(setting.valuePtr); diff --git a/src/main.cpp b/src/main.cpp index e0ad316a..db1c0742 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,6 +10,7 @@ #include #include "Battery.h" +#include "BluetoothManager.h" #include "CrossPointSettings.h" #include "CrossPointState.h" #include "KOReaderCredentialStore.h" @@ -200,6 +201,11 @@ void enterDeepSleep() { exitActivity(); enterNewActivity(new SleepActivity(renderer, mappedInputManager)); + // Shutdown Bluetooth to save power and memory + if (BLUETOOTH_MANAGER.isInitialized()) { + BLUETOOTH_MANAGER.shutdown(); + } + einkDisplay.deepSleep(); Serial.printf("[%lu] [ ] Power button press calibration value: %lu ms\n", millis(), t2 - t1); Serial.printf("[%lu] [ ] Entering deep sleep.\n", millis()); @@ -292,6 +298,15 @@ void setup() { SETTINGS.loadFromFile(); KOREADER_STORE.loadFromFile(); + // Initialize Bluetooth if enabled (before display to minimize RAM impact) + if (SETTINGS.bluetoothEnabled == CrossPointSettings::BLUETOOTH_MODE::ON) { + if (!BLUETOOTH_MANAGER.initialize()) { + Serial.printf("[%lu] [BLE] Failed to initialize Bluetooth\n", millis()); + // Fall back to disabled state + SETTINGS.bluetoothEnabled = CrossPointSettings::BLUETOOTH_MODE::OFF; + } + } + // verify power button press duration after we've read settings. verifyWakeupLongPress();