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 <noreply@anthropic.com>
This commit is contained in:
2026-01-19 16:49:54 -05:00
parent 9b740ebdd0
commit c95cd33343
22 changed files with 1402 additions and 956 deletions

View File

@@ -1,70 +0,0 @@
#ifndef BLE_SERVICES_H
#define BLE_SERVICES_H
#include <Arduino.h>
// ============================================
// 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

View File

@@ -4,7 +4,7 @@
// ============================================
// Firmware Version
// ============================================
#define FIRMWARE_VERSION "1.0.5"
#define FIRMWARE_VERSION "1.0.6"
// ============================================
// AP Mode Configuration

View File

@@ -0,0 +1,105 @@
#ifndef DEVICE_STATE_H
#define DEVICE_STATE_H
#include <Arduino.h>
#include <functional>
// ============================================
// 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<void(DeviceType newType)>;
using ValuesChangedCallback = std::function<void(const SimulatedValues& values)>;
using ConnectionChangedCallback = std::function<void(const ConnectionState& state)>;
// ============================================
// 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

View File

@@ -0,0 +1,58 @@
#ifndef BLE_SERVICE_H
#define BLE_SERVICE_H
#include <Arduino.h>
// ============================================
// 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

View File

@@ -1,27 +1,20 @@
#ifndef CONFIG_MANAGER_H
#define CONFIG_MANAGER_H
#ifndef CONFIG_SERVICE_H
#define CONFIG_SERVICE_H
#include <Arduino.h>
#include <Preferences.h>
// ============================================
// 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

View File

@@ -0,0 +1,45 @@
#ifndef MQTT_SERVICE_H
#define MQTT_SERVICE_H
#include <Arduino.h>
// ============================================
// 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

View File

@@ -0,0 +1,35 @@
#ifndef WEB_SERVICE_H
#define WEB_SERVICE_H
#include <Arduino.h>
// ============================================
// 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

View File

@@ -0,0 +1,49 @@
#ifndef WIFI_SERVICE_H
#define WIFI_SERVICE_H
#include <Arduino.h>
// ============================================
// 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

View File

@@ -1,40 +0,0 @@
#ifndef WEB_PORTAL_H
#define WEB_PORTAL_H
#include <Arduino.h>
/**
* 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