Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
[English](README_EN.md) | [中文](README.md)

[![Build Status](https://github.com/thomas-hiddenpeak/TianshanOS/actions/workflows/build.yml/badge.svg)](https://github.com/thomas-hiddenpeak/TianshanOS/actions/workflows/build.yml)
![Lighthouse Performance](https://img.shields.io/badge/Lighthouse-97-brightgreen?logo=lighthouse&logoColor=white)
[![License](https://img.shields.io/github/license/thomas-hiddenpeak/TianshanOS)](LICENSE)
![ESP-IDF](https://img.shields.io/badge/ESP--IDF-v5.5+-green?logo=espressif)
![C](https://img.shields.io/badge/C-99-blue?logo=c)
Expand Down
1 change: 1 addition & 0 deletions README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
[English](README_EN.md) | [中文](README.md)

[![Build Status](https://github.com/thomas-hiddenpeak/TianshanOS/actions/workflows/build.yml/badge.svg)](https://github.com/thomas-hiddenpeak/TianshanOS/actions/workflows/build.yml)
![Lighthouse Performance](https://img.shields.io/badge/Lighthouse-97-brightgreen?logo=lighthouse&logoColor=white)
[![Release](https://img.shields.io/github/v/release/thomas-hiddenpeak/TianshanOS)](https://github.com/thomas-hiddenpeak/TianshanOS/releases/latest)
[![License](https://img.shields.io/github/license/thomas-hiddenpeak/TianshanOS)](LICENSE)
![ESP-IDF](https://img.shields.io/badge/ESP--IDF-v5.5+-green?logo=espressif)
Expand Down
56 changes: 36 additions & 20 deletions components/ts_api/src/ts_api_system.c
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,16 @@ static esp_err_t api_system_tasks(const cJSON *params, ts_api_result_t *result)
/**
* @brief system.cpu - Get CPU core statistics
*/
/**
* CPU 使用率采用增量计算(delta):
* 记录上一次采样的 IDLE 任务运行时间和总运行时间,
* 通过两次采样的差值计算出当前(最近一个周期)的 CPU 使用率,
* 而非自启动以来的累计平均值。
*/
static uint32_t s_prev_total_runtime = 0;
static uint32_t s_prev_idle_runtime[2] = {0, 0}; /* ESP32-S3 最多 2 核 */
static bool s_cpu_prev_valid = false;

static esp_err_t api_system_cpu(const cJSON *params, ts_api_result_t *result)
{
cJSON *data = cJSON_CreateObject();
Expand All @@ -257,48 +267,54 @@ static esp_err_t api_system_cpu(const cJSON *params, ts_api_result_t *result)
uint32_t total_runtime;
UBaseType_t actual_count = uxTaskGetSystemState(task_array, task_count, &total_runtime);

/* Calculate per-core CPU usage */
uint32_t core_runtime[2] = {0, 0}; /* ESP32-S3 has max 2 cores */
/* 只需要每个核心的 IDLE 任务运行时间 */
uint32_t idle_runtime[2] = {0, 0};

/* Sum up runtime for each core and track idle tasks */
for (UBaseType_t i = 0; i < actual_count; i++) {
BaseType_t core_id = task_array[i].xCoreID;
uint32_t task_runtime = task_array[i].ulRunTimeCounter;

if (core_id >= 0 && core_id < num_cores) {
core_runtime[core_id] += task_runtime;

/* Check if this is an IDLE task */
if (strncmp(task_array[i].pcTaskName, "IDLE", 4) == 0) {
idle_runtime[core_id] = task_runtime;
if (strncmp(task_array[i].pcTaskName, "IDLE", 4) == 0) {
BaseType_t core_id = task_array[i].xCoreID;
if (core_id >= 0 && core_id < num_cores) {
idle_runtime[core_id] = task_array[i].ulRunTimeCounter;
}
}
}

cJSON *cores = cJSON_AddArrayToObject(data, "cores");
uint32_t total_usage_sum = 0;

/* 增量计算:使用两次采样的差值 */
uint32_t delta_total = total_runtime - s_prev_total_runtime;

for (int i = 0; i < num_cores; i++) {
cJSON *core = cJSON_CreateObject();
cJSON_AddNumberToObject(core, "id", i);

/* Calculate CPU usage percentage (100% - idle%) */
uint32_t cpu_percent = 0;
if (core_runtime[i] > 0) {
/* Usage = (total - idle) / total * 100 */
uint32_t busy_time = core_runtime[i] - idle_runtime[i];
cpu_percent = (busy_time * 100) / core_runtime[i];

if (s_cpu_prev_valid && delta_total > 0) {
/* 每个核心可用时间 = 总时间增量(timer 对所有核心公用) */
uint32_t delta_idle = idle_runtime[i] - s_prev_idle_runtime[i];

/* 使用率 = 100% - 空闲率 */
if (delta_idle < delta_total) {
cpu_percent = ((delta_total - delta_idle) * 100) / delta_total;
}
/* 限幅保护(避免计数器回绕时出错) */
if (cpu_percent > 100) cpu_percent = 100;
}

cJSON_AddNumberToObject(core, "usage", cpu_percent);
cJSON_AddNumberToObject(core, "runtime", core_runtime[i]);
cJSON_AddNumberToObject(core, "idle_runtime", idle_runtime[i]);

cJSON_AddItemToArray(cores, core);
total_usage_sum += cpu_percent;
}

/* 保存本次采样数据,供下次增量计算使用 */
s_prev_total_runtime = total_runtime;
for (int i = 0; i < num_cores && i < 2; i++) {
s_prev_idle_runtime[i] = idle_runtime[i];
}
s_cpu_prev_valid = true;

/* Overall CPU usage (average across cores) */
cJSON_AddNumberToObject(data, "total_usage", num_cores > 0 ? total_usage_sum / num_cores : 0);
cJSON_AddNumberToObject(data, "total_runtime", total_runtime);
Expand Down
23 changes: 22 additions & 1 deletion components/ts_drivers/src/ts_fan.c
Original file line number Diff line number Diff line change
Expand Up @@ -514,13 +514,34 @@ esp_err_t ts_fan_set_mode(ts_fan_id_t fan, ts_fan_mode_t mode)
if (fan >= TS_FAN_MAX) return ESP_ERR_INVALID_ARG;
if (!s_fans[fan].initialized) return ESP_ERR_INVALID_STATE;

ts_fan_mode_t old_mode = s_fans[fan].mode;
s_fans[fan].mode = mode;

if (mode == TS_FAN_MODE_OFF) {
update_pwm(&s_fans[fan], 0);
}

TS_LOGI(TAG, "Fan %d mode set to %d", fan, mode);
/* 切换到曲线/自动模式时,重置迟滞状态以允许立即生效 */
if ((mode == TS_FAN_MODE_CURVE || mode == TS_FAN_MODE_AUTO) && old_mode != mode) {
/* 将 last_stable_temp 设为一个远离实际温度的值,确保首次评估通过迟滞检查 */
s_fans[fan].last_stable_temp = -1000; /* -100.0°C,远低于任何实际温度 */
s_fans[fan].last_speed_change_time = 0; /* 允许立即调速 */

/* 立即获取当前温度 */
if (s_auto_temp_enabled) {
ts_temp_data_t temp_data;
int16_t current_temp = ts_temp_get_effective(&temp_data);
if (current_temp > TS_TEMP_MIN_VALID) {
s_fans[fan].temperature = current_temp;
}
}

TS_LOGI(TAG, "Fan %d mode -> %d: hysteresis reset, temp=%d",
fan, mode, s_fans[fan].temperature);
} else {
TS_LOGI(TAG, "Fan %d mode set to %d", fan, mode);
}

return ESP_OK;
}

Expand Down
83 changes: 71 additions & 12 deletions components/ts_net/src/ts_http_server.c
Original file line number Diff line number Diff line change
Expand Up @@ -218,16 +218,33 @@ esp_err_t ts_http_send_json(ts_http_request_t *req, int status, const char *json
return ts_http_send_response(req, status, "application/json", json);
}

/**
* @brief 检查客户端是否支持 gzip 编码
*/
static bool client_accepts_gzip(httpd_req_t *req)
{
char buf[128];
if (httpd_req_get_hdr_value_str(req, "Accept-Encoding", buf, sizeof(buf)) == ESP_OK) {
return (strstr(buf, "gzip") != NULL);
}
return false;
}

/**
* @brief 判断文件是否为可 gzip 压缩的文本资源
*/
static bool is_compressible(const char *ext)
{
if (!ext) return false;
return (strcmp(ext, ".js") == 0 || strcmp(ext, ".css") == 0 ||
strcmp(ext, ".html") == 0 || strcmp(ext, ".json") == 0);
}

esp_err_t ts_http_send_file(ts_http_request_t *req, const char *filepath)
{
if (!req || !filepath) return ESP_ERR_INVALID_ARG;

ssize_t size = ts_storage_size(filepath);
if (size < 0) {
return ts_http_send_error(req, 404, "File not found");
}

// Detect content type
/* ===== Content-Type 检测 ===== */
const char *ext = strrchr(filepath, '.');
const char *content_type = "application/octet-stream";
if (ext) {
Expand All @@ -239,18 +256,61 @@ esp_err_t ts_http_send_file(ts_http_request_t *req, const char *filepath)
else if (strcmp(ext, ".jpg") == 0 || strcmp(ext, ".jpeg") == 0) content_type = "image/jpeg";
else if (strcmp(ext, ".svg") == 0) content_type = "image/svg+xml";
else if (strcmp(ext, ".ico") == 0) content_type = "image/x-icon";
else if (strcmp(ext, ".woff2") == 0) content_type = "font/woff2";
else if (strcmp(ext, ".woff") == 0) content_type = "font/woff";
}

/* ===== 尝试发送 gzip 预压缩版本 ===== */
const char *actual_filepath = filepath;
char gz_path[140];
bool use_gzip = false;

if (is_compressible(ext) && client_accepts_gzip(req->req)) {
snprintf(gz_path, sizeof(gz_path), "%s.gz", filepath);
if (ts_storage_exists(gz_path)) {
actual_filepath = gz_path;
use_gzip = true;
}
}

ssize_t size = ts_storage_size(actual_filepath);
if (size < 0) {
/* .gz 不存在时回退到原始文件 */
actual_filepath = filepath;
use_gzip = false;
size = ts_storage_size(filepath);
if (size < 0) {
return ts_http_send_error(req, 404, "File not found");
}
}

/* ===== 设置响应头 ===== */
httpd_resp_set_type(req->req, content_type);

// 对于小文件(<32KB),直接发送
if (use_gzip) {
httpd_resp_set_hdr(req->req, "Content-Encoding", "gzip");
httpd_resp_set_hdr(req->req, "Vary", "Accept-Encoding");
}

/* Cache-Control:index.html 不缓存(确保更新),其他静态资源长期缓存 */
const char *basename = strrchr(filepath, '/');
if (basename && strcmp(basename, "/index.html") == 0) {
httpd_resp_set_hdr(req->req, "Cache-Control", "no-cache");
} else if (ext) {
/* JS/CSS/字体/图片:缓存 7 天 */
httpd_resp_set_hdr(req->req, "Cache-Control", "public, max-age=604800, immutable");
}

/* ===== 发送文件内容 ===== */

/* 对于小文件(<32KB),直接发送 */
if (size < 32 * 1024) {
char *buf = TS_MALLOC_PSRAM(size);
if (!buf) {
return ts_http_send_error(req, 500, "Memory allocation failed");
}

if (ts_storage_read_file(filepath, buf, size) != size) {
if (ts_storage_read_file(actual_filepath, buf, size) != size) {
free(buf);
return ts_http_send_error(req, 500, "Failed to read file");
}
Expand All @@ -260,13 +320,12 @@ esp_err_t ts_http_send_file(ts_http_request_t *req, const char *filepath)
return ret;
}

// 对于大文件,使用分块传输(chunked transfer)
FILE *f = fopen(filepath, "r");
/* 对于大文件,使用分块传输(chunked transfer) */
FILE *f = fopen(actual_filepath, "r");
if (!f) {
return ts_http_send_error(req, 500, "Failed to open file");
}

// 分块大小:8KB
#define CHUNK_SIZE 8192
char *chunk = TS_MALLOC_PSRAM(CHUNK_SIZE);
if (!chunk) {
Expand All @@ -288,7 +347,7 @@ esp_err_t ts_http_send_file(ts_http_request_t *req, const char *filepath)
}
} while (read_len == CHUNK_SIZE && ret == ESP_OK);

// 发送结束标记(空块)
/* 发送结束标记(空块) */
if (ret == ESP_OK) {
httpd_resp_send_chunk(req->req, NULL, 0);
}
Expand Down
49 changes: 47 additions & 2 deletions components/ts_webui/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,53 @@ idf_component_register(
json
)

# Embed web files
# ===========================================================================
# Web 资源构建时优化:minify + gzip 预压缩
#
# 流程:源文件 -> 复制到 build/web -> minify(JS/CSS) -> gzip -> SPIFFS 打包
# 原始源文件不会被修改,所有处理在构建目录中进行
# ===========================================================================
set(WEB_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/web")
set(WEB_BUILD_DIR "${CMAKE_CURRENT_BINARY_DIR}/web_optimized")
set(MINIFY_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/../../tools/minify_web.py")

if(EXISTS "${WEB_SRC_DIR}")
spiffs_create_partition_image(www ${WEB_SRC_DIR} FLASH_IN_PROJECT)

# 收集所有源文件(用于依赖追踪)
file(GLOB_RECURSE WEB_ALL_FILES "${WEB_SRC_DIR}/*")

# 步骤 1:复制 web 目录到构建目录
add_custom_command(
OUTPUT "${WEB_BUILD_DIR}/.stamp_copy"
COMMAND ${CMAKE_COMMAND} -E remove_directory "${WEB_BUILD_DIR}"
COMMAND ${CMAKE_COMMAND} -E copy_directory "${WEB_SRC_DIR}" "${WEB_BUILD_DIR}"
COMMAND ${CMAKE_COMMAND} -E touch "${WEB_BUILD_DIR}/.stamp_copy"
DEPENDS ${WEB_ALL_FILES}
COMMENT "Copying web assets to build directory"
VERBATIM
)

# 步骤 2+3:Minify JS/CSS + Gzip 预压缩(纯 Python,无外部依赖)
find_program(PYTHON3 python3)
if(PYTHON3 AND EXISTS "${MINIFY_SCRIPT}")
add_custom_command(
OUTPUT "${WEB_BUILD_DIR}/.stamp_optimize"
COMMAND ${PYTHON3} "${MINIFY_SCRIPT}" "${WEB_BUILD_DIR}" --gzip
COMMAND ${CMAKE_COMMAND} -E touch "${WEB_BUILD_DIR}/.stamp_optimize"
DEPENDS "${WEB_BUILD_DIR}/.stamp_copy" "${MINIFY_SCRIPT}"
COMMENT "Minifying + gzipping web resources"
VERBATIM
)
set(FINAL_STAMP "${WEB_BUILD_DIR}/.stamp_optimize")
message(STATUS "Web optimization enabled (minify + gzip)")
else()
set(FINAL_STAMP "${WEB_BUILD_DIR}/.stamp_copy")
message(STATUS "Web optimization skipped (python3 or script not found)")
endif()

add_custom_target(optimize_web ALL DEPENDS ${FINAL_STAMP})

# 从优化后的构建目录创建 SPIFFS 镜像
spiffs_create_partition_image(www ${WEB_BUILD_DIR} FLASH_IN_PROJECT)

endif()
Loading