diff --git a/Provider/main/api/system/info.cpp b/Provider/main/api/system/info.cpp index 2cee812..c6aa9e0 100644 --- a/Provider/main/api/system/info.cpp +++ b/Provider/main/api/system/info.cpp @@ -1,14 +1,17 @@ -#pragma once - +// SDK #include "cJSON.h" #include "esp_app_format.h" +#include "esp_chip_info.h" #include "esp_http_server.h" #include "esp_ota_ops.h" #include "esp_system.h" #include "esp_timer.h" +// Project +#include "appstate.hpp" +#include "types.hpp" -static esp_err_t api_system_info_handler(httpd_req_t *req) { +internal esp_err_t api_system_info_handler(httpd_req_t *req) { httpd_resp_set_type(req, "application/json"); cJSON *root = cJSON_CreateObject(); @@ -38,14 +41,10 @@ static esp_err_t api_system_info_handler(httpd_req_t *req) { const esp_app_desc_t *app_desc = esp_app_get_description(); cJSON_AddStringToObject(root, "firmware", app_desc->version); - // Relying on internal variables from main.cpp due to unity build - extern bool ethernetInitialized; - extern bool wifiInitialized; - const char *conn_type = "offline"; - if (ethernetInitialized) { + if (g_Ethernet_Initialized) { conn_type = "ethernet"; - } else if (wifiInitialized) { + } else if (g_Wifi_Initialized) { conn_type = "wifi"; } cJSON_AddStringToObject(root, "connection", conn_type); @@ -59,8 +58,8 @@ static esp_err_t api_system_info_handler(httpd_req_t *req) { return ESP_OK; } -static const httpd_uri_t api_system_info_uri = {.uri = "/api/system/info", - .method = HTTP_GET, - .handler = - api_system_info_handler, - .user_ctx = NULL}; +internal const httpd_uri_t api_system_info_uri = {.uri = "/api/system/info", + .method = HTTP_GET, + .handler = + api_system_info_handler, + .user_ctx = NULL}; diff --git a/Provider/main/api/system/reboot.cpp b/Provider/main/api/system/reboot.cpp index abd751c..0c2be90 100644 --- a/Provider/main/api/system/reboot.cpp +++ b/Provider/main/api/system/reboot.cpp @@ -1,14 +1,13 @@ -#pragma once - #include "cJSON.h" #include "esp_http_server.h" #include "esp_system.h" #include "esp_timer.h" +#include "types.hpp" -static void restart_timer_callback(void *arg) { esp_restart(); } +internal void restart_timer_callback(void *arg) { esp_restart(); } -static esp_err_t api_system_reboot_handler(httpd_req_t *req) { +internal esp_err_t api_system_reboot_handler(httpd_req_t *req) { httpd_resp_set_type(req, "application/json"); cJSON *root = cJSON_CreateObject(); @@ -24,18 +23,20 @@ static esp_err_t api_system_reboot_handler(httpd_req_t *req) { .callback = &restart_timer_callback, .arg = (void *)0, .dispatch_method = ESP_TIMER_TASK, - .name = "restart_timer"}; + .name = "restart_timer", + .skip_unhandled_events = false}; esp_timer_handle_t restart_timer; esp_timer_create(&restart_timer_args, &restart_timer); - // Schedule reboot 1 second from now to allow HTTP response to flush - esp_timer_start_once(restart_timer, 1000000); + // Schedule reboot 1 second (in microseconds) from now to allow HTTP response + // to flush + esp_timer_start_once(restart_timer, 1'000'000); return ESP_OK; } -static const httpd_uri_t api_system_reboot_uri = {.uri = "/api/system/reboot", - .method = HTTP_POST, - .handler = - api_system_reboot_handler, - .user_ctx = NULL}; +internal const httpd_uri_t api_system_reboot_uri = { + .uri = "/api/system/reboot", + .method = HTTP_POST, + .handler = api_system_reboot_handler, + .user_ctx = NULL}; diff --git a/Provider/main/appstate.hpp b/Provider/main/appstate.hpp new file mode 100644 index 0000000..4e007b9 --- /dev/null +++ b/Provider/main/appstate.hpp @@ -0,0 +1,7 @@ +#pragma once + +#include "types.hpp" + +// Shared Application State (Unity Build) +internal bool g_Ethernet_Initialized = false; +internal bool g_Wifi_Initialized = false; diff --git a/Provider/main/http_server.cpp b/Provider/main/http_server.cpp new file mode 100644 index 0000000..e930950 --- /dev/null +++ b/Provider/main/http_server.cpp @@ -0,0 +1,183 @@ +#include +#include +#include +#include + +// SDK +#include "esp_http_server.h" +#include "esp_log.h" +#include "esp_vfs.h" + +// Project +#include "api/system/info.cpp" +#include "api/system/reboot.cpp" + +internal const char *TAG = "HTTP_SERVER"; + +#define FILE_PATH_MAX (ESP_VFS_PATH_MAX + 128) +#define SCRATCH_BUFSIZE 4096 + +typedef struct { + char scratch[SCRATCH_BUFSIZE]; +} http_server_data_t; + +#ifdef CONFIG_CALENDINK_DEPLOY_WEB_PAGES +// Set HTTP response content type according to file extension +internal esp_err_t set_content_type_from_file(httpd_req_t *req, + const char *filepath) { + const char *type = "text/plain"; + if (strstr(filepath, ".html")) { + type = "text/html"; + } else if (strstr(filepath, ".js")) { + type = "application/javascript"; + } else if (strstr(filepath, ".css")) { + type = "text/css"; + } else if (strstr(filepath, ".png")) { + type = "image/png"; + } else if (strstr(filepath, ".ico")) { + type = "image/x-icon"; + } else if (strstr(filepath, ".svg")) { + type = "text/xml"; + } + return httpd_resp_set_type(req, type); +} + +// Handler to serve static files from LittleFS +internal esp_err_t static_file_handler(httpd_req_t *req) { + char filepath[FILE_PATH_MAX]; + + // Construct real file path + strlcpy(filepath, "/www", sizeof(filepath)); + if (req->uri[strlen(req->uri) - 1] == '/') { + strlcat(filepath, "/index.html", sizeof(filepath)); + } else { + strlcat(filepath, req->uri, sizeof(filepath)); + } + + // Default to index.html if file doesn't exist (SPA routing support) + struct stat file_stat; + if (stat(filepath, &file_stat) == -1) { + // Try gzipped first, then fallback to index.html + char filepath_gz[FILE_PATH_MAX]; + snprintf(filepath_gz, sizeof(filepath_gz), "%s.gz", filepath); + if (stat(filepath_gz, &file_stat) == 0) { + strlcpy(filepath, filepath_gz, sizeof(filepath)); + httpd_resp_set_hdr(req, "Content-Encoding", "gzip"); + } else { + ESP_LOGW(TAG, "File not found: %s, falling back to index.html", filepath); + snprintf(filepath, sizeof(filepath), "%s/index.html", "/www"); + if (stat(filepath, &file_stat) == -1) { + // If index.html doesn't exist, try index.html.gz + snprintf(filepath_gz, sizeof(filepath_gz), "%s/index.html.gz", "/www"); + if (stat(filepath_gz, &file_stat) == 0) { + strlcpy(filepath, filepath_gz, sizeof(filepath)); + httpd_resp_set_hdr(req, "Content-Encoding", "gzip"); + } else { + ESP_LOGE(TAG, "index.html not found too."); + httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "File not found"); + return ESP_FAIL; + } + } + } + } else { + // Since ESP32 handles .gz transparently if we tell it to via headers + // If we requested explicitly a .gz file, set the header + if (strstr(filepath, ".gz")) { + httpd_resp_set_hdr(req, "Content-Encoding", "gzip"); + } + } + + int fd = open(filepath, O_RDONLY, 0); + if (fd == -1) { + ESP_LOGE(TAG, "Failed to open file: %s", filepath); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, + "Failed to read existing file"); + return ESP_FAIL; + } + + set_content_type_from_file(req, filepath); + + http_server_data_t *rest_context = (http_server_data_t *)req->user_ctx; + char *chunk = rest_context->scratch; + ssize_t read_bytes; + + do { + read_bytes = read(fd, chunk, SCRATCH_BUFSIZE); + if (read_bytes == -1) { + ESP_LOGE(TAG, "Failed to read file: %s", filepath); + } else if (read_bytes > 0) { + if (httpd_resp_send_chunk(req, chunk, read_bytes) != ESP_OK) { + close(fd); + ESP_LOGE(TAG, "File sending failed!"); + httpd_resp_sendstr_chunk(req, NULL); // Abort sending + return ESP_FAIL; + } + } + } while (read_bytes > 0); + + close(fd); + httpd_resp_send_chunk(req, NULL, 0); // End response + + return ESP_OK; +} +#endif + +// Handler for CORS Preflight OPTIONS requests +internal esp_err_t cors_options_handler(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"); + httpd_resp_set_status(req, "204 No Content"); + httpd_resp_send(req, NULL, 0); + return ESP_OK; +} + +internal httpd_handle_t start_webserver(void) { + http_server_data_t *rest_context = + (http_server_data_t *)calloc(1, sizeof(http_server_data_t)); + if (rest_context == NULL) { + ESP_LOGE(TAG, "No memory for rest context"); + return NULL; + } + + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.uri_match_fn = httpd_uri_match_wildcard; + config.max_uri_handlers = 10; // We have info, reboot, options, and static + + httpd_handle_t server = NULL; + ESP_LOGI(TAG, "Starting HTTP Server on port: '%d'", config.server_port); + + if (httpd_start(&server, &config) == ESP_OK) { + // Register CORS OPTIONS handler for API routes + httpd_uri_t cors_options_uri = {.uri = "/api/*", + .method = HTTP_OPTIONS, + .handler = cors_options_handler, + .user_ctx = NULL}; + httpd_register_uri_handler(server, &cors_options_uri); + + // Register system API routes + httpd_register_uri_handler(server, &api_system_info_uri); + httpd_register_uri_handler(server, &api_system_reboot_uri); + +#ifdef CONFIG_CALENDINK_DEPLOY_WEB_PAGES + // Register static file handler last as a catch-all wildcard if deployed + httpd_uri_t static_get_uri = {.uri = "/*", + .method = HTTP_GET, + .handler = static_file_handler, + .user_ctx = rest_context}; + httpd_register_uri_handler(server, &static_get_uri); +#endif + + return server; + } + + ESP_LOGE(TAG, "Error starting server!"); + free(rest_context); + return NULL; +} + +internal void stop_webserver(httpd_handle_t server) { + if (server) { + httpd_stop(server); + } +} diff --git a/Provider/main/main.cpp b/Provider/main/main.cpp index 049bc2f..42fee23 100644 --- a/Provider/main/main.cpp +++ b/Provider/main/main.cpp @@ -10,26 +10,32 @@ #include "sdkconfig.h" #include "soc/gpio_num.h" -// Project cpp -#include "connect.cpp" -#include "led_status.cpp" +// Project headers +#include "appstate.hpp" +#include "types.hpp" -// TODO : Make it configurable -internal constexpr bool blockUntilEthernetEstablished = false; -internal bool ethernetInitialized = false; -internal bool wifiInitialized = false; +// Project cpp (Unity Build entry) +// clang-format off +#include "connect.cpp" +#include "http_server.cpp" +#include "led_status.cpp" +// clang-format on + +internal constexpr bool kBlockUntilEthernetEstablished = false; extern "C" void app_main() { printf("Hello, Worldi!\n"); + httpd_handle_t web_server = NULL; + ESP_ERROR_CHECK(nvs_flash_init()); ESP_ERROR_CHECK(esp_event_loop_create_default()); setup_led(); set_led_status(led_status::ConnectingEthernet); - ethernetInitialized = true; - esp_err_t result = connect_ethernet(blockUntilEthernetEstablished); + 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)); @@ -37,7 +43,7 @@ extern "C" void app_main() { } // Check for ethernet connection until its made - if (!blockUntilEthernetEstablished) { + if (!kBlockUntilEthernetEstablished) { uint8 retries = 1; do { set_led_status(led_status::ConnectingEthernet); @@ -56,20 +62,20 @@ extern "C" void app_main() { if (result != ESP_OK) { printf("Ethernet failed, trying wifi\n"); disconnect_ethernet(); - ethernetInitialized = false; + g_Ethernet_Initialized = false; set_led_status(led_status::ConnectingWifi); - wifiInitialized = true; + g_Wifi_Initialized = true; result = connect_wifi(CONFIG_CALENDINK_WIFI_SSID, CONFIG_CALENDINK_WIFI_PASSWORD, - blockUntilEthernetEstablished); + kBlockUntilEthernetEstablished); if (result != ESP_OK) { set_led_status(led_status::Failed); vTaskDelay(pdMS_TO_TICKS(1000)); goto shutdown; } - if (!blockUntilEthernetEstablished) { + if (!kBlockUntilEthernetEstablished) { uint8 retries = 1; do { set_led_status(led_status::ConnectingWifi); @@ -98,20 +104,25 @@ extern "C" void app_main() { } printf("Connected!\n"); - vTaskDelay(pdMS_TO_TICKS(5000)); - // TODO Main loop + // Start the webserver + web_server = start_webserver(); shutdown: printf("Shutting down.\n"); - if (ethernetInitialized) { - disconnect_ethernet(); - ethernetInitialized = false; + if (web_server) { + stop_webserver(web_server); + web_server = NULL; } - if (wifiInitialized) { + + if (g_Ethernet_Initialized) { + disconnect_ethernet(); + g_Ethernet_Initialized = false; + } + if (g_Wifi_Initialized) { disconnect_wifi(); - wifiInitialized = false; + g_Wifi_Initialized = false; } destroy_led();