Added MQTT Heartbeat and changed Firware Versioning System

This commit is contained in:
2025-12-03 18:22:17 +02:00
parent a7f1bd1667
commit b04590d270
9 changed files with 304 additions and 24 deletions

View File

@@ -13,7 +13,8 @@ MQTTAsyncClient::MQTTAsyncClient(ConfigManager& configManager, Networking& netwo
: _configManager(configManager)
, _networking(networking)
, _messageCallback(nullptr)
, _mqttReconnectTimer(nullptr) {
, _mqttReconnectTimer(nullptr)
, _heartbeatTimer(nullptr) {
_instance = this; // Set static instance pointer
@@ -25,6 +26,15 @@ MQTTAsyncClient::MQTTAsyncClient(ConfigManager& configManager, Networking& netwo
(void*)0, // Timer ID (can store data)
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) {
xTimerDelete(_mqttReconnectTimer, portMAX_DELAY);
}
if (_heartbeatTimer) {
xTimerStop(_heartbeatTimer, 0);
xTimerDelete(_heartbeatTimer, portMAX_DELAY);
}
_mqttClient.disconnect();
}
@@ -155,6 +169,9 @@ void MQTTAsyncClient::onMqttConnect(bool sessionPresent) {
// Subscribe to control topic
subscribe();
// 🔥 Start heartbeat timer
startHeartbeat();
}
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));
// Stop heartbeat timer when disconnected
stopHeartbeat();
if (_networking.isConnected()) {
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) {
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();
}
}

View File

@@ -113,4 +113,12 @@ private:
static const unsigned long MQTT_RECONNECT_DELAY = 5000; // 5 seconds
void attemptReconnection();
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();
};

View File

@@ -47,7 +47,7 @@ public:
String deviceUID = ""; // 🏷️ Factory-set UID (NVS) - NO DEFAULT
String hwType = ""; // 🔧 Factory-set hardware type (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)
};
/**

View File

@@ -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() {
LOG_INFO("Forcing reconnection...");
setState(NetworkState::RECONNECTING);

View File

@@ -85,6 +85,7 @@ public:
// Returns whether the network is currently connected
bool isConnected() const;
String getLocalIP() const;
String getGateway() const;
ConnectionType getActiveConnection() const { return _activeConnection; }
NetworkState getState() const { return _state; }

View File

@@ -126,8 +126,8 @@ void OTAManager::checkForUpdates(const String& channel) {
channel.c_str(), _configManager.getHardwareVariant().c_str());
if (checkVersion(channel)) {
float currentVersion = getCurrentVersion();
LOG_INFO("Current version: %.1f, Available version: %.1f (Channel: %s)",
uint16_t currentVersion = getCurrentVersion();
LOG_INFO("Current version: %u, Available version: %u (Channel: %s)",
currentVersion, _availableVersion, channel.c_str());
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();
return fwVersionStr.toFloat();
// Parse integer directly: "130" -> 130
return fwVersionStr.toInt();
}
void OTAManager::setStatus(Status status, ErrorCode error) {
@@ -243,14 +244,14 @@ bool OTAManager::checkVersion(const String& channel) {
continue; // Try next server
}
// Extract metadata
_availableVersion = doc["version"].as<float>();
// Extract metadata - all integers now
_availableVersion = doc["version"].as<uint16_t>();
_availableChecksum = doc["checksum"].as<String>();
_updateChannel = doc["channel"].as<String>();
_isMandatory = doc["mandatory"].as<bool>();
_isEmergency = doc["emergency"].as<bool>();
_minVersion = doc["minVersion"].as<float>(); // ✅ NEW
_expectedFileSize = doc["fileSize"].as<size_t>(); // ✅ NEW
_minVersion = doc["minVersion"].as<uint16_t>();
_expectedFileSize = doc["fileSize"].as<size_t>();
// ✅ NEW: Validate channel matches requested
if (_updateChannel != channel) {
@@ -270,16 +271,16 @@ bool OTAManager::checkVersion(const String& channel) {
}
// ✅ NEW: Check minVersion compatibility
float currentVersion = getCurrentVersion();
if (_minVersion > 0.0f && currentVersion < _minVersion) {
LOG_ERROR("OTA: Current version %.1f is below minimum required %.1f",
uint16_t currentVersion = getCurrentVersion();
if (_minVersion > 0 && currentVersion < _minVersion) {
LOG_ERROR("OTA: Current version %u is below minimum required %u",
currentVersion, _minVersion);
LOG_ERROR("OTA: Intermediate update required first - cannot proceed");
_lastError = ErrorCode::VERSION_TOO_LOW;
continue; // Try next server
}
if (_availableVersion == 0.0f) {
if (_availableVersion == 0) {
LOG_ERROR("OTA: Invalid version in metadata from %s", baseUrl.c_str());
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: Expected file size: %u bytes, Min version: %.1f",
LOG_INFO("OTA: Expected file size: %u bytes, Min version: %u",
_expectedFileSize, _minVersion);
return true; // Success!
} else {
@@ -571,7 +572,8 @@ bool OTAManager::installFromSD(const String& filePath) {
}
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();
delay(500);
ESP.restart();

View File

@@ -85,8 +85,8 @@ public:
// Status and info
Status getStatus() const { return _status; }
ErrorCode getLastError() const { return _lastError; }
float getCurrentVersion() const;
float getAvailableVersion() const { return _availableVersion; }
uint16_t getCurrentVersion() const;
uint16_t getAvailableVersion() const { return _availableVersion; }
bool isUpdateAvailable() const { return _updateAvailable; }
// Callbacks
@@ -106,8 +106,8 @@ private:
Player* _player; // NEW: Player reference for idle check
Status _status;
ErrorCode _lastError;
float _availableVersion;
float _minVersion; // NEW: Minimum required version
uint16_t _availableVersion;
uint16_t _minVersion; // NEW: Minimum required version
size_t _expectedFileSize; // NEW: Expected firmware file size
bool _updateAvailable;
String _availableChecksum;