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

523 lines
22 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 "freertos/event_groups.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.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"
// WiFi event group
static EventGroupHandle_t s_wifi_event_group;
static int s_retry_num = 0;
// 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));
}
}
}
// WiFi event handler
static void event_handler(void* arg, esp_event_base_t event_base,
int32_t event_id, void* event_data)
{
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
esp_wifi_connect();
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
if (s_retry_num < WIFI_MAXIMUM_RETRY) {
esp_wifi_connect();
s_retry_num++;
ESP_LOGI(SYSTEM_TAG, "retry to connect to the AP");
} else {
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
}
ESP_LOGI(SYSTEM_TAG, "connect to the AP fail");
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
ESP_LOGI(SYSTEM_TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip));
s_retry_num = 0;
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
}
}
// 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);
ESP_LOGI(SYSTEM_TAG, "Status request - Mode: %s, Current: %d%%, Target: %d%%, State: %s, Ramping: %s",
motor_mode_to_string(state->mode), state->current_speed, state->target_speed,
motor_state_to_string(state->state), state->ramping ? "YES" : "NO");
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";
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);
// 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 wifi_init_sta(void)
{
s_wifi_event_group = xEventGroupCreate();
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
esp_event_handler_instance_t instance_any_id;
esp_event_handler_instance_t instance_got_ip;
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
ESP_EVENT_ANY_ID,
&event_handler,
NULL,
&instance_any_id));
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
IP_EVENT_STA_GOT_IP,
&event_handler,
NULL,
&instance_got_ip));
wifi_config_t wifi_config = {
.sta = {
.ssid = WIFI_SSID,
.password = WIFI_PASS,
.threshold.authmode = WIFI_AUTH_WPA2_PSK,
.pmf_cfg = {
.capable = true,
.required = false
},
},
};
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
ESP_LOGI(SYSTEM_TAG, "wifi_init_sta finished.");
EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
pdFALSE,
pdFALSE,
portMAX_DELAY);
if (bits & WIFI_CONNECTED_BIT) {
ESP_LOGI(SYSTEM_TAG, "connected to ap SSID:%s", WIFI_SSID);
} else if (bits & WIFI_FAIL_BIT) {
ESP_LOGI(SYSTEM_TAG, "Failed to connect to SSID:%s", WIFI_SSID);
} else {
ESP_LOGE(SYSTEM_TAG, "UNEXPECTED EVENT");
}
}
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));
}
ESP_LOGI(SYSTEM_TAG, "Connecting to WiFi network: %s", WIFI_SSID);
wifi_init_sta();
// Start HTTP server
start_webserver();
// Report final system state after initialization
const motor_state_t* final_state = motor_get_state();
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");
ESP_LOGI(SYSTEM_TAG, "Final motor state: 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");
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, "State Manager: Enhanced NVS operations with validation and recovery");
ESP_LOGI(SYSTEM_TAG, "Open your browser and go to: http://[ESP32_IP_ADDRESS]");
ESP_LOGI(SYSTEM_TAG, "Check the monitor output above for your IP address");
// Main loop - reset watchdog periodically and update motor cooldown
while (1) {
feed_watchdog();
// Update motor cooldown time for status reporting
motor_update_cooldown_time();
vTaskDelay(pdMS_TO_TICKS(WATCHDOG_FEED_INTERVAL_MS));
}
}