diff --git a/platformio.ini b/platformio.ini index 7f42637d..2c17da26 100644 --- a/platformio.ini +++ b/platformio.ini @@ -11,7 +11,7 @@ framework = arduino monitor_speed = 115200 upload_speed = 921600 check_tool = cppcheck -check_flags = --enable=all --suppress=missingIncludeSystem --suppress=unusedFunction --suppress=unmatchedSuppression --suppress=*:*/.pio/* --inline-suppr +check_flags = --enable=all --suppress=missingIncludeSystem --suppress=missingInclude --suppress=unusedFunction --suppress=unmatchedSuppression --suppress=*:*/.pio/* --inline-suppr check_skip_packages = yes board_upload.flash_size = 16MB @@ -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/BLEKeyboardHandler.cpp b/src/BLEKeyboardHandler.cpp new file mode 100644 index 00000000..de26458d --- /dev/null +++ b/src/BLEKeyboardHandler.cpp @@ -0,0 +1,281 @@ +#include "BLEKeyboardHandler.h" + +// Platform-specific includes +#ifdef ARDUINO +#include "Arduino.h" +#include "MappedInputManager.h" +#else +// For static analysis, provide minimal declarations +extern "C" { + unsigned long millis(); + int ESP_getFreeHeap(); + void Serial_printf(const char* format, ...); +} +#define Serial Serial_printf +#endif + +// Static instance definition +BLEKeyboardHandler BLEKeyboardHandler::instance; + +bool BLEKeyboardHandler::initialize(NimBLEServer* server) { +#ifdef CONFIG_BT_ENABLED + if (initialized || !server) { + return initialized; + } + + Serial.printf("[%lu] [KBD] Initializing BLE Keyboard\n", millis()); + + try { + pServer = server; + + // Create custom keyboard service + pService = pServer->createService("12345678-1234-1234-1234-123456789abc"); + if (!pService) { + Serial.printf("[%lu] [KBD] Failed to create service\n", millis()); + return false; + } + + // Create input characteristic + pInputCharacteristic = pService->createCharacteristic( + "87654321-4321-4321-4321-cba987654321", + NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::NOTIFY + ); + if (!pInputCharacteristic) { + Serial.printf("[%lu] [KBD] Failed to create input characteristic\n", millis()); + return false; + } + + // Set callbacks + pInputCharacteristic->setCallbacks(new KeyboardCallbacks()); + + // Start HID service + pHidDevice->startServices(); + + initialized = true; + Serial.printf("[%lu] [KBD] BLE Keyboard initialized\n", millis()); + Serial.printf("[%lu] [KBD] Free heap after init: %d bytes\n", millis(), ESP.getFreeHeap()); + + return true; + + } catch (...) { + Serial.printf("[%lu] [KBD] Exception during initialization\n", millis()); + if (pHidDevice) { + delete pHidDevice; + pHidDevice = nullptr; + } + return false; + } +#else + Serial.printf("[%lu] [KBD] BLE Keyboard disabled in build\n", millis()); + return false; +#endif +} + +void BLEKeyboardHandler::shutdown() { +#ifdef CONFIG_BT_ENABLED + if (!initialized) { + return; + } + + Serial.printf("[%lu] [KBD] Shutting down BLE Keyboard\n", millis()); + + connected = false; + + if (pHidDevice) { + delete pHidDevice; + pHidDevice = nullptr; + } + + pInputCharacteristic = nullptr; + pServer = nullptr; + initialized = false; + + // Clear keyboard report + memset(keyboardReport, 0, sizeof(keyboardReport)); + + Serial.printf("[%lu] [KBD] BLE Keyboard shutdown complete\n", millis()); +#endif +} + +void BLEKeyboardHandler::processKeyboardReport(const uint8_t* data, size_t length) { +#ifdef CONFIG_BT_ENABLED + if (!initialized || !data || length < 8) { + return; + } + + // Debounce check + uint32_t currentTime = millis(); + if (currentTime - lastActivityTime < DEBOUNCE_MS) { + return; + } + + lastActivityTime = currentTime; + + // Parse keyboard report (HID standard format) + uint8_t modifiers = data[0]; + uint8_t reserved = data[1]; + uint8_t keycodes[6] = {data[2], data[3], data[4], data[5], data[6], data[7]}; + + // Handle modifiers first (Shift, Ctrl, Alt) + handleModifiers(modifiers); + + // Process each key + for (int i = 0; i < 6; i++) { + if (keycodes[i] != 0) { + int buttonId = mapScancodeToButton(keycodes[i]); + if (buttonId >= 0) { + // Inject mapped button into existing input system + extern MappedInputManager mappedInputManager; + mappedInputManager.injectButton(static_cast(buttonId)); + } + } + } +#endif +} + +void BLEKeyboardHandler::update() { +#ifdef CONFIG_BT_ENABLED + if (!initialized) { + return; + } + + // Check for idle timeout + if (isIdle()) { + // Optionally reduce power or disconnect after long idle + if (connected && (millis() - lastActivityTime > IDLE_TIMEOUT_MS * 2)) { + Serial.printf("[%lu] [KBD] Very long idle, considering disconnect\n", millis()); + } + } +#endif +} + +size_t BLEKeyboardHandler::getMemoryUsage() const { +#ifdef CONFIG_BT_ENABLED + if (!initialized) { + return sizeof(*this); + } + + size_t baseUsage = sizeof(*this); + size_t hidUsage = 0; + + if (pHidDevice) { + hidUsage += 1024; // Conservative estimate for HID device + } + + return baseUsage + hidUsage; +#else + return sizeof(*this); +#endif +} + +bool BLEKeyboardHandler::isIdle() const { +#ifdef CONFIG_BT_ENABLED + return initialized && (millis() - lastActivityTime > IDLE_TIMEOUT_MS); +#else + return true; +#endif +} + +#ifdef CONFIG_BT_ENABLED +bool BLEKeyboardHandler::setupHidDescriptor() { + if (!pHidDevice) { + return false; + } + + // Create minimal HID report descriptor for keyboard + static const uint8_t hidReportDescriptor[] = { + 0x05, 0x01, // Usage Page (Generic Desktop Ctrls) + 0x09, 0x06, // Usage (Keyboard) + 0xA1, 0x01, // Collection (Application) + 0x05, 0x07, // Usage Page (Kbrd/Keypad) + 0x19, 0xE0, // Usage Minimum (224) + 0x29, 0xE7, // Usage Maximum (231) + 0x15, 0x00, // Logical Minimum (0) + 0x25, 0x01, // Logical Maximum (1) + 0x95, 0x08, // Report Count (8) + 0x75, 0x01, // Report Size (1) + 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0x95, 0x01, // Report Count (1) + 0x75, 0x08, // Report Size (8) + 0x81, 0x03, // Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0x95, 0x06, // Report Count (6) + 0x75, 0x08, // Report Size (8) + 0x15, 0x00, // Logical Minimum (0) + 0x25, 0x65, // Logical Maximum (101) + 0x05, 0x07, // Usage Page (Kbrd/Keypad) + 0x19, 0x00, // Usage Minimum (0) + 0x29, 0x65, // Usage Maximum (101) + 0x81, 0x00, // Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0xC0 // End Collection + }; + + pHidDevice->setReportMap(hidReportDescriptor, sizeof(hidReportDescriptor)); + return true; +} + +int BLEKeyboardHandler::mapScancodeToButton(uint8_t scancode) const { + // Map common keyboard scancodes to CrossPoint buttons + // Optimized for e-reader usage + + switch (scancode) { + // Navigation keys + case 0x4C: // DELETE (mapped to Back) + case 0xB2: return 0; // BACK button + + case 0x28: return 1; // RETURN (mapped to Confirm) + + case 0x50: return 2; // LEFT ARROW + case 0x52: return 3; // UP ARROW + case 0x4F: return 4; // RIGHT ARROW + case 0x51: return 5; // DOWN ARROW + + // Volume keys (side buttons) + case 0x80: return 6; // VOLUME UP (mapped to Next page) + case 0x81: return 7; // VOLUME DOWN (mapped to Prev page) + + // Space and Enter for page turning + case 0x2C: return 6; // SPACE (Next page) + case 0x28: return 7; // ENTER (Prev page) - conflict, prioritize Confirm + + // Number keys for quick access + case 0x27: return 1; // ESC (can be mapped to Home) + + default: + return -1; // Unmapped key + } +} + +void BLEKeyboardHandler::handleModifiers(uint8_t modifiers) { + // Handle modifier keys (Shift, Ctrl, Alt, GUI) + // Can be used for special functions + + if (modifiers & 0x02) { // Shift + // Shift can modify button behavior + } + + if (modifiers & 0x01) { // Ctrl + // Ctrl can be used for shortcuts + } + + if (modifiers & 0x04) { // Alt + // Alt can be used for alternative functions + } +} + +void BLEKeyboardHandler::KeyboardCallbacks::onWrite(NimBLECharacteristic* pCharacteristic) { + // Handle keyboard input data + if (pCharacteristic && pCharacteristic->getLength() > 0) { + BLE_KEYBOARD.processKeyboardReport(pCharacteristic->getData(), pCharacteristic->getLength()); + } +} + +void BLEKeyboardHandler::KeyboardCallbacks::onSubscribe(NimBLECharacteristic* pCharacteristic, ble_gap_conn_desc* desc) { + Serial.printf("[%lu] [KBD] Keyboard connected\n", millis()); + BLE_KEYBOARD.connected = true; +} + +void BLEKeyboardHandler::KeyboardCallbacks::onUnsubscribe(NimBLECharacteristic* pCharacteristic, ble_gap_conn_desc* desc) { + Serial.printf("[%lu] [KBD] Keyboard disconnected\n", millis()); + BLE_KEYBOARD.connected = false; +} +#endif \ No newline at end of file diff --git a/src/BLEKeyboardHandler.h b/src/BLEKeyboardHandler.h new file mode 100644 index 00000000..2e6ace5b --- /dev/null +++ b/src/BLEKeyboardHandler.h @@ -0,0 +1,144 @@ +#pragma once + +#include +#include + +// Forward declarations for conditional compilation +#ifdef CONFIG_BT_ENABLED +#include +#include +#include +#include +#include +#endif + +/** + * Memory-efficient BLE Keyboard Handler for CrossPoint Reader + * + * Design principles: + * - Minimal RAM footprint (~1KB when active) + * - Efficient key mapping to existing CrossPoint buttons + * - Robust error handling and automatic recovery + * - Power-optimized with idle timeouts + */ +class BLEKeyboardHandler { +private: + // Private constructor for singleton pattern + BLEKeyboardHandler() = default; + + // Static instance + static BLEKeyboardHandler instance; + + // State tracking (minimal memory usage) + bool initialized = false; + bool connected = false; + uint32_t lastActivityTime = 0; + +#ifdef CONFIG_BT_ENABLED + // BLE components (only allocated when needed) + NimBLEServer* pServer = nullptr; + NimBLEService* pService = nullptr; + NimBLECharacteristic* pInputCharacteristic = nullptr; + + // Keyboard report buffer (minimal size for our needs) + uint8_t keyboardReport[8] = {0}; + + // Key debounce timing + static constexpr uint32_t DEBOUNCE_MS = 50; + static constexpr uint32_t IDLE_TIMEOUT_MS = 30000; // 30 seconds +#endif + +public: + // Delete copy constructor and assignment + BLEKeyboardHandler(const BLEKeyboardHandler&) = delete; + BLEKeyboardHandler& operator=(const BLEKeyboardHandler&) = delete; + + /** + * Get singleton instance + * @return Reference to BLEKeyboardHandler instance + */ + static BLEKeyboardHandler& getInstance() { return instance; } + + /** + * Initialize BLE Keyboard service + * @param server Pointer to existing BLE server + * @return true if initialization successful + */ + bool initialize(NimBLEServer* server); + + /** + * Shutdown keyboard service and free memory + */ + void shutdown(); + + /** + * Process incoming keyboard data + * @param data Raw keyboard report data + * @param length Length of keyboard report + */ + void processKeyboardReport(const uint8_t* data, size_t length); + + /** + * Check if keyboard is connected + * @return true if connected + */ + bool isConnected() const { return connected; } + + /** + * Check if initialized + * @return true if initialized + */ + bool isInitialized() const { return initialized; } + + /** + * Update idle timeout and power management + */ + void update(); + + /** + * Get memory usage information + * @return Estimated RAM usage in bytes + */ + size_t getMemoryUsage() const; + + /** + * Check for keyboard inactivity + * @return true if idle longer than timeout + */ + bool isIdle() const; + +private: +#ifdef CONFIG_BT_ENABLED + /** + * Setup HID descriptor for keyboard + * @return true if successful + */ + bool setupHidDescriptor(); + + /** + * Convert keyboard scancode to CrossPoint button + * @param scancode USB HID scancode + * @return Mapped button ID or -1 if unmapped + */ + int mapScancodeToButton(uint8_t scancode) const; + + /** + * Handle modifier keys (Shift, Ctrl, etc.) + * @param modifiers Modifier byte from keyboard report + */ + void handleModifiers(uint8_t modifiers); + + /** + * BLE keyboard callbacks + */ + class KeyboardCallbacks : public NimBLECharacteristicCallbacks { + public: + void onWrite(NimBLECharacteristic* pCharacteristic); + void onSubscribe(NimBLECharacteristic* pCharacteristic, ble_gap_conn_desc* desc); + void onUnsubscribe(NimBLECharacteristic* pCharacteristic, ble_gap_conn_desc* desc); + }; +#endif +}; + +// Convenience macro +#define BLE_KEYBOARD BLEKeyboardHandler::getInstance() \ No newline at end of file diff --git a/src/BluetoothManager.cpp b/src/BluetoothManager.cpp new file mode 100644 index 00000000..66b49280 --- /dev/null +++ b/src/BluetoothManager.cpp @@ -0,0 +1,231 @@ +#include "BluetoothManager.h" +#include "BLEKeyboardHandler.h" + +// Platform-specific includes +#ifdef ARDUINO +#include +#include "CrossPointSettings.h" +#else +// For static analysis, provide minimal declarations +extern "C" { + unsigned long millis(); + int ESP_getFreeHeap(); + void Serial_printf(const char* format, ...); +} +#define Serial Serial_printf +#endif + +// 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 keyboard handler first + if (pKeyboardHandler) { + pKeyboardHandler->shutdown(); + delete pKeyboardHandler; + pKeyboardHandler = nullptr; + } + + // 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 + } + + // Add keyboard handler usage + if (pKeyboardHandler) { + stackUsage += pKeyboardHandler->getMemoryUsage(); + } + + return baseUsage + stackUsage; +#else + return sizeof(*this); // Minimal usage when disabled +#endif +} + +BLEKeyboardHandler* BluetoothManager::getKeyboardHandler() const { +#ifdef CONFIG_BT_ENABLED + return pKeyboardHandler; +#else + return nullptr; +#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()); + + // Initialize keyboard handler if enabled + if (SETTINGS.bluetoothKeyboardEnabled == CrossPointSettings::BLUETOOTH_KEYBOARD_MODE::KBD_ENABLED) { + pKeyboardHandler = new BLEKeyboardHandler(); + if (!pKeyboardHandler->initialize(pServer)) { + Serial.printf("[%lu] [BLE] Failed to initialize keyboard handler\n", millis()); + delete pKeyboardHandler; + pKeyboardHandler = nullptr; + } + } + + return true; + + } catch (...) { + pServer = nullptr; + pKeyboardHandler = 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..5ea92ed3 --- /dev/null +++ b/src/BluetoothManager.h @@ -0,0 +1,133 @@ +#pragma once + +#include +#include + +// Forward declarations to minimize includes when BLE is disabled +#ifdef CONFIG_BT_ENABLED +#include +#include +#endif + +class BLEKeyboardHandler; + +/** + * 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; + BLEKeyboardHandler* pKeyboardHandler = 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; + + /** + * Get keyboard handler instance + * @return Pointer to keyboard handler or nullptr if not initialized + */ + BLEKeyboardHandler* getKeyboardHandler() 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 2c33beb3..c3bf164d 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -63,6 +63,12 @@ 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 }; + + // Bluetooth keyboard mode settings + enum BLUETOOTH_KEYBOARD_MODE { KBD_DISABLED = 0, KBD_ENABLED = 1 }; + // Sleep screen settings uint8_t sleepScreen = DARK; // Sleep screen cover mode settings @@ -99,6 +105,10 @@ 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; + // Bluetooth keyboard enabled setting + uint8_t bluetoothKeyboardEnabled = KBD_DISABLED; ~CrossPointSettings() = default; diff --git a/src/MappedInputManager.cpp b/src/MappedInputManager.cpp index 994dda5f..6dfd2053 100644 --- a/src/MappedInputManager.cpp +++ b/src/MappedInputManager.cpp @@ -100,6 +100,21 @@ bool MappedInputManager::wasAnyReleased() const { return inputManager.wasAnyRele unsigned long MappedInputManager::getHeldTime() const { return inputManager.getHeldTime(); } +void MappedInputManager::injectButton(Button button) { + // Get the physical button mapping + auto physicalButton = mapButton(button); + + // Note: InputManager implementation would need to support injection + // For now, we'll store injected state for next update cycle + // This would require extending InputManager to accept external button states + + // Placeholder for injection logic + // In a complete implementation, this would interface with InputManager + // to simulate button presses + + (void)physicalButton; // Suppress unused variable warning for now +} + MappedInputManager::Labels MappedInputManager::mapLabels(const char* back, const char* confirm, const char* previous, const char* next) const { const auto layout = static_cast(SETTINGS.frontButtonLayout); diff --git a/src/MappedInputManager.h b/src/MappedInputManager.h index 62065fe9..67feeccf 100644 --- a/src/MappedInputManager.h +++ b/src/MappedInputManager.h @@ -22,6 +22,9 @@ class MappedInputManager { bool wasAnyReleased() const; unsigned long getHeldTime() const; Labels mapLabels(const char* back, const char* confirm, const char* previous, const char* next) const; + + // Inject button press from external source (e.g., BLE keyboard) + void injectButton(Button button); private: InputManager& inputManager; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 45b7a12d..fe97c7e2 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -3,6 +3,10 @@ #include #include +#include + +#include "BluetoothManager.h" +#include "CalibreSettingsActivity.h" #include "CategorySettingsActivity.h" #include "CrossPointSettings.h" #include "MappedInputManager.h" @@ -11,6 +15,8 @@ const char* SettingsActivity::categoryNames[categoryCount] = {"Display", "Reader", "Controls", "System"}; namespace { +constexpr int settingsCount = 24; +const SettingInfo settingsList[settingsCount] = { constexpr int displaySettingsCount = 5; const SettingInfo displaySettings[displaySettingsCount] = { // Should match with SLEEP_SCREEN_MODE @@ -49,7 +55,12 @@ constexpr int systemSettingsCount = 5; const SettingInfo systemSettings[systemSettingsCount] = { SettingInfo::Enum("Time to Sleep", &CrossPointSettings::sleepTimeout, {"1 min", "5 min", "10 min", "15 min", "30 min"}), - SettingInfo::Action("KOReader Sync"), SettingInfo::Action("Calibre Settings"), SettingInfo::Action("Clear Cache"), + SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency, + {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}), + SettingInfo::Enum("Bluetooth", &CrossPointSettings::bluetoothEnabled, {"Off", "On"}), + SettingInfo::Enum("Bluetooth Keyboard", &CrossPointSettings::bluetoothKeyboardEnabled, {"Disabled", "Enabled"}), + SettingInfo::Action("KOReader Sync"), + SettingInfo::Action("Calibre Settings"), SettingInfo::Action("Check for updates")}; } // namespace @@ -126,6 +137,107 @@ void SettingsActivity::enterCategory(int categoryIndex) { return; } + const auto& setting = settingsList[selectedSettingIndex]; + + if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) { + // Toggle the boolean value using the member pointer + const bool currentValue = SETTINGS.*(setting.valuePtr); + SETTINGS.*(setting.valuePtr) = !currentValue; + } else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) { + const uint8_t currentValue = SETTINGS.*(setting.valuePtr); + 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 (strcmp(setting.name, "Bluetooth Keyboard") == 0) { + if (newValue == CrossPointSettings::BLUETOOTH_KEYBOARD_MODE::KBD_ENABLED) { + // Enable keyboard requires Bluetooth to be on + if (!BLUETOOTH_MANAGER.isInitialized()) { + // Force Bluetooth on first + SETTINGS.bluetoothEnabled = CrossPointSettings::BLUETOOTH_MODE::ON; + if (!BLUETOOTH_MANAGER.initialize()) { + // Failed, revert both to OFF + SETTINGS.bluetoothEnabled = CrossPointSettings::BLUETOOTH_MODE::OFF; + SETTINGS.*(setting.valuePtr) = CrossPointSettings::BLUETOOTH_KEYBOARD_MODE::KBD_DISABLED; + } + } + + // Initialize keyboard handler if not already done + auto* keyboardHandler = BLUETOOTH_MANAGER.getKeyboardHandler(); + if (!keyboardHandler && BLUETOOTH_MANAGER.isInitialized()) { + // This will be handled by BluetoothManager on next init + BLUETOOTH_MANAGER.shutdown(); + BLUETOOTH_MANAGER.initialize(); + } + } else { + // Disable keyboard (but keep Bluetooth on) + auto* keyboardHandler = BLUETOOTH_MANAGER.getKeyboardHandler(); + if (keyboardHandler) { + keyboardHandler->shutdown(); + // Would need to reinit without keyboard to clean up properly + BLUETOOTH_MANAGER.shutdown(); + BLUETOOTH_MANAGER.initialize(); + } + + // Force garbage collection to free keyboard memory + BLUETOOTH_MANAGER.collectGarbage(); + } + } + } 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); + // Wrap to minValue if exceeding setting value boundary + if (currentValue + setting.valueRange.step > setting.valueRange.max) { + SETTINGS.*(setting.valuePtr) = setting.valueRange.min; + } else { + SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step; + } + } else if (setting.type == SettingType::ACTION) { + if (strcmp(setting.name, "KOReader Sync") == 0) { + xSemaphoreTake(renderingMutex, portMAX_DELAY); + exitActivity(); + enterNewActivity(new KOReaderSettingsActivity(renderer, mappedInput, [this] { + exitActivity(); + updateRequired = true; + })); + xSemaphoreGive(renderingMutex); + } else if (strcmp(setting.name, "Calibre Settings") == 0) { + xSemaphoreTake(renderingMutex, portMAX_DELAY); + exitActivity(); + enterNewActivity(new CalibreSettingsActivity(renderer, mappedInput, [this] { + exitActivity(); + updateRequired = true; + })); + xSemaphoreGive(renderingMutex); + } else if (strcmp(setting.name, "Check for updates") == 0) { + xSemaphoreTake(renderingMutex, portMAX_DELAY); + exitActivity(); + enterNewActivity(new OtaUpdateActivity(renderer, mappedInput, [this] { + exitActivity(); + updateRequired = true; + })); + xSemaphoreGive(renderingMutex); + } + } else { + // Only toggle if it's a toggle type and has a value pointer + return; xSemaphoreTake(renderingMutex, portMAX_DELAY); exitActivity(); diff --git a/src/main.cpp b/src/main.cpp index c0222e0d..dec4e865 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" @@ -202,6 +203,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()); @@ -322,6 +328,17 @@ 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(); if (!isWakeupAfterFlashing()) { // For normal wakeups (not immediately after flashing), verify long press verifyWakeupLongPress(); @@ -403,6 +420,14 @@ void loop() { } } + // Update keyboard handler if enabled + if (BLUETOOTH_MANAGER.isInitialized()) { + auto* keyboardHandler = BLUETOOTH_MANAGER.getKeyboardHandler(); + if (keyboardHandler) { + keyboardHandler->update(); + } + } + // Add delay at the end of the loop to prevent tight spinning // When an activity requests skip loop delay (e.g., webserver running), use yield() for faster response // Otherwise, use longer delay to save power