// STD Lib #include #include // SDK #include "esp_log.h" #include "esp_ota_ops.h" #include "esp_pm.h" #include "esp_psram.h" #include "esp_system.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "nvs.h" #include "nvs_flash.h" #include "sdkconfig.h" #include "soc/gpio_num.h" // Project headers #include "appstate.hpp" #include "types.hpp" // Project cpp (Unity Build entry) // clang-format off #include "led_status.cpp" #include "connect.cpp" #include "http_server.cpp" #include "mdns_service.cpp" #include "udp_logger.cpp" #include "lodepng_alloc.cpp" #include "lodepng/lodepng.cpp" #include "lv_setup.cpp" // clang-format on internal const char *kTagMain = "MAIN"; // Global Application State Definitions bool g_Ethernet_Initialized = false; bool g_Wifi_Initialized = false; uint8_t g_Active_WWW_Partition = 0; constexpr bool kBlockUntilEthernetEstablished = false; internal void my_timer_callback(void *arg) { ESP_LOGI(kTagMain, "Timer finished! Turning Led Off..."); destroy_led(); } extern "C" void app_main() { ESP_LOGI(kTagMain, "Hello, Calendink OTA! [V0.1.1]"); ESP_LOGI(kTagMain, "PSRAM size: %d bytes", esp_psram_get_size()); #if CONFIG_PM_ENABLE esp_pm_config_t pm_config = {}; pm_config.max_freq_mhz = 240; pm_config.min_freq_mhz = 40; #if CONFIG_CALENDINK_ALLOW_LIGHT_SLEEP pm_config.light_sleep_enable = true; #else pm_config.light_sleep_enable = false; #endif esp_pm_configure(&pm_config); ESP_LOGI(kTagMain, "Dynamic Power Management initialized. Light sleep %s.", pm_config.light_sleep_enable ? "ENABLED" : "DISABLED"); #endif httpd_handle_t web_server = NULL; esp_err_t err = nvs_flash_init(); if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { ESP_ERROR_CHECK(nvs_flash_erase()); err = nvs_flash_init(); } ESP_ERROR_CHECK(err); nvs_handle_t my_handle; if (nvs_open("storage", NVS_READWRITE, &my_handle) == ESP_OK) { // Read active www partition from NVS err = nvs_get_u8(my_handle, "www_part", &g_Active_WWW_Partition); if (err == ESP_OK) { ESP_LOGI(kTagMain, "NVS: Found active www partition: %d", g_Active_WWW_Partition); } if (err == ESP_ERR_NVS_NOT_FOUND) { // First boot (no NVS key yet): default to www_0 ESP_LOGI(kTagMain, "No www_part in NVS, defaulting to 0."); g_Active_WWW_Partition = 0; nvs_set_u8(my_handle, "www_part", 0); nvs_commit(my_handle); } else if (err != ESP_OK) { ESP_LOGE(kTagMain, "Error reading www_part from NVS: %s", esp_err_to_name(err)); g_Active_WWW_Partition = 0; } if (g_Active_WWW_Partition > 1) { g_Active_WWW_Partition = 0; } nvs_close(my_handle); } else { ESP_LOGE(kTagMain, "Error opening NVS handle!"); } // Detect if this is the first boot after a new flash (Firmware or Frontend) bool is_new_flash = false; if (nvs_open("storage", NVS_READWRITE, &my_handle) == ESP_OK) { // 1. Check Firmware Compile Time char last_time[64] = {0}; size_t time_size = sizeof(last_time); const char *current_time = __DATE__ " " __TIME__; if (nvs_get_str(my_handle, "last_fw_time", last_time, &time_size) != ESP_OK || strcmp(last_time, current_time) != 0) { ESP_LOGI(kTagMain, "New firmware detected! (Last: %s, Current: %s)", last_time[0] ? last_time : "None", current_time); is_new_flash = true; nvs_set_str(my_handle, "last_fw_time", current_time); } // 2. Check Frontend Partition Fingerprint (www_0) const esp_partition_t *www0_p = esp_partition_find_first( ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_LITTLEFS, "www_0"); if (www0_p) { uint8_t current_sha[32]; if (esp_partition_get_sha256(www0_p, current_sha) == ESP_OK) { uint8_t last_sha[32] = {0}; size_t sha_size = sizeof(last_sha); if (nvs_get_blob(my_handle, "www0_sha", last_sha, &sha_size) != ESP_OK || memcmp(last_sha, current_sha, 32) != 0) { ESP_LOGI(kTagMain, "New frontend partition detected via SHA256!"); is_new_flash = true; nvs_set_blob(my_handle, "www0_sha", current_sha, 32); } } } if (is_new_flash) { nvs_commit(my_handle); } nvs_close(my_handle); } // If we are running from FACTORY and a new flash was detected, override to // www_0 { const esp_partition_t *running = esp_ota_get_running_partition(); if (running != NULL && running->subtype == ESP_PARTITION_SUBTYPE_APP_FACTORY) { if (is_new_flash && g_Active_WWW_Partition != 0) { ESP_LOGW(kTagMain, "FACTORY APP + NEW FLASH: Overriding www_part to 0 (was %d)", g_Active_WWW_Partition); g_Active_WWW_Partition = 0; // Persist the override if (nvs_open("storage", NVS_READWRITE, &my_handle) == ESP_OK) { nvs_set_u8(my_handle, "www_part", 0); nvs_commit(my_handle); nvs_close(my_handle); } } } } ESP_ERROR_CHECK(esp_event_loop_create_default()); setup_led(); set_led_status(led_status::ConnectingEthernet); g_Ethernet_Initialized = true; esp_err_t result = connect_ethernet(kBlockUntilEthernetEstablished); if (result != ESP_OK) { set_led_status(led_status::Failed); vTaskDelay(pdMS_TO_TICKS(1000)); goto shutdown; } // Check for ethernet connection until its made if (!kBlockUntilEthernetEstablished) { uint8 retries = 1; do { set_led_status(led_status::ConnectingEthernet); result = check_ethernet_connection(retries); if (result == ESP_ERR_INVALID_STATE) { ESP_LOGW(kTagMain, "Ethernet cable not plugged in, skipping retries."); break; } if (result != ESP_OK) { set_led_status(led_status::Failed); vTaskDelay(pdMS_TO_TICKS(1000)); } retries++; } while (result == ESP_ERR_TIMEOUT && retries <= CONFIG_CALENDINK_ETH_RETRIES); } if (result != ESP_OK) { ESP_LOGW(kTagMain, "Ethernet failed, trying wifi"); disconnect_ethernet(); g_Ethernet_Initialized = false; set_led_status(led_status::ConnectingWifi); g_Wifi_Initialized = true; result = connect_wifi(CONFIG_CALENDINK_WIFI_SSID, CONFIG_CALENDINK_WIFI_PASSWORD, kBlockUntilEthernetEstablished); if (result != ESP_OK) { set_led_status(led_status::Failed); vTaskDelay(pdMS_TO_TICKS(1000)); goto shutdown; } if (!kBlockUntilEthernetEstablished) { uint8 retries = 1; do { set_led_status(led_status::ConnectingWifi); result = check_wifi_connection(retries); if (result != ESP_OK) { set_led_status(led_status::Failed); vTaskDelay(pdMS_TO_TICKS(1000)); } retries++; } while (result == ESP_ERR_TIMEOUT && retries <= CONFIG_CALENDINK_WIFI_RETRIES); } if (result != ESP_OK) { ESP_LOGE(kTagMain, "Wifi failed."); goto shutdown; } set_led_status(led_status::ReadyWifi); ESP_LOGI(kTagMain, "Will use Wifi!"); } else { set_led_status(led_status::ReadyEthernet); ESP_LOGI(kTagMain, "Will use Ethernet!"); } ESP_LOGI(kTagMain, "Connected! IP acquired."); #if !defined(NDEBUG) start_udp_logging(514); #endif // Start LVGL ESP_LOGI(kTagMain, "ABOUT TO START LVGL"); vTaskDelay(pdMS_TO_TICKS(500)); setup_lvgl(); ESP_LOGI(kTagMain, "LVGL STARTED"); vTaskDelay(pdMS_TO_TICKS(500)); // Start the webserver web_server = start_webserver(); // Start mDNS start_mdns(); // Mark the current app as valid to cancel rollback, only if it's an OTA app { const esp_partition_t *running = esp_ota_get_running_partition(); if (running != NULL && running->subtype >= ESP_PARTITION_SUBTYPE_APP_OTA_MIN && running->subtype < ESP_PARTITION_SUBTYPE_APP_OTA_MAX) { esp_ota_mark_app_valid_cancel_rollback(); } } { const esp_timer_create_args_t timer_args = {.callback = &my_timer_callback, .arg = nullptr, .dispatch_method = ESP_TIMER_TASK, .name = "Led Turn Off", .skip_unhandled_events = true}; // Create and start timer if needed, or this was just stub code? esp_timer_handle_t timer_handle; if (esp_timer_create(&timer_args, &timer_handle) == ESP_OK) { esp_timer_start_once(timer_handle, 5'000'000); // 5 sec cooldown } } // Keep the main task alive indefinitely { #if CONFIG_PM_PROFILING int pm_dump_counter = 0; #endif while (true) { vTaskDelay(pdMS_TO_TICKS(100)); #if CONFIG_PM_PROFILING if (++pm_dump_counter >= 50) { // Every 5 seconds pm_dump_counter = 0; ESP_LOGI(kTagMain, "--- PM Profiling Dump ---"); char *ptr = nullptr; size_t size = 0; FILE *f = open_memstream(&ptr, &size); if (f != nullptr) { esp_pm_dump_locks(f); fclose(f); if (ptr != nullptr) { char *saveptr; char *line = strtok_r(ptr, "\n", &saveptr); while (line != nullptr) { ESP_LOGI(kTagMain, "%s", line); line = strtok_r(nullptr, "\n", &saveptr); } free(ptr); } } } #endif } } shutdown: ESP_LOGE(kTagMain, "Shutting down."); if (web_server) { stop_webserver(web_server, g_Active_WWW_Partition); web_server = NULL; } if (g_Ethernet_Initialized) { disconnect_ethernet(); g_Ethernet_Initialized = false; } if (g_Wifi_Initialized) { disconnect_wifi(); g_Wifi_Initialized = false; } destroy_led(); ESP_ERROR_CHECK(esp_event_loop_delete_default()); ESP_ERROR_CHECK(nvs_flash_deinit()); }