mirror of
https://github.com/sweidac/kidsbox.git
synced 2026-10-28 06:52:57 +01:00
- Replace Arduino WiFi.h in Networking.cpp with native esp_wifi/esp_netif; uses event-driven connect with FreeRTOS EventGroup and 30 s timeout - Remove initArduino() / Arduino.h from main.cpp - Remove deprecated arduino-esp32 submodule (was uninitialized and marked deprecated since it was first added in Dec 2021) - rc522.h: guard VSPI_HOST → SPI3_HOST for ESP-IDF 5.x compatibility - WebInterface.cpp: drop redundant esp_wifi.h (esp_netif.h is sufficient) - CMakeLists.txt: add esp_wifi + nvs_flash to REQUIRES https://claude.ai/code/session_01MhAnh68DPNnTjcKqjNYDmk
291 lines
9.8 KiB
C++
291 lines
9.8 KiB
C++
#include "WebInterface.hpp"
|
|
#include "AudioPlayer.hpp"
|
|
#include "CacheManager.hpp"
|
|
|
|
#include <esp_http_server.h>
|
|
#include <esp_log.h>
|
|
#include <esp_vfs.h>
|
|
#include <board.h>
|
|
#include <periph_sdcard.h>
|
|
#include <esp_system.h>
|
|
#include <esp_netif.h>
|
|
#include <fcntl.h>
|
|
#include <errno.h>
|
|
#include <string.h>
|
|
#include <dirent.h>
|
|
#include <sys/stat.h>
|
|
#include <string>
|
|
#include <sstream>
|
|
|
|
static const char* TAG = "WebInterface";
|
|
|
|
#define FILE_PATH_MAX (ESP_VFS_PATH_MAX + 128)
|
|
#define SCRATCH_BUFSIZE (10240)
|
|
#define CHECK_FILE_EXTENSION(f, ext) (strcasecmp(&(f)[strlen(f) - strlen(ext)], (ext)) == 0)
|
|
|
|
typedef struct {
|
|
char base_path[ESP_VFS_PATH_MAX + 1];
|
|
char scratch[SCRATCH_BUFSIZE];
|
|
} rest_server_context_t;
|
|
|
|
/* ── helpers ──────────────────────────────────────────────────────────── */
|
|
|
|
static esp_err_t set_content_type(httpd_req_t* req, const char* filepath) {
|
|
const char* type = "text/plain";
|
|
if(CHECK_FILE_EXTENSION(filepath, ".html")) type = "text/html";
|
|
else if(CHECK_FILE_EXTENSION(filepath, ".js")) type = "application/javascript";
|
|
else if(CHECK_FILE_EXTENSION(filepath, ".css")) type = "text/css";
|
|
else if(CHECK_FILE_EXTENSION(filepath, ".png")) type = "image/png";
|
|
else if(CHECK_FILE_EXTENSION(filepath, ".ico")) type = "image/x-icon";
|
|
else if(CHECK_FILE_EXTENSION(filepath, ".svg")) type = "image/svg+xml";
|
|
else if(CHECK_FILE_EXTENSION(filepath, ".json"))type = "application/json";
|
|
return httpd_resp_set_type(req, type);
|
|
}
|
|
|
|
static void send_json(httpd_req_t* req, const std::string& json) {
|
|
httpd_resp_set_type(req, "application/json");
|
|
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
|
|
httpd_resp_sendstr(req, json.c_str());
|
|
}
|
|
|
|
static void send_json_ok(httpd_req_t* req) {
|
|
send_json(req, "{\"ok\":true}");
|
|
}
|
|
|
|
static void send_json_error(httpd_req_t* req, const char* msg) {
|
|
std::string body = std::string("{\"error\":\"") + msg + "\"}";
|
|
httpd_resp_set_status(req, "400 Bad Request");
|
|
send_json(req, body);
|
|
}
|
|
|
|
/* ── static file handler ──────────────────────────────────────────────── */
|
|
|
|
static esp_err_t rest_common_get_handler(httpd_req_t* req) {
|
|
char filepath[FILE_PATH_MAX];
|
|
rest_server_context_t* ctx = (rest_server_context_t*)req->user_ctx;
|
|
strlcpy(filepath, ctx->base_path, sizeof(filepath));
|
|
|
|
if(req->uri[strlen(req->uri) - 1] == '/') {
|
|
strlcat(filepath, "/index.html", sizeof(filepath));
|
|
} else {
|
|
strlcat(filepath, req->uri, sizeof(filepath));
|
|
}
|
|
|
|
int fd = open(filepath, O_RDONLY);
|
|
if(fd == -1) {
|
|
ESP_LOGE(TAG, "Failed to open %s (errno %d)", filepath, errno);
|
|
httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "File not found");
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
set_content_type(req, filepath);
|
|
|
|
char* chunk = ctx->scratch;
|
|
ssize_t n;
|
|
while((n = read(fd, chunk, SCRATCH_BUFSIZE)) > 0) {
|
|
if(httpd_resp_send_chunk(req, chunk, n) != ESP_OK) {
|
|
close(fd);
|
|
httpd_resp_sendstr_chunk(req, NULL);
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Send error");
|
|
return ESP_FAIL;
|
|
}
|
|
}
|
|
close(fd);
|
|
httpd_resp_send_chunk(req, NULL, 0);
|
|
return ESP_OK;
|
|
}
|
|
|
|
/* ── API handlers (use shared_ptrs captured from WebInterface) ──────── */
|
|
|
|
/* GET /api/info — device info JSON */
|
|
static AudioPlayer* g_audio = nullptr;
|
|
static CacheManager* g_cache = nullptr;
|
|
|
|
static esp_err_t api_info_handler(httpd_req_t* req) {
|
|
/* Get device IP */
|
|
esp_netif_ip_info_t ip_info = {};
|
|
esp_netif_t* netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
|
|
if(netif) esp_netif_get_ip_info(netif, &ip_info);
|
|
|
|
char ip_str[16] = "0.0.0.0";
|
|
snprintf(ip_str, sizeof(ip_str), IPSTR, IP2STR(&ip_info.ip));
|
|
|
|
int vol = g_audio ? g_audio->getVolume() : 0;
|
|
bool playing = g_audio ? g_audio->isPlaying() : false;
|
|
size_t cache_bytes = g_cache ? g_cache->totalBytes() : 0;
|
|
|
|
/* Free heap */
|
|
uint32_t free_heap = esp_get_free_heap_size();
|
|
|
|
std::ostringstream json;
|
|
json << "{"
|
|
<< "\"ip\":\"" << ip_str << "\","
|
|
<< "\"volume\":" << vol << ","
|
|
<< "\"playing\":" << (playing ? "true" : "false") << ","
|
|
<< "\"cacheBytes\":" << cache_bytes << ","
|
|
<< "\"freeHeap\":" << free_heap
|
|
<< "}";
|
|
|
|
send_json(req, json.str());
|
|
return ESP_OK;
|
|
}
|
|
|
|
/* GET /api/storage — list cached files as JSON array */
|
|
static esp_err_t api_storage_get_handler(httpd_req_t* req) {
|
|
if(!g_cache) { send_json(req, "[]"); return ESP_OK; }
|
|
|
|
auto entries = g_cache->listEntries();
|
|
std::ostringstream json;
|
|
json << "[";
|
|
for(size_t i = 0; i < entries.size(); i++) {
|
|
if(i > 0) json << ",";
|
|
json << "{\"key\":\"" << entries[i].key << "\","
|
|
<< "\"size\":" << entries[i].size << "}";
|
|
}
|
|
json << "]";
|
|
send_json(req, json.str());
|
|
return ESP_OK;
|
|
}
|
|
|
|
/* DELETE /api/storage?key=<hex> — delete one entry, or all if no key */
|
|
static esp_err_t api_storage_delete_handler(httpd_req_t* req) {
|
|
if(!g_cache) { send_json_ok(req); return ESP_OK; }
|
|
|
|
char key_buf[32] = {};
|
|
if(httpd_req_get_url_query_str(req, key_buf, sizeof(key_buf)) == ESP_OK) {
|
|
char key[32] = {};
|
|
if(httpd_query_key_value(key_buf, "key", key, sizeof(key)) == ESP_OK && key[0]) {
|
|
bool ok = g_cache->deleteEntry(std::string(key));
|
|
if(!ok) { send_json_error(req, "key not found"); return ESP_OK; }
|
|
} else {
|
|
g_cache->deleteAll();
|
|
}
|
|
} else {
|
|
g_cache->deleteAll();
|
|
}
|
|
send_json_ok(req);
|
|
return ESP_OK;
|
|
}
|
|
|
|
/* POST /api/playback body: action=play|pause|stop|volume&value=<0-100> */
|
|
static esp_err_t api_playback_handler(httpd_req_t* req) {
|
|
char buf[128] = {};
|
|
int len = httpd_req_recv(req, buf, sizeof(buf) - 1);
|
|
if(len <= 0) { send_json_error(req, "empty body"); return ESP_OK; }
|
|
buf[len] = '\0';
|
|
|
|
char action[16] = {};
|
|
httpd_query_key_value(buf, "action", action, sizeof(action));
|
|
|
|
if(!g_audio) { send_json_error(req, "audio not ready"); return ESP_OK; }
|
|
|
|
if(strcmp(action, "pause") == 0) {
|
|
g_audio->pause();
|
|
} else if(strcmp(action, "stop") == 0) {
|
|
g_audio->stop();
|
|
} else if(strcmp(action, "volume") == 0) {
|
|
char val_str[8] = {};
|
|
httpd_query_key_value(buf, "value", val_str, sizeof(val_str));
|
|
int vol = atoi(val_str);
|
|
g_audio->setVolume(vol);
|
|
} else {
|
|
send_json_error(req, "unknown action");
|
|
return ESP_OK;
|
|
}
|
|
|
|
send_json_ok(req);
|
|
return ESP_OK;
|
|
}
|
|
|
|
/* GET /api/bluetooth — returns BT status (stub) */
|
|
static esp_err_t api_bluetooth_get_handler(httpd_req_t* req) {
|
|
send_json(req, "{\"enabled\":false,\"note\":\"bluetooth not yet implemented\"}");
|
|
return ESP_OK;
|
|
}
|
|
|
|
/* POST /api/bluetooth body: enabled=true|false (stub) */
|
|
static esp_err_t api_bluetooth_post_handler(httpd_req_t* req) {
|
|
send_json(req, "{\"ok\":false,\"note\":\"bluetooth not yet implemented\"}");
|
|
return ESP_OK;
|
|
}
|
|
|
|
/* ── WebInterface ────────────────────────────────────────────────────── */
|
|
|
|
WebInterface::WebInterface() {}
|
|
|
|
void WebInterface::start() {
|
|
initSD();
|
|
server = start_webserver();
|
|
}
|
|
|
|
void WebInterface::finishResourceRegistrations() {
|
|
registerApiHandlers();
|
|
registerFileServer();
|
|
}
|
|
|
|
void WebInterface::initSD() {
|
|
esp_periph_config_t periph_cfg = DEFAULT_ESP_PERIPH_SET_CONFIG();
|
|
esp_periph_set_handle_t set = esp_periph_set_init(&periph_cfg);
|
|
|
|
if(audio_board_sdcard_init(set, SD_MODE_1_LINE) == ESP_FAIL) {
|
|
ESP_LOGE(TAG, "SDCard not mounted");
|
|
} else {
|
|
ESP_LOGI(TAG, "SDCard mounted");
|
|
}
|
|
}
|
|
|
|
httpd_handle_t WebInterface::start_webserver() {
|
|
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
|
config.lru_purge_enable = true;
|
|
config.uri_match_fn = httpd_uri_match_wildcard;
|
|
config.max_uri_handlers = 16;
|
|
|
|
httpd_handle_t srv = NULL;
|
|
ESP_LOGI(TAG, "Starting server on port %d", config.server_port);
|
|
if(httpd_start(&srv, &config) == ESP_OK) return srv;
|
|
|
|
ESP_LOGE(TAG, "Failed to start server");
|
|
return NULL;
|
|
}
|
|
|
|
void WebInterface::registerResource(httpd_uri_t* config) {
|
|
if(!server) return;
|
|
ESP_LOGI(TAG, "Register %s", config->uri);
|
|
httpd_register_uri_handler(server, config);
|
|
}
|
|
|
|
void WebInterface::registerApiHandlers() {
|
|
/* Expose singletons to the static C handlers */
|
|
g_audio = audioPlayer.get();
|
|
g_cache = cacheManager.get();
|
|
|
|
httpd_uri_t info = { "/api/info", HTTP_GET, api_info_handler, nullptr };
|
|
httpd_uri_t stg_g = { "/api/storage", HTTP_GET, api_storage_get_handler, nullptr };
|
|
httpd_uri_t stg_d = { "/api/storage", HTTP_DELETE, api_storage_delete_handler, nullptr };
|
|
httpd_uri_t pb = { "/api/playback", HTTP_POST, api_playback_handler, nullptr };
|
|
httpd_uri_t bt_g = { "/api/bluetooth", HTTP_GET, api_bluetooth_get_handler, nullptr };
|
|
httpd_uri_t bt_p = { "/api/bluetooth", HTTP_POST, api_bluetooth_post_handler, nullptr };
|
|
|
|
registerResource(&info);
|
|
registerResource(&stg_g);
|
|
registerResource(&stg_d);
|
|
registerResource(&pb);
|
|
registerResource(&bt_g);
|
|
registerResource(&bt_p);
|
|
}
|
|
|
|
void WebInterface::registerFileServer() {
|
|
rest_server_context_t* ctx = (rest_server_context_t*)calloc(1, sizeof(rest_server_context_t));
|
|
if(!ctx) { ESP_LOGE(TAG, "No memory for file server context"); return; }
|
|
|
|
strlcpy(ctx->base_path, "/sdcard/www", sizeof(ctx->base_path));
|
|
|
|
httpd_uri_t file_uri = {
|
|
.uri = "/*",
|
|
.method = HTTP_GET,
|
|
.handler = rest_common_get_handler,
|
|
.user_ctx = ctx
|
|
};
|
|
registerResource(&file_uri);
|
|
}
|