Squashed commit of the following:

commit e8b53dc953
Author: Patedam <pgillen.pro@gmail.com>
Date:   Tue Mar 3 01:15:17 2026 -0500

    Updated backend to make sure it works properly with frontend. Fixed one frontend issue (free heap was not correctly named)

commit 3428808f83
Author: Patedam <pgillen.pro@gmail.com>
Date:   Tue Mar 3 00:36:01 2026 -0500

    Fixing various build errors. Activated minimal build

commit 59364ac22d
Author: Patedam <pgillen.pro@gmail.com>
Date:   Tue Mar 3 00:03:24 2026 -0500

    Added info and reboot api into the backend. Created the basics for a backend server.

commit 37291557eb
Author: Patedam <pgillen.pro@gmail.com>
Date:   Mon Mar 2 23:05:10 2026 -0500

    feat: Add API endpoints for system reboot and retrieving system information.

commit a010b0c352
Author: Patedam <pgillen.pro@gmail.com>
Date:   Mon Mar 2 22:56:13 2026 -0500

    Added AgentTasks into git ignore we will use it to store current tasks so we can createnew contexts chat when its too big

commit 75bab78137
Author: Patedam <pgillen.pro@gmail.com>
Date:   Mon Mar 2 22:42:29 2026 -0500

    feat: Initialize ESP-IDF project with core build configuration, component dependencies, and web frontend deployment.

commit bba4c63f93
Author: Patedam <pgillen.pro@gmail.com>
Date:   Mon Mar 2 22:21:52 2026 -0500

    docs: Add backend architecture documentation for the ESP32-S3 provider.
This commit is contained in:
2026-03-03 01:17:31 -05:00
parent a078977572
commit 2916ad9c99
16 changed files with 571 additions and 26 deletions

View File

@@ -1,2 +1,15 @@
idf_component_register(SRCS "main.cpp"
INCLUDE_DIRS ".")
# Needed as we use minimal build
PRIV_REQUIRES esp_http_server esp_eth
esp_wifi nvs_flash esp_netif vfs
json app_update esp_timer
INCLUDE_DIRS ".")
if(CONFIG_CALENDINK_DEPLOY_WEB_PAGES)
set(WEB_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../frontend")
if(EXISTS ${WEB_SRC_DIR}/dist)
littlefs_create_partition_image(www ${WEB_SRC_DIR}/dist FLASH_IN_PROJECT)
else()
message(FATAL_ERROR "'${WEB_SRC_DIR}/dist' doesn't exist. Run 'npm run build' in frontend/ first.")
endif()
endif()

View File

@@ -25,3 +25,21 @@ menu "CalendarInk Network Configuration"
Number of times to retry the WiFi connection before failing completely.
endmenu
menu "Calendink Web Server"
config CALENDINK_DEPLOY_WEB_PAGES
bool "Deploy web pages to device's LittleFS"
default n
help
If enabled, the frontend dist/ folder will be flashed
to the 'www' LittleFS partition during build.
Disable for fast backend-only iteration.
config CALENDINK_WEB_MOUNT_POINT
string "Website mount point in VFS"
default "/www"
help
VFS path where the LittleFS partition is mounted.
endmenu

View File

@@ -0,0 +1,66 @@
// 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"
internal esp_err_t api_system_info_handler(httpd_req_t *req) {
httpd_resp_set_type(req, "application/json");
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
cJSON *root = cJSON_CreateObject();
esp_chip_info_t chip_info;
esp_chip_info(&chip_info);
const char *model = "ESP32-S3";
if (chip_info.model == CHIP_ESP32) {
model = "ESP32";
} else if (chip_info.model == CHIP_ESP32S2) {
model = "ESP32-S2";
} else if (chip_info.model == CHIP_ESP32S3) {
model = "ESP32-S3";
} else if (chip_info.model == CHIP_ESP32C3) {
model = "ESP32-C3";
}
cJSON_AddStringToObject(root, "chip", model);
uint32_t free_heap = esp_get_free_heap_size();
cJSON_AddNumberToObject(root, "free_heap", free_heap);
uint64_t uptime_sec = esp_timer_get_time() / 1000000;
cJSON_AddNumberToObject(root, "uptime", uptime_sec);
const esp_app_desc_t *app_desc = esp_app_get_description();
cJSON_AddStringToObject(root, "firmware", app_desc->version);
const char *conn_type = "offline";
if (g_Ethernet_Initialized) {
conn_type = "ethernet";
} else if (g_Wifi_Initialized) {
conn_type = "wifi";
}
cJSON_AddStringToObject(root, "connection", conn_type);
const char *sys_info = cJSON_Print(root);
httpd_resp_sendstr(req, sys_info);
free((void *)sys_info);
cJSON_Delete(root);
return ESP_OK;
}
internal const httpd_uri_t api_system_info_uri = {.uri = "/api/system/info",
.method = HTTP_GET,
.handler =
api_system_info_handler,
.user_ctx = NULL};

View File

@@ -0,0 +1,43 @@
#include "cJSON.h"
#include "esp_http_server.h"
#include "esp_system.h"
#include "esp_timer.h"
#include "types.hpp"
internal void restart_timer_callback(void *arg) { esp_restart(); }
internal esp_err_t api_system_reboot_handler(httpd_req_t *req) {
httpd_resp_set_type(req, "application/json");
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
cJSON *root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "status", "rebooting");
const char *response_text = cJSON_Print(root);
httpd_resp_sendstr(req, response_text);
free((void *)response_text);
cJSON_Delete(root);
const esp_timer_create_args_t restart_timer_args = {
.callback = &restart_timer_callback,
.arg = (void *)0,
.dispatch_method = ESP_TIMER_TASK,
.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 (in microseconds) from now to allow HTTP response
// to flush
esp_timer_start_once(restart_timer, 1'000'000);
return ESP_OK;
}
internal const httpd_uri_t api_system_reboot_uri = {
.uri = "/api/system/reboot",
.method = HTTP_POST,
.handler = api_system_reboot_handler,
.user_ctx = NULL};

View File

@@ -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;

View File

@@ -0,0 +1,183 @@
#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
// 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);
}
}

View File

@@ -16,3 +16,4 @@ dependencies:
# public: true
espressif/led_strip: ^3.0.3
espressif/ethernet_init: ^1.3.0
joltwallet/littlefs: "^1.20" # https://github.com/joltwallet/esp_littlefs

View File

@@ -2,7 +2,6 @@
#include <stdio.h>
// SDK
#include "driver/gpio.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
@@ -10,26 +9,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 +42,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 +61,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 +103,30 @@ extern "C" void app_main() {
}
printf("Connected!\n");
vTaskDelay(pdMS_TO_TICKS(5000));
// TODO Main loop
// Start the webserver
web_server = start_webserver();
// Keep the main task alive indefinitely
while (true) {
vTaskDelay(pdMS_TO_TICKS(1000));
}
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();