2026-01-19 16:49:54 -05:00
|
|
|
#include "services/ble_service.h"
|
|
|
|
|
#include "device_state.h"
|
2026-01-19 11:50:10 -05:00
|
|
|
#include "config.h"
|
|
|
|
|
#include <NimBLEDevice.h>
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
// BLE Server and Characteristics
|
|
|
|
|
// ============================================
|
|
|
|
|
static NimBLEServer* pServer = nullptr;
|
|
|
|
|
static NimBLEAdvertising* pAdvertising = nullptr;
|
|
|
|
|
static NimBLECharacteristic* pHeartRateMeasurement = nullptr;
|
2026-01-19 15:40:51 -05:00
|
|
|
static NimBLECharacteristic* pBatteryLevel = nullptr;
|
2026-01-19 11:50:10 -05:00
|
|
|
static NimBLECharacteristic* pTreadmillData = nullptr;
|
|
|
|
|
|
2026-01-19 16:49:54 -05:00
|
|
|
static bool bleInitialized = false;
|
|
|
|
|
|
|
|
|
|
// Forward declare for callback
|
|
|
|
|
static void onBleConnect();
|
|
|
|
|
static void onBleDisconnect();
|
2026-01-19 11:50:10 -05:00
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
// Server Callbacks
|
|
|
|
|
// ============================================
|
|
|
|
|
class ServerCallbacks : public NimBLEServerCallbacks {
|
|
|
|
|
void onConnect(NimBLEServer* pServer) override {
|
2026-01-19 16:49:54 -05:00
|
|
|
onBleConnect();
|
2026-01-19 11:50:10 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void onDisconnect(NimBLEServer* pServer) override {
|
2026-01-19 16:49:54 -05:00
|
|
|
onBleDisconnect();
|
2026-01-19 11:50:10 -05:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-19 16:49:54 -05:00
|
|
|
static void onBleConnect() {
|
|
|
|
|
deviceState.setBleClientConnected(true);
|
|
|
|
|
Serial.println("BLE client connected");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void onBleDisconnect() {
|
|
|
|
|
deviceState.setBleClientConnected(false);
|
|
|
|
|
Serial.println("BLE client disconnected");
|
|
|
|
|
NimBLEDevice::startAdvertising();
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-19 11:50:10 -05:00
|
|
|
// ============================================
|
2026-01-19 16:49:54 -05:00
|
|
|
// Singleton
|
2026-01-19 11:50:10 -05:00
|
|
|
// ============================================
|
2026-01-19 16:49:54 -05:00
|
|
|
BleService& BleService::getInstance() {
|
|
|
|
|
static BleService instance;
|
|
|
|
|
return instance;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
// Lifecycle
|
|
|
|
|
// ============================================
|
|
|
|
|
void BleService::setup() {
|
|
|
|
|
initBLE();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void BleService::loop() {
|
|
|
|
|
if (!deviceState.isBleStarted()) return;
|
|
|
|
|
|
|
|
|
|
// Send notifications at regular intervals
|
|
|
|
|
if (millis() - lastNotify >= BLE_NOTIFY_INTERVAL) {
|
|
|
|
|
lastNotify = millis();
|
|
|
|
|
|
|
|
|
|
const auto& values = deviceState.getValues();
|
|
|
|
|
|
|
|
|
|
if (deviceState.getDeviceType() == DeviceType::HEART_RATE) {
|
|
|
|
|
notifyHeartRate(values.heartRate);
|
|
|
|
|
} else if (deviceState.getDeviceType() == DeviceType::TREADMILL) {
|
|
|
|
|
// Accumulate distance
|
|
|
|
|
deviceState.accumulateTreadmillDistance(BLE_NOTIFY_INTERVAL / 1000.0);
|
|
|
|
|
const auto& updatedValues = deviceState.getValues();
|
|
|
|
|
notifyTreadmill(updatedValues.treadmillSpeed,
|
|
|
|
|
updatedValues.treadmillIncline,
|
|
|
|
|
updatedValues.treadmillDistance);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void BleService::initBLE() {
|
|
|
|
|
if (bleInitialized) return;
|
|
|
|
|
|
2026-01-19 11:50:10 -05:00
|
|
|
NimBLEDevice::init("BLE Simulator");
|
|
|
|
|
NimBLEDevice::setPower(ESP_PWR_LVL_P9);
|
|
|
|
|
|
|
|
|
|
pServer = NimBLEDevice::createServer();
|
|
|
|
|
pServer->setCallbacks(new ServerCallbacks());
|
|
|
|
|
|
|
|
|
|
pAdvertising = NimBLEDevice::getAdvertising();
|
|
|
|
|
|
2026-01-19 16:49:54 -05:00
|
|
|
bleInitialized = true;
|
2026-01-19 11:50:10 -05:00
|
|
|
Serial.println("BLE initialized");
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-19 16:49:54 -05:00
|
|
|
void BleService::stop() {
|
2026-01-19 11:50:10 -05:00
|
|
|
if (pAdvertising) {
|
|
|
|
|
pAdvertising->stop();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pHeartRateMeasurement = nullptr;
|
2026-01-19 15:40:51 -05:00
|
|
|
pBatteryLevel = nullptr;
|
2026-01-19 11:50:10 -05:00
|
|
|
pTreadmillData = nullptr;
|
|
|
|
|
|
|
|
|
|
Serial.println("BLE stopped");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
// Heart Rate Service Setup
|
|
|
|
|
// ============================================
|
2026-01-19 16:49:54 -05:00
|
|
|
void BleService::setupHeartRate() {
|
2026-01-19 11:50:10 -05:00
|
|
|
Serial.println("Setting up Heart Rate Service...");
|
|
|
|
|
|
|
|
|
|
if (pAdvertising) {
|
|
|
|
|
pAdvertising->stop();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create Heart Rate Service
|
2026-01-19 15:40:51 -05:00
|
|
|
NimBLEService* pHRService = pServer->createService(HEART_RATE_SERVICE_UUID);
|
2026-01-19 11:50:10 -05:00
|
|
|
|
2026-01-19 15:40:51 -05:00
|
|
|
pHeartRateMeasurement = pHRService->createCharacteristic(
|
2026-01-19 11:50:10 -05:00
|
|
|
HEART_RATE_MEASUREMENT_UUID,
|
|
|
|
|
NIMBLE_PROPERTY::NOTIFY
|
|
|
|
|
);
|
|
|
|
|
|
2026-01-19 15:40:51 -05:00
|
|
|
NimBLECharacteristic* pBodySensorLocation = pHRService->createCharacteristic(
|
2026-01-19 11:50:10 -05:00
|
|
|
BODY_SENSOR_LOCATION_UUID,
|
|
|
|
|
NIMBLE_PROPERTY::READ
|
|
|
|
|
);
|
|
|
|
|
uint8_t sensorLocation = 1; // Chest
|
|
|
|
|
pBodySensorLocation->setValue(&sensorLocation, 1);
|
|
|
|
|
|
2026-01-19 15:40:51 -05:00
|
|
|
pHRService->start();
|
|
|
|
|
|
|
|
|
|
// Create Battery Service
|
|
|
|
|
NimBLEService* pBatteryService = pServer->createService(BATTERY_SERVICE_UUID);
|
|
|
|
|
|
|
|
|
|
pBatteryLevel = pBatteryService->createCharacteristic(
|
|
|
|
|
BATTERY_LEVEL_UUID,
|
|
|
|
|
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY
|
|
|
|
|
);
|
2026-01-19 16:49:54 -05:00
|
|
|
uint8_t initialBattery = deviceState.getValues().batteryLevel;
|
2026-01-19 15:40:51 -05:00
|
|
|
pBatteryLevel->setValue(&initialBattery, 1);
|
|
|
|
|
|
|
|
|
|
pBatteryService->start();
|
2026-01-19 11:50:10 -05:00
|
|
|
|
|
|
|
|
// Configure advertising
|
|
|
|
|
pAdvertising->addServiceUUID(HEART_RATE_SERVICE_UUID);
|
2026-01-19 15:40:51 -05:00
|
|
|
pAdvertising->addServiceUUID(BATTERY_SERVICE_UUID);
|
2026-01-19 11:50:10 -05:00
|
|
|
pAdvertising->setScanResponse(true);
|
|
|
|
|
pAdvertising->setMinPreferred(0x06);
|
|
|
|
|
pAdvertising->setMaxPreferred(0x12);
|
|
|
|
|
|
|
|
|
|
NimBLEDevice::setDeviceName("HR Simulator");
|
|
|
|
|
NimBLEDevice::startAdvertising();
|
|
|
|
|
|
2026-01-19 15:40:51 -05:00
|
|
|
Serial.println("Heart Rate + Battery Services started, advertising...");
|
2026-01-19 11:50:10 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================
|
2026-01-19 16:49:54 -05:00
|
|
|
// Treadmill Service Setup
|
2026-01-19 11:50:10 -05:00
|
|
|
// ============================================
|
2026-01-19 16:49:54 -05:00
|
|
|
void BleService::setupTreadmill() {
|
2026-01-19 11:50:10 -05:00
|
|
|
Serial.println("Setting up Fitness Machine Service (Treadmill)...");
|
|
|
|
|
|
|
|
|
|
if (pAdvertising) {
|
|
|
|
|
pAdvertising->stop();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
NimBLEService* pService = pServer->createService(FITNESS_MACHINE_SERVICE_UUID);
|
|
|
|
|
|
|
|
|
|
NimBLECharacteristic* pFeature = pService->createCharacteristic(
|
|
|
|
|
FITNESS_MACHINE_FEATURE_UUID,
|
|
|
|
|
NIMBLE_PROPERTY::READ
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
uint8_t featureData[8] = {
|
2026-01-19 16:49:54 -05:00
|
|
|
0x0B, 0x20, 0x00, 0x00,
|
|
|
|
|
0x00, 0x00, 0x00, 0x00
|
2026-01-19 11:50:10 -05:00
|
|
|
};
|
|
|
|
|
pFeature->setValue(featureData, 8);
|
|
|
|
|
|
|
|
|
|
pTreadmillData = pService->createCharacteristic(
|
|
|
|
|
TREADMILL_DATA_UUID,
|
|
|
|
|
NIMBLE_PROPERTY::NOTIFY
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
pService->start();
|
|
|
|
|
|
|
|
|
|
pAdvertising->addServiceUUID(FITNESS_MACHINE_SERVICE_UUID);
|
|
|
|
|
pAdvertising->setScanResponse(true);
|
|
|
|
|
pAdvertising->setMinPreferred(0x06);
|
|
|
|
|
pAdvertising->setMaxPreferred(0x12);
|
|
|
|
|
|
|
|
|
|
NimBLEDevice::setDeviceName("Treadmill Sim");
|
|
|
|
|
NimBLEDevice::startAdvertising();
|
|
|
|
|
|
|
|
|
|
Serial.println("Fitness Machine Service (Treadmill) started, advertising...");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================
|
2026-01-19 16:49:54 -05:00
|
|
|
// Notifications
|
2026-01-19 11:50:10 -05:00
|
|
|
// ============================================
|
2026-01-19 16:49:54 -05:00
|
|
|
void BleService::notifyHeartRate(uint8_t bpm) {
|
|
|
|
|
if (!pHeartRateMeasurement || !deviceState.getConnectionState().bleClientConnected) return;
|
|
|
|
|
|
2026-01-19 11:50:10 -05:00
|
|
|
uint8_t hrData[2];
|
2026-01-19 16:49:54 -05:00
|
|
|
hrData[0] = 0x00; // Flags
|
2026-01-19 11:50:10 -05:00
|
|
|
hrData[1] = bpm;
|
|
|
|
|
|
|
|
|
|
pHeartRateMeasurement->setValue(hrData, 2);
|
|
|
|
|
pHeartRateMeasurement->notify();
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-19 16:49:54 -05:00
|
|
|
void BleService::updateBattery(uint8_t level) {
|
2026-01-19 15:40:51 -05:00
|
|
|
if (!pBatteryLevel) return;
|
|
|
|
|
|
|
|
|
|
if (level > 100) level = 100;
|
|
|
|
|
pBatteryLevel->setValue(&level, 1);
|
|
|
|
|
pBatteryLevel->notify();
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-19 16:49:54 -05:00
|
|
|
void BleService::notifyTreadmill(uint16_t speed, int16_t incline, uint32_t distance) {
|
|
|
|
|
if (!pTreadmillData || !deviceState.getConnectionState().bleClientConnected) return;
|
|
|
|
|
|
2026-01-19 15:40:51 -05:00
|
|
|
uint8_t data[11];
|
2026-01-19 11:50:10 -05:00
|
|
|
|
2026-01-19 15:40:51 -05:00
|
|
|
uint16_t flags = 0x000C;
|
2026-01-19 11:50:10 -05:00
|
|
|
data[0] = flags & 0xFF;
|
|
|
|
|
data[1] = (flags >> 8) & 0xFF;
|
|
|
|
|
|
|
|
|
|
data[2] = speed & 0xFF;
|
|
|
|
|
data[3] = (speed >> 8) & 0xFF;
|
|
|
|
|
|
2026-01-19 15:40:51 -05:00
|
|
|
data[4] = distance & 0xFF;
|
|
|
|
|
data[5] = (distance >> 8) & 0xFF;
|
|
|
|
|
data[6] = (distance >> 16) & 0xFF;
|
|
|
|
|
|
|
|
|
|
data[7] = incline & 0xFF;
|
|
|
|
|
data[8] = (incline >> 8) & 0xFF;
|
2026-01-19 11:50:10 -05:00
|
|
|
|
|
|
|
|
int16_t rampAngle = 0;
|
2026-01-19 15:40:51 -05:00
|
|
|
data[9] = rampAngle & 0xFF;
|
|
|
|
|
data[10] = (rampAngle >> 8) & 0xFF;
|
2026-01-19 11:50:10 -05:00
|
|
|
|
2026-01-19 15:40:51 -05:00
|
|
|
pTreadmillData->setValue(data, 11);
|
2026-01-19 11:50:10 -05:00
|
|
|
pTreadmillData->notify();
|
|
|
|
|
}
|