From c95cd33343c14f23fe73495fdf3a00061d9a02ac Mon Sep 17 00:00:00 2001 From: Matt Hills Date: Mon, 19 Jan 2026 16:49:54 -0500 Subject: [PATCH] v1.2.0: Firmware refactor to service-oriented architecture + WiFi fixes Firmware: - Fixed WiFi reconnection bug after power cycle by adding WiFi.persistent(false) and disabling auto-connect/auto-reconnect - Fixed AP mode not broadcasting by properly handling WIFI_AP_STA mode transitions - Refactored to service-oriented architecture: - device_state: Central state management with event callbacks - config_service: NVS persistence (renamed from config_manager) - wifi_service: WiFi STA/AP management (extracted from main.cpp) - mqtt_service: MQTT client and message routing (extracted from main.cpp) - ble_service: BLE GATT services (renamed from ble_services) - web_service: HTTP configuration portal (renamed from web_portal) - main.cpp reduced from ~470 lines to ~60 lines (thin orchestrator) - Each service is self-contained with setup()/loop() pattern Backend: - Added heart rate variation endpoint - UI improvements Co-Authored-By: Claude Opus 4.5 --- firmware/esp32_ble_sim/include/ble_services.h | 70 --- firmware/esp32_ble_sim/include/config.h | 2 +- firmware/esp32_ble_sim/include/device_state.h | 105 +++++ .../include/services/ble_service.h | 58 +++ .../config_service.h} | 54 +-- .../include/services/mqtt_service.h | 45 ++ .../include/services/web_service.h | 35 ++ .../include/services/wifi_service.h | 49 ++ firmware/esp32_ble_sim/include/web_portal.h | 40 -- firmware/esp32_ble_sim/src/config_manager.cpp | 79 ---- firmware/esp32_ble_sim/src/device_state.cpp | 104 ++++ firmware/esp32_ble_sim/src/main.cpp | 443 +----------------- .../ble_service.cpp} | 176 +++---- .../src/services/config_service.cpp | 85 ++++ .../src/services/mqtt_service.cpp | 203 ++++++++ .../web_service.cpp} | 118 ++--- .../src/services/wifi_service.cpp | 162 +++++++ src/api/hr_variation.py | 159 +++++++ src/api/main.py | 13 +- src/api/mqtt_client.py | 10 +- src/api/routes.py | 72 +++ src/web/static/index.html | 276 +++++------ 22 files changed, 1402 insertions(+), 956 deletions(-) delete mode 100644 firmware/esp32_ble_sim/include/ble_services.h create mode 100644 firmware/esp32_ble_sim/include/device_state.h create mode 100644 firmware/esp32_ble_sim/include/services/ble_service.h rename firmware/esp32_ble_sim/include/{config_manager.h => services/config_service.h} (53%) create mode 100644 firmware/esp32_ble_sim/include/services/mqtt_service.h create mode 100644 firmware/esp32_ble_sim/include/services/web_service.h create mode 100644 firmware/esp32_ble_sim/include/services/wifi_service.h delete mode 100644 firmware/esp32_ble_sim/include/web_portal.h delete mode 100644 firmware/esp32_ble_sim/src/config_manager.cpp create mode 100644 firmware/esp32_ble_sim/src/device_state.cpp rename firmware/esp32_ble_sim/src/{ble_services.cpp => services/ble_service.cpp} (58%) create mode 100644 firmware/esp32_ble_sim/src/services/config_service.cpp create mode 100644 firmware/esp32_ble_sim/src/services/mqtt_service.cpp rename firmware/esp32_ble_sim/src/{web_portal.cpp => services/web_service.cpp} (83%) create mode 100644 firmware/esp32_ble_sim/src/services/wifi_service.cpp create mode 100644 src/api/hr_variation.py diff --git a/firmware/esp32_ble_sim/include/ble_services.h b/firmware/esp32_ble_sim/include/ble_services.h deleted file mode 100644 index 6da0217..0000000 --- a/firmware/esp32_ble_sim/include/ble_services.h +++ /dev/null @@ -1,70 +0,0 @@ -#ifndef BLE_SERVICES_H -#define BLE_SERVICES_H - -#include - -// ============================================ -// BLE Service UUIDs (Bluetooth SIG standard) -// ============================================ - -// Heart Rate Service -#define HEART_RATE_SERVICE_UUID "180D" -#define HEART_RATE_MEASUREMENT_UUID "2A37" -#define BODY_SENSOR_LOCATION_UUID "2A38" - -// Battery Service -#define BATTERY_SERVICE_UUID "180F" -#define BATTERY_LEVEL_UUID "2A19" - -// Fitness Machine Service -#define FITNESS_MACHINE_SERVICE_UUID "1826" -#define TREADMILL_DATA_UUID "2ACD" -#define FITNESS_MACHINE_FEATURE_UUID "2ACC" - -// ============================================ -// Function Prototypes -// ============================================ - -/** - * Initialize NimBLE stack (call once at startup) - */ -void initBLE(); - -/** - * Stop BLE advertising and deinit - */ -void stopBLE(); - -/** - * Setup BLE as Heart Rate Monitor - * Creates Heart Rate Service with measurement characteristic - */ -void setupBLE_HeartRate(); - -/** - * Setup BLE as Treadmill (Fitness Machine) - * Creates Fitness Machine Service with treadmill data - */ -void setupBLE_Treadmill(); - -/** - * Send heart rate notification - * @param bpm Heart rate in beats per minute - */ -void notifyHeartRate(uint8_t bpm); - -/** - * Update battery level - * @param level Battery level 0-100% - */ -void updateBatteryLevel(uint8_t level); - -/** - * Send treadmill data notification - * @param speed Speed in 0.01 km/h units - * @param incline Incline in 0.1% units - * @param distance Total distance in meters - */ -void notifyTreadmill(uint16_t speed, int16_t incline, uint32_t distance); - -#endif // BLE_SERVICES_H diff --git a/firmware/esp32_ble_sim/include/config.h b/firmware/esp32_ble_sim/include/config.h index 3a4b0d6..f9af1d0 100644 --- a/firmware/esp32_ble_sim/include/config.h +++ b/firmware/esp32_ble_sim/include/config.h @@ -4,7 +4,7 @@ // ============================================ // Firmware Version // ============================================ -#define FIRMWARE_VERSION "1.0.5" +#define FIRMWARE_VERSION "1.0.6" // ============================================ // AP Mode Configuration diff --git a/firmware/esp32_ble_sim/include/device_state.h b/firmware/esp32_ble_sim/include/device_state.h new file mode 100644 index 0000000..bbeb40f --- /dev/null +++ b/firmware/esp32_ble_sim/include/device_state.h @@ -0,0 +1,105 @@ +#ifndef DEVICE_STATE_H +#define DEVICE_STATE_H + +#include +#include + +// ============================================ +// Device Types +// ============================================ +enum class DeviceType { + NONE, + HEART_RATE, + TREADMILL +}; + +// ============================================ +// Connection State +// ============================================ +struct ConnectionState { + bool wifiConnected = false; + bool mqttConnected = false; + bool bleClientConnected = false; + String ipAddress = ""; +}; + +// ============================================ +// Simulated Device Values +// ============================================ +struct SimulatedValues { + // Heart Rate Monitor + uint8_t heartRate = 70; + uint8_t batteryLevel = 100; + + // Treadmill + uint16_t treadmillSpeed = 0; // 0.01 km/h resolution + int16_t treadmillIncline = 0; // 0.1% resolution + uint32_t treadmillDistance = 0; // meters + float distanceAccumulator = 0.0; // fractional distance +}; + +// ============================================ +// Event Callbacks +// ============================================ +using DeviceTypeChangedCallback = std::function; +using ValuesChangedCallback = std::function; +using ConnectionChangedCallback = std::function; + +// ============================================ +// Device State Manager (Singleton) +// ============================================ +class DeviceState { +public: + static DeviceState& getInstance(); + + // Prevent copying + DeviceState(const DeviceState&) = delete; + DeviceState& operator=(const DeviceState&) = delete; + + // Device type + DeviceType getDeviceType() const { return deviceType; } + void setDeviceType(DeviceType type); + bool isBleStarted() const { return deviceType != DeviceType::NONE; } + const char* getDeviceTypeString() const; + + // Simulated values + const SimulatedValues& getValues() const { return values; } + void setHeartRate(uint8_t bpm); + void setBatteryLevel(uint8_t level); + void setTreadmillSpeed(float speedKmh); + void setTreadmillIncline(float inclinePercent); + void setTreadmillDistance(uint32_t meters); + void resetTreadmillDistance(); + void accumulateTreadmillDistance(float deltaSeconds); + + // Connection state + const ConnectionState& getConnectionState() const { return connectionState; } + void setWifiConnected(bool connected, const String& ip = ""); + void setMqttConnected(bool connected); + void setBleClientConnected(bool connected); + + // Event registration + void onDeviceTypeChanged(DeviceTypeChangedCallback callback); + void onValuesChanged(ValuesChangedCallback callback); + void onConnectionChanged(ConnectionChangedCallback callback); + +private: + DeviceState() = default; + + DeviceType deviceType = DeviceType::NONE; + SimulatedValues values; + ConnectionState connectionState; + + // Callbacks + DeviceTypeChangedCallback deviceTypeCallback = nullptr; + ValuesChangedCallback valuesCallback = nullptr; + ConnectionChangedCallback connectionCallback = nullptr; + + void notifyValuesChanged(); + void notifyConnectionChanged(); +}; + +// Convenience macro for accessing singleton +#define deviceState DeviceState::getInstance() + +#endif // DEVICE_STATE_H diff --git a/firmware/esp32_ble_sim/include/services/ble_service.h b/firmware/esp32_ble_sim/include/services/ble_service.h new file mode 100644 index 0000000..4fd0660 --- /dev/null +++ b/firmware/esp32_ble_sim/include/services/ble_service.h @@ -0,0 +1,58 @@ +#ifndef BLE_SERVICE_H +#define BLE_SERVICE_H + +#include + +// ============================================ +// BLE Service UUIDs (Bluetooth SIG standard) +// ============================================ +#define HEART_RATE_SERVICE_UUID "180D" +#define HEART_RATE_MEASUREMENT_UUID "2A37" +#define BODY_SENSOR_LOCATION_UUID "2A38" +#define BATTERY_SERVICE_UUID "180F" +#define BATTERY_LEVEL_UUID "2A19" +#define FITNESS_MACHINE_SERVICE_UUID "1826" +#define TREADMILL_DATA_UUID "2ACD" +#define FITNESS_MACHINE_FEATURE_UUID "2ACC" + +// ============================================ +// BLE Service +// Manages BLE advertising and GATT services +// ============================================ +class BleService { +public: + static BleService& getInstance(); + + // Prevent copying + BleService(const BleService&) = delete; + BleService& operator=(const BleService&) = delete; + + // Lifecycle + void setup(); + void loop(); + + // Configuration + void setupHeartRate(); + void setupTreadmill(); + void stop(); + + // Notifications + void notifyHeartRate(uint8_t bpm); + void notifyTreadmill(uint16_t speed, int16_t incline, uint32_t distance); + void updateBattery(uint8_t level); + + // State + bool isClientConnected() const { return deviceConnected; } + +private: + BleService() = default; + + bool deviceConnected = false; + unsigned long lastNotify = 0; + + void initBLE(); +}; + +#define bleService BleService::getInstance() + +#endif // BLE_SERVICE_H diff --git a/firmware/esp32_ble_sim/include/config_manager.h b/firmware/esp32_ble_sim/include/services/config_service.h similarity index 53% rename from firmware/esp32_ble_sim/include/config_manager.h rename to firmware/esp32_ble_sim/include/services/config_service.h index 6fcd990..84287eb 100644 --- a/firmware/esp32_ble_sim/include/config_manager.h +++ b/firmware/esp32_ble_sim/include/services/config_service.h @@ -1,27 +1,20 @@ -#ifndef CONFIG_MANAGER_H -#define CONFIG_MANAGER_H +#ifndef CONFIG_SERVICE_H +#define CONFIG_SERVICE_H #include #include // ============================================ -// Configuration Structure +// Config Service +// Manages persistent configuration in NVS // ============================================ -struct DeviceConfig { - bool configured = false; - String wifiSsid = ""; - String wifiPassword = ""; - String mqttHost = ""; - uint16_t mqttPort = 1883; - String deviceId = ""; -}; - -// ============================================ -// Configuration Manager Class -// ============================================ -class ConfigManager { +class ConfigService { public: - ConfigManager(); + static ConfigService& getInstance(); + + // Prevent copying + ConfigService(const ConfigService&) = delete; + ConfigService& operator=(const ConfigService&) = delete; // Load config from NVS bool load(); @@ -33,14 +26,14 @@ public: void clear(); // Check if configured - bool isConfigured() const { return config.configured; } + bool isConfigured() const { return configured; } // Getters - const String& getWifiSsid() const { return config.wifiSsid; } - const String& getWifiPassword() const { return config.wifiPassword; } - const String& getMqttHost() const { return config.mqttHost; } - uint16_t getMqttPort() const { return config.mqttPort; } - const String& getDeviceId() const { return config.deviceId; } + const String& getWifiSsid() const { return wifiSsid; } + const String& getWifiPassword() const { return wifiPassword; } + const String& getMqttHost() const { return mqttHost; } + uint16_t getMqttPort() const { return mqttPort; } + const String& getDeviceId() const { return deviceId; } // Setters void setWifiCredentials(const String& ssid, const String& password); @@ -54,11 +47,18 @@ public: String getDefaultDeviceId() const; private: - DeviceConfig config; + ConfigService() = default; + + bool configured = false; + String wifiSsid = ""; + String wifiPassword = ""; + String mqttHost = ""; + uint16_t mqttPort = 1883; + String deviceId = ""; + Preferences preferences; }; -// Global instance -extern ConfigManager configManager; +#define configService ConfigService::getInstance() -#endif // CONFIG_MANAGER_H +#endif // CONFIG_SERVICE_H diff --git a/firmware/esp32_ble_sim/include/services/mqtt_service.h b/firmware/esp32_ble_sim/include/services/mqtt_service.h new file mode 100644 index 0000000..4911f59 --- /dev/null +++ b/firmware/esp32_ble_sim/include/services/mqtt_service.h @@ -0,0 +1,45 @@ +#ifndef MQTT_SERVICE_H +#define MQTT_SERVICE_H + +#include + +// ============================================ +// MQTT Service +// Manages MQTT connection and message routing +// ============================================ +class MqttService { +public: + static MqttService& getInstance(); + + // Prevent copying + MqttService(const MqttService&) = delete; + MqttService& operator=(const MqttService&) = delete; + + // Lifecycle + void setup(); + void loop(); + + // State + bool isConnected() const { return mqttConnected; } + + // Publishing + void publishStatus(); + void publishValues(); + +private: + MqttService() = default; + + bool mqttConnected = false; + unsigned long lastMqttAttempt = 0; + unsigned long lastStatusReport = 0; + + void connectToMQTT(); + void handleMessage(char* topic, byte* payload, unsigned int length); + + // Static callback wrapper for PubSubClient + static void messageCallback(char* topic, byte* payload, unsigned int length); +}; + +#define mqttService MqttService::getInstance() + +#endif // MQTT_SERVICE_H diff --git a/firmware/esp32_ble_sim/include/services/web_service.h b/firmware/esp32_ble_sim/include/services/web_service.h new file mode 100644 index 0000000..ceb72e9 --- /dev/null +++ b/firmware/esp32_ble_sim/include/services/web_service.h @@ -0,0 +1,35 @@ +#ifndef WEB_SERVICE_H +#define WEB_SERVICE_H + +#include + +// ============================================ +// Web Service +// Manages HTTP server for configuration portal +// ============================================ +class WebService { +public: + static WebService& getInstance(); + + // Prevent copying + WebService(const WebService&) = delete; + WebService& operator=(const WebService&) = delete; + + // Lifecycle + void setup(); + void loop(); + +private: + WebService() = default; + + void handleRoot(); + void handleGetStatus(); + void handlePostConfig(); + void handleReset(); + void handleResetDistance(); + void handleSetBattery(); +}; + +#define webService WebService::getInstance() + +#endif // WEB_SERVICE_H diff --git a/firmware/esp32_ble_sim/include/services/wifi_service.h b/firmware/esp32_ble_sim/include/services/wifi_service.h new file mode 100644 index 0000000..480b651 --- /dev/null +++ b/firmware/esp32_ble_sim/include/services/wifi_service.h @@ -0,0 +1,49 @@ +#ifndef WIFI_SERVICE_H +#define WIFI_SERVICE_H + +#include + +// ============================================ +// WiFi Service +// Manages WiFi STA and AP modes +// ============================================ +class WiFiService { +public: + static WiFiService& getInstance(); + + // Prevent copying + WiFiService(const WiFiService&) = delete; + WiFiService& operator=(const WiFiService&) = delete; + + // Lifecycle + void setup(); + void loop(); + + // State queries + bool isConnected() const { return wifiConnected; } + bool isApActive() const { return apModeActive; } + String getIP() const; + + // Actions + void startAP(); + void stopAP(); + void reconnect(); // Trigger reconnection attempt + +private: + WiFiService() = default; + + bool wifiConnected = false; + bool apModeActive = false; + unsigned long lastWifiAttempt = 0; + int wifiRetryCount = 0; + + static const int MAX_WIFI_RETRIES = 5; + + void connectToWiFi(); + void startAPMode(); + void stopAPMode(); +}; + +#define wifiService WiFiService::getInstance() + +#endif // WIFI_SERVICE_H diff --git a/firmware/esp32_ble_sim/include/web_portal.h b/firmware/esp32_ble_sim/include/web_portal.h deleted file mode 100644 index e9ba606..0000000 --- a/firmware/esp32_ble_sim/include/web_portal.h +++ /dev/null @@ -1,40 +0,0 @@ -#ifndef WEB_PORTAL_H -#define WEB_PORTAL_H - -#include - -/** - * Initialize and start the web portal - * Runs on AP interface at 192.168.4.1 - */ -void setupWebPortal(); - -/** - * Handle web portal requests (call in loop) - */ -void handleWebPortal(); - -/** - * Get status info for web portal display - */ -struct PortalStatus { - bool wifiConnected; - bool mqttConnected; - String ipAddress; - String deviceType; - bool bleStarted; - uint32_t treadmillDistance; - uint8_t batteryLevel; -}; - -// Callback for resetting treadmill distance -typedef void (*ResetDistanceCallback)(); -void setResetDistanceCallback(ResetDistanceCallback callback); - -// Callback for setting battery level -typedef void (*SetBatteryCallback)(uint8_t level); -void setSetBatteryCallback(SetBatteryCallback callback); - -void updatePortalStatus(const PortalStatus& status); - -#endif // WEB_PORTAL_H diff --git a/firmware/esp32_ble_sim/src/config_manager.cpp b/firmware/esp32_ble_sim/src/config_manager.cpp deleted file mode 100644 index 3df4392..0000000 --- a/firmware/esp32_ble_sim/src/config_manager.cpp +++ /dev/null @@ -1,79 +0,0 @@ -#include "config_manager.h" -#include "config.h" - -// Global instance -ConfigManager configManager; - -ConfigManager::ConfigManager() {} - -bool ConfigManager::load() { - preferences.begin(NVS_NAMESPACE, true); // Read-only - - config.configured = preferences.getBool("configured", false); - config.wifiSsid = preferences.getString("wifi_ssid", ""); - config.wifiPassword = preferences.getString("wifi_pass", ""); - config.mqttHost = preferences.getString("mqtt_host", ""); - config.mqttPort = preferences.getUShort("mqtt_port", DEFAULT_MQTT_PORT); - config.deviceId = preferences.getString("device_id", getDefaultDeviceId()); - - preferences.end(); - - return config.configured; -} - -void ConfigManager::save() { - preferences.begin(NVS_NAMESPACE, false); // Read-write - - preferences.putBool("configured", config.configured); - preferences.putString("wifi_ssid", config.wifiSsid); - preferences.putString("wifi_pass", config.wifiPassword); - preferences.putString("mqtt_host", config.mqttHost); - preferences.putUShort("mqtt_port", config.mqttPort); - preferences.putString("device_id", config.deviceId); - - preferences.end(); - - Serial.println("Configuration saved to NVS"); -} - -void ConfigManager::clear() { - preferences.begin(NVS_NAMESPACE, false); - preferences.clear(); - preferences.end(); - - config = DeviceConfig(); - Serial.println("Configuration cleared"); -} - -void ConfigManager::setWifiCredentials(const String& ssid, const String& password) { - config.wifiSsid = ssid; - config.wifiPassword = password; - if (ssid.length() > 0) { - config.configured = true; - } -} - -void ConfigManager::setMqttConfig(const String& host, uint16_t port) { - config.mqttHost = host; - config.mqttPort = port; -} - -void ConfigManager::setDeviceId(const String& id) { - config.deviceId = id.length() > 0 ? id : getDefaultDeviceId(); -} - -String ConfigManager::getAPName() const { - uint32_t chipId = 0; - for (int i = 0; i < 17; i += 8) { - chipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i; - } - return String(AP_SSID_PREFIX) + String(chipId, HEX); -} - -String ConfigManager::getDefaultDeviceId() const { - uint32_t chipId = 0; - for (int i = 0; i < 17; i += 8) { - chipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i; - } - return String(DEFAULT_DEVICE_ID_PREFIX) + String(chipId, HEX); -} diff --git a/firmware/esp32_ble_sim/src/device_state.cpp b/firmware/esp32_ble_sim/src/device_state.cpp new file mode 100644 index 0000000..bce0077 --- /dev/null +++ b/firmware/esp32_ble_sim/src/device_state.cpp @@ -0,0 +1,104 @@ +#include "device_state.h" + +DeviceState& DeviceState::getInstance() { + static DeviceState instance; + return instance; +} + +const char* DeviceState::getDeviceTypeString() const { + switch (deviceType) { + case DeviceType::HEART_RATE: return "Heart Rate"; + case DeviceType::TREADMILL: return "Treadmill"; + default: return "Not configured"; + } +} + +void DeviceState::setDeviceType(DeviceType type) { + if (deviceType != type) { + deviceType = type; + if (deviceTypeCallback) { + deviceTypeCallback(type); + } + } +} + +void DeviceState::setHeartRate(uint8_t bpm) { + values.heartRate = bpm; + notifyValuesChanged(); +} + +void DeviceState::setBatteryLevel(uint8_t level) { + if (level > 100) level = 100; + values.batteryLevel = level; + notifyValuesChanged(); +} + +void DeviceState::setTreadmillSpeed(float speedKmh) { + values.treadmillSpeed = (uint16_t)(speedKmh * 100); + notifyValuesChanged(); +} + +void DeviceState::setTreadmillIncline(float inclinePercent) { + values.treadmillIncline = (int16_t)(inclinePercent * 10); + notifyValuesChanged(); +} + +void DeviceState::setTreadmillDistance(uint32_t meters) { + values.treadmillDistance = meters; + values.distanceAccumulator = (float)meters; + notifyValuesChanged(); +} + +void DeviceState::resetTreadmillDistance() { + values.treadmillDistance = 0; + values.distanceAccumulator = 0.0; + Serial.println("Treadmill distance reset to 0"); + notifyValuesChanged(); +} + +void DeviceState::accumulateTreadmillDistance(float deltaSeconds) { + // Speed is in 0.01 km/h units + // meters per second = (speed/100) * 1000 / 3600 = speed / 360 + values.distanceAccumulator += (values.treadmillSpeed / 360.0) * deltaSeconds; + values.treadmillDistance = (uint32_t)values.distanceAccumulator; +} + +void DeviceState::setWifiConnected(bool connected, const String& ip) { + connectionState.wifiConnected = connected; + connectionState.ipAddress = connected ? ip : ""; + notifyConnectionChanged(); +} + +void DeviceState::setMqttConnected(bool connected) { + connectionState.mqttConnected = connected; + notifyConnectionChanged(); +} + +void DeviceState::setBleClientConnected(bool connected) { + connectionState.bleClientConnected = connected; + notifyConnectionChanged(); +} + +void DeviceState::onDeviceTypeChanged(DeviceTypeChangedCallback callback) { + deviceTypeCallback = callback; +} + +void DeviceState::onValuesChanged(ValuesChangedCallback callback) { + valuesCallback = callback; +} + +void DeviceState::onConnectionChanged(ConnectionChangedCallback callback) { + connectionCallback = callback; +} + +void DeviceState::notifyValuesChanged() { + if (valuesCallback) { + valuesCallback(values); + } +} + +void DeviceState::notifyConnectionChanged() { + if (connectionCallback) { + connectionCallback(connectionState); + } +} diff --git a/firmware/esp32_ble_sim/src/main.cpp b/firmware/esp32_ble_sim/src/main.cpp index 9c3e0c8..a6b7d47 100644 --- a/firmware/esp32_ble_sim/src/main.cpp +++ b/firmware/esp32_ble_sim/src/main.cpp @@ -4,377 +4,24 @@ * Simulates BLE fitness devices (Heart Rate Monitor, Treadmill) * Controlled via MQTT from pyBTMCP server * - * Features: - * - AP mode for configuration when not connected to WiFi (192.168.4.1) - * - Connects to configured WiFi for MQTT control - * - Configuration stored in NVS (flash once, configure via web) + * Architecture: + * - device_state: Central state management with event callbacks + * - config_service: NVS persistence for configuration + * - wifi_service: WiFi STA/AP management + * - mqtt_service: MQTT client and message routing + * - ble_service: BLE GATT services and notifications + * - web_service: HTTP configuration portal */ #include -#include -#include -#include -#include - #include "config.h" -#include "config_manager.h" -#include "web_portal.h" -#include "ble_services.h" +#include "device_state.h" +#include "services/config_service.h" +#include "services/wifi_service.h" +#include "services/mqtt_service.h" +#include "services/ble_service.h" +#include "services/web_service.h" -// ============================================ -// Global State -// ============================================ -WiFiClient wifiClient; -PubSubClient mqtt(wifiClient); - -// Current device configuration -enum DeviceType { - DEVICE_NONE, - DEVICE_HEART_RATE, - DEVICE_TREADMILL -}; - -DeviceType currentDeviceType = DEVICE_NONE; -bool bleStarted = false; -bool wifiConnected = false; -bool mqttConnected = false; -bool apModeActive = false; - -// Simulated values -uint8_t heartRate = 70; -uint8_t batteryLevel = 100; // Battery level 0-100% -uint16_t treadmillSpeed = 0; // 0.01 km/h resolution -int16_t treadmillIncline = 0; // 0.1% resolution -uint32_t treadmillDistance = 0; // Total distance in meters -float distanceAccumulator = 0.0; // Fractional distance accumulator - -// Timing -unsigned long lastNotify = 0; -unsigned long lastStatus = 0; -unsigned long lastWifiAttempt = 0; -unsigned long lastMqttAttempt = 0; - -// ============================================ -// WiFi Functions -// ============================================ -void startAPMode() { - if (apModeActive) return; - - WiFi.mode(WIFI_AP); - WiFi.softAPConfig(AP_IP, AP_GATEWAY, AP_SUBNET); - String apName = configManager.getAPName(); - WiFi.softAP(apName.c_str(), AP_PASSWORD); - apModeActive = true; - - Serial.println("\n========================================"); - Serial.println("Access Point Started"); - Serial.print(" SSID: "); - Serial.println(apName); - Serial.print(" Config URL: http://"); - Serial.println(WiFi.softAPIP()); - Serial.println("========================================\n"); -} - -void stopAPMode() { - if (!apModeActive) return; - - WiFi.softAPdisconnect(true); - WiFi.mode(WIFI_STA); - apModeActive = false; - - Serial.println("Access Point stopped (WiFi connected)"); -} - -void setupWiFi() { - if (configManager.isConfigured()) { - // Start in STA mode, try to connect - WiFi.mode(WIFI_STA); - Serial.println("Starting in STA mode (configured)"); - } else { - // No config - start AP for setup - startAPMode(); - } -} - -void connectToWiFi() { - if (!configManager.isConfigured()) { - // Not configured - ensure AP is running - if (!apModeActive) { - startAPMode(); - } - return; - } - - if (WiFi.status() == WL_CONNECTED) { - if (!wifiConnected) { - wifiConnected = true; - Serial.print("WiFi connected! IP: "); - Serial.println(WiFi.localIP()); - - // Stop AP mode when connected - stopAPMode(); - } - return; - } - - // WiFi not connected - if (wifiConnected) { - // Was connected, now disconnected - start AP - wifiConnected = false; - mqttConnected = false; - Serial.println("WiFi disconnected!"); - startAPMode(); - } - - // Don't spam connection attempts - if (millis() - lastWifiAttempt < WIFI_CONNECT_TIMEOUT) { - return; - } - lastWifiAttempt = millis(); - - // If AP is active, switch to AP+STA to allow connection attempts - if (apModeActive) { - WiFi.mode(WIFI_AP_STA); - } - - Serial.print("Connecting to WiFi: "); - Serial.println(configManager.getWifiSsid()); - - WiFi.begin( - configManager.getWifiSsid().c_str(), - configManager.getWifiPassword().c_str() - ); -} - -// ============================================ -// MQTT Functions -// ============================================ -void publishStatus() { - if (!mqtt.connected()) return; - - JsonDocument doc; - doc["online"] = true; - doc["firmware_version"] = FIRMWARE_VERSION; - - const char* typeStr = "none"; - if (currentDeviceType == DEVICE_HEART_RATE) typeStr = "heart_rate"; - else if (currentDeviceType == DEVICE_TREADMILL) typeStr = "treadmill"; - doc["type"] = typeStr; - - doc["ble_started"] = bleStarted; - doc["ip"] = WiFi.localIP().toString(); - - String payload; - serializeJson(doc, payload); - - String topic = String("ble-sim/") + configManager.getDeviceId() + "/status"; - mqtt.publish(topic.c_str(), payload.c_str(), true); // retained -} - -void publishValues() { - if (!mqtt.connected()) return; - - JsonDocument doc; - - if (currentDeviceType == DEVICE_HEART_RATE) { - doc["heart_rate"] = heartRate; - doc["battery"] = batteryLevel; - } else if (currentDeviceType == DEVICE_TREADMILL) { - doc["speed"] = treadmillSpeed / 100.0; - doc["incline"] = treadmillIncline / 10.0; - doc["distance"] = treadmillDistance; - } - - String payload; - serializeJson(doc, payload); - - String topic = String("ble-sim/") + configManager.getDeviceId() + "/values"; - mqtt.publish(topic.c_str(), payload.c_str()); -} - -void handleMqttMessage(char* topic, byte* payload, unsigned int length) { - JsonDocument doc; - DeserializationError error = deserializeJson(doc, payload, length); - - if (error) { - Serial.print("JSON parse error: "); - Serial.println(error.c_str()); - return; - } - - String topicStr = String(topic); - String baseTopic = String("ble-sim/") + configManager.getDeviceId(); - - // Handle configuration - if (topicStr == baseTopic + "/config") { - String type = doc["type"] | ""; - - Serial.print("Configuring as: "); - Serial.println(type); - - if (type == "heart_rate") { - currentDeviceType = DEVICE_HEART_RATE; - setupBLE_HeartRate(); - bleStarted = true; - } else if (type == "treadmill") { - currentDeviceType = DEVICE_TREADMILL; - setupBLE_Treadmill(); - bleStarted = true; - } else { - currentDeviceType = DEVICE_NONE; - stopBLE(); - bleStarted = false; - } - - publishStatus(); - } - // Handle value updates - else if (topicStr == baseTopic + "/set") { - if (doc["heart_rate"].is()) { - heartRate = doc["heart_rate"]; - Serial.print("Heart rate set to: "); - Serial.println(heartRate); - } - if (doc["battery"].is()) { - batteryLevel = doc["battery"]; - if (batteryLevel > 100) batteryLevel = 100; - updateBatteryLevel(batteryLevel); - Serial.print("Battery level set to: "); - Serial.println(batteryLevel); - } - if (doc["speed"].is()) { - float speed = doc["speed"]; - treadmillSpeed = (uint16_t)(speed * 100); - Serial.print("Speed set to: "); - Serial.println(speed); - } - if (doc["incline"].is()) { - float incline = doc["incline"]; - treadmillIncline = (int16_t)(incline * 10); - Serial.print("Incline set to: "); - Serial.println(incline); - } - if (doc["distance"].is()) { - treadmillDistance = doc["distance"]; - distanceAccumulator = (float)treadmillDistance; // Sync accumulator - Serial.print("Distance set to: "); - Serial.println(treadmillDistance); - } - - publishValues(); - } -} - -void setupMQTT() { - mqtt.setCallback(handleMqttMessage); - mqtt.setBufferSize(512); -} - -void connectToMQTT() { - if (!configManager.isConfigured() || !wifiConnected) { - return; - } - - if (mqtt.connected()) { - if (!mqttConnected) { - mqttConnected = true; - } - return; - } - - // Don't spam connection attempts - if (millis() - lastMqttAttempt < MQTT_RECONNECT_INTERVAL) { - return; - } - lastMqttAttempt = millis(); - - mqttConnected = false; - - // Update MQTT server config - mqtt.setServer( - configManager.getMqttHost().c_str(), - configManager.getMqttPort() - ); - - Serial.print("Connecting to MQTT at "); - Serial.print(configManager.getMqttHost()); - Serial.print(":"); - Serial.println(configManager.getMqttPort()); - - String clientId = String("esp32-") + String(random(0xffff), HEX); - - // Set up Last Will and Testament (LWT) for disconnect detection - String statusTopic = String("ble-sim/") + configManager.getDeviceId() + "/status"; - String willMessage = "{\"online\":false}"; - - // Connect with LWT: topic, QoS 1, retain true, message - if (mqtt.connect(clientId.c_str(), statusTopic.c_str(), 1, true, willMessage.c_str())) { - mqttConnected = true; - Serial.println("MQTT connected with LWT!"); - - // Subscribe to control topics - String configTopic = String("ble-sim/") + configManager.getDeviceId() + "/config"; - String setTopic = String("ble-sim/") + configManager.getDeviceId() + "/set"; - - mqtt.subscribe(configTopic.c_str()); - mqtt.subscribe(setTopic.c_str()); - - Serial.print("Subscribed to: "); - Serial.println(configTopic); - - // Publish initial status (with retain so new subscribers see current state) - publishStatus(); - } else { - Serial.print("MQTT connection failed, rc="); - Serial.println(mqtt.state()); - } -} - -// ============================================ -// Reset Treadmill Distance -// ============================================ -void resetTreadmillDistance() { - treadmillDistance = 0; - distanceAccumulator = 0.0; - Serial.println("Treadmill distance reset to 0"); -} - -// ============================================ -// Set Battery Level (from web UI) -// ============================================ -void setBatteryLevelCallback(uint8_t level) { - batteryLevel = level; - updateBatteryLevel(batteryLevel); - Serial.print("Battery level set via web UI: "); - Serial.println(batteryLevel); -} - -// ============================================ -// Update Portal Status -// ============================================ -void updateStatus() { - PortalStatus status; - status.wifiConnected = wifiConnected; - status.mqttConnected = mqttConnected; - status.bleStarted = bleStarted; - status.ipAddress = wifiConnected ? WiFi.localIP().toString() : ""; - status.treadmillDistance = treadmillDistance; - status.batteryLevel = batteryLevel; - - if (currentDeviceType == DEVICE_HEART_RATE) { - status.deviceType = "Heart Rate"; - } else if (currentDeviceType == DEVICE_TREADMILL) { - status.deviceType = "Treadmill"; - } else { - status.deviceType = "Not configured"; - } - - updatePortalStatus(status); -} - -// ============================================ -// Main Setup & Loop -// ============================================ void setup() { Serial.begin(115200); delay(1000); @@ -386,28 +33,20 @@ void setup() { Serial.println("========================================\n"); // Load configuration from NVS - bool hasConfig = configManager.load(); + bool hasConfig = configService.load(); Serial.print("Device ID: "); - Serial.println(configManager.getDeviceId()); + Serial.println(configService.getDeviceId()); Serial.print("Configured: "); Serial.println(hasConfig ? "Yes" : "No"); - // Start WiFi (AP always on, STA if configured) - setupWiFi(); - - // Start web configuration portal - setupWebPortal(); - setResetDistanceCallback(resetTreadmillDistance); - setSetBatteryCallback(setBatteryLevelCallback); - - // Initialize MQTT - setupMQTT(); - - // Initialize BLE (don't start advertising yet) - initBLE(); + // Initialize all services + wifiService.setup(); + webService.setup(); + mqttService.setup(); + bleService.setup(); Serial.println("\nReady!"); - if (!configManager.isConfigured()) { + if (!configService.isConfigured()) { Serial.println("Configure at: http://192.168.4.1"); } else { Serial.println("Connecting to WiFi..."); @@ -416,39 +55,9 @@ void setup() { } void loop() { - // Handle web portal requests - handleWebPortal(); - - // Maintain WiFi STA connection (if configured) - connectToWiFi(); - - // Maintain MQTT connection - connectToMQTT(); - mqtt.loop(); - - // Update portal status display - updateStatus(); - - // Send BLE notifications - if (bleStarted && millis() - lastNotify >= BLE_NOTIFY_INTERVAL) { - lastNotify = millis(); - - if (currentDeviceType == DEVICE_HEART_RATE) { - notifyHeartRate(heartRate); - } else if (currentDeviceType == DEVICE_TREADMILL) { - // Accumulate distance based on speed - // Speed is in 0.01 km/h units, interval is 1 second - // meters per second = (speed/100) * 1000 / 3600 = speed / 360 - distanceAccumulator += treadmillSpeed / 360.0; - treadmillDistance = (uint32_t)distanceAccumulator; - - notifyTreadmill(treadmillSpeed, treadmillIncline, treadmillDistance); - } - } - - // Periodic status report to MQTT - if (mqttConnected && millis() - lastStatus >= STATUS_REPORT_INTERVAL) { - lastStatus = millis(); - publishStatus(); - } + // Run all service loops + wifiService.loop(); + webService.loop(); + mqttService.loop(); + bleService.loop(); } diff --git a/firmware/esp32_ble_sim/src/ble_services.cpp b/firmware/esp32_ble_sim/src/services/ble_service.cpp similarity index 58% rename from firmware/esp32_ble_sim/src/ble_services.cpp rename to firmware/esp32_ble_sim/src/services/ble_service.cpp index 57f2ca3..04ada84 100644 --- a/firmware/esp32_ble_sim/src/ble_services.cpp +++ b/firmware/esp32_ble_sim/src/services/ble_service.cpp @@ -1,14 +1,6 @@ -/** - * BLE Services Implementation - * - * Implements standard Bluetooth SIG GATT services: - * - Heart Rate Service (0x180D) - * - Fitness Machine Service (0x1826) - Treadmill - */ - -#include "ble_services.h" +#include "services/ble_service.h" +#include "device_state.h" #include "config.h" -#include "config_manager.h" #include // ============================================ @@ -16,41 +8,80 @@ // ============================================ static NimBLEServer* pServer = nullptr; static NimBLEAdvertising* pAdvertising = nullptr; - -// Heart Rate static NimBLECharacteristic* pHeartRateMeasurement = nullptr; - -// Battery static NimBLECharacteristic* pBatteryLevel = nullptr; - -// Treadmill static NimBLECharacteristic* pTreadmillData = nullptr; -static bool deviceConnected = false; -static bool oldDeviceConnected = false; +static bool bleInitialized = false; + +// Forward declare for callback +static void onBleConnect(); +static void onBleDisconnect(); // ============================================ // Server Callbacks // ============================================ class ServerCallbacks : public NimBLEServerCallbacks { void onConnect(NimBLEServer* pServer) override { - deviceConnected = true; - Serial.println("BLE client connected"); + onBleConnect(); } void onDisconnect(NimBLEServer* pServer) override { - deviceConnected = false; - Serial.println("BLE client disconnected"); - - // Restart advertising - NimBLEDevice::startAdvertising(); + onBleDisconnect(); } }; +static void onBleConnect() { + deviceState.setBleClientConnected(true); + Serial.println("BLE client connected"); +} + +static void onBleDisconnect() { + deviceState.setBleClientConnected(false); + Serial.println("BLE client disconnected"); + NimBLEDevice::startAdvertising(); +} + // ============================================ -// BLE Initialization +// Singleton // ============================================ -void initBLE() { +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; + NimBLEDevice::init("BLE Simulator"); NimBLEDevice::setPower(ESP_PWR_LVL_P9); @@ -59,15 +90,15 @@ void initBLE() { pAdvertising = NimBLEDevice::getAdvertising(); + bleInitialized = true; Serial.println("BLE initialized"); } -void stopBLE() { +void BleService::stop() { if (pAdvertising) { pAdvertising->stop(); } - // Clear services (will be recreated on next setup) pHeartRateMeasurement = nullptr; pBatteryLevel = nullptr; pTreadmillData = nullptr; @@ -78,10 +109,9 @@ void stopBLE() { // ============================================ // Heart Rate Service Setup // ============================================ -void setupBLE_HeartRate() { +void BleService::setupHeartRate() { Serial.println("Setting up Heart Rate Service..."); - // Stop any current advertising if (pAdvertising) { pAdvertising->stop(); } @@ -89,16 +119,11 @@ void setupBLE_HeartRate() { // Create Heart Rate Service NimBLEService* pHRService = pServer->createService(HEART_RATE_SERVICE_UUID); - // Heart Rate Measurement Characteristic - // Flags: Notify pHeartRateMeasurement = pHRService->createCharacteristic( HEART_RATE_MEASUREMENT_UUID, NIMBLE_PROPERTY::NOTIFY ); - // Body Sensor Location Characteristic - // Flags: Read - // Value: 1 = Chest NimBLECharacteristic* pBodySensorLocation = pHRService->createCharacteristic( BODY_SENSOR_LOCATION_UUID, NIMBLE_PROPERTY::READ @@ -106,22 +131,18 @@ void setupBLE_HeartRate() { uint8_t sensorLocation = 1; // Chest pBodySensorLocation->setValue(&sensorLocation, 1); - // Start Heart Rate service pHRService->start(); // Create Battery Service NimBLEService* pBatteryService = pServer->createService(BATTERY_SERVICE_UUID); - // Battery Level Characteristic - // Flags: Read, Notify pBatteryLevel = pBatteryService->createCharacteristic( BATTERY_LEVEL_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY ); - uint8_t initialBattery = 100; + uint8_t initialBattery = deviceState.getValues().batteryLevel; pBatteryLevel->setValue(&initialBattery, 1); - // Start Battery service pBatteryService->start(); // Configure advertising @@ -131,145 +152,94 @@ void setupBLE_HeartRate() { pAdvertising->setMinPreferred(0x06); pAdvertising->setMaxPreferred(0x12); - // Update device name NimBLEDevice::setDeviceName("HR Simulator"); - - // Start advertising NimBLEDevice::startAdvertising(); Serial.println("Heart Rate + Battery Services started, advertising..."); } // ============================================ -// Treadmill (Fitness Machine) Service Setup +// Treadmill Service Setup // ============================================ -void setupBLE_Treadmill() { +void BleService::setupTreadmill() { Serial.println("Setting up Fitness Machine Service (Treadmill)..."); - // Stop any current advertising if (pAdvertising) { pAdvertising->stop(); } - // Create Fitness Machine Service NimBLEService* pService = pServer->createService(FITNESS_MACHINE_SERVICE_UUID); - // Fitness Machine Feature Characteristic - // Flags: Read NimBLECharacteristic* pFeature = pService->createCharacteristic( FITNESS_MACHINE_FEATURE_UUID, NIMBLE_PROPERTY::READ ); - // Feature flags for treadmill: - // Byte 0-3: Fitness Machine Features - // Bit 0: Average Speed Supported - // Bit 1: Cadence Supported - // Bit 2: Total Distance Supported - // Bit 3: Inclination Supported - // Bit 13: Elapsed Time Supported - // Byte 4-7: Target Setting Features uint8_t featureData[8] = { - 0x0B, 0x20, 0x00, 0x00, // Features: Speed, Cadence, Distance, Inclination, Elapsed Time - 0x00, 0x00, 0x00, 0x00 // Target settings (none) + 0x0B, 0x20, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 }; pFeature->setValue(featureData, 8); - // Treadmill Data Characteristic - // Flags: Notify pTreadmillData = pService->createCharacteristic( TREADMILL_DATA_UUID, NIMBLE_PROPERTY::NOTIFY ); - // Start the service pService->start(); - // Configure advertising pAdvertising->addServiceUUID(FITNESS_MACHINE_SERVICE_UUID); pAdvertising->setScanResponse(true); pAdvertising->setMinPreferred(0x06); pAdvertising->setMaxPreferred(0x12); - // Update device name NimBLEDevice::setDeviceName("Treadmill Sim"); - - // Start advertising NimBLEDevice::startAdvertising(); Serial.println("Fitness Machine Service (Treadmill) started, advertising..."); } // ============================================ -// Heart Rate Notification +// Notifications // ============================================ -void notifyHeartRate(uint8_t bpm) { - if (!pHeartRateMeasurement || !deviceConnected) return; +void BleService::notifyHeartRate(uint8_t bpm) { + if (!pHeartRateMeasurement || !deviceState.getConnectionState().bleClientConnected) return; - // Heart Rate Measurement format: - // Byte 0: Flags - // Bit 0: Heart Rate Value Format (0 = UINT8, 1 = UINT16) - // Bit 1-2: Sensor Contact Status - // Bit 3: Energy Expended Status - // Bit 4: RR-Interval - // Byte 1: Heart Rate Value (UINT8) uint8_t hrData[2]; - hrData[0] = 0x00; // Flags: UINT8 format, no contact detection + hrData[0] = 0x00; // Flags hrData[1] = bpm; pHeartRateMeasurement->setValue(hrData, 2); pHeartRateMeasurement->notify(); } -// ============================================ -// Battery Level Update -// ============================================ -void updateBatteryLevel(uint8_t level) { +void BleService::updateBattery(uint8_t level) { if (!pBatteryLevel) return; - // Clamp to 0-100 if (level > 100) level = 100; - pBatteryLevel->setValue(&level, 1); pBatteryLevel->notify(); } -// ============================================ -// Treadmill Data Notification -// ============================================ -void notifyTreadmill(uint16_t speed, int16_t incline, uint32_t distance) { - if (!pTreadmillData || !deviceConnected) return; +void BleService::notifyTreadmill(uint16_t speed, int16_t incline, uint32_t distance) { + if (!pTreadmillData || !deviceState.getConnectionState().bleClientConnected) return; - // Treadmill Data format (per Bluetooth FTMS spec): - // Byte 0-1: Flags - // Bit 0: More Data (0 = Instantaneous Speed present) - // Bit 1: Average Speed present - // Bit 2: Total Distance present - // Bit 3: Inclination and Ramp Angle present - // Following bytes: Data fields based on flags (in order of flag bits) - - // We'll include: Instantaneous Speed + Total Distance + Inclination + Ramp Angle uint8_t data[11]; - // Flags: Total Distance present (bit 2) + Inclination and Ramp Angle present (bit 3) uint16_t flags = 0x000C; data[0] = flags & 0xFF; data[1] = (flags >> 8) & 0xFF; - // Instantaneous Speed (always present when More Data=0, uint16, 0.01 km/h resolution) data[2] = speed & 0xFF; data[3] = (speed >> 8) & 0xFF; - // Total Distance (uint24, meters) - 3 bytes, little endian data[4] = distance & 0xFF; data[5] = (distance >> 8) & 0xFF; data[6] = (distance >> 16) & 0xFF; - // Inclination (sint16, 0.1% resolution) data[7] = incline & 0xFF; data[8] = (incline >> 8) & 0xFF; - // Ramp Angle Setting (sint16, 0.1 degree resolution) - set to 0 int16_t rampAngle = 0; data[9] = rampAngle & 0xFF; data[10] = (rampAngle >> 8) & 0xFF; diff --git a/firmware/esp32_ble_sim/src/services/config_service.cpp b/firmware/esp32_ble_sim/src/services/config_service.cpp new file mode 100644 index 0000000..37d49c7 --- /dev/null +++ b/firmware/esp32_ble_sim/src/services/config_service.cpp @@ -0,0 +1,85 @@ +#include "services/config_service.h" +#include "config.h" + +ConfigService& ConfigService::getInstance() { + static ConfigService instance; + return instance; +} + +bool ConfigService::load() { + preferences.begin(NVS_NAMESPACE, true); // Read-only + + configured = preferences.getBool("configured", false); + wifiSsid = preferences.getString("wifi_ssid", ""); + wifiPassword = preferences.getString("wifi_pass", ""); + mqttHost = preferences.getString("mqtt_host", ""); + mqttPort = preferences.getUShort("mqtt_port", DEFAULT_MQTT_PORT); + deviceId = preferences.getString("device_id", getDefaultDeviceId()); + + preferences.end(); + + return configured; +} + +void ConfigService::save() { + preferences.begin(NVS_NAMESPACE, false); // Read-write + + preferences.putBool("configured", configured); + preferences.putString("wifi_ssid", wifiSsid); + preferences.putString("wifi_pass", wifiPassword); + preferences.putString("mqtt_host", mqttHost); + preferences.putUShort("mqtt_port", mqttPort); + preferences.putString("device_id", deviceId); + + preferences.end(); + + Serial.println("Configuration saved to NVS"); +} + +void ConfigService::clear() { + preferences.begin(NVS_NAMESPACE, false); + preferences.clear(); + preferences.end(); + + configured = false; + wifiSsid = ""; + wifiPassword = ""; + mqttHost = ""; + mqttPort = DEFAULT_MQTT_PORT; + deviceId = ""; + + Serial.println("Configuration cleared"); +} + +void ConfigService::setWifiCredentials(const String& ssid, const String& password) { + wifiSsid = ssid; + wifiPassword = password; + if (ssid.length() > 0) { + configured = true; + } +} + +void ConfigService::setMqttConfig(const String& host, uint16_t port) { + mqttHost = host; + mqttPort = port; +} + +void ConfigService::setDeviceId(const String& id) { + deviceId = id.length() > 0 ? id : getDefaultDeviceId(); +} + +String ConfigService::getAPName() const { + uint32_t chipId = 0; + for (int i = 0; i < 17; i += 8) { + chipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i; + } + return String(AP_SSID_PREFIX) + String(chipId, HEX); +} + +String ConfigService::getDefaultDeviceId() const { + uint32_t chipId = 0; + for (int i = 0; i < 17; i += 8) { + chipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i; + } + return String(DEFAULT_DEVICE_ID_PREFIX) + String(chipId, HEX); +} diff --git a/firmware/esp32_ble_sim/src/services/mqtt_service.cpp b/firmware/esp32_ble_sim/src/services/mqtt_service.cpp new file mode 100644 index 0000000..d711ce8 --- /dev/null +++ b/firmware/esp32_ble_sim/src/services/mqtt_service.cpp @@ -0,0 +1,203 @@ +#include "services/mqtt_service.h" +#include "services/config_service.h" +#include "services/wifi_service.h" +#include "services/ble_service.h" +#include "device_state.h" +#include "config.h" +#include +#include +#include + +// MQTT client (needs WiFiClient) +static WiFiClient wifiClient; +static PubSubClient mqtt(wifiClient); + +MqttService& MqttService::getInstance() { + static MqttService instance; + return instance; +} + +void MqttService::messageCallback(char* topic, byte* payload, unsigned int length) { + getInstance().handleMessage(topic, payload, length); +} + +void MqttService::setup() { + mqtt.setCallback(messageCallback); + mqtt.setBufferSize(512); +} + +void MqttService::loop() { + connectToMQTT(); + mqtt.loop(); + + // Periodic status report + if (mqttConnected && millis() - lastStatusReport >= STATUS_REPORT_INTERVAL) { + lastStatusReport = millis(); + publishStatus(); + } +} + +void MqttService::connectToMQTT() { + if (!configService.isConfigured() || !wifiService.isConnected()) { + return; + } + + if (mqtt.connected()) { + if (!mqttConnected) { + mqttConnected = true; + deviceState.setMqttConnected(true); + } + return; + } + + // Don't spam connection attempts + if (millis() - lastMqttAttempt < MQTT_RECONNECT_INTERVAL) { + return; + } + lastMqttAttempt = millis(); + + mqttConnected = false; + deviceState.setMqttConnected(false); + + mqtt.setServer( + configService.getMqttHost().c_str(), + configService.getMqttPort() + ); + + Serial.print("Connecting to MQTT at "); + Serial.print(configService.getMqttHost()); + Serial.print(":"); + Serial.println(configService.getMqttPort()); + + String clientId = String("esp32-") + String(random(0xffff), HEX); + + // LWT for disconnect detection + String statusTopic = String("ble-sim/") + configService.getDeviceId() + "/status"; + String willMessage = "{\"online\":false}"; + + if (mqtt.connect(clientId.c_str(), statusTopic.c_str(), 1, true, willMessage.c_str())) { + mqttConnected = true; + deviceState.setMqttConnected(true); + Serial.println("MQTT connected with LWT!"); + + // Subscribe to control topics + String configTopic = String("ble-sim/") + configService.getDeviceId() + "/config"; + String setTopic = String("ble-sim/") + configService.getDeviceId() + "/set"; + + mqtt.subscribe(configTopic.c_str()); + mqtt.subscribe(setTopic.c_str()); + + Serial.print("Subscribed to: "); + Serial.println(configTopic); + + publishStatus(); + } else { + Serial.print("MQTT connection failed, rc="); + Serial.println(mqtt.state()); + } +} + +void MqttService::handleMessage(char* topic, byte* payload, unsigned int length) { + JsonDocument doc; + DeserializationError error = deserializeJson(doc, payload, length); + + if (error) { + Serial.print("JSON parse error: "); + Serial.println(error.c_str()); + return; + } + + String topicStr = String(topic); + String baseTopic = String("ble-sim/") + configService.getDeviceId(); + + // Handle device type configuration + if (topicStr == baseTopic + "/config") { + String type = doc["type"] | ""; + + Serial.print("Configuring as: "); + Serial.println(type); + + if (type == "heart_rate") { + deviceState.setDeviceType(DeviceType::HEART_RATE); + bleService.setupHeartRate(); + } else if (type == "treadmill") { + deviceState.setDeviceType(DeviceType::TREADMILL); + bleService.setupTreadmill(); + } else { + deviceState.setDeviceType(DeviceType::NONE); + bleService.stop(); + } + + publishStatus(); + } + // Handle value updates + else if (topicStr == baseTopic + "/set") { + if (doc["heart_rate"].is()) { + deviceState.setHeartRate(doc["heart_rate"]); + Serial.print("Heart rate set to: "); + Serial.println(deviceState.getValues().heartRate); + } + if (doc["battery"].is()) { + deviceState.setBatteryLevel(doc["battery"]); + bleService.updateBattery(deviceState.getValues().batteryLevel); + Serial.print("Battery level set to: "); + Serial.println(deviceState.getValues().batteryLevel); + } + if (doc["speed"].is()) { + deviceState.setTreadmillSpeed(doc["speed"]); + Serial.print("Speed set to: "); + Serial.println(doc["speed"].as()); + } + if (doc["incline"].is()) { + deviceState.setTreadmillIncline(doc["incline"]); + Serial.print("Incline set to: "); + Serial.println(doc["incline"].as()); + } + if (doc["distance"].is()) { + deviceState.setTreadmillDistance(doc["distance"]); + Serial.print("Distance set to: "); + Serial.println(deviceState.getValues().treadmillDistance); + } + + publishValues(); + } +} + +void MqttService::publishStatus() { + if (!mqtt.connected()) return; + + JsonDocument doc; + doc["online"] = true; + doc["firmware_version"] = FIRMWARE_VERSION; + doc["type"] = deviceState.getDeviceTypeString(); + doc["ble_started"] = deviceState.isBleStarted(); + doc["ip"] = wifiService.getIP(); + + String payload; + serializeJson(doc, payload); + + String topic = String("ble-sim/") + configService.getDeviceId() + "/status"; + mqtt.publish(topic.c_str(), payload.c_str(), true); +} + +void MqttService::publishValues() { + if (!mqtt.connected()) return; + + JsonDocument doc; + const auto& values = deviceState.getValues(); + + if (deviceState.getDeviceType() == DeviceType::HEART_RATE) { + doc["heart_rate"] = values.heartRate; + doc["battery"] = values.batteryLevel; + } else if (deviceState.getDeviceType() == DeviceType::TREADMILL) { + doc["speed"] = values.treadmillSpeed / 100.0; + doc["incline"] = values.treadmillIncline / 10.0; + doc["distance"] = values.treadmillDistance; + } + + String payload; + serializeJson(doc, payload); + + String topic = String("ble-sim/") + configService.getDeviceId() + "/values"; + mqtt.publish(topic.c_str(), payload.c_str()); +} diff --git a/firmware/esp32_ble_sim/src/web_portal.cpp b/firmware/esp32_ble_sim/src/services/web_service.cpp similarity index 83% rename from firmware/esp32_ble_sim/src/web_portal.cpp rename to firmware/esp32_ble_sim/src/services/web_service.cpp index 233ef30..f5e5a63 100644 --- a/firmware/esp32_ble_sim/src/web_portal.cpp +++ b/firmware/esp32_ble_sim/src/services/web_service.cpp @@ -1,12 +1,11 @@ -#include "web_portal.h" -#include "config_manager.h" +#include "services/web_service.h" +#include "services/config_service.h" +#include "services/wifi_service.h" +#include "device_state.h" #include #include -WebServer server(80); -PortalStatus currentStatus = {}; -ResetDistanceCallback resetDistanceCallback = nullptr; -SetBatteryCallback setBatteryCallback = nullptr; +static WebServer server(80); // HTML template with embedded CSS and JS const char INDEX_HTML[] PROGMEM = R"rawliteral( @@ -108,7 +107,7 @@ const char INDEX_HTML[] PROGMEM = R"rawliteral(

BLE Simulator

-

Loading... • UI v1.1.0

+

Loading... • UI v1.2.0

Status

@@ -178,7 +177,6 @@ const char INDEX_HTML[] PROGMEM = R"rawliteral(