#include "status_web.h" #include #include #include "esp_app_desc.h" #include "esp_check.h" #include "esp_heap_caps.h" #include "esp_http_server.h" #include "esp_log.h" #include "esp_netif.h" #include "esp_netif_ip_addr.h" #include "esp_timer.h" #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" #include "mqtt_control.h" #include "wifi-connect.h" static const char *TAG = "status_web"; static httpd_handle_t s_server = NULL; static SemaphoreHandle_t s_lock = NULL; static uint64_t s_snapshot_update_ms = 0; static uint32_t s_snapshot_update_count = 0; static status_web_snapshot_t s_snapshot = { .temp = "--", .hum = "--", .soil = "--", .lux = "--", .pump_on = false, .light_on = false, .auto_mode = true, .soil_on_threshold = 35.0f, .soil_off_threshold = 45.0f, .light_on_threshold = 100.0f, .light_off_threshold = 350.0f, .i2c_ready = false, .soil_sensor_ready = false, .loop_counter = 0, }; static const char *wifi_status_text(wifi_connect_status_t status) { switch (status) { case WIFI_CONNECT_STATUS_IDLE: return "idle"; case WIFI_CONNECT_STATUS_PROVISIONING: return "provisioning"; case WIFI_CONNECT_STATUS_CONNECTING: return "connecting"; case WIFI_CONNECT_STATUS_CONNECTED: return "connected"; case WIFI_CONNECT_STATUS_FAILED: return "failed"; case WIFI_CONNECT_STATUS_TIMEOUT: return "timeout"; default: return "unknown"; } } static void get_sta_ip_text(char *out, size_t out_size) { if (out == NULL || out_size == 0) { return; } snprintf(out, out_size, "--"); esp_netif_t *sta = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); if (sta == NULL) { return; } esp_netif_ip_info_t ip_info; if (esp_netif_get_ip_info(sta, &ip_info) == ESP_OK) { snprintf(out, out_size, IPSTR, IP2STR(&ip_info.ip)); } } static const char *s_page_html = "" "" "BotanicalBuddy Status" "
" "

BotanicalBuddy 设备状态总览

" "
独立状态服务(port 8080),每2秒自动刷新
" "

传感与控制

" "
空气温度
--
" "
空气湿度
--
" "
土壤湿度
--
" "
光照强度
--
" "
水泵
--
" "
补光灯
--
" "
控制模式
--
" "
soil_on/off
--
" "
light_on/off
--
" "
" "

连接与系统

" "
Wi-Fi 状态
--
" "
STA IP
--
" "
MQTT 连接
--
" "
I2C Ready
--
" "
Soil Sensor Ready
--
" "
主循环计数
--
" "
运行时长
--
" "
空闲堆/最小堆
--
" "
固件版本
--
" "
快照更新时间
--
" "
快照序号
--
" "
快照年龄
--
" "
" "

原始 JSON

