mirror of
https://github.com/mattintech/pyBTMCP.git
synced 2026-07-11 12:01:53 +00:00
v1.1.2: Smooth HR variation, validation, and UI improvements
- Fix HR variation to use smooth sinusoidal algorithm instead of erratic jumps - Clamp HR values to ±3 of target (was exceeding bounds) - Add Pydantic Field validation for device values in API - Add runtime BPM validation in MCP server - Add battery slider control for HR monitor - Add distance display and reset button for treadmill - Add UI/API version display in header - Firmware: web portal improvements, battery/distance controls Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,10 @@
|
||||
#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"
|
||||
@@ -49,11 +53,18 @@ void setupBLE_Treadmill();
|
||||
*/
|
||||
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);
|
||||
void notifyTreadmill(uint16_t speed, int16_t incline, uint32_t distance);
|
||||
|
||||
#endif // BLE_SERVICES_H
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// ============================================
|
||||
// Firmware Version
|
||||
// ============================================
|
||||
#define FIRMWARE_VERSION "1.0.0"
|
||||
#define FIRMWARE_VERSION "1.0.5"
|
||||
|
||||
// ============================================
|
||||
// AP Mode Configuration
|
||||
|
||||
@@ -23,8 +23,18 @@ struct PortalStatus {
|
||||
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
|
||||
|
||||
@@ -20,6 +20,9 @@ static NimBLEAdvertising* pAdvertising = nullptr;
|
||||
// Heart Rate
|
||||
static NimBLECharacteristic* pHeartRateMeasurement = nullptr;
|
||||
|
||||
// Battery
|
||||
static NimBLECharacteristic* pBatteryLevel = nullptr;
|
||||
|
||||
// Treadmill
|
||||
static NimBLECharacteristic* pTreadmillData = nullptr;
|
||||
|
||||
@@ -66,6 +69,7 @@ void stopBLE() {
|
||||
|
||||
// Clear services (will be recreated on next setup)
|
||||
pHeartRateMeasurement = nullptr;
|
||||
pBatteryLevel = nullptr;
|
||||
pTreadmillData = nullptr;
|
||||
|
||||
Serial.println("BLE stopped");
|
||||
@@ -83,11 +87,11 @@ void setupBLE_HeartRate() {
|
||||
}
|
||||
|
||||
// Create Heart Rate Service
|
||||
NimBLEService* pService = pServer->createService(HEART_RATE_SERVICE_UUID);
|
||||
NimBLEService* pHRService = pServer->createService(HEART_RATE_SERVICE_UUID);
|
||||
|
||||
// Heart Rate Measurement Characteristic
|
||||
// Flags: Notify
|
||||
pHeartRateMeasurement = pService->createCharacteristic(
|
||||
pHeartRateMeasurement = pHRService->createCharacteristic(
|
||||
HEART_RATE_MEASUREMENT_UUID,
|
||||
NIMBLE_PROPERTY::NOTIFY
|
||||
);
|
||||
@@ -95,18 +99,34 @@ void setupBLE_HeartRate() {
|
||||
// Body Sensor Location Characteristic
|
||||
// Flags: Read
|
||||
// Value: 1 = Chest
|
||||
NimBLECharacteristic* pBodySensorLocation = pService->createCharacteristic(
|
||||
NimBLECharacteristic* pBodySensorLocation = pHRService->createCharacteristic(
|
||||
BODY_SENSOR_LOCATION_UUID,
|
||||
NIMBLE_PROPERTY::READ
|
||||
);
|
||||
uint8_t sensorLocation = 1; // Chest
|
||||
pBodySensorLocation->setValue(&sensorLocation, 1);
|
||||
|
||||
// Start the service
|
||||
pService->start();
|
||||
// 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;
|
||||
pBatteryLevel->setValue(&initialBattery, 1);
|
||||
|
||||
// Start Battery service
|
||||
pBatteryService->start();
|
||||
|
||||
// Configure advertising
|
||||
pAdvertising->addServiceUUID(HEART_RATE_SERVICE_UUID);
|
||||
pAdvertising->addServiceUUID(BATTERY_SERVICE_UUID);
|
||||
pAdvertising->setScanResponse(true);
|
||||
pAdvertising->setMinPreferred(0x06);
|
||||
pAdvertising->setMaxPreferred(0x12);
|
||||
@@ -117,7 +137,7 @@ void setupBLE_HeartRate() {
|
||||
// Start advertising
|
||||
NimBLEDevice::startAdvertising();
|
||||
|
||||
Serial.println("Heart Rate Service started, advertising...");
|
||||
Serial.println("Heart Rate + Battery Services started, advertising...");
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -201,10 +221,23 @@ void notifyHeartRate(uint8_t bpm) {
|
||||
pHeartRateMeasurement->notify();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Battery Level Update
|
||||
// ============================================
|
||||
void updateBatteryLevel(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) {
|
||||
void notifyTreadmill(uint16_t speed, int16_t incline, uint32_t distance) {
|
||||
if (!pTreadmillData || !deviceConnected) return;
|
||||
|
||||
// Treadmill Data format (per Bluetooth FTMS spec):
|
||||
@@ -213,13 +246,13 @@ void notifyTreadmill(uint16_t speed, int16_t incline) {
|
||||
// Bit 1: Average Speed present
|
||||
// Bit 2: Total Distance present
|
||||
// Bit 3: Inclination and Ramp Angle present
|
||||
// Following bytes: Data fields based on flags
|
||||
// Following bytes: Data fields based on flags (in order of flag bits)
|
||||
|
||||
// We'll include: Instantaneous Speed + Inclination + Ramp Angle
|
||||
uint8_t data[8];
|
||||
// We'll include: Instantaneous Speed + Total Distance + Inclination + Ramp Angle
|
||||
uint8_t data[11];
|
||||
|
||||
// Flags: Inclination and Ramp Angle present (bit 3)
|
||||
uint16_t flags = 0x0008;
|
||||
// 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;
|
||||
|
||||
@@ -227,15 +260,20 @@ void notifyTreadmill(uint16_t speed, int16_t incline) {
|
||||
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[4] = incline & 0xFF;
|
||||
data[5] = (incline >> 8) & 0xFF;
|
||||
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[6] = rampAngle & 0xFF;
|
||||
data[7] = (rampAngle >> 8) & 0xFF;
|
||||
data[9] = rampAngle & 0xFF;
|
||||
data[10] = (rampAngle >> 8) & 0xFF;
|
||||
|
||||
pTreadmillData->setValue(data, 8);
|
||||
pTreadmillData->setValue(data, 11);
|
||||
pTreadmillData->notify();
|
||||
}
|
||||
|
||||
@@ -42,8 +42,11 @@ 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;
|
||||
@@ -165,7 +168,7 @@ void publishStatus() {
|
||||
serializeJson(doc, payload);
|
||||
|
||||
String topic = String("ble-sim/") + configManager.getDeviceId() + "/status";
|
||||
mqtt.publish(topic.c_str(), payload.c_str());
|
||||
mqtt.publish(topic.c_str(), payload.c_str(), true); // retained
|
||||
}
|
||||
|
||||
void publishValues() {
|
||||
@@ -175,9 +178,11 @@ void publishValues() {
|
||||
|
||||
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;
|
||||
@@ -230,6 +235,13 @@ void handleMqttMessage(char* topic, byte* payload, unsigned int length) {
|
||||
Serial.print("Heart rate set to: ");
|
||||
Serial.println(heartRate);
|
||||
}
|
||||
if (doc["battery"].is<int>()) {
|
||||
batteryLevel = doc["battery"];
|
||||
if (batteryLevel > 100) batteryLevel = 100;
|
||||
updateBatteryLevel(batteryLevel);
|
||||
Serial.print("Battery level set to: ");
|
||||
Serial.println(batteryLevel);
|
||||
}
|
||||
if (doc["speed"].is<float>()) {
|
||||
float speed = doc["speed"];
|
||||
treadmillSpeed = (uint16_t)(speed * 100);
|
||||
@@ -242,6 +254,12 @@ void handleMqttMessage(char* topic, byte* payload, unsigned int length) {
|
||||
Serial.print("Incline set to: ");
|
||||
Serial.println(incline);
|
||||
}
|
||||
if (doc["distance"].is<int>()) {
|
||||
treadmillDistance = doc["distance"];
|
||||
distanceAccumulator = (float)treadmillDistance; // Sync accumulator
|
||||
Serial.print("Distance set to: ");
|
||||
Serial.println(treadmillDistance);
|
||||
}
|
||||
|
||||
publishValues();
|
||||
}
|
||||
@@ -285,9 +303,14 @@ void connectToMQTT() {
|
||||
|
||||
String clientId = String("esp32-") + String(random(0xffff), HEX);
|
||||
|
||||
if (mqtt.connect(clientId.c_str())) {
|
||||
// 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!");
|
||||
Serial.println("MQTT connected with LWT!");
|
||||
|
||||
// Subscribe to control topics
|
||||
String configTopic = String("ble-sim/") + configManager.getDeviceId() + "/config";
|
||||
@@ -299,7 +322,7 @@ void connectToMQTT() {
|
||||
Serial.print("Subscribed to: ");
|
||||
Serial.println(configTopic);
|
||||
|
||||
// Publish initial status
|
||||
// Publish initial status (with retain so new subscribers see current state)
|
||||
publishStatus();
|
||||
} else {
|
||||
Serial.print("MQTT connection failed, rc=");
|
||||
@@ -307,6 +330,25 @@ void connectToMQTT() {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 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
|
||||
// ============================================
|
||||
@@ -316,6 +358,8 @@ void updateStatus() {
|
||||
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";
|
||||
@@ -353,6 +397,8 @@ void setup() {
|
||||
|
||||
// Start web configuration portal
|
||||
setupWebPortal();
|
||||
setResetDistanceCallback(resetTreadmillDistance);
|
||||
setSetBatteryCallback(setBatteryLevelCallback);
|
||||
|
||||
// Initialize MQTT
|
||||
setupMQTT();
|
||||
@@ -390,7 +436,13 @@ void loop() {
|
||||
if (currentDeviceType == DEVICE_HEART_RATE) {
|
||||
notifyHeartRate(heartRate);
|
||||
} else if (currentDeviceType == DEVICE_TREADMILL) {
|
||||
notifyTreadmill(treadmillSpeed, treadmillIncline);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
WebServer server(80);
|
||||
PortalStatus currentStatus = {};
|
||||
ResetDistanceCallback resetDistanceCallback = nullptr;
|
||||
SetBatteryCallback setBatteryCallback = nullptr;
|
||||
|
||||
// HTML template with embedded CSS and JS
|
||||
const char INDEX_HTML[] PROGMEM = R"rawliteral(
|
||||
@@ -78,6 +80,27 @@ const char INDEX_HTML[] PROGMEM = R"rawliteral(
|
||||
button:hover { background: #22c55e; }
|
||||
.btn-danger { background: #f87171; }
|
||||
.btn-danger:hover { background: #ef4444; }
|
||||
.btn-secondary { background: #6366f1; margin-top: 10px; }
|
||||
.btn-secondary:hover { background: #4f46e5; }
|
||||
.distance-value { font-size: 24px; font-weight: bold; color: #4ade80; }
|
||||
.battery-value { font-size: 24px; font-weight: bold; color: #4ade80; }
|
||||
.hidden { display: none; }
|
||||
input[type="range"] {
|
||||
-webkit-appearance: none;
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
background: #0f3460;
|
||||
margin: 10px 0;
|
||||
}
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #4ade80;
|
||||
cursor: pointer;
|
||||
}
|
||||
.msg { padding: 10px; border-radius: 8px; margin-bottom: 15px; }
|
||||
.msg-success { background: #064e3b; }
|
||||
.msg-error { background: #7f1d1d; }
|
||||
@@ -85,7 +108,7 @@ const char INDEX_HTML[] PROGMEM = R"rawliteral(
|
||||
</head>
|
||||
<body>
|
||||
<h1>BLE Simulator</h1>
|
||||
<p class="subtitle" id="apName">Loading...</p>
|
||||
<p class="subtitle"><span id="apName">Loading...</span> • UI v1.1.0</p>
|
||||
|
||||
<div class="card">
|
||||
<h2>Status</h2>
|
||||
@@ -107,6 +130,24 @@ const char INDEX_HTML[] PROGMEM = R"rawliteral(
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card hidden" id="heartRateCard">
|
||||
<h2>Heart Rate Monitor</h2>
|
||||
<div class="status-row">
|
||||
<span>Battery Level</span>
|
||||
<span class="battery-value" id="batteryValue">100%</span>
|
||||
</div>
|
||||
<input type="range" id="batterySlider" min="0" max="100" value="100">
|
||||
</div>
|
||||
|
||||
<div class="card hidden" id="treadmillCard">
|
||||
<h2>Treadmill</h2>
|
||||
<div class="status-row">
|
||||
<span>Distance</span>
|
||||
<span class="distance-value" id="distance">0 m</span>
|
||||
</div>
|
||||
<button class="btn-secondary" onclick="resetDistance()">Reset Distance</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>WiFi Configuration</h2>
|
||||
<div id="message"></div>
|
||||
@@ -164,6 +205,16 @@ const char INDEX_HTML[] PROGMEM = R"rawliteral(
|
||||
updateStatusDots(data.status);
|
||||
} catch (e) {
|
||||
console.error('Failed to update status:', e);
|
||||
// Show disconnected state when device is unreachable
|
||||
updateStatusDots({
|
||||
wifiConnected: false,
|
||||
mqttConnected: false,
|
||||
bleStarted: false,
|
||||
deviceType: 'Not configured',
|
||||
ipAddress: '',
|
||||
treadmillDistance: 0,
|
||||
batteryLevel: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,6 +229,25 @@ const char INDEX_HTML[] PROGMEM = R"rawliteral(
|
||||
document.getElementById('bleStatus').textContent = status.bleStarted ? status.deviceType : 'Not started';
|
||||
|
||||
document.getElementById('ipAddr').textContent = status.ipAddress || '-';
|
||||
|
||||
// Show heart rate card only when configured as heart rate
|
||||
const heartRateCard = document.getElementById('heartRateCard');
|
||||
if (status.deviceType === 'Heart Rate') {
|
||||
heartRateCard.classList.remove('hidden');
|
||||
document.getElementById('batteryValue').textContent = status.batteryLevel + '%';
|
||||
document.getElementById('batterySlider').value = status.batteryLevel;
|
||||
} else {
|
||||
heartRateCard.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Show treadmill card only when configured as treadmill
|
||||
const treadmillCard = document.getElementById('treadmillCard');
|
||||
if (status.deviceType === 'Treadmill') {
|
||||
treadmillCard.classList.remove('hidden');
|
||||
document.getElementById('distance').textContent = status.treadmillDistance + ' m';
|
||||
} else {
|
||||
treadmillCard.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('configForm').addEventListener('submit', async (e) => {
|
||||
@@ -223,9 +293,45 @@ const char INDEX_HTML[] PROGMEM = R"rawliteral(
|
||||
}
|
||||
}
|
||||
|
||||
// Load config once, then only update status
|
||||
loadConfig();
|
||||
setInterval(updateStatus, 5000);
|
||||
async function resetDistance() {
|
||||
try {
|
||||
await fetch('/api/reset-distance', { method: 'POST' });
|
||||
document.getElementById('distance').textContent = '0 m';
|
||||
} catch (e) {
|
||||
console.error('Failed to reset distance:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Battery slider handler
|
||||
document.getElementById('batterySlider').addEventListener('input', async (e) => {
|
||||
const level = e.target.value;
|
||||
document.getElementById('batteryValue').textContent = level + '%';
|
||||
try {
|
||||
await fetch('/api/set-battery', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ level: parseInt(level) })
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to set battery:', err);
|
||||
}
|
||||
});
|
||||
|
||||
// Load config with retry, then update status periodically
|
||||
async function init() {
|
||||
let retries = 3;
|
||||
while (retries > 0) {
|
||||
try {
|
||||
await loadConfig();
|
||||
break;
|
||||
} catch (e) {
|
||||
retries--;
|
||||
if (retries > 0) await new Promise(r => setTimeout(r, 1000));
|
||||
}
|
||||
}
|
||||
}
|
||||
init();
|
||||
setInterval(updateStatus, 3000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -252,6 +358,8 @@ void handleGetStatus() {
|
||||
status["bleStarted"] = currentStatus.bleStarted;
|
||||
status["deviceType"] = currentStatus.deviceType;
|
||||
status["ipAddress"] = currentStatus.ipAddress;
|
||||
status["treadmillDistance"] = currentStatus.treadmillDistance;
|
||||
status["batteryLevel"] = currentStatus.batteryLevel;
|
||||
|
||||
String response;
|
||||
serializeJson(doc, response);
|
||||
@@ -299,11 +407,55 @@ void handleReset() {
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
void handleResetDistance() {
|
||||
if (resetDistanceCallback) {
|
||||
resetDistanceCallback();
|
||||
Serial.println("Treadmill distance reset");
|
||||
}
|
||||
server.send(200, "application/json", "{\"success\":true}");
|
||||
}
|
||||
|
||||
void setResetDistanceCallback(ResetDistanceCallback callback) {
|
||||
resetDistanceCallback = callback;
|
||||
}
|
||||
|
||||
void handleSetBattery() {
|
||||
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;
|
||||
}
|
||||
|
||||
uint8_t level = doc["level"] | 100;
|
||||
if (level > 100) level = 100;
|
||||
|
||||
if (setBatteryCallback) {
|
||||
setBatteryCallback(level);
|
||||
Serial.print("Battery level set to: ");
|
||||
Serial.println(level);
|
||||
}
|
||||
|
||||
server.send(200, "application/json", "{\"success\":true}");
|
||||
}
|
||||
|
||||
void setSetBatteryCallback(SetBatteryCallback callback) {
|
||||
setBatteryCallback = callback;
|
||||
}
|
||||
|
||||
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.on("/api/reset-distance", HTTP_POST, handleResetDistance);
|
||||
server.on("/api/set-battery", HTTP_POST, handleSetBattery);
|
||||
|
||||
server.begin();
|
||||
Serial.println("Web portal started on http://192.168.4.1");
|
||||
|
||||
Reference in New Issue
Block a user