basic bluetooth: on/off

This commit is contained in:
Amram ELBAZ 2026-01-20 19:41:19 +01:00
parent 6d68466891
commit 68bd425822
6 changed files with 364 additions and 2 deletions

View File

@ -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

186
src/BluetoothManager.cpp Normal file
View File

@ -0,0 +1,186 @@
#include "BluetoothManager.h"
#include <Arduino.h>
// 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

124
src/BluetoothManager.h Normal file
View File

@ -0,0 +1,124 @@
#pragma once
#include <stdint.h>
#include <stddef.h>
// Forward declarations to minimize includes when BLE is disabled
#ifdef CONFIG_BT_ENABLED
#include <NimBLEDevice.h>
#include <NimBLEServer.h>
#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()

View File

@ -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;

View File

@ -5,6 +5,7 @@
#include <cstring>
#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<uint8_t>(setting.enumValues.size());
const uint8_t newValue = (currentValue + 1) % static_cast<uint8_t>(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);

View File

@ -10,6 +10,7 @@
#include <cstring>
#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();