From 35275b8ccf996391f1c03cc39b6fd13e27d47920 Mon Sep 17 00:00:00 2001 From: Ben Nicholson Date: Thu, 24 Sep 2026 22:56:06 +1000 Subject: [PATCH] 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 --- components/aes67_health/CMakeLists.txt | 3 +- components/aes67_health/aes67_health.c | 16 ++- .../aes67_health/include/aes67_health.h | 1 + components/aes67_net/CMakeLists.txt | 3 +- components/aes67_net/aes67_net.c | 33 ++++- components/aes67_tx/CMakeLists.txt | 3 +- components/aes67_tx/aes67_tx.c | 12 +- components/aes67_web/CMakeLists.txt | 5 +- components/aes67_web/aes67_web.c | 119 +++++++++++++++++- components/aes67_web/include/aes67_web.h | 15 ++- main/CMakeLists.txt | 3 +- main/main.c | 6 + 12 files changed, 205 insertions(+), 14 deletions(-) diff --git a/components/aes67_health/CMakeLists.txt b/components/aes67_health/CMakeLists.txt index b60f280..558a93b 100644 --- a/components/aes67_health/CMakeLists.txt +++ b/components/aes67_health/CMakeLists.txt @@ -1,2 +1,3 @@ idf_component_register(SRCS "aes67_health.c" - INCLUDE_DIRS "include") + INCLUDE_DIRS "include" + PRIV_REQUIRES aes67_web esp_app_format esp_timer heap) diff --git a/components/aes67_health/aes67_health.c b/components/aes67_health/aes67_health.c index 0f4e1ad..33f1a2c 100644 --- a/components/aes67_health/aes67_health.c +++ b/components/aes67_health/aes67_health.c @@ -1,7 +1,19 @@ #include "aes67_health.h" -// Stub (build step 0). +#include "aes67_web.h" +#include "esp_app_desc.h" +#include "esp_heap_caps.h" +#include "esp_timer.h" + +static void health_status(cJSON *st) +{ + cJSON_AddStringToObject(st, "fw", esp_app_get_description()->version); + cJSON_AddNumberToObject(st, "uptime_s", (double)(esp_timer_get_time() / 1000000)); + cJSON_AddNumberToObject(st, "heap_free", heap_caps_get_free_size(MALLOC_CAP_INTERNAL)); + cJSON_AddNumberToObject(st, "psram_free", heap_caps_get_free_size(MALLOC_CAP_SPIRAM)); +} + esp_err_t aes67_health_init(void) { - return ESP_OK; + return status_register(health_status); } diff --git a/components/aes67_health/include/aes67_health.h b/components/aes67_health/include/aes67_health.h index ede789d..a5fdce9 100644 --- a/components/aes67_health/include/aes67_health.h +++ b/components/aes67_health/include/aes67_health.h @@ -4,4 +4,5 @@ #include "esp_err.h" +// Registers the health fields (fw, uptime_s, heap_free, psram_free) in /api/status. esp_err_t aes67_health_init(void); diff --git a/components/aes67_net/CMakeLists.txt b/components/aes67_net/CMakeLists.txt index a42eb34..775ca4b 100644 --- a/components/aes67_net/CMakeLists.txt +++ b/components/aes67_net/CMakeLists.txt @@ -1,3 +1,4 @@ idf_component_register(SRCS "aes67_net.c" INCLUDE_DIRS "include" - REQUIRES esp_eth esp_netif esp_event) + REQUIRES esp_eth esp_netif esp_event + PRIV_REQUIRES aes67_web) diff --git a/components/aes67_net/aes67_net.c b/components/aes67_net/aes67_net.c index e3563ca..11acfb8 100644 --- a/components/aes67_net/aes67_net.c +++ b/components/aes67_net/aes67_net.c @@ -1,5 +1,8 @@ #include "aes67_net.h" +#include + +#include "aes67_web.h" #include "esp_eth.h" #include "esp_event.h" #include "esp_log.h" @@ -7,6 +10,9 @@ static const char *TAG = "net"; +static esp_netif_t *s_netif; +static char s_link[24] = "down"; + static void eth_event(void *arg, esp_event_base_t base, int32_t id, void *data) { esp_eth_handle_t eth = *(esp_eth_handle_t *)data; @@ -22,10 +28,13 @@ static void eth_event(void *arg, esp_event_base_t base, int32_t id, void *data) ESP_LOGI(TAG, "link up: %s Mbps %s duplex, MAC %02x:%02x:%02x:%02x:%02x:%02x", speed == ETH_SPEED_100M ? "100" : "10", duplex == ETH_DUPLEX_FULL ? "full" : "half", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + snprintf(s_link, sizeof(s_link), "up, %s Mbps %s", speed == ETH_SPEED_100M ? "100" : "10", + duplex == ETH_DUPLEX_FULL ? "full" : "half"); break; } case ETHERNET_EVENT_DISCONNECTED: ESP_LOGW(TAG, "link down"); + snprintf(s_link, sizeof(s_link), "down"); break; default: break; @@ -39,6 +48,25 @@ static void ip_event(void *arg, esp_event_base_t base, int32_t id, void *data) IP2STR(&ev->ip_info.ip), IP2STR(&ev->ip_info.netmask), IP2STR(&ev->ip_info.gw)); } +static void net_status(cJSON *st) +{ + char ip[16] = "", mac_s[18] = ""; + esp_netif_ip_info_t info; + if (esp_netif_get_ip_info(s_netif, &info) == ESP_OK && info.ip.addr) { + snprintf(ip, sizeof(ip), IPSTR, IP2STR(&info.ip)); + } + uint8_t mac[6]; + if (esp_netif_get_mac(s_netif, mac) == ESP_OK) { + snprintf(mac_s, sizeof(mac_s), "%02x:%02x:%02x:%02x:%02x:%02x", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + } + // Single untagged interface until the VLAN split: AES67 and web share it. + cJSON_AddStringToObject(st, "ip", ip); + cJSON_AddStringToObject(st, "aes67_ip", ip); + cJSON_AddStringToObject(st, "mac", mac_s); + cJSON_AddStringToObject(st, "link", s_link); +} + esp_err_t aes67_net_init(esp_eth_handle_t eth) { ESP_ERROR_CHECK(esp_netif_init()); @@ -48,11 +76,12 @@ esp_err_t aes67_net_init(esp_eth_handle_t eth) } esp_netif_config_t netif_cfg = ESP_NETIF_DEFAULT_ETH(); - esp_netif_t *netif = esp_netif_new(&netif_cfg); - ESP_ERROR_CHECK(esp_netif_attach(netif, esp_eth_new_netif_glue(eth))); + s_netif = esp_netif_new(&netif_cfg); + ESP_ERROR_CHECK(esp_netif_attach(s_netif, esp_eth_new_netif_glue(eth))); ESP_ERROR_CHECK(esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, eth_event, NULL)); ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, ip_event, NULL)); + status_register(net_status); return esp_eth_start(eth); } diff --git a/components/aes67_tx/CMakeLists.txt b/components/aes67_tx/CMakeLists.txt index 4c4f9d5..78a1bb6 100644 --- a/components/aes67_tx/CMakeLists.txt +++ b/components/aes67_tx/CMakeLists.txt @@ -1,2 +1,3 @@ idf_component_register(SRCS "aes67_tx.c" - INCLUDE_DIRS "include") + INCLUDE_DIRS "include" + PRIV_REQUIRES aes67_web) diff --git a/components/aes67_tx/aes67_tx.c b/components/aes67_tx/aes67_tx.c index 7fd3493..400ee63 100644 --- a/components/aes67_tx/aes67_tx.c +++ b/components/aes67_tx/aes67_tx.c @@ -1,7 +1,15 @@ #include "aes67_tx.h" -// Stub (build step 0). +#include "aes67_web.h" + +// Stub (build step 2): counters stay 0 until the RTP sender exists (step 4). +static void tx_status(cJSON *st) +{ + cJSON_AddNumberToObject(st, "tx_packets", 0); + cJSON_AddNumberToObject(st, "underruns", 0); +} + esp_err_t aes67_tx_init(void) { - return ESP_OK; + return status_register(tx_status); } diff --git a/components/aes67_web/CMakeLists.txt b/components/aes67_web/CMakeLists.txt index 9f1836a..8d00110 100644 --- a/components/aes67_web/CMakeLists.txt +++ b/components/aes67_web/CMakeLists.txt @@ -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") diff --git a/components/aes67_web/aes67_web.c b/components/aes67_web/aes67_web.c index 7a14d10..6415018 100644 --- a/components/aes67_web/aes67_web.c +++ b/components/aes67_web/aes67_web.c @@ -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; } diff --git a/components/aes67_web/include/aes67_web.h b/components/aes67_web/include/aes67_web.h index ad76887..f782ac1 100644 --- a/components/aes67_web/include/aes67_web.h +++ b/components/aes67_web/include/aes67_web.h @@ -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); diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index aa3939a..22726e7 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,3 +1,4 @@ idf_component_register(SRCS "main.c" INCLUDE_DIRS "." - REQUIRES esp_app_format esp_hw_support aes67_board aes67_net) + REQUIRES esp_app_format esp_hw_support + aes67_board aes67_net aes67_health aes67_tx aes67_web) diff --git a/main/main.c b/main/main.c index 512b309..371ef8d 100644 --- a/main/main.c +++ b/main/main.c @@ -1,5 +1,8 @@ #include "aes67_board.h" +#include "aes67_health.h" #include "aes67_net.h" +#include "aes67_tx.h" +#include "aes67_web.h" #include "esp_app_desc.h" #include "esp_chip_info.h" #include "esp_log.h" @@ -20,4 +23,7 @@ void app_main(void) esp_eth_handle_t eth; ESP_ERROR_CHECK(aes67_board_eth_init(ð)); ESP_ERROR_CHECK(aes67_net_init(eth)); + ESP_ERROR_CHECK(aes67_health_init()); + ESP_ERROR_CHECK(aes67_tx_init()); + ESP_ERROR_CHECK(aes67_web_start()); }