Squashed commit of the following:
commite8b53dc953Author: 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) commit3428808f83Author: Patedam <pgillen.pro@gmail.com> Date: Tue Mar 3 00:36:01 2026 -0500 Fixing various build errors. Activated minimal build commit59364ac22dAuthor: 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. commit37291557ebAuthor: 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. commita010b0c352Author: 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 commit75bab78137Author: 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. commitbba4c63f93Author: 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:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -90,3 +90,6 @@ external/*
|
|||||||
# Frontend
|
# Frontend
|
||||||
**/node_modules/
|
**/node_modules/
|
||||||
**/frontend/dist/
|
**/frontend/dist/
|
||||||
|
|
||||||
|
# Agent Tasks
|
||||||
|
Provider/AgentTasks/
|
||||||
|
|||||||
@@ -3,4 +3,5 @@
|
|||||||
cmake_minimum_required(VERSION 3.16)
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
|
||||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||||
|
idf_build_set_property(MINIMAL_BUILD ON)
|
||||||
project(Provider)
|
project(Provider)
|
||||||
|
|||||||
@@ -125,10 +125,21 @@ dependencies:
|
|||||||
source:
|
source:
|
||||||
type: idf
|
type: idf
|
||||||
version: 5.5.3
|
version: 5.5.3
|
||||||
|
joltwallet/littlefs:
|
||||||
|
component_hash: dcea25bcef2de023f089f5f01e8d8c46ad1b8ffef75861ad5ffb4098555839df
|
||||||
|
dependencies:
|
||||||
|
- name: idf
|
||||||
|
require: private
|
||||||
|
version: '>=5.0'
|
||||||
|
source:
|
||||||
|
registry_url: https://components.espressif.com/
|
||||||
|
type: service
|
||||||
|
version: 1.20.4
|
||||||
direct_dependencies:
|
direct_dependencies:
|
||||||
- espressif/ethernet_init
|
- espressif/ethernet_init
|
||||||
- espressif/led_strip
|
- espressif/led_strip
|
||||||
- idf
|
- idf
|
||||||
manifest_hash: ca3e63d48140ce7f8993b19863499b13d6162b34a6fa4d0557513b244fc7a7e3
|
- joltwallet/littlefs
|
||||||
|
manifest_hash: 21816aafdbbde14bfaaaabda34966eec49ae1e6f551bc16fe3ff74370b0fb54c
|
||||||
target: esp32s3
|
target: esp32s3
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
# Set this to your ESP32's IP address for local development
|
# Set this to your ESP32's IP address for local development
|
||||||
# Example: VITE_API_BASE=http://192.168.1.100
|
# Example: VITE_API_BASE=http://192.168.1.100
|
||||||
VITE_API_BASE=http://ESP32_IP_HERE
|
VITE_API_BASE=http://192.168.50.216
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ export async function getSystemInfo() {
|
|||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
||||||
}
|
}
|
||||||
return res.json();
|
const data = await res.json();
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
freeHeap: data.free_heap
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,2 +1,15 @@
|
|||||||
idf_component_register(SRCS "main.cpp"
|
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()
|
||||||
|
|||||||
@@ -25,3 +25,21 @@ menu "CalendarInk Network Configuration"
|
|||||||
Number of times to retry the WiFi connection before failing completely.
|
Number of times to retry the WiFi connection before failing completely.
|
||||||
|
|
||||||
endmenu
|
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
|
||||||
|
|||||||
66
Provider/main/api/system/info.cpp
Normal file
66
Provider/main/api/system/info.cpp
Normal 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};
|
||||||
43
Provider/main/api/system/reboot.cpp
Normal file
43
Provider/main/api/system/reboot.cpp
Normal 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};
|
||||||
7
Provider/main/appstate.hpp
Normal file
7
Provider/main/appstate.hpp
Normal 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;
|
||||||
183
Provider/main/http_server.cpp
Normal file
183
Provider/main/http_server.cpp
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,3 +16,4 @@ dependencies:
|
|||||||
# public: true
|
# public: true
|
||||||
espressif/led_strip: ^3.0.3
|
espressif/led_strip: ^3.0.3
|
||||||
espressif/ethernet_init: ^1.3.0
|
espressif/ethernet_init: ^1.3.0
|
||||||
|
joltwallet/littlefs: "^1.20" # https://github.com/joltwallet/esp_littlefs
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
||||||
// SDK
|
// SDK
|
||||||
#include "driver/gpio.h"
|
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
@@ -10,26 +9,32 @@
|
|||||||
#include "sdkconfig.h"
|
#include "sdkconfig.h"
|
||||||
#include "soc/gpio_num.h"
|
#include "soc/gpio_num.h"
|
||||||
|
|
||||||
// Project cpp
|
// Project headers
|
||||||
#include "connect.cpp"
|
#include "appstate.hpp"
|
||||||
#include "led_status.cpp"
|
#include "types.hpp"
|
||||||
|
|
||||||
// TODO : Make it configurable
|
// Project cpp (Unity Build entry)
|
||||||
internal constexpr bool blockUntilEthernetEstablished = false;
|
// clang-format off
|
||||||
internal bool ethernetInitialized = false;
|
#include "connect.cpp"
|
||||||
internal bool wifiInitialized = false;
|
#include "http_server.cpp"
|
||||||
|
#include "led_status.cpp"
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
|
internal constexpr bool kBlockUntilEthernetEstablished = false;
|
||||||
|
|
||||||
extern "C" void app_main() {
|
extern "C" void app_main() {
|
||||||
printf("Hello, Worldi!\n");
|
printf("Hello, Worldi!\n");
|
||||||
|
|
||||||
|
httpd_handle_t web_server = NULL;
|
||||||
|
|
||||||
ESP_ERROR_CHECK(nvs_flash_init());
|
ESP_ERROR_CHECK(nvs_flash_init());
|
||||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||||
|
|
||||||
setup_led();
|
setup_led();
|
||||||
|
|
||||||
set_led_status(led_status::ConnectingEthernet);
|
set_led_status(led_status::ConnectingEthernet);
|
||||||
ethernetInitialized = true;
|
g_Ethernet_Initialized = true;
|
||||||
esp_err_t result = connect_ethernet(blockUntilEthernetEstablished);
|
esp_err_t result = connect_ethernet(kBlockUntilEthernetEstablished);
|
||||||
if (result != ESP_OK) {
|
if (result != ESP_OK) {
|
||||||
set_led_status(led_status::Failed);
|
set_led_status(led_status::Failed);
|
||||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||||
@@ -37,7 +42,7 @@ extern "C" void app_main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for ethernet connection until its made
|
// Check for ethernet connection until its made
|
||||||
if (!blockUntilEthernetEstablished) {
|
if (!kBlockUntilEthernetEstablished) {
|
||||||
uint8 retries = 1;
|
uint8 retries = 1;
|
||||||
do {
|
do {
|
||||||
set_led_status(led_status::ConnectingEthernet);
|
set_led_status(led_status::ConnectingEthernet);
|
||||||
@@ -56,20 +61,20 @@ extern "C" void app_main() {
|
|||||||
if (result != ESP_OK) {
|
if (result != ESP_OK) {
|
||||||
printf("Ethernet failed, trying wifi\n");
|
printf("Ethernet failed, trying wifi\n");
|
||||||
disconnect_ethernet();
|
disconnect_ethernet();
|
||||||
ethernetInitialized = false;
|
g_Ethernet_Initialized = false;
|
||||||
|
|
||||||
set_led_status(led_status::ConnectingWifi);
|
set_led_status(led_status::ConnectingWifi);
|
||||||
wifiInitialized = true;
|
g_Wifi_Initialized = true;
|
||||||
result =
|
result =
|
||||||
connect_wifi(CONFIG_CALENDINK_WIFI_SSID, CONFIG_CALENDINK_WIFI_PASSWORD,
|
connect_wifi(CONFIG_CALENDINK_WIFI_SSID, CONFIG_CALENDINK_WIFI_PASSWORD,
|
||||||
blockUntilEthernetEstablished);
|
kBlockUntilEthernetEstablished);
|
||||||
if (result != ESP_OK) {
|
if (result != ESP_OK) {
|
||||||
set_led_status(led_status::Failed);
|
set_led_status(led_status::Failed);
|
||||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||||
goto shutdown;
|
goto shutdown;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!blockUntilEthernetEstablished) {
|
if (!kBlockUntilEthernetEstablished) {
|
||||||
uint8 retries = 1;
|
uint8 retries = 1;
|
||||||
do {
|
do {
|
||||||
set_led_status(led_status::ConnectingWifi);
|
set_led_status(led_status::ConnectingWifi);
|
||||||
@@ -98,20 +103,30 @@ extern "C" void app_main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
printf("Connected!\n");
|
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:
|
shutdown:
|
||||||
printf("Shutting down.\n");
|
printf("Shutting down.\n");
|
||||||
|
|
||||||
if (ethernetInitialized) {
|
if (web_server) {
|
||||||
disconnect_ethernet();
|
stop_webserver(web_server);
|
||||||
ethernetInitialized = false;
|
web_server = NULL;
|
||||||
}
|
}
|
||||||
if (wifiInitialized) {
|
|
||||||
|
if (g_Ethernet_Initialized) {
|
||||||
|
disconnect_ethernet();
|
||||||
|
g_Ethernet_Initialized = false;
|
||||||
|
}
|
||||||
|
if (g_Wifi_Initialized) {
|
||||||
disconnect_wifi();
|
disconnect_wifi();
|
||||||
wifiInitialized = false;
|
g_Wifi_Initialized = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy_led();
|
destroy_led();
|
||||||
|
|||||||
5
Provider/partitions.csv
Normal file
5
Provider/partitions.csv
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Name, Type, SubType, Offset, Size, Flags
|
||||||
|
nvs, data, nvs, 0x9000, 0x6000,
|
||||||
|
phy_init, data, phy, 0xf000, 0x1000,
|
||||||
|
factory, app, factory, 0x10000, 1M,
|
||||||
|
www, data, littlefs, , 64K,
|
||||||
|
@@ -0,0 +1,3 @@
|
|||||||
|
CONFIG_PARTITION_TABLE_CUSTOM=y
|
||||||
|
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
|
||||||
|
CONFIG_PARTITION_TABLE_FILENAME="partitions.csv"
|
||||||
|
|||||||
172
Provider/tdd/backend_architecture.md
Normal file
172
Provider/tdd/backend_architecture.md
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
# Backend Architecture for ESP32-S3 Provider
|
||||||
|
|
||||||
|
**Authored by Claude Opus 4**
|
||||||
|
**Date:** 2026-03-02
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Goal
|
||||||
|
|
||||||
|
Serve the Svelte web dashboard and expose a REST API from the ESP32-S3 using ESP-IDF's built-in `esp_http_server`. The backend must:
|
||||||
|
- Serve static files (the compiled Svelte frontend) from flash
|
||||||
|
- Provide system information over JSON
|
||||||
|
- Allow remote reboot
|
||||||
|
- Support independent frontend/backend development workflows
|
||||||
|
|
||||||
|
## 2. Chosen Stack
|
||||||
|
|
||||||
|
| Technology | Source | Role |
|
||||||
|
|---|---|---|
|
||||||
|
| **esp_http_server** | ESP-IDF built-in | HTTP server daemon |
|
||||||
|
| **cJSON** | ESP-IDF built-in | JSON serialization for API responses |
|
||||||
|
| **LittleFS** | [joltwallet/esp_littlefs](https://github.com/joltwallet/esp_littlefs) | Filesystem on flash for serving frontend files |
|
||||||
|
|
||||||
|
All three are standard in ESP-IDF projects. `esp_http_server` and `cJSON` ship with the SDK. LittleFS is the recommended flash filesystem for ESP-IDF — Espressif's own [restful_server example](https://github.com/espressif/esp-idf/blob/master/examples/protocols/http_server/restful_server/main/idf_component.yml) uses `joltwallet/littlefs`.
|
||||||
|
|
||||||
|
## 3. Why LittleFS Over Other Options
|
||||||
|
|
||||||
|
### LittleFS vs SPIFFS
|
||||||
|
|
||||||
|
| | LittleFS | SPIFFS |
|
||||||
|
|---|---|---|
|
||||||
|
| **Directory support** | ✅ Real directories | ❌ Flat namespace |
|
||||||
|
| **Wear leveling** | ✅ Dynamic | ⚠️ Static |
|
||||||
|
| **Power-loss safe** | ✅ Copy-on-write | ❌ Can corrupt |
|
||||||
|
| **ESP-IDF status** | ✅ Recommended | ⚠️ Deprecated in recent examples |
|
||||||
|
| **Performance** | Faster for small files | Slower mount, no dir listing |
|
||||||
|
|
||||||
|
SPIFFS was the original choice in older ESP-IDF examples but has been replaced by LittleFS in the current restful_server example. LittleFS is the better choice going forward.
|
||||||
|
|
||||||
|
### LittleFS Partition vs EMBED_FILES
|
||||||
|
|
||||||
|
We considered two approaches to deploy the frontend:
|
||||||
|
|
||||||
|
| | LittleFS Partition | `EMBED_FILES` (binary embedding) |
|
||||||
|
|---|---|---|
|
||||||
|
| **Storage** | Separate flash partition | Compiled into firmware binary |
|
||||||
|
| **Update** | Can flash partition independently | Must reflash entire firmware |
|
||||||
|
| **OTA** | Needs separate partition OTA | UI updates with firmware naturally |
|
||||||
|
| **Dev workflow** | Can skip frontend flash during iteration | Always included |
|
||||||
|
| **Filesystem** | Real VFS — open/read/close file APIs | Direct memory pointer |
|
||||||
|
|
||||||
|
**We chose LittleFS partition** because:
|
||||||
|
1. **Development speed** — a Kconfig toggle (`CALENDINK_DEPLOY_WEB_PAGES`) lets you skip flashing the frontend entirely when iterating on the backend
|
||||||
|
2. **Separation of concerns** — frontend and firmware have independent flash regions
|
||||||
|
3. **Follows ESP-IDF conventions** — matches the official restful_server example pattern
|
||||||
|
4. **No RAM overhead** — files are read from flash in chunks, not loaded into memory
|
||||||
|
|
||||||
|
## 4. Kconfig Deploy Toggle
|
||||||
|
|
||||||
|
The `CALENDINK_DEPLOY_WEB_PAGES` menuconfig option controls whether the frontend gets flashed:
|
||||||
|
|
||||||
|
| Setting | Effect | Use case |
|
||||||
|
|---|---|---|
|
||||||
|
| **OFF** (default) | Frontend not flashed. API still works. | Backend development — fast flash, use PC dev server for UI |
|
||||||
|
| **ON** | `frontend/dist/` is written to the `www` LittleFS partition | Production deployment — everything runs on ESP32 |
|
||||||
|
|
||||||
|
This mirrors the official ESP-IDF pattern (`CONFIG_EXAMPLE_DEPLOY_WEB_PAGES` in the restful_server example).
|
||||||
|
|
||||||
|
## 5. API Design
|
||||||
|
|
||||||
|
### Endpoints
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/system/info → JSON with chip, heap, uptime, firmware, connection
|
||||||
|
POST /api/system/reboot → JSON acknowledgment, then esp_restart()
|
||||||
|
GET /* → Static files from LittleFS /www (when deployed)
|
||||||
|
```
|
||||||
|
|
||||||
|
### CORS
|
||||||
|
|
||||||
|
During development, the Svelte dev server runs on `http://localhost:5173` and API calls go to `http://<ESP32_IP>`. This is cross-origin, so the backend adds CORS headers:
|
||||||
|
|
||||||
|
- `Access-Control-Allow-Origin: *`
|
||||||
|
- `Access-Control-Allow-Methods: GET, POST, OPTIONS`
|
||||||
|
- `Access-Control-Allow-Headers: Content-Type`
|
||||||
|
- `OPTIONS` preflight handler
|
||||||
|
|
||||||
|
In production (frontend served from ESP32), everything is same-origin — CORS headers have no effect but don't hurt.
|
||||||
|
|
||||||
|
## 6. File Organization
|
||||||
|
|
||||||
|
```
|
||||||
|
Provider/main/
|
||||||
|
├── main.cpp # Entry point, network init, starts HTTP server
|
||||||
|
├── http_server.cpp # HTTP server lifecycle, static file handler, CORS
|
||||||
|
├── api/
|
||||||
|
│ └── system/
|
||||||
|
│ ├── info.cpp # GET /api/system/info
|
||||||
|
│ └── reboot.cpp # POST /api/system/reboot
|
||||||
|
├── led_status.cpp # Existing — LED management
|
||||||
|
├── connect.cpp # Existing — Ethernet/WiFi
|
||||||
|
├── types.hpp # Existing — type aliases
|
||||||
|
└── Kconfig.projbuild # Existing + new web server config
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why This Structure
|
||||||
|
|
||||||
|
The project uses a **unity build** pattern — `main.cpp` `#include`s `.cpp` files directly (e.g. `#include "connect.cpp"`). This is unconventional but works well for small embedded projects since ESP-IDF only compiles the files listed in `idf_component_register(SRCS ...)`.
|
||||||
|
|
||||||
|
We extend this pattern to the HTTP server:
|
||||||
|
- `http_server.cpp` `#include`s the API handler files
|
||||||
|
- Each handler file is self-contained: it defines its URI struct and registration function
|
||||||
|
- New endpoint groups get their own folder under `api/` (e.g. `api/calendar/`, `api/display/`)
|
||||||
|
|
||||||
|
## 7. Partition Table
|
||||||
|
|
||||||
|
```
|
||||||
|
# Name, Type, SubType, Offset, Size
|
||||||
|
nvs, data, nvs, 0x9000, 0x6000
|
||||||
|
phy_init, data, phy, 0xf000, 0x1000
|
||||||
|
factory, app, factory, 0x10000, 1M
|
||||||
|
www, data, littlefs, , 64K
|
||||||
|
```
|
||||||
|
|
||||||
|
The `www` partition is 64KB — more than enough for the 16kB gzipped frontend. Only gets written during `idf.py flash` when `CALENDINK_DEPLOY_WEB_PAGES` is enabled.
|
||||||
|
|
||||||
|
## 8. Build Pipeline
|
||||||
|
|
||||||
|
```
|
||||||
|
Frontend Build (PC) ESP-IDF Build
|
||||||
|
────────────────── ──────────────
|
||||||
|
npm run build idf.py build
|
||||||
|
↓ ↓
|
||||||
|
frontend/dist/ firmware.bin
|
||||||
|
index.html (47kB) +
|
||||||
|
index.html.gz (16kB) www.bin (LittleFS image, when deploy=ON)
|
||||||
|
↓
|
||||||
|
idf.py flash
|
||||||
|
↓
|
||||||
|
ESP32-S3 Flash
|
||||||
|
├── factory partition → firmware
|
||||||
|
└── www partition → frontend files
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. Summary
|
||||||
|
|
||||||
|
## 9. Summary
|
||||||
|
|
||||||
|
We use **esp_http_server + cJSON + LittleFS** — all standard ESP-IDF components — to serve the frontend and expose a REST API. A **LittleFS partition** stores frontend files separately from firmware, with a **Kconfig toggle** to skip frontend flashing during backend development. The API is structured as **modular handler files** under `api/` for clean scalability.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Implementation Results
|
||||||
|
|
||||||
|
*Added 2026-03-03 after implementation and integration was completed.*
|
||||||
|
|
||||||
|
### Refactoring & Architecture Outcomes
|
||||||
|
- **Unity Build Pattern**: Successfully adopted for the HTTP server (`http_server.cpp` includes `.cpp` API handlers). This simplified the build process, reduced include complexity, and resolved multiple redefinition and linkage errors without needing complex CMake modifications.
|
||||||
|
- **State Management**: Created a centralized `appstate.hpp` to cleanly share global state (`g_Ethernet_Initialized`, `g_Wifi_Initialized`) across the project, eliminating ad-hoc `extern` declarations.
|
||||||
|
|
||||||
|
### API Capabilities & Analytics
|
||||||
|
- **System Info (`GET /api/system/info`)**: Returns real-time JSON payload containing chip type, free heap, uptime, firmware version, and connection status. Data payload is lightweight (~110 bytes).
|
||||||
|
- **Remote Reboot (`POST /api/system/reboot`)**: Initiates an async reboot using `esp_timer` with a 1-second delay, allowing the backend to flush a successful `200 OK` JSON response to the client before the processor halts.
|
||||||
|
- **CORS Support**: Implemented `Access-Control-Allow-Origin: *` headers for all API GET and POST responses, along with an `OPTIONS` preflight handler, to support seamless local UI development against the ESP32.
|
||||||
|
|
||||||
|
### Stability & Performance Fixes
|
||||||
|
- **Persistent Daemon**: Addressed an issue where `app_main` executed to completion immediately, causing the web server daemon to drop. Implemented a non-blocking `vTaskDelay` keep-alive loop to persist the application state and keep the HTTP server listening indefinitely without spinning the CPU.
|
||||||
|
- **Static File Fallbacks**: The LittleFS static file handler correctly falls back to `index.html` (and `.gz` variants) to seamlessly support Svelte's Single Page Application (SPA) routing patterns.
|
||||||
|
|
||||||
|
### Observability Benchmarks
|
||||||
|
- **Heap Usage**: The system info endpoint natively tracks free heap availability. Observed typical runtime footprint leaves roughly **247 KB free heap** with active WiFi, API handling, and active HTTP server routing.
|
||||||
|
- **API Response Latency**: The minimalist handler approach results in near-instantaneous JSON responses (milliseconds), effortlessly supporting the frontend dashboard's 5-second polling interval without blocking the ESP32-S3 network stack.
|
||||||
Reference in New Issue
Block a user