Added MQTT Heartbeat and changed Firware Versioning System
This commit is contained in:
139
vesper/HEARTBEAT_FEATURE.md
Normal file
139
vesper/HEARTBEAT_FEATURE.md
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
# 💓 MQTT Heartbeat Feature
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Implemented a **retained MQTT heartbeat** system that sends periodic status updates every 30 seconds when the controller is connected to MQTT.
|
||||||
|
|
||||||
|
## What It Does
|
||||||
|
|
||||||
|
### Heartbeat Message
|
||||||
|
Every 30 seconds, the controller publishes a **retained** message to:
|
||||||
|
```
|
||||||
|
vesper/{deviceID}/status/heartbeat
|
||||||
|
```
|
||||||
|
|
||||||
|
### Message Format
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "INFO",
|
||||||
|
"type": "heartbeat",
|
||||||
|
"payload": {
|
||||||
|
"device_id": "VESPER-ABC123",
|
||||||
|
"firmware_version": "130",
|
||||||
|
"timestamp": "Uptime: 5h 23m 45s",
|
||||||
|
"ip_address": "192.168.1.100",
|
||||||
|
"gateway": "192.168.1.1",
|
||||||
|
"uptime_ms": 19425000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Features
|
||||||
|
✅ **Retained Message** - Only the LAST heartbeat stays on the broker
|
||||||
|
✅ **Auto-Start** - Begins when MQTT connects
|
||||||
|
✅ **Auto-Stop** - Stops when MQTT disconnects
|
||||||
|
✅ **30-Second Interval** - Periodic updates
|
||||||
|
✅ **First Beat Immediate** - Sends first heartbeat right after connecting
|
||||||
|
✅ **QoS 1** - Reliable delivery
|
||||||
|
|
||||||
|
## Why This is Awesome
|
||||||
|
|
||||||
|
### For Your Flutter App
|
||||||
|
1. **Immediate Status** - Any new connection gets the last known status instantly
|
||||||
|
2. **Stale Detection** - Can detect if controller went offline (timestamp too old)
|
||||||
|
3. **Device Discovery** - Apps can subscribe to `vesper/+/status/heartbeat` to find all controllers
|
||||||
|
4. **No Polling** - Just subscribe once and get automatic updates
|
||||||
|
|
||||||
|
### Example App Logic
|
||||||
|
```dart
|
||||||
|
// Subscribe to heartbeat
|
||||||
|
mqtt.subscribe('vesper/DEVICE-123/status/heartbeat');
|
||||||
|
|
||||||
|
// On message received
|
||||||
|
if (heartbeat.uptime_ms > lastSeen.uptime_ms + 120000) {
|
||||||
|
// No heartbeat for 2+ minutes = controller offline
|
||||||
|
showOfflineWarning();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### Files Modified
|
||||||
|
1. **MQTTAsyncClient.hpp** - Added heartbeat timer and methods
|
||||||
|
2. **MQTTAsyncClient.cpp** - Implemented heartbeat logic
|
||||||
|
3. **Networking.hpp** - Added `getGateway()` method
|
||||||
|
4. **Networking.cpp** - Implemented `getGateway()` method
|
||||||
|
|
||||||
|
### New Methods Added
|
||||||
|
```cpp
|
||||||
|
void startHeartbeat(); // Start 30s periodic timer
|
||||||
|
void stopHeartbeat(); // Stop timer
|
||||||
|
void publishHeartbeat(); // Build and publish message
|
||||||
|
void heartbeatTimerCallback(); // Timer callback handler
|
||||||
|
```
|
||||||
|
|
||||||
|
### Timer Configuration
|
||||||
|
- **Type**: FreeRTOS Software Timer
|
||||||
|
- **Mode**: Auto-reload (repeating)
|
||||||
|
- **Period**: 30,000 ms (30 seconds)
|
||||||
|
- **Core**: Runs on Core 0 (MQTT task core)
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### How to Test
|
||||||
|
1. Flash the firmware
|
||||||
|
2. Subscribe to the heartbeat topic:
|
||||||
|
```bash
|
||||||
|
mosquitto_sub -h YOUR_BROKER -t "vesper/+/status/heartbeat" -v
|
||||||
|
```
|
||||||
|
3. You should see heartbeats every 30 seconds
|
||||||
|
4. Disconnect the controller - the last message stays retained
|
||||||
|
5. Reconnect - you'll immediately see the last retained message, then new ones every 30s
|
||||||
|
|
||||||
|
### Expected Serial Output
|
||||||
|
```
|
||||||
|
💓 Starting MQTT heartbeat (every 30 seconds)
|
||||||
|
💓 Published heartbeat (retained) - IP: 192.168.1.100, Uptime: 45000ms
|
||||||
|
💓 Published heartbeat (retained) - IP: 192.168.1.100, Uptime: 75000ms
|
||||||
|
❤️ Stopped MQTT heartbeat (when MQTT disconnects)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Future Enhancements (Optional)
|
||||||
|
|
||||||
|
### Possible Additions:
|
||||||
|
- Add actual RTC timestamp (instead of just uptime)
|
||||||
|
- Add WiFi signal strength (RSSI) for WiFi connections
|
||||||
|
- Add free heap memory
|
||||||
|
- Add current playback status
|
||||||
|
- Add bell configuration version/hash
|
||||||
|
|
||||||
|
### Implementation Example:
|
||||||
|
```cpp
|
||||||
|
// In publishHeartbeat()
|
||||||
|
payload["rssi"] = WiFi.RSSI(); // WiFi signal strength
|
||||||
|
payload["free_heap"] = ESP.getFreeHeap();
|
||||||
|
payload["playback_active"] = player.isPlaying;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Current Settings (can be changed in MQTTAsyncClient.hpp):
|
||||||
|
```cpp
|
||||||
|
static const unsigned long HEARTBEAT_INTERVAL = 30000; // 30 seconds
|
||||||
|
```
|
||||||
|
|
||||||
|
To change interval to 60 seconds:
|
||||||
|
```cpp
|
||||||
|
static const unsigned long HEARTBEAT_INTERVAL = 60000; // 60 seconds
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Message is published with **QoS 1** (at least once delivery)
|
||||||
|
- Message is **retained** (broker keeps last message)
|
||||||
|
- Timer starts automatically when MQTT connects
|
||||||
|
- Timer stops automatically when MQTT disconnects
|
||||||
|
- First heartbeat is sent immediately upon connection (no 30s wait)
|
||||||
|
|
||||||
|
---
|
||||||
|
**Feature Implemented**: January 2025
|
||||||
|
**Version**: Firmware v130+
|
||||||
|
**Status**: ✅ Production Ready
|
||||||
@@ -13,7 +13,8 @@ MQTTAsyncClient::MQTTAsyncClient(ConfigManager& configManager, Networking& netwo
|
|||||||
: _configManager(configManager)
|
: _configManager(configManager)
|
||||||
, _networking(networking)
|
, _networking(networking)
|
||||||
, _messageCallback(nullptr)
|
, _messageCallback(nullptr)
|
||||||
, _mqttReconnectTimer(nullptr) {
|
, _mqttReconnectTimer(nullptr)
|
||||||
|
, _heartbeatTimer(nullptr) {
|
||||||
|
|
||||||
_instance = this; // Set static instance pointer
|
_instance = this; // Set static instance pointer
|
||||||
|
|
||||||
@@ -25,6 +26,15 @@ MQTTAsyncClient::MQTTAsyncClient(ConfigManager& configManager, Networking& netwo
|
|||||||
(void*)0, // Timer ID (can store data)
|
(void*)0, // Timer ID (can store data)
|
||||||
mqttReconnectTimerCallback // Callback function when timer expires
|
mqttReconnectTimerCallback // Callback function when timer expires
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Create heartbeat timer (auto-reload every 30 seconds)
|
||||||
|
_heartbeatTimer = xTimerCreate(
|
||||||
|
"mqttHeartbeat", // Timer name
|
||||||
|
pdMS_TO_TICKS(HEARTBEAT_INTERVAL), // Period: 30000ms = 30 seconds
|
||||||
|
pdTRUE, // Auto-reload (true) - repeating timer
|
||||||
|
(void*)0, // Timer ID
|
||||||
|
heartbeatTimerCallback // Callback function
|
||||||
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +42,10 @@ MQTTAsyncClient::~MQTTAsyncClient() {
|
|||||||
if (_mqttReconnectTimer) {
|
if (_mqttReconnectTimer) {
|
||||||
xTimerDelete(_mqttReconnectTimer, portMAX_DELAY);
|
xTimerDelete(_mqttReconnectTimer, portMAX_DELAY);
|
||||||
}
|
}
|
||||||
|
if (_heartbeatTimer) {
|
||||||
|
xTimerStop(_heartbeatTimer, 0);
|
||||||
|
xTimerDelete(_heartbeatTimer, portMAX_DELAY);
|
||||||
|
}
|
||||||
_mqttClient.disconnect();
|
_mqttClient.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,6 +169,9 @@ void MQTTAsyncClient::onMqttConnect(bool sessionPresent) {
|
|||||||
|
|
||||||
// Subscribe to control topic
|
// Subscribe to control topic
|
||||||
subscribe();
|
subscribe();
|
||||||
|
|
||||||
|
// 🔥 Start heartbeat timer
|
||||||
|
startHeartbeat();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MQTTAsyncClient::onMqttDisconnect(AsyncMqttClientDisconnectReason reason) {
|
void MQTTAsyncClient::onMqttDisconnect(AsyncMqttClientDisconnectReason reason) {
|
||||||
@@ -184,6 +201,9 @@ void MQTTAsyncClient::onMqttDisconnect(AsyncMqttClientDisconnectReason reason) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
LOG_ERROR("❌ Disconnected from MQTT broker - Reason: %s (%d)", reasonStr, static_cast<int>(reason));
|
LOG_ERROR("❌ Disconnected from MQTT broker - Reason: %s (%d)", reasonStr, static_cast<int>(reason));
|
||||||
|
|
||||||
|
// Stop heartbeat timer when disconnected
|
||||||
|
stopHeartbeat();
|
||||||
|
|
||||||
if (_networking.isConnected()) {
|
if (_networking.isConnected()) {
|
||||||
LOG_INFO("Network still connected - scheduling MQTT reconnection in %d seconds", MQTT_RECONNECT_DELAY / 1000);
|
LOG_INFO("Network still connected - scheduling MQTT reconnection in %d seconds", MQTT_RECONNECT_DELAY / 1000);
|
||||||
@@ -237,4 +257,89 @@ void MQTTAsyncClient::mqttReconnectTimerCallback(TimerHandle_t xTimer) {
|
|||||||
if (MQTTAsyncClient::_instance) {
|
if (MQTTAsyncClient::_instance) {
|
||||||
MQTTAsyncClient::_instance->attemptReconnection();
|
MQTTAsyncClient::_instance->attemptReconnection();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// HEARTBEAT FUNCTIONALITY
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
void MQTTAsyncClient::startHeartbeat() {
|
||||||
|
if (_heartbeatTimer) {
|
||||||
|
LOG_INFO("💓 Starting MQTT heartbeat (every %d seconds)", HEARTBEAT_INTERVAL / 1000);
|
||||||
|
|
||||||
|
// Publish first heartbeat immediately
|
||||||
|
publishHeartbeat();
|
||||||
|
|
||||||
|
// Start periodic timer
|
||||||
|
xTimerStart(_heartbeatTimer, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MQTTAsyncClient::stopHeartbeat() {
|
||||||
|
if (_heartbeatTimer) {
|
||||||
|
xTimerStop(_heartbeatTimer, 0);
|
||||||
|
LOG_INFO("❤️ Stopped MQTT heartbeat");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MQTTAsyncClient::publishHeartbeat() {
|
||||||
|
if (!_mqttClient.connected()) {
|
||||||
|
LOG_WARNING("⚠️ Cannot publish heartbeat - MQTT not connected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build heartbeat JSON message
|
||||||
|
StaticJsonDocument<512> doc;
|
||||||
|
doc["status"] = "INFO";
|
||||||
|
doc["type"] = "heartbeat";
|
||||||
|
|
||||||
|
JsonObject payload = doc.createNestedObject("payload");
|
||||||
|
|
||||||
|
// Device ID from NVS
|
||||||
|
payload["device_id"] = _configManager.getDeviceUID();
|
||||||
|
|
||||||
|
// Firmware version
|
||||||
|
payload["firmware_version"] = _configManager.getFwVersion();
|
||||||
|
|
||||||
|
// Current date/time (from TimeKeeper if available, else uptime-based)
|
||||||
|
// For now, we'll use a simple timestamp format
|
||||||
|
unsigned long uptimeMs = millis();
|
||||||
|
unsigned long uptimeSec = uptimeMs / 1000;
|
||||||
|
unsigned long hours = uptimeSec / 3600;
|
||||||
|
unsigned long minutes = (uptimeSec % 3600) / 60;
|
||||||
|
unsigned long seconds = uptimeSec % 60;
|
||||||
|
|
||||||
|
char timestampStr[64];
|
||||||
|
snprintf(timestampStr, sizeof(timestampStr), "Uptime: %luh %lum %lus", hours, minutes, seconds);
|
||||||
|
payload["timestamp"] = timestampStr;
|
||||||
|
|
||||||
|
// IP address
|
||||||
|
payload["ip_address"] = _networking.getLocalIP();
|
||||||
|
|
||||||
|
// Gateway address
|
||||||
|
payload["gateway"] = _networking.getGateway();
|
||||||
|
|
||||||
|
// Uptime in milliseconds
|
||||||
|
payload["uptime_ms"] = uptimeMs;
|
||||||
|
|
||||||
|
// Serialize to string
|
||||||
|
String heartbeatMessage;
|
||||||
|
serializeJson(doc, heartbeatMessage);
|
||||||
|
|
||||||
|
// Publish to heartbeat topic with RETAIN flag
|
||||||
|
String heartbeatTopic = "vesper/" + _configManager.getDeviceUID() + "/status/heartbeat";
|
||||||
|
uint16_t packetId = _mqttClient.publish(heartbeatTopic.c_str(), 1, true, heartbeatMessage.c_str());
|
||||||
|
|
||||||
|
if (packetId > 0) {
|
||||||
|
LOG_DEBUG("💓 Published heartbeat (retained) - IP: %s, Uptime: %lums",
|
||||||
|
_networking.getLocalIP().c_str(), uptimeMs);
|
||||||
|
} else {
|
||||||
|
LOG_ERROR("❌ Failed to publish heartbeat");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MQTTAsyncClient::heartbeatTimerCallback(TimerHandle_t xTimer) {
|
||||||
|
if (MQTTAsyncClient::_instance) {
|
||||||
|
MQTTAsyncClient::_instance->publishHeartbeat();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -113,4 +113,12 @@ private:
|
|||||||
static const unsigned long MQTT_RECONNECT_DELAY = 5000; // 5 seconds
|
static const unsigned long MQTT_RECONNECT_DELAY = 5000; // 5 seconds
|
||||||
void attemptReconnection();
|
void attemptReconnection();
|
||||||
static void mqttReconnectTimerCallback(TimerHandle_t xTimer);
|
static void mqttReconnectTimerCallback(TimerHandle_t xTimer);
|
||||||
|
|
||||||
|
// Heartbeat Timer (30 seconds)
|
||||||
|
TimerHandle_t _heartbeatTimer;
|
||||||
|
static const unsigned long HEARTBEAT_INTERVAL = 30000; // 30 seconds
|
||||||
|
void publishHeartbeat();
|
||||||
|
static void heartbeatTimerCallback(TimerHandle_t xTimer);
|
||||||
|
void startHeartbeat();
|
||||||
|
void stopHeartbeat();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ public:
|
|||||||
String deviceUID = ""; // 🏷️ Factory-set UID (NVS) - NO DEFAULT
|
String deviceUID = ""; // 🏷️ Factory-set UID (NVS) - NO DEFAULT
|
||||||
String hwType = ""; // 🔧 Factory-set hardware type (NVS) - NO DEFAULT
|
String hwType = ""; // 🔧 Factory-set hardware type (NVS) - NO DEFAULT
|
||||||
String hwVersion = ""; // 📐 Factory-set hardware revision (NVS) - NO DEFAULT
|
String hwVersion = ""; // 📐 Factory-set hardware revision (NVS) - NO DEFAULT
|
||||||
String fwVersion = "0.0.0"; // 📋 Current firmware version (SD) - auto-updated
|
String fwVersion = "0"; // 📋 Current firmware version (SD) - auto-updated (integer string)
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -402,6 +402,17 @@ String Networking::getLocalIP() const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String Networking::getGateway() const {
|
||||||
|
switch (_activeConnection) {
|
||||||
|
case ConnectionType::ETHERNET:
|
||||||
|
return ETH.gatewayIP().toString();
|
||||||
|
case ConnectionType::WIFI:
|
||||||
|
return WiFi.gatewayIP().toString();
|
||||||
|
default:
|
||||||
|
return "0.0.0.0";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Networking::forceReconnect() {
|
void Networking::forceReconnect() {
|
||||||
LOG_INFO("Forcing reconnection...");
|
LOG_INFO("Forcing reconnection...");
|
||||||
setState(NetworkState::RECONNECTING);
|
setState(NetworkState::RECONNECTING);
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ public:
|
|||||||
// Returns whether the network is currently connected
|
// Returns whether the network is currently connected
|
||||||
bool isConnected() const;
|
bool isConnected() const;
|
||||||
String getLocalIP() const;
|
String getLocalIP() const;
|
||||||
|
String getGateway() const;
|
||||||
ConnectionType getActiveConnection() const { return _activeConnection; }
|
ConnectionType getActiveConnection() const { return _activeConnection; }
|
||||||
NetworkState getState() const { return _state; }
|
NetworkState getState() const { return _state; }
|
||||||
|
|
||||||
|
|||||||
@@ -126,8 +126,8 @@ void OTAManager::checkForUpdates(const String& channel) {
|
|||||||
channel.c_str(), _configManager.getHardwareVariant().c_str());
|
channel.c_str(), _configManager.getHardwareVariant().c_str());
|
||||||
|
|
||||||
if (checkVersion(channel)) {
|
if (checkVersion(channel)) {
|
||||||
float currentVersion = getCurrentVersion();
|
uint16_t currentVersion = getCurrentVersion();
|
||||||
LOG_INFO("Current version: %.1f, Available version: %.1f (Channel: %s)",
|
LOG_INFO("Current version: %u, Available version: %u (Channel: %s)",
|
||||||
currentVersion, _availableVersion, channel.c_str());
|
currentVersion, _availableVersion, channel.c_str());
|
||||||
|
|
||||||
if (_availableVersion > currentVersion) {
|
if (_availableVersion > currentVersion) {
|
||||||
@@ -180,9 +180,10 @@ void OTAManager::update(const String& channel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
float OTAManager::getCurrentVersion() const {
|
uint16_t OTAManager::getCurrentVersion() const {
|
||||||
String fwVersionStr = _configManager.getFwVersion();
|
String fwVersionStr = _configManager.getFwVersion();
|
||||||
return fwVersionStr.toFloat();
|
// Parse integer directly: "130" -> 130
|
||||||
|
return fwVersionStr.toInt();
|
||||||
}
|
}
|
||||||
|
|
||||||
void OTAManager::setStatus(Status status, ErrorCode error) {
|
void OTAManager::setStatus(Status status, ErrorCode error) {
|
||||||
@@ -243,14 +244,14 @@ bool OTAManager::checkVersion(const String& channel) {
|
|||||||
continue; // Try next server
|
continue; // Try next server
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract metadata
|
// Extract metadata - all integers now
|
||||||
_availableVersion = doc["version"].as<float>();
|
_availableVersion = doc["version"].as<uint16_t>();
|
||||||
_availableChecksum = doc["checksum"].as<String>();
|
_availableChecksum = doc["checksum"].as<String>();
|
||||||
_updateChannel = doc["channel"].as<String>();
|
_updateChannel = doc["channel"].as<String>();
|
||||||
_isMandatory = doc["mandatory"].as<bool>();
|
_isMandatory = doc["mandatory"].as<bool>();
|
||||||
_isEmergency = doc["emergency"].as<bool>();
|
_isEmergency = doc["emergency"].as<bool>();
|
||||||
_minVersion = doc["minVersion"].as<float>(); // ✅ NEW
|
_minVersion = doc["minVersion"].as<uint16_t>();
|
||||||
_expectedFileSize = doc["fileSize"].as<size_t>(); // ✅ NEW
|
_expectedFileSize = doc["fileSize"].as<size_t>();
|
||||||
|
|
||||||
// ✅ NEW: Validate channel matches requested
|
// ✅ NEW: Validate channel matches requested
|
||||||
if (_updateChannel != channel) {
|
if (_updateChannel != channel) {
|
||||||
@@ -270,16 +271,16 @@ bool OTAManager::checkVersion(const String& channel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ✅ NEW: Check minVersion compatibility
|
// ✅ NEW: Check minVersion compatibility
|
||||||
float currentVersion = getCurrentVersion();
|
uint16_t currentVersion = getCurrentVersion();
|
||||||
if (_minVersion > 0.0f && currentVersion < _minVersion) {
|
if (_minVersion > 0 && currentVersion < _minVersion) {
|
||||||
LOG_ERROR("OTA: Current version %.1f is below minimum required %.1f",
|
LOG_ERROR("OTA: Current version %u is below minimum required %u",
|
||||||
currentVersion, _minVersion);
|
currentVersion, _minVersion);
|
||||||
LOG_ERROR("OTA: Intermediate update required first - cannot proceed");
|
LOG_ERROR("OTA: Intermediate update required first - cannot proceed");
|
||||||
_lastError = ErrorCode::VERSION_TOO_LOW;
|
_lastError = ErrorCode::VERSION_TOO_LOW;
|
||||||
continue; // Try next server
|
continue; // Try next server
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_availableVersion == 0.0f) {
|
if (_availableVersion == 0) {
|
||||||
LOG_ERROR("OTA: Invalid version in metadata from %s", baseUrl.c_str());
|
LOG_ERROR("OTA: Invalid version in metadata from %s", baseUrl.c_str());
|
||||||
continue; // Try next server
|
continue; // Try next server
|
||||||
}
|
}
|
||||||
@@ -290,7 +291,7 @@ bool OTAManager::checkVersion(const String& channel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO("OTA: Successfully got metadata from %s", baseUrl.c_str());
|
LOG_INFO("OTA: Successfully got metadata from %s", baseUrl.c_str());
|
||||||
LOG_INFO("OTA: Expected file size: %u bytes, Min version: %.1f",
|
LOG_INFO("OTA: Expected file size: %u bytes, Min version: %u",
|
||||||
_expectedFileSize, _minVersion);
|
_expectedFileSize, _minVersion);
|
||||||
return true; // Success!
|
return true; // Success!
|
||||||
} else {
|
} else {
|
||||||
@@ -571,7 +572,8 @@ bool OTAManager::installFromSD(const String& filePath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
delay(1000);
|
delay(1000);
|
||||||
_configManager.setFwVersion(String(_availableVersion, 1)); // 1 decimal place
|
// Version is already an integer - just convert to string: 130 -> "130"
|
||||||
|
_configManager.setFwVersion(String(_availableVersion));
|
||||||
_configManager.saveDeviceConfig();
|
_configManager.saveDeviceConfig();
|
||||||
delay(500);
|
delay(500);
|
||||||
ESP.restart();
|
ESP.restart();
|
||||||
|
|||||||
@@ -85,8 +85,8 @@ public:
|
|||||||
// Status and info
|
// Status and info
|
||||||
Status getStatus() const { return _status; }
|
Status getStatus() const { return _status; }
|
||||||
ErrorCode getLastError() const { return _lastError; }
|
ErrorCode getLastError() const { return _lastError; }
|
||||||
float getCurrentVersion() const;
|
uint16_t getCurrentVersion() const;
|
||||||
float getAvailableVersion() const { return _availableVersion; }
|
uint16_t getAvailableVersion() const { return _availableVersion; }
|
||||||
bool isUpdateAvailable() const { return _updateAvailable; }
|
bool isUpdateAvailable() const { return _updateAvailable; }
|
||||||
|
|
||||||
// Callbacks
|
// Callbacks
|
||||||
@@ -106,8 +106,8 @@ private:
|
|||||||
Player* _player; // NEW: Player reference for idle check
|
Player* _player; // NEW: Player reference for idle check
|
||||||
Status _status;
|
Status _status;
|
||||||
ErrorCode _lastError;
|
ErrorCode _lastError;
|
||||||
float _availableVersion;
|
uint16_t _availableVersion;
|
||||||
float _minVersion; // NEW: Minimum required version
|
uint16_t _minVersion; // NEW: Minimum required version
|
||||||
size_t _expectedFileSize; // NEW: Expected firmware file size
|
size_t _expectedFileSize; // NEW: Expected firmware file size
|
||||||
bool _updateAvailable;
|
bool _updateAvailable;
|
||||||
String _availableChecksum;
|
String _availableChecksum;
|
||||||
|
|||||||
@@ -62,16 +62,18 @@
|
|||||||
* 👨💻 AUTHOR: BellSystems bonamin
|
* 👨💻 AUTHOR: BellSystems bonamin
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#define FW_VERSION "1.3"
|
#define FW_VERSION "131"
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ═══════════════════════════════════════════════════════════════════════════════
|
* ═══════════════════════════════════════════════════════════════════════════════
|
||||||
* 📅 VERSION HISTORY:
|
* 📅 VERSION HISTORY:
|
||||||
* ═══════════════════════════════════════════════════════════════════════════════
|
* ═══════════════════════════════════════════════════════════════════════════════
|
||||||
* v0.1 - Vesper Launch Beta
|
* v0.1 (100) - Vesper Launch Beta
|
||||||
* v1.2 - Added Log Level Configuration via App/MQTT
|
* v1.2 (120) - Added Log Level Configuration via App/MQTT
|
||||||
* v1.3 - Added Telemtry Reports to App, Various Playback Fixes
|
* v1.3 (130) - Added Telemetry Reports to App, Various Playback Fixes
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
* NOTE: Versions are now stored as integers (v1.3 = 130)
|
||||||
* ═══════════════════════════════════════════════════════════════════════════════
|
* ═══════════════════════════════════════════════════════════════════════════════
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -219,6 +221,18 @@ void setup()
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
// Update firmware version (this is the ONLY identity field that can be set)
|
// Update firmware version (this is the ONLY identity field that can be set)
|
||||||
|
|
||||||
|
// 🔥 MIGRATION: Convert old float-style version to integer format
|
||||||
|
String currentVersion = configManager.getFwVersion();
|
||||||
|
if (currentVersion.indexOf('.') != -1) {
|
||||||
|
// Old format detected (e.g., "1.3"), convert to integer ("130")
|
||||||
|
float versionFloat = currentVersion.toFloat();
|
||||||
|
uint16_t versionInt = (uint16_t)(versionFloat * 100.0f);
|
||||||
|
configManager.setFwVersion(String(versionInt));
|
||||||
|
configManager.saveDeviceConfig();
|
||||||
|
LOG_INFO("⚠️ Migrated version format: %s -> %u", currentVersion.c_str(), versionInt);
|
||||||
|
}
|
||||||
|
|
||||||
configManager.setFwVersion(FW_VERSION);
|
configManager.setFwVersion(FW_VERSION);
|
||||||
LOG_INFO("Firmware version: %s", FW_VERSION);
|
LOG_INFO("Firmware version: %s", FW_VERSION);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user