mirror of
https://github.com/mattintech/pyBTMCP.git
synced 2026-07-11 13:11:53 +00:00
Initial release v1.0.0 - BLE Device Simulator with MCP Integration
Features: - MCP server with tools for AI-controlled device simulation - FastAPI backend with REST API and WebSocket support - Real-time device updates via WebSocket - Web UI for manual device control - ESP32 firmware for BLE device simulation - Docker containerization with MQTT broker Supported device types: - Heart Rate Monitor (BLE Heart Rate Service) - Treadmill (BLE Fitness Machine Service) - Bike Trainer (BLE Cycling Power Service) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
5
firmware/esp32_ble_sim/.gitignore
vendored
Normal file
5
firmware/esp32_ble_sim/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.pio
|
||||
.vscode/.browse.c_cpp.db*
|
||||
.vscode/c_cpp_properties.json
|
||||
.vscode/launch.json
|
||||
.vscode/ipch
|
||||
59
firmware/esp32_ble_sim/include/ble_services.h
Normal file
59
firmware/esp32_ble_sim/include/ble_services.h
Normal file
@@ -0,0 +1,59 @@
|
||||
#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"
|
||||
|
||||
// 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);
|
||||
|
||||
/**
|
||||
* Send treadmill data notification
|
||||
* @param speed Speed in 0.01 km/h units
|
||||
* @param incline Incline in 0.1% units
|
||||
*/
|
||||
void notifyTreadmill(uint16_t speed, int16_t incline);
|
||||
|
||||
#endif // BLE_SERVICES_H
|
||||
37
firmware/esp32_ble_sim/include/config.h
Normal file
37
firmware/esp32_ble_sim/include/config.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#ifndef CONFIG_H
|
||||
#define CONFIG_H
|
||||
|
||||
// ============================================
|
||||
// Firmware Version
|
||||
// ============================================
|
||||
#define FIRMWARE_VERSION "1.0.0"
|
||||
|
||||
// ============================================
|
||||
// AP Mode Configuration
|
||||
// ============================================
|
||||
#define AP_SSID_PREFIX "BLE-Sim-" // Will append chip ID for uniqueness
|
||||
#define AP_PASSWORD "" // Open network (or set a password)
|
||||
#define AP_IP IPAddress(192, 168, 4, 1)
|
||||
#define AP_GATEWAY IPAddress(192, 168, 4, 1)
|
||||
#define AP_SUBNET IPAddress(255, 255, 255, 0)
|
||||
|
||||
// ============================================
|
||||
// Default Values (used until configured)
|
||||
// ============================================
|
||||
#define DEFAULT_MQTT_PORT 1883
|
||||
#define DEFAULT_DEVICE_ID_PREFIX "esp32-"
|
||||
|
||||
// ============================================
|
||||
// Timing Configuration
|
||||
// ============================================
|
||||
#define BLE_NOTIFY_INTERVAL 1000 // BLE notification interval (ms)
|
||||
#define MQTT_RECONNECT_INTERVAL 5000 // MQTT reconnect attempt interval (ms)
|
||||
#define STATUS_REPORT_INTERVAL 10000 // Status report to MQTT (ms)
|
||||
#define WIFI_CONNECT_TIMEOUT 15000 // WiFi connection timeout (ms)
|
||||
|
||||
// ============================================
|
||||
// NVS Configuration Keys
|
||||
// ============================================
|
||||
#define NVS_NAMESPACE "ble-sim"
|
||||
|
||||
#endif // CONFIG_H
|
||||
64
firmware/esp32_ble_sim/include/config_manager.h
Normal file
64
firmware/esp32_ble_sim/include/config_manager.h
Normal file
@@ -0,0 +1,64 @@
|
||||
#ifndef CONFIG_MANAGER_H
|
||||
#define CONFIG_MANAGER_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Preferences.h>
|
||||
|
||||
// ============================================
|
||||
// Configuration Structure
|
||||
// ============================================
|
||||
struct DeviceConfig {
|
||||
bool configured = false;
|
||||
String wifiSsid = "";
|
||||
String wifiPassword = "";
|
||||
String mqttHost = "";
|
||||
uint16_t mqttPort = 1883;
|
||||
String deviceId = "";
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// Configuration Manager Class
|
||||
// ============================================
|
||||
class ConfigManager {
|
||||
public:
|
||||
ConfigManager();
|
||||
|
||||
// Load config from NVS
|
||||
bool load();
|
||||
|
||||
// Save config to NVS
|
||||
void save();
|
||||
|
||||
// Clear all config
|
||||
void clear();
|
||||
|
||||
// Check if configured
|
||||
bool isConfigured() const { return config.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; }
|
||||
|
||||
// Setters
|
||||
void setWifiCredentials(const String& ssid, const String& password);
|
||||
void setMqttConfig(const String& host, uint16_t port);
|
||||
void setDeviceId(const String& id);
|
||||
|
||||
// Get unique AP name based on chip ID
|
||||
String getAPName() const;
|
||||
|
||||
// Get unique default device ID based on chip ID
|
||||
String getDefaultDeviceId() const;
|
||||
|
||||
private:
|
||||
DeviceConfig config;
|
||||
Preferences preferences;
|
||||
};
|
||||
|
||||
// Global instance
|
||||
extern ConfigManager configManager;
|
||||
|
||||
#endif // CONFIG_MANAGER_H
|
||||
30
firmware/esp32_ble_sim/include/web_portal.h
Normal file
30
firmware/esp32_ble_sim/include/web_portal.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#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;
|
||||
};
|
||||
|
||||
void updatePortalStatus(const PortalStatus& status);
|
||||
|
||||
#endif // WEB_PORTAL_H
|
||||
27
firmware/esp32_ble_sim/platformio.ini
Normal file
27
firmware/esp32_ble_sim/platformio.ini
Normal file
@@ -0,0 +1,27 @@
|
||||
; PlatformIO Project Configuration File
|
||||
;
|
||||
; BLE Device Simulator for ESP32
|
||||
; Supports: Heart Rate Monitor, Treadmill (Fitness Machine)
|
||||
|
||||
[env:esp32]
|
||||
platform = espressif32
|
||||
board = esp32dev
|
||||
framework = arduino
|
||||
monitor_speed = 115200
|
||||
|
||||
; Use NimBLE for smaller footprint and better BLE support
|
||||
lib_deps =
|
||||
h2zero/NimBLE-Arduino@^1.4.0
|
||||
knolleary/PubSubClient@^2.8
|
||||
bblanchon/ArduinoJson@^7.0.0
|
||||
|
||||
; Build flags
|
||||
build_flags =
|
||||
-D CONFIG_BT_NIMBLE_ROLE_PERIPHERAL=1
|
||||
-D CONFIG_BT_NIMBLE_ROLE_CENTRAL=0
|
||||
-D CONFIG_BT_NIMBLE_ROLE_OBSERVER=0
|
||||
-D CONFIG_BT_NIMBLE_ROLE_BROADCASTER=1
|
||||
|
||||
; Upload settings (adjust port as needed)
|
||||
; upload_port = /dev/cu.usbserial-*
|
||||
; monitor_port = /dev/cu.usbserial-*
|
||||
241
firmware/esp32_ble_sim/src/ble_services.cpp
Normal file
241
firmware/esp32_ble_sim/src/ble_services.cpp
Normal file
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* BLE Services Implementation
|
||||
*
|
||||
* Implements standard Bluetooth SIG GATT services:
|
||||
* - Heart Rate Service (0x180D)
|
||||
* - Fitness Machine Service (0x1826) - Treadmill
|
||||
*/
|
||||
|
||||
#include "ble_services.h"
|
||||
#include "config.h"
|
||||
#include "config_manager.h"
|
||||
#include <NimBLEDevice.h>
|
||||
|
||||
// ============================================
|
||||
// BLE Server and Characteristics
|
||||
// ============================================
|
||||
static NimBLEServer* pServer = nullptr;
|
||||
static NimBLEAdvertising* pAdvertising = nullptr;
|
||||
|
||||
// Heart Rate
|
||||
static NimBLECharacteristic* pHeartRateMeasurement = nullptr;
|
||||
|
||||
// Treadmill
|
||||
static NimBLECharacteristic* pTreadmillData = nullptr;
|
||||
|
||||
static bool deviceConnected = false;
|
||||
static bool oldDeviceConnected = false;
|
||||
|
||||
// ============================================
|
||||
// Server Callbacks
|
||||
// ============================================
|
||||
class ServerCallbacks : public NimBLEServerCallbacks {
|
||||
void onConnect(NimBLEServer* pServer) override {
|
||||
deviceConnected = true;
|
||||
Serial.println("BLE client connected");
|
||||
}
|
||||
|
||||
void onDisconnect(NimBLEServer* pServer) override {
|
||||
deviceConnected = false;
|
||||
Serial.println("BLE client disconnected");
|
||||
|
||||
// Restart advertising
|
||||
NimBLEDevice::startAdvertising();
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// BLE Initialization
|
||||
// ============================================
|
||||
void initBLE() {
|
||||
NimBLEDevice::init("BLE Simulator");
|
||||
NimBLEDevice::setPower(ESP_PWR_LVL_P9);
|
||||
|
||||
pServer = NimBLEDevice::createServer();
|
||||
pServer->setCallbacks(new ServerCallbacks());
|
||||
|
||||
pAdvertising = NimBLEDevice::getAdvertising();
|
||||
|
||||
Serial.println("BLE initialized");
|
||||
}
|
||||
|
||||
void stopBLE() {
|
||||
if (pAdvertising) {
|
||||
pAdvertising->stop();
|
||||
}
|
||||
|
||||
// Clear services (will be recreated on next setup)
|
||||
pHeartRateMeasurement = nullptr;
|
||||
pTreadmillData = nullptr;
|
||||
|
||||
Serial.println("BLE stopped");
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Heart Rate Service Setup
|
||||
// ============================================
|
||||
void setupBLE_HeartRate() {
|
||||
Serial.println("Setting up Heart Rate Service...");
|
||||
|
||||
// Stop any current advertising
|
||||
if (pAdvertising) {
|
||||
pAdvertising->stop();
|
||||
}
|
||||
|
||||
// Create Heart Rate Service
|
||||
NimBLEService* pService = pServer->createService(HEART_RATE_SERVICE_UUID);
|
||||
|
||||
// Heart Rate Measurement Characteristic
|
||||
// Flags: Notify
|
||||
pHeartRateMeasurement = pService->createCharacteristic(
|
||||
HEART_RATE_MEASUREMENT_UUID,
|
||||
NIMBLE_PROPERTY::NOTIFY
|
||||
);
|
||||
|
||||
// Body Sensor Location Characteristic
|
||||
// Flags: Read
|
||||
// Value: 1 = Chest
|
||||
NimBLECharacteristic* pBodySensorLocation = pService->createCharacteristic(
|
||||
BODY_SENSOR_LOCATION_UUID,
|
||||
NIMBLE_PROPERTY::READ
|
||||
);
|
||||
uint8_t sensorLocation = 1; // Chest
|
||||
pBodySensorLocation->setValue(&sensorLocation, 1);
|
||||
|
||||
// Start the service
|
||||
pService->start();
|
||||
|
||||
// Configure advertising
|
||||
pAdvertising->addServiceUUID(HEART_RATE_SERVICE_UUID);
|
||||
pAdvertising->setScanResponse(true);
|
||||
pAdvertising->setMinPreferred(0x06);
|
||||
pAdvertising->setMaxPreferred(0x12);
|
||||
|
||||
// Update device name
|
||||
NimBLEDevice::setDeviceName("HR Simulator");
|
||||
|
||||
// Start advertising
|
||||
NimBLEDevice::startAdvertising();
|
||||
|
||||
Serial.println("Heart Rate Service started, advertising...");
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Treadmill (Fitness Machine) Service Setup
|
||||
// ============================================
|
||||
void setupBLE_Treadmill() {
|
||||
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)
|
||||
};
|
||||
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
|
||||
// ============================================
|
||||
void notifyHeartRate(uint8_t bpm) {
|
||||
if (!pHeartRateMeasurement || !deviceConnected) 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[1] = bpm;
|
||||
|
||||
pHeartRateMeasurement->setValue(hrData, 2);
|
||||
pHeartRateMeasurement->notify();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Treadmill Data Notification
|
||||
// ============================================
|
||||
void notifyTreadmill(uint16_t speed, int16_t incline) {
|
||||
if (!pTreadmillData || !deviceConnected) 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
|
||||
|
||||
// We'll include: Instantaneous Speed + Inclination + Ramp Angle
|
||||
uint8_t data[8];
|
||||
|
||||
// Flags: Inclination and Ramp Angle present (bit 3)
|
||||
uint16_t flags = 0x0008;
|
||||
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;
|
||||
|
||||
// Inclination (sint16, 0.1% resolution)
|
||||
data[4] = incline & 0xFF;
|
||||
data[5] = (incline >> 8) & 0xFF;
|
||||
|
||||
// Ramp Angle Setting (sint16, 0.1 degree resolution) - set to 0
|
||||
int16_t rampAngle = 0;
|
||||
data[6] = rampAngle & 0xFF;
|
||||
data[7] = (rampAngle >> 8) & 0xFF;
|
||||
|
||||
pTreadmillData->setValue(data, 8);
|
||||
pTreadmillData->notify();
|
||||
}
|
||||
79
firmware/esp32_ble_sim/src/config_manager.cpp
Normal file
79
firmware/esp32_ble_sim/src/config_manager.cpp
Normal file
@@ -0,0 +1,79 @@
|
||||
#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);
|
||||
}
|
||||
402
firmware/esp32_ble_sim/src/main.cpp
Normal file
402
firmware/esp32_ble_sim/src/main.cpp
Normal file
@@ -0,0 +1,402 @@
|
||||
/**
|
||||
* ESP32 BLE Device Simulator
|
||||
*
|
||||
* 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)
|
||||
*/
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <WiFi.h>
|
||||
#include <PubSubClient.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <NimBLEDevice.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "config_manager.h"
|
||||
#include "web_portal.h"
|
||||
#include "ble_services.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;
|
||||
uint16_t treadmillSpeed = 0; // 0.01 km/h resolution
|
||||
int16_t treadmillIncline = 0; // 0.1% resolution
|
||||
|
||||
// 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());
|
||||
}
|
||||
|
||||
void publishValues() {
|
||||
if (!mqtt.connected()) return;
|
||||
|
||||
JsonDocument doc;
|
||||
|
||||
if (currentDeviceType == DEVICE_HEART_RATE) {
|
||||
doc["heart_rate"] = heartRate;
|
||||
} else if (currentDeviceType == DEVICE_TREADMILL) {
|
||||
doc["speed"] = treadmillSpeed / 100.0;
|
||||
doc["incline"] = treadmillIncline / 10.0;
|
||||
}
|
||||
|
||||
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<int>()) {
|
||||
heartRate = doc["heart_rate"];
|
||||
Serial.print("Heart rate set to: ");
|
||||
Serial.println(heartRate);
|
||||
}
|
||||
if (doc["speed"].is<float>()) {
|
||||
float speed = doc["speed"];
|
||||
treadmillSpeed = (uint16_t)(speed * 100);
|
||||
Serial.print("Speed set to: ");
|
||||
Serial.println(speed);
|
||||
}
|
||||
if (doc["incline"].is<float>()) {
|
||||
float incline = doc["incline"];
|
||||
treadmillIncline = (int16_t)(incline * 10);
|
||||
Serial.print("Incline set to: ");
|
||||
Serial.println(incline);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (mqtt.connect(clientId.c_str())) {
|
||||
mqttConnected = true;
|
||||
Serial.println("MQTT connected!");
|
||||
|
||||
// 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
|
||||
publishStatus();
|
||||
} else {
|
||||
Serial.print("MQTT connection failed, rc=");
|
||||
Serial.println(mqtt.state());
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Update Portal Status
|
||||
// ============================================
|
||||
void updateStatus() {
|
||||
PortalStatus status;
|
||||
status.wifiConnected = wifiConnected;
|
||||
status.mqttConnected = mqttConnected;
|
||||
status.bleStarted = bleStarted;
|
||||
status.ipAddress = wifiConnected ? WiFi.localIP().toString() : "";
|
||||
|
||||
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);
|
||||
|
||||
Serial.println("\n========================================");
|
||||
Serial.println(" ESP32 BLE Device Simulator");
|
||||
Serial.print(" Firmware: v");
|
||||
Serial.println(FIRMWARE_VERSION);
|
||||
Serial.println("========================================\n");
|
||||
|
||||
// Load configuration from NVS
|
||||
bool hasConfig = configManager.load();
|
||||
Serial.print("Device ID: ");
|
||||
Serial.println(configManager.getDeviceId());
|
||||
Serial.print("Configured: ");
|
||||
Serial.println(hasConfig ? "Yes" : "No");
|
||||
|
||||
// Start WiFi (AP always on, STA if configured)
|
||||
setupWiFi();
|
||||
|
||||
// Start web configuration portal
|
||||
setupWebPortal();
|
||||
|
||||
// Initialize MQTT
|
||||
setupMQTT();
|
||||
|
||||
// Initialize BLE (don't start advertising yet)
|
||||
initBLE();
|
||||
|
||||
Serial.println("\nReady!");
|
||||
if (!configManager.isConfigured()) {
|
||||
Serial.println("Configure at: http://192.168.4.1");
|
||||
} else {
|
||||
Serial.println("Connecting to WiFi...");
|
||||
}
|
||||
Serial.println("Waiting for MQTT commands...\n");
|
||||
}
|
||||
|
||||
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) {
|
||||
notifyTreadmill(treadmillSpeed, treadmillIncline);
|
||||
}
|
||||
}
|
||||
|
||||
// Periodic status report to MQTT
|
||||
if (mqttConnected && millis() - lastStatus >= STATUS_REPORT_INTERVAL) {
|
||||
lastStatus = millis();
|
||||
publishStatus();
|
||||
}
|
||||
}
|
||||
318
firmware/esp32_ble_sim/src/web_portal.cpp
Normal file
318
firmware/esp32_ble_sim/src/web_portal.cpp
Normal file
@@ -0,0 +1,318 @@
|
||||
#include "web_portal.h"
|
||||
#include "config_manager.h"
|
||||
#include <WebServer.h>
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
WebServer server(80);
|
||||
PortalStatus currentStatus = {};
|
||||
|
||||
// HTML template with embedded CSS and JS
|
||||
const char INDEX_HTML[] PROGMEM = R"rawliteral(
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>BLE Simulator Setup</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: -apple-system, sans-serif;
|
||||
background: #1a1a2e;
|
||||
color: #e4e4e4;
|
||||
padding: 20px;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
h1 { margin-bottom: 10px; }
|
||||
.subtitle { color: #888; margin-bottom: 20px; }
|
||||
.card {
|
||||
background: #16213e;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.card h2 {
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.status-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #0f3460;
|
||||
}
|
||||
.status-row:last-child { border: none; }
|
||||
.status-dot {
|
||||
width: 10px; height: 10px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.online { background: #4ade80; }
|
||||
.offline { background: #f87171; }
|
||||
label { display: block; margin-bottom: 5px; color: #888; font-size: 14px; }
|
||||
input, select {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
margin-bottom: 15px;
|
||||
background: #0f3460;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
color: #e4e4e4;
|
||||
font-size: 16px;
|
||||
}
|
||||
input:focus { outline: 2px solid #4ade80; }
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
background: #4ade80;
|
||||
color: #1a1a2e;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover { background: #22c55e; }
|
||||
.btn-danger { background: #f87171; }
|
||||
.btn-danger:hover { background: #ef4444; }
|
||||
.msg { padding: 10px; border-radius: 8px; margin-bottom: 15px; }
|
||||
.msg-success { background: #064e3b; }
|
||||
.msg-error { background: #7f1d1d; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>BLE Simulator</h1>
|
||||
<p class="subtitle" id="apName">Loading...</p>
|
||||
|
||||
<div class="card">
|
||||
<h2>Status</h2>
|
||||
<div class="status-row">
|
||||
<span>WiFi</span>
|
||||
<span><span class="status-dot" id="wifiDot"></span><span id="wifiStatus">-</span></span>
|
||||
</div>
|
||||
<div class="status-row">
|
||||
<span>MQTT</span>
|
||||
<span><span class="status-dot" id="mqttDot"></span><span id="mqttStatus">-</span></span>
|
||||
</div>
|
||||
<div class="status-row">
|
||||
<span>BLE</span>
|
||||
<span><span class="status-dot" id="bleDot"></span><span id="bleStatus">-</span></span>
|
||||
</div>
|
||||
<div class="status-row">
|
||||
<span>IP Address</span>
|
||||
<span id="ipAddr">-</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>WiFi Configuration</h2>
|
||||
<div id="message"></div>
|
||||
<form id="configForm">
|
||||
<label>WiFi Network Name (SSID)</label>
|
||||
<input type="text" name="ssid" id="ssid" required>
|
||||
|
||||
<label>WiFi Password</label>
|
||||
<input type="password" name="password" id="password">
|
||||
|
||||
<label>MQTT Server IP</label>
|
||||
<input type="text" name="mqtt_host" id="mqtt_host" placeholder="192.168.1.100" required>
|
||||
|
||||
<label>MQTT Port</label>
|
||||
<input type="number" name="mqtt_port" id="mqtt_port" value="1883">
|
||||
|
||||
<label>Device ID</label>
|
||||
<input type="text" name="device_id" id="device_id" placeholder="esp32-01">
|
||||
|
||||
<button type="submit">Save & Connect</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<button class="btn-danger" onclick="resetConfig()">Reset Configuration</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let configLoaded = false;
|
||||
|
||||
// Load config once on page load
|
||||
async function loadConfig() {
|
||||
try {
|
||||
const res = await fetch('/api/status');
|
||||
const data = await res.json();
|
||||
|
||||
document.getElementById('apName').textContent = data.apName;
|
||||
document.getElementById('ssid').value = data.config.ssid || '';
|
||||
document.getElementById('mqtt_host').value = data.config.mqttHost || '';
|
||||
document.getElementById('mqtt_port').value = data.config.mqttPort || 1883;
|
||||
document.getElementById('device_id').value = data.config.deviceId || '';
|
||||
|
||||
updateStatusDots(data.status);
|
||||
configLoaded = true;
|
||||
} catch (e) {
|
||||
console.error('Failed to load config:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Update only status indicators (not form fields)
|
||||
async function updateStatus() {
|
||||
try {
|
||||
const res = await fetch('/api/status');
|
||||
const data = await res.json();
|
||||
updateStatusDots(data.status);
|
||||
} catch (e) {
|
||||
console.error('Failed to update status:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function updateStatusDots(status) {
|
||||
document.getElementById('wifiDot').className = 'status-dot ' + (status.wifiConnected ? 'online' : 'offline');
|
||||
document.getElementById('wifiStatus').textContent = status.wifiConnected ? 'Connected' : 'Disconnected';
|
||||
|
||||
document.getElementById('mqttDot').className = 'status-dot ' + (status.mqttConnected ? 'online' : 'offline');
|
||||
document.getElementById('mqttStatus').textContent = status.mqttConnected ? 'Connected' : 'Disconnected';
|
||||
|
||||
document.getElementById('bleDot').className = 'status-dot ' + (status.bleStarted ? 'online' : 'offline');
|
||||
document.getElementById('bleStatus').textContent = status.bleStarted ? status.deviceType : 'Not started';
|
||||
|
||||
document.getElementById('ipAddr').textContent = status.ipAddress || '-';
|
||||
}
|
||||
|
||||
document.getElementById('configForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
const data = {
|
||||
ssid: form.ssid.value,
|
||||
password: form.password.value,
|
||||
mqtt_host: form.mqtt_host.value,
|
||||
mqtt_port: parseInt(form.mqtt_port.value),
|
||||
device_id: form.device_id.value
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
const result = await res.json();
|
||||
|
||||
document.getElementById('message').innerHTML =
|
||||
'<div class="msg msg-success">Configuration saved! Reconnecting...</div>';
|
||||
|
||||
setTimeout(updateStatus, 3000);
|
||||
} catch (e) {
|
||||
document.getElementById('message').innerHTML =
|
||||
'<div class="msg msg-error">Failed to save configuration</div>';
|
||||
}
|
||||
});
|
||||
|
||||
async function resetConfig() {
|
||||
if (!confirm('Reset all configuration?')) return;
|
||||
|
||||
try {
|
||||
await fetch('/api/reset', { method: 'POST' });
|
||||
document.getElementById('message').innerHTML =
|
||||
'<div class="msg msg-success">Configuration reset! Rebooting...</div>';
|
||||
setTimeout(() => location.reload(), 3000);
|
||||
} catch (e) {
|
||||
document.getElementById('message').innerHTML =
|
||||
'<div class="msg msg-error">Failed to reset</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Load config once, then only update status
|
||||
loadConfig();
|
||||
setInterval(updateStatus, 5000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
)rawliteral";
|
||||
|
||||
void handleRoot() {
|
||||
server.send(200, "text/html", INDEX_HTML);
|
||||
}
|
||||
|
||||
void handleGetStatus() {
|
||||
JsonDocument doc;
|
||||
|
||||
doc["apName"] = configManager.getAPName();
|
||||
|
||||
JsonObject config = doc["config"].to<JsonObject>();
|
||||
config["ssid"] = configManager.getWifiSsid();
|
||||
config["mqttHost"] = configManager.getMqttHost();
|
||||
config["mqttPort"] = configManager.getMqttPort();
|
||||
config["deviceId"] = configManager.getDeviceId();
|
||||
|
||||
JsonObject status = doc["status"].to<JsonObject>();
|
||||
status["wifiConnected"] = currentStatus.wifiConnected;
|
||||
status["mqttConnected"] = currentStatus.mqttConnected;
|
||||
status["bleStarted"] = currentStatus.bleStarted;
|
||||
status["deviceType"] = currentStatus.deviceType;
|
||||
status["ipAddress"] = currentStatus.ipAddress;
|
||||
|
||||
String response;
|
||||
serializeJson(doc, response);
|
||||
server.send(200, "application/json", response);
|
||||
}
|
||||
|
||||
void handlePostConfig() {
|
||||
if (!server.hasArg("plain")) {
|
||||
server.send(400, "application/json", "{\"error\":\"No body\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
JsonDocument doc;
|
||||
DeserializationError error = deserializeJson(doc, server.arg("plain"));
|
||||
|
||||
if (error) {
|
||||
server.send(400, "application/json", "{\"error\":\"Invalid JSON\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Update configuration
|
||||
String ssid = doc["ssid"] | "";
|
||||
String password = doc["password"] | "";
|
||||
String mqttHost = doc["mqtt_host"] | "";
|
||||
uint16_t mqttPort = doc["mqtt_port"] | 1883;
|
||||
String deviceId = doc["device_id"] | "";
|
||||
|
||||
configManager.setWifiCredentials(ssid, password);
|
||||
configManager.setMqttConfig(mqttHost, mqttPort);
|
||||
configManager.setDeviceId(deviceId);
|
||||
configManager.save();
|
||||
|
||||
server.send(200, "application/json", "{\"success\":true}");
|
||||
|
||||
// Trigger reconnect (will be handled in main loop)
|
||||
Serial.println("Configuration updated, reconnecting...");
|
||||
}
|
||||
|
||||
void handleReset() {
|
||||
configManager.clear();
|
||||
server.send(200, "application/json", "{\"success\":true}");
|
||||
|
||||
Serial.println("Configuration reset, rebooting...");
|
||||
delay(1000);
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
void setupWebPortal() {
|
||||
server.on("/", HTTP_GET, handleRoot);
|
||||
server.on("/api/status", HTTP_GET, handleGetStatus);
|
||||
server.on("/api/config", HTTP_POST, handlePostConfig);
|
||||
server.on("/api/reset", HTTP_POST, handleReset);
|
||||
|
||||
server.begin();
|
||||
Serial.println("Web portal started on http://192.168.4.1");
|
||||
}
|
||||
|
||||
void handleWebPortal() {
|
||||
server.handleClient();
|
||||
}
|
||||
|
||||
void updatePortalStatus(const PortalStatus& status) {
|
||||
currentStatus = status;
|
||||
}
|
||||
Reference in New Issue
Block a user