{}
" "" "
"; static esp_err_t status_root_handler(httpd_req_t *req) { httpd_resp_set_type(req, "text/html"); return httpd_resp_send(req, s_page_html, HTTPD_RESP_USE_STRLEN); } static esp_err_t status_api_handler(httpd_req_t *req) { status_web_snapshot_t snap; uint64_t snapshot_update_ms = 0; uint32_t snapshot_update_count = 0; xSemaphoreTake(s_lock, portMAX_DELAY); snap = s_snapshot; snapshot_update_ms = s_snapshot_update_ms; snapshot_update_count = s_snapshot_update_count; xSemaphoreGive(s_lock); const esp_app_desc_t *app_desc = esp_app_get_description(); uint64_t now_ms = (uint64_t)(esp_timer_get_time() / 1000); uint64_t snapshot_age_ms = (snapshot_update_ms > 0 && now_ms >= snapshot_update_ms) ? (now_ms - snapshot_update_ms) : 0; const uint32_t free_heap = esp_get_free_heap_size(); const uint32_t min_free_heap = esp_get_minimum_free_heap_size(); const uint32_t largest_block = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT); const bool mqtt_connected = mqtt_control_is_connected(); const wifi_connect_status_t wifi_status = wifi_connect_get_status(); char ip_text[16] = {0}; get_sta_ip_text(ip_text, sizeof(ip_text)); char json[640]; int len = snprintf(json, sizeof(json), "{\"temp\":\"%s\",\"hum\":\"%s\",\"soil\":\"%s\",\"lux\":\"%s\",\"pump\":\"%s\",\"light\":\"%s\",\"mode\":\"%s\",\"soil_on\":%.1f,\"soil_off\":%.1f,\"light_on\":%.1f,\"light_off\":%.1f,\"wifi_status\":\"%s\",\"sta_ip\":\"%s\",\"mqtt_connected\":%s,\"i2c_ready\":%s,\"soil_sensor_ready\":%s,\"loop_counter\":%lu,\"uptime_ms\":%llu,\"free_heap\":%lu,\"min_free_heap\":%lu,\"largest_block\":%lu,\"app_version\":\"%s\",\"snapshot_update_ms\":%llu,\"snapshot_update_count\":%lu,\"snapshot_age_ms\":%llu}", snap.temp, snap.hum, snap.soil, snap.lux, snap.pump_on ? "on" : "off", snap.light_on ? "on" : "off", snap.auto_mode ? "auto" : "manual", snap.soil_on_threshold, snap.soil_off_threshold, snap.light_on_threshold, snap.light_off_threshold, wifi_status_text(wifi_status), ip_text, mqtt_connected ? "true" : "false", snap.i2c_ready ? "true" : "false", snap.soil_sensor_ready ? "true" : "false", (unsigned long)snap.loop_counter, (unsigned long long)now_ms, (unsigned long)free_heap, (unsigned long)min_free_heap, (unsigned long)largest_block, app_desc ? app_desc->version : "unknown", (unsigned long long)snapshot_update_ms, (unsigned long)snapshot_update_count, (unsigned long long)snapshot_age_ms); if (len <= 0 || len >= (int)sizeof(json)) { return ESP_FAIL; } httpd_resp_set_type(req, "application/json"); return httpd_resp_sendstr(req, json); } esp_err_t status_web_start(uint16_t port) { if (s_server != NULL) { return ESP_OK; } if (s_lock == NULL) { s_lock = xSemaphoreCreateMutex(); ESP_RETURN_ON_FALSE(s_lock != NULL, ESP_ERR_NO_MEM, TAG, "create mutex failed"); } httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.server_port = port; config.ctrl_port = (uint16_t)(port + 1); ESP_RETURN_ON_ERROR(httpd_start(&s_server, &config), TAG, "httpd_start failed"); const httpd_uri_t root = { .uri = "/", .method = HTTP_GET, .handler = status_root_handler, .user_ctx = NULL, }; const httpd_uri_t api = { .uri = "/api/status", .method = HTTP_GET, .handler = status_api_handler, .user_ctx = NULL, }; ESP_RETURN_ON_ERROR(httpd_register_uri_handler(s_server, &root), TAG, "register root failed"); ESP_RETURN_ON_ERROR(httpd_register_uri_handler(s_server, &api), TAG, "register api failed"); ESP_LOGI(TAG, "status web started at port %u", (unsigned)port); return ESP_OK; } esp_err_t status_web_update(const status_web_snapshot_t *snapshot) { ESP_RETURN_ON_FALSE(snapshot != NULL, ESP_ERR_INVALID_ARG, TAG, "snapshot is null"); ESP_RETURN_ON_FALSE(s_lock != NULL, ESP_ERR_INVALID_STATE, TAG, "status web not started"); xSemaphoreTake(s_lock, portMAX_DELAY); s_snapshot = *snapshot; s_snapshot_update_ms = (uint64_t)(esp_timer_get_time() / 1000); s_snapshot_update_count++; xSemaphoreGive(s_lock); return ESP_OK; }