Files
maxxfan-controller/main/maxxfan-controller.c

526 lines
23 KiB
C
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_log.h"
#include "esp_http_server.h"
#include "esp_task_wdt.h"
#include "nvs.h"
#include "cJSON.h"
// Project modules
#include "config.h"
#include "motor_control.h"
#include "state_manager.h"
#include "wifi_manager.h"
// HTTP server handle
static httpd_handle_t server = NULL;
// Task handles for watchdog
static TaskHandle_t main_task_handle = NULL;
// Compact HTML web page for control
static const char* html_page =
"<!DOCTYPE html><html><head><title>Maxxfan</title><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><style>"
"body{font-family:Arial;margin:20px;background:#f0f0f0}.container{max-width:500px;margin:0 auto;background:white;padding:20px;border-radius:8px}"
"h1{color:#333;text-align:center;margin:0 0 20px}button{padding:12px 20px;margin:5px;border:none;border-radius:4px;cursor:pointer;font-size:14px}"
".off{background:#f44336;color:white}.exhaust{background:#ff9800;color:white}.intake{background:#4CAF50;color:white}.on{background:#2196F3;color:white}"
".status{background:#e3f2fd;padding:15px;border-radius:4px;margin:15px 0}.ramping{background:#fff3e0;padding:8px;margin:8px 0;display:none}"
".cooldown{background:#ffebee;padding:8px;margin:8px 0;color:#c62828;display:none}"
".error{background:#ffebee;padding:8px;margin:8px 0;color:#c62828;display:none}.slider{width:100%;height:30px;margin:10px 0}"
"</style></head><body><div class=\"container\"><h1>Maxxfan Controller</h1>"
"<div class=\"status\"><h4>Status</h4><p>Mode: <span id=\"mode\">OFF</span></p><p>Speed: <span id=\"speed\">0</span>%</p>"
"<p>Target: <span id=\"target\">0</span>%</p><p>State: <span id=\"state\">IDLE</span></p>"
"<p>Last ON: <span id=\"lastOn\">EXHAUST @ 50%</span></p>"
"<div id=\"rampStatus\" class=\"ramping\">Ramping...</div>"
"<div id=\"cooldownStatus\" class=\"cooldown\">Direction change cooldown: <span id=\"cooldownTime\">0</span>s</div>"
"<div id=\"errorStatus\" class=\"error\">Error</div><small id=\"connectionStatus\">Connecting...</small></div>"
"<div><h3>Fan Control</h3><button class=\"off\" onclick=\"setFan('off',0)\">OFF</button>"
"<button class=\"on\" onclick=\"setFan('on',0)\">ON (Resume Last)</button>"
"<button class=\"exhaust\" onclick=\"setFan('exhaust',50)\">Exhaust 50%</button>"
"<button class=\"intake\" onclick=\"setFan('intake',50)\">Intake 50%</button></div>"
"<div><h3>Speed Control</h3><label>Speed: <span id=\"speedValue\">50</span>%</label>"
"<input type=\"range\" id=\"speedSlider\" class=\"slider\" min=\"0\" max=\"100\" value=\"50\" oninput=\"updateSpeed(this.value)\">"
"<button class=\"exhaust\" onclick=\"setFanSpeed('exhaust')\">Set Exhaust</button>"
"<button class=\"intake\" onclick=\"setFanSpeed('intake')\">Set Intake</button></div></div>"
"<script>let currentSpeed=50,updateInterval=null,errorCount=0;"
"function updateSpeed(v){currentSpeed=parseInt(v);document.getElementById('speedValue').textContent=v}"
"function showError(m){document.getElementById('errorStatus').innerHTML='Error: '+m;document.getElementById('errorStatus').style.display='block';"
"document.getElementById('connectionStatus').textContent='Error'}"
"function hideError(){document.getElementById('errorStatus').style.display='none';"
"document.getElementById('connectionStatus').textContent='Connected';errorCount=0}"
"function setFan(mode,speed){if(mode==='on'){fetch('/fan',{method:'POST',headers:{'Content-Type':'application/json'},"
"body:JSON.stringify({mode:'on',speed:0})})"
".then(r=>{if(!r.ok)throw new Error('HTTP '+r.status);return r.json()})"
".then(d=>{updateStatus(d);hideError()}).catch(e=>{console.error(e);showError(e.message)});return;}"
"currentSpeed=speed;document.getElementById('speedSlider').value=speed;"
"document.getElementById('speedValue').textContent=speed;fetch('/fan',{method:'POST',"
"headers:{'Content-Type':'application/json'},body:JSON.stringify({mode:mode,speed:parseInt(speed)})})"
".then(r=>{if(!r.ok)throw new Error('HTTP '+r.status);return r.json()})"
".then(d=>{updateStatus(d);hideError()}).catch(e=>{console.error(e);showError(e.message)})}"
"function setFanSpeed(mode){fetch('/fan',{method:'POST',headers:{'Content-Type':'application/json'},"
"body:JSON.stringify({mode:mode,speed:parseInt(currentSpeed)})})"
".then(r=>{if(!r.ok)throw new Error('HTTP '+r.status);return r.json()})"
".then(d=>{updateStatus(d);hideError()}).catch(e=>{console.error(e);showError(e.message)})}"
"function updateStatus(data){document.getElementById('mode').textContent=data.mode.toUpperCase();"
"document.getElementById('speed').textContent=data.current_speed;"
"document.getElementById('target').textContent=data.target_speed;"
"document.getElementById('state').textContent=data.state.toUpperCase();"
"if(data.last_on_mode&&data.last_on_speed){document.getElementById('lastOn').textContent=data.last_on_mode.toUpperCase()+' @ '+data.last_on_speed+'%';}"
"document.getElementById('rampStatus').style.display=data.ramping?'block':'none';"
"let cooldownDiv=document.getElementById('cooldownStatus');"
"if(data.cooldown_remaining>0){cooldownDiv.style.display='block';"
"document.getElementById('cooldownTime').textContent=Math.ceil(data.cooldown_remaining/1000);}else{cooldownDiv.style.display='none';}}"
"function getStatus(){fetch('/status').then(r=>{if(!r.ok)throw new Error('HTTP '+r.status);return r.json()})"
".then(d=>{updateStatus(d);hideError()}).catch(e=>{errorCount++;if(errorCount>=3)showError('Connection lost')})}"
"function startUpdates(){if(updateInterval)clearInterval(updateInterval);updateInterval=setInterval(getStatus,1000)}"
"document.addEventListener('DOMContentLoaded',function(){getStatus();startUpdates()})</script></body></html>";
// Initialize watchdog timer
void init_watchdog(void) {
ESP_LOGI(SYSTEM_TAG, "Setting up watchdog monitoring...");
// Get current task handle and add to watchdog
main_task_handle = xTaskGetCurrentTaskHandle();
esp_err_t result = esp_task_wdt_add(main_task_handle);
if (result == ESP_OK) {
ESP_LOGI(SYSTEM_TAG, "Main task added to watchdog monitoring");
} else if (result == ESP_ERR_INVALID_ARG) {
ESP_LOGI(SYSTEM_TAG, "Task already monitored by watchdog");
} else {
ESP_LOGW(SYSTEM_TAG, "Watchdog not available: %s", esp_err_to_name(result));
main_task_handle = NULL; // Disable watchdog feeding
}
}
// Feed the watchdog
void feed_watchdog(void) {
if (main_task_handle != NULL) {
esp_err_t result = esp_task_wdt_reset();
if (result != ESP_OK) {
MOTOR_LOGD(SYSTEM_TAG, "Watchdog reset failed: %s", esp_err_to_name(result));
}
}
}
// Helper function to set CORS headers
static void set_cors_headers(httpd_req_t *req) {
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
httpd_resp_set_hdr(req, "Access-Control-Allow-Methods", "GET, POST, OPTIONS");
httpd_resp_set_hdr(req, "Access-Control-Allow-Headers", "Content-Type, Accept");
httpd_resp_set_hdr(req, "Cache-Control", "no-cache");
}
// HTTP handler for the main web page
static esp_err_t root_get_handler(httpd_req_t *req)
{
set_cors_headers(req);
httpd_resp_set_type(req, "text/html");
httpd_resp_send(req, html_page, HTTPD_RESP_USE_STRLEN);
return ESP_OK;
}
// HTTP handler for fan status (GET /status)
static esp_err_t status_get_handler(httpd_req_t *req)
{
// Update cooldown time before reporting
motor_update_cooldown_time();
// Get current motor state
const motor_state_t* state = motor_get_state();
motor_mode_t last_on_mode;
int last_on_speed;
motor_get_last_on_state(&last_on_mode, &last_on_speed);
// Get WiFi information
wifi_info_t wifi_info;
wifi_manager_get_info(&wifi_info);
char ip_str[16];
wifi_manager_get_ip_string(ip_str, sizeof(ip_str));
ESP_LOGI(SYSTEM_TAG, "Status request - Mode: %s, Current: %d%%, Target: %d%%, State: %s, Ramping: %s, WiFi: %s (%s)",
motor_mode_to_string(state->mode), state->current_speed, state->target_speed,
motor_state_to_string(state->state), state->ramping ? "YES" : "NO",
wifi_manager_status_to_string(wifi_info.status), ip_str);
set_cors_headers(req);
httpd_resp_set_type(req, "application/json");
cJSON *json = cJSON_CreateObject();
const char* mode_str = "off";
if (state->mode == MOTOR_EXHAUST) mode_str = "exhaust";
else if (state->mode == MOTOR_INTAKE) mode_str = "intake";
const char* state_str = "idle";
switch (state->state) {
case MOTOR_STATE_RAMPING: state_str = "ramping"; break;
case MOTOR_STATE_STOPPING: state_str = "stopping"; break;
case MOTOR_STATE_COOLDOWN: state_str = "cooldown"; break;
case MOTOR_STATE_RESTARTING: state_str = "restarting"; break;
default: state_str = "idle"; break;
}
const char* last_on_mode_str = "exhaust";
if (last_on_mode == MOTOR_INTAKE) last_on_mode_str = "intake";
// Motor status
cJSON_AddStringToObject(json, "mode", mode_str);
cJSON_AddNumberToObject(json, "current_speed", state->current_speed);
cJSON_AddNumberToObject(json, "target_speed", state->target_speed);
cJSON_AddStringToObject(json, "state", state_str);
cJSON_AddBoolToObject(json, "ramping", state->ramping);
cJSON_AddNumberToObject(json, "cooldown_remaining", state->cooldown_remaining_ms);
cJSON_AddStringToObject(json, "last_on_mode", last_on_mode_str);
cJSON_AddNumberToObject(json, "last_on_speed", last_on_speed);
// WiFi status
cJSON *wifi_json = cJSON_CreateObject();
cJSON_AddStringToObject(wifi_json, "status", wifi_manager_status_to_string(wifi_info.status));
cJSON_AddStringToObject(wifi_json, "ssid", wifi_info.ssid);
cJSON_AddStringToObject(wifi_json, "ip", ip_str);
cJSON_AddNumberToObject(wifi_json, "rssi", wifi_info.rssi);
cJSON_AddBoolToObject(wifi_json, "connected", wifi_manager_is_connected());
cJSON_AddItemToObject(json, "wifi", wifi_json);
// Add pending command info if in cooldown
if (state->state == MOTOR_STATE_COOLDOWN) {
const char* pending_mode_str = "off";
if (state->pending_mode == MOTOR_EXHAUST) pending_mode_str = "exhaust";
else if (state->pending_mode == MOTOR_INTAKE) pending_mode_str = "intake";
cJSON_AddStringToObject(json, "pending_mode", pending_mode_str);
cJSON_AddNumberToObject(json, "pending_speed", state->pending_speed);
}
char *json_string = cJSON_Print(json);
if (json_string) {
httpd_resp_send(req, json_string, strlen(json_string));
free(json_string);
} else {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "JSON creation failed");
}
cJSON_Delete(json);
return ESP_OK;
}
// HTTP handler for fan control (POST /fan)
static esp_err_t fan_post_handler(httpd_req_t *req)
{
char buf[MAX_JSON_BUFFER_SIZE];
int ret, remaining = req->content_len;
if (remaining >= sizeof(buf)) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Content too long");
return ESP_FAIL;
}
ret = httpd_req_recv(req, buf, remaining);
if (ret <= 0) {
if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
httpd_resp_send_err(req, HTTPD_408_REQ_TIMEOUT, "Request timeout");
}
return ESP_FAIL;
}
buf[ret] = '\0';
ESP_LOGI(SYSTEM_TAG, "Received POST data: %s", buf);
cJSON *json = cJSON_Parse(buf);
if (json == NULL) {
ESP_LOGE(SYSTEM_TAG, "JSON parsing failed");
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON");
return ESP_FAIL;
}
cJSON *mode_json = cJSON_GetObjectItem(json, "mode");
cJSON *speed_json = cJSON_GetObjectItem(json, "speed");
if (!cJSON_IsString(mode_json) || (!cJSON_IsNumber(speed_json) && !cJSON_IsString(speed_json))) {
ESP_LOGE(SYSTEM_TAG, "JSON parsing failed - mode: %s, speed: %s",
mode_json ? (cJSON_IsString(mode_json) ? mode_json->valuestring : "not_string") : "null",
speed_json ? (cJSON_IsNumber(speed_json) ? "number" : (cJSON_IsString(speed_json) ? speed_json->valuestring : "not_number_or_string")) : "null");
cJSON_Delete(json);
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing mode or speed");
return ESP_FAIL;
}
const char* mode_str = mode_json->valuestring;
int speed;
// Handle both number and string speed values
if (cJSON_IsNumber(speed_json)) {
speed = (int)speed_json->valuedouble;
} else if (cJSON_IsString(speed_json)) {
speed = atoi(speed_json->valuestring);
} else {
speed = 0;
}
motor_mode_t mode = MOTOR_OFF;
// Handle special "ON" command - resume last settings
if (strcmp(mode_str, "on") == 0) {
ESP_LOGI(SYSTEM_TAG, "ON button pressed - resuming last state");
motor_resume_last_state();
// Save state after ON button using state manager
esp_err_t save_result = state_manager_save();
if (save_result != ESP_OK) {
ESP_LOGW(SYSTEM_TAG, "Failed to save state after ON button: %s", esp_err_to_name(save_result));
}
cJSON_Delete(json);
return status_get_handler(req);
} else if (strcmp(mode_str, "exhaust") == 0) {
mode = MOTOR_EXHAUST;
} else if (strcmp(mode_str, "intake") == 0) {
mode = MOTOR_INTAKE;
}
ESP_LOGI(SYSTEM_TAG, "HTTP Request: mode=%s, speed=%d", mode_str, speed);
motor_set_speed(mode, speed);
// Save state after any motor command using state manager
esp_err_t save_result = state_manager_save();
if (save_result != ESP_OK) {
ESP_LOGW(SYSTEM_TAG, "Failed to save state after motor command: %s", esp_err_to_name(save_result));
}
cJSON_Delete(json);
// Send response with updated status
return status_get_handler(req);
}
// HTTP handler for OPTIONS requests (CORS preflight)
static esp_err_t options_handler(httpd_req_t *req)
{
set_cors_headers(req);
httpd_resp_set_status(req, "200 OK");
httpd_resp_send(req, NULL, 0);
return ESP_OK;
}
// Start HTTP server
static httpd_handle_t start_webserver(void)
{
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.server_port = HTTP_SERVER_PORT;
config.max_uri_handlers = HTTP_MAX_URI_HANDLERS;
config.recv_wait_timeout = HTTP_RECV_TIMEOUT_SEC;
config.send_wait_timeout = HTTP_SEND_TIMEOUT_SEC;
ESP_LOGI(SYSTEM_TAG, "Starting server on port: '%d'", config.server_port);
if (httpd_start(&server, &config) == ESP_OK) {
ESP_LOGI(SYSTEM_TAG, "Registering URI handlers");
// Root handler
httpd_uri_t root = {
.uri = "/",
.method = HTTP_GET,
.handler = root_get_handler,
.user_ctx = NULL
};
httpd_register_uri_handler(server, &root);
// Status handler
httpd_uri_t status = {
.uri = "/status",
.method = HTTP_GET,
.handler = status_get_handler,
.user_ctx = NULL
};
httpd_register_uri_handler(server, &status);
// Fan control handler
httpd_uri_t fan = {
.uri = "/fan",
.method = HTTP_POST,
.handler = fan_post_handler,
.user_ctx = NULL
};
httpd_register_uri_handler(server, &fan);
// OPTIONS handler for CORS preflight
httpd_uri_t options_status = {
.uri = "/status",
.method = HTTP_OPTIONS,
.handler = options_handler,
.user_ctx = NULL
};
httpd_register_uri_handler(server, &options_status);
httpd_uri_t options_fan = {
.uri = "/fan",
.method = HTTP_OPTIONS,
.handler = options_handler,
.user_ctx = NULL
};
httpd_register_uri_handler(server, &options_fan);
return server;
}
ESP_LOGI(SYSTEM_TAG, "Error starting server!");
return NULL;
}
void app_main(void)
{
ESP_LOGI(SYSTEM_TAG, "Starting Maxxfan HTTP Controller with State Preservation!");
// Initialize state manager (includes NVS initialization)
ESP_LOGI(SYSTEM_TAG, "Initializing state manager...");
esp_err_t ret = state_manager_init();
if (ret != ESP_OK) {
ESP_LOGE(SYSTEM_TAG, "Failed to initialize state manager: %s", esp_err_to_name(ret));
return;
}
// Initialize watchdog timer
init_watchdog();
// Initialize motor control system
ESP_LOGI(SYSTEM_TAG, "Initializing motor control system...");
ret = motor_control_init();
if (ret != ESP_OK) {
ESP_LOGE(SYSTEM_TAG, "Failed to initialize motor control: %s", esp_err_to_name(ret));
return;
}
// Load saved state and restore motor if appropriate
ESP_LOGI(SYSTEM_TAG, "Loading saved state and applying restoration logic...");
esp_err_t load_result = state_manager_load_and_restore();
if (load_result == ESP_OK) {
ESP_LOGI(SYSTEM_TAG, "✓ State loaded and restoration logic applied");
} else if (load_result == ESP_ERR_NVS_NOT_FOUND) {
ESP_LOGI(SYSTEM_TAG, " No saved state found, using defaults");
} else {
ESP_LOGW(SYSTEM_TAG, "⚠️ Failed to load state: %s", esp_err_to_name(load_result));
}
// Initialize WiFi manager
ESP_LOGI(SYSTEM_TAG, "Initializing WiFi manager...");
ret = wifi_manager_init();
if (ret != ESP_OK) {
ESP_LOGE(SYSTEM_TAG, "Failed to initialize WiFi manager: %s", esp_err_to_name(ret));
return;
}
// Connect to WiFi using default credentials
ESP_LOGI(SYSTEM_TAG, "Connecting to WiFi network: %s", WIFI_SSID);
ret = wifi_manager_connect_default();
if (ret != ESP_OK) {
ESP_LOGE(SYSTEM_TAG, "Failed to start WiFi connection: %s", esp_err_to_name(ret));
return;
}
// Wait for WiFi connection (with timeout)
ESP_LOGI(SYSTEM_TAG, "Waiting for WiFi connection...");
esp_err_t wifi_result = wifi_manager_wait_for_connection(30000); // 30 second timeout
if (wifi_result == ESP_OK) {
char ip_str[16];
wifi_manager_get_ip_string(ip_str, sizeof(ip_str));
wifi_info_t wifi_info;
wifi_manager_get_info(&wifi_info);
ESP_LOGI(SYSTEM_TAG, "✓ WiFi connected successfully!");
ESP_LOGI(SYSTEM_TAG, " SSID: %s", wifi_info.ssid);
ESP_LOGI(SYSTEM_TAG, " IP Address: %s", ip_str);
ESP_LOGI(SYSTEM_TAG, " Signal Strength: %d dBm", wifi_info.rssi);
} else if (wifi_result == ESP_ERR_TIMEOUT) {
ESP_LOGW(SYSTEM_TAG, "⚠️ WiFi connection timeout - continuing with limited functionality");
} else {
ESP_LOGW(SYSTEM_TAG, "⚠️ WiFi connection failed - continuing with limited functionality");
}
// Start HTTP server (even if WiFi failed, for debugging)
ESP_LOGI(SYSTEM_TAG, "Starting web server...");
httpd_handle_t web_server = start_webserver();
if (web_server) {
ESP_LOGI(SYSTEM_TAG, "✓ Web server started successfully");
} else {
ESP_LOGE(SYSTEM_TAG, "✗ Failed to start web server");
}
// Report final system state after initialization
const motor_state_t* final_state = motor_get_state();
wifi_info_t final_wifi_info;
wifi_manager_get_info(&final_wifi_info);
char final_ip_str[16];
wifi_manager_get_ip_string(final_ip_str, sizeof(final_ip_str));
ESP_LOGI(SYSTEM_TAG, "=== SYSTEM INITIALIZATION COMPLETE ===");
ESP_LOGI(SYSTEM_TAG, "Reset Reason: %s", state_manager_get_reset_reason_string());
ESP_LOGI(SYSTEM_TAG, "Watchdog Reset: %s", state_manager_was_watchdog_reset() ? "YES" : "NO");
// Motor status
ESP_LOGI(SYSTEM_TAG, "Motor: mode=%s, target=%d%%, current=%d%%, state=%s",
motor_mode_to_string(final_state->mode), final_state->target_speed,
final_state->current_speed, motor_state_to_string(final_state->state));
if (final_state->mode != MOTOR_OFF && final_state->target_speed > 0) {
ESP_LOGI(SYSTEM_TAG, "🔄 Motor restored to: %s @ %d%%",
motor_mode_to_string(final_state->mode), final_state->target_speed);
} else {
ESP_LOGI(SYSTEM_TAG, "⏸️ Motor remains OFF");
}
motor_mode_t last_on_mode;
int last_on_speed;
motor_get_last_on_state(&last_on_mode, &last_on_speed);
ESP_LOGI(SYSTEM_TAG, "Last ON state: %s @ %d%%",
motor_mode_to_string(last_on_mode), last_on_speed);
ESP_LOGI(SYSTEM_TAG, "User turned off: %s", motor_get_user_turned_off() ? "YES" : "NO");
// WiFi status
ESP_LOGI(SYSTEM_TAG, "WiFi: status=%s, SSID=%s, IP=%s, RSSI=%d dBm",
wifi_manager_status_to_string(final_wifi_info.status),
final_wifi_info.ssid, final_ip_str, final_wifi_info.rssi);
// Connection statistics
uint32_t total_attempts, successful_connections;
esp_err_t last_wifi_error;
wifi_manager_get_stats(&total_attempts, &successful_connections, &last_wifi_error);
ESP_LOGI(SYSTEM_TAG, "WiFi Stats: %lu attempts, %lu successful", total_attempts, successful_connections);
ESP_LOGI(SYSTEM_TAG, "Saved state exists: %s", state_manager_has_saved_state() ? "YES" : "NO");
ESP_LOGI(SYSTEM_TAG, "=====================================");
ESP_LOGI(SYSTEM_TAG, "=== Enhanced Maxxfan Controller Ready! ===");
ESP_LOGI(SYSTEM_TAG, "Features: State Preservation, Direction Safety, Motor Ramping, ON Button");
ESP_LOGI(SYSTEM_TAG, "Safety: %d-second cooldown for direction changes", DIRECTION_CHANGE_COOLDOWN_MS / 1000);
ESP_LOGI(SYSTEM_TAG, "Memory: Remembers settings after power loss (except watchdog resets)");
ESP_LOGI(SYSTEM_TAG, "WiFi: Enhanced connection management with auto-reconnect");
ESP_LOGI(SYSTEM_TAG, "State Manager: Enhanced NVS operations with validation and recovery");
if (wifi_manager_is_connected()) {
ESP_LOGI(SYSTEM_TAG, "🌐 Open your browser and go to: http://%s", final_ip_str);
} else {
ESP_LOGI(SYSTEM_TAG, "⚠️ WiFi not connected - check network settings");
}
// Main loop - reset watchdog periodically and update motor cooldown
uint32_t loop_count = 0;
while (1) {
feed_watchdog();
// Update motor cooldown time for status reporting
motor_update_cooldown_time();
// Periodic WiFi status logging (every 30 seconds)
if (++loop_count % 10 == 0) {
wifi_status_t current_status = wifi_manager_get_status();
if (current_status != WIFI_STATUS_CONNECTED) {
ESP_LOGW(SYSTEM_TAG, "WiFi status: %s", wifi_manager_status_to_string(current_status));
}
}
vTaskDelay(pdMS_TO_TICKS(WATCHDOG_FEED_INTERVAL_MS));
}
}