Step 2.1: web server, embedded UI, /api/status, /api/reboot
- aes67_web: httpd (wildcard URI matching), serves web/index.html embedded via EMBED_TXTFILES, status registry (status_register) and route registry (web_register_uri), GET /api/status, POST /api/reboot (restarts 0.5 s after the response). - Status fields come from each core component: aes67_net (ip, aes67_ip, mac, link), aes67_health (fw, uptime_s, heap_free, psram_free), aes67_tx stub (tx_packets, underruns = 0). No ptp object yet, so the UI shows "no data". - Verified on board: / serves index.html byte-identical (27601 B), /api/status returns live values, reboot via API returns in ~12 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,2 +1,5 @@
|
||||
idf_component_register(SRCS "aes67_web.c"
|
||||
INCLUDE_DIRS "include")
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES esp_http_server json
|
||||
PRIV_REQUIRES esp_timer
|
||||
EMBED_TXTFILES "${PROJECT_DIR}/web/index.html")
|
||||
|
||||
@@ -1,7 +1,122 @@
|
||||
#include "aes67_web.h"
|
||||
|
||||
// Stub (build step 0).
|
||||
esp_err_t aes67_web_init(void)
|
||||
#include "esp_log.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_timer.h"
|
||||
|
||||
#define MAX_STATUS_CBS 16
|
||||
#define MAX_URIS 32
|
||||
#define MAX_EARLY_URIS 16
|
||||
|
||||
static const char *TAG = "web";
|
||||
|
||||
extern const char index_html_start[] asm("_binary_index_html_start");
|
||||
extern const char index_html_end[] asm("_binary_index_html_end");
|
||||
|
||||
static httpd_handle_t s_httpd;
|
||||
static status_cb_t s_status_cbs[MAX_STATUS_CBS];
|
||||
static int s_status_n;
|
||||
static httpd_uri_t s_early_uris[MAX_EARLY_URIS];
|
||||
static int s_early_n;
|
||||
|
||||
esp_err_t status_register(status_cb_t cb)
|
||||
{
|
||||
if (s_status_n >= MAX_STATUS_CBS) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
s_status_cbs[s_status_n++] = cb;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t web_register_uri(const httpd_uri_t *uri)
|
||||
{
|
||||
if (s_httpd) {
|
||||
return httpd_register_uri_handler(s_httpd, uri);
|
||||
}
|
||||
if (s_early_n >= MAX_EARLY_URIS) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
s_early_uris[s_early_n++] = *uri;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t web_send_json(httpd_req_t *req, const cJSON *json)
|
||||
{
|
||||
char *body = cJSON_PrintUnformatted(json);
|
||||
if (!body) {
|
||||
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "out of memory");
|
||||
}
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_set_hdr(req, "Cache-Control", "no-store");
|
||||
esp_err_t err = httpd_resp_sendstr(req, body);
|
||||
cJSON_free(body);
|
||||
return err;
|
||||
}
|
||||
|
||||
static esp_err_t index_get(httpd_req_t *req)
|
||||
{
|
||||
httpd_resp_set_type(req, "text/html; charset=utf-8");
|
||||
// EMBED_TXTFILES appends a NUL; don't send it.
|
||||
return httpd_resp_send(req, index_html_start, index_html_end - index_html_start - 1);
|
||||
}
|
||||
|
||||
static esp_err_t status_get(httpd_req_t *req)
|
||||
{
|
||||
cJSON *st = cJSON_CreateObject();
|
||||
for (int i = 0; i < s_status_n; i++) {
|
||||
s_status_cbs[i](st);
|
||||
}
|
||||
esp_err_t err = web_send_json(req, st);
|
||||
cJSON_Delete(st);
|
||||
return err;
|
||||
}
|
||||
|
||||
static void reboot_cb(void *arg)
|
||||
{
|
||||
esp_restart();
|
||||
}
|
||||
|
||||
static esp_err_t reboot_post(httpd_req_t *req)
|
||||
{
|
||||
ESP_LOGW(TAG, "reboot requested via API");
|
||||
httpd_resp_sendstr(req, "");
|
||||
// Let the response go out before restarting.
|
||||
const esp_timer_create_args_t args = { .callback = reboot_cb, .name = "reboot" };
|
||||
esp_timer_handle_t t;
|
||||
if (esp_timer_create(&args, &t) == ESP_OK) {
|
||||
esp_timer_start_once(t, 500 * 1000);
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t aes67_web_start(void)
|
||||
{
|
||||
httpd_config_t cfg = HTTPD_DEFAULT_CONFIG();
|
||||
cfg.max_uri_handlers = MAX_URIS;
|
||||
cfg.stack_size = 8192;
|
||||
cfg.lru_purge_enable = true;
|
||||
cfg.uri_match_fn = httpd_uri_match_wildcard;
|
||||
|
||||
esp_err_t err = httpd_start(&s_httpd, &cfg);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "httpd start failed: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
static const httpd_uri_t core[] = {
|
||||
{ .uri = "/", .method = HTTP_GET, .handler = index_get },
|
||||
{ .uri = "/index.html", .method = HTTP_GET, .handler = index_get },
|
||||
{ .uri = "/api/status", .method = HTTP_GET, .handler = status_get },
|
||||
{ .uri = "/api/reboot", .method = HTTP_POST, .handler = reboot_post },
|
||||
};
|
||||
for (int i = 0; i < sizeof(core) / sizeof(core[0]); i++) {
|
||||
httpd_register_uri_handler(s_httpd, &core[i]);
|
||||
}
|
||||
for (int i = 0; i < s_early_n; i++) {
|
||||
httpd_register_uri_handler(s_httpd, &s_early_uris[i]);
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "httpd started, index.html %d bytes, %d project routes",
|
||||
(int)(index_html_end - index_html_start - 1), s_early_n);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,19 @@
|
||||
// Core component: must not depend on main/ (project code).
|
||||
#pragma once
|
||||
|
||||
#include "cJSON.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_http_server.h"
|
||||
|
||||
esp_err_t aes67_web_init(void);
|
||||
// Adds fields to the /api/status object. Called on every GET /api/status.
|
||||
typedef void (*status_cb_t)(cJSON *status);
|
||||
|
||||
// Registries: call before or after aes67_web_start(); routes registered early are added on start.
|
||||
esp_err_t status_register(status_cb_t cb);
|
||||
esp_err_t web_register_uri(const httpd_uri_t *uri);
|
||||
|
||||
// Start httpd on port 80 with the core routes (/, /api/status, /api/reboot).
|
||||
esp_err_t aes67_web_start(void);
|
||||
|
||||
// Send a cJSON object as the response body (application/json).
|
||||
esp_err_t web_send_json(httpd_req_t *req, const cJSON *json);
|
||||
|
||||
Reference in New Issue
Block a user