Skip to content
Closed
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
12 changes: 3 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,9 @@ TianshanOS 是一个**面向配置而非面向代码**的嵌入式操作系统

### 系统架构

```mermaid
flowchart TB
UI["CLI / WebUI / HTTPS API"] --> API["Core API (ts_api)"]
API --> S1["服务管理"] & S2["自动化引擎"] & S3["安全模块"]
S1 & S2 & S3 --> Event["事件总线 (ts_event)"]
Event --> Config["配置管理 (NVS/SD卡)"]
Config --> HAL["硬件抽象层 (GPIO/PWM/I2C/SPI/UART/ADC)"]
HAL --> Platform["平台适配层 (ESP32-S3 / ESP32-P4)"]
```
![TianshanOS 系统架构](assets/system-architecture.png)

图源:[assets/system-architecture.mmd](assets/system-architecture.mmd)

---

Expand Down
130 changes: 130 additions & 0 deletions assets/system-architecture.mmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
%%{init: {
"theme": "base",
"themeVariables": {
"background": "#ffffff",
"primaryColor": "#eef6ff",
"primaryTextColor": "#172033",
"primaryBorderColor": "#2563eb",
"lineColor": "#52637a",
"secondaryColor": "#f8fafc",
"tertiaryColor": "#ecfdf5",
"fontFamily": "Inter, Noto Sans SC, PingFang SC, Microsoft YaHei, Arial, sans-serif"
},
"flowchart": {
"defaultRenderer": "elk",
"curve": "basis",
"nodeSpacing": 26,
"rankSpacing": 42,
"padding": 18,
"htmlLabels": true
}
}}%%

flowchart LR
subgraph L1["外部交互与接入"]
direction TB
Boundary["系统边界<br/>用户操作 / 遥测输入 / 固件与证书"]
Browser["WebUI 浏览器<br/>仪表盘 / 配置 / OTA"]
CLI["串口 / SSH CLI<br/>命令 / 脚本 / 调试"]
Services["PKI / OTA 服务<br/>证书 / 固件 / 配置包"]
Hosts["AGX / LPMU / 上层网络<br/>控制 / 遥测 / 接入"]
Entry["统一请求入口<br/>HTTP / WebSocket / mTLS / CLI"]
WebUI["ts_webui<br/>静态资源 / REST 网关 / WS 广播"]
Console["ts_console<br/>命令路由 / 脚本 / i18n"]
HTTPS["ts_https<br/>mTLS API Server"]
end

subgraph L3["统一控制面"]
direction TB
CoreAPI["ts_api<br/>统一 REST / CLI 语义<br/>权限校验 / 参数模型 / 返回模型"]
end

subgraph L4["核心运行时合同"]
direction TB
Runtime["运行时主干<br/>生命周期 / 事件 / 配置 / 持久化"]
Service["ts_service<br/>8 阶段启动 / 依赖 / 健康检查"]
Event["ts_event<br/>发布订阅 / 异步事件"]
Config["ts_config<br/>SD 优先 + NVS 备份 / Schema 迁移"]
Storage["ts_storage<br/>SPIFFS / SD / FATFS"]
LogMem["ts_log + ts_mempool<br/>诊断输出 / 受控内存池"]
end

subgraph L5["系统能力层"]
direction TB
Capability["能力编排<br/>共享 API / 事件 / 配置合同"]
Automation["自动化与变量<br/>ts_automation / ts_jsonpath"]
DeviceOps["设备控制与监测<br/>ts_drivers / ts_led"]
Connectivity["网络与远程接入<br/>ts_net / HTTP / NAT / DHCP"]
SecurityDelivery["安全与交付<br/>ts_security / ts_cert / ts_pki_client<br/>ts_config_pack / ts_ota"]
end

subgraph L6["硬件与平台边界"]
direction TB
Platform["平台边界<br/>逻辑功能 -> 外设 / 引脚 / 总线"]
HAL["ts_hal<br/>GPIO / PWM / I2C / SPI / UART / ADC"]
Board["boards/rm01_esp32s3<br/>pins.json / services.json"]
ESPIDF["ESP-IDF 5.5+<br/>ESP32-S3 / ESP32-P4 适配"]
end

subgraph L7["硬件与持久化资源"]
direction TB
Resources["资源层<br/>实际设备 / 存储 / 分区"]
ManagedCompute["被管理计算<br/>Jetson AGX / LattePanda Mu"]
PowerCooling["电源与散热<br/>INA3221 / PZEM / ADC / PWM / 温度源"]
NetworkMux["网络与切换<br/>W5500 / USB MUX / 上层网络"]
Visual["状态与灯效<br/>WS2812"]
Persistence["持久化与分区<br/>SD 卡 / NVS / SPIFFS / OTA 分区"]
end

Boundary --> Entry
Entry --> CoreAPI
CoreAPI --> Runtime
Runtime --> Capability
Capability --> Platform
Platform --> Resources

Boundary --- Browser
Boundary --- CLI
Boundary --- Services
Boundary --- Hosts

Entry --- WebUI
Entry --- Console
Entry --- HTTPS

Runtime --- Service
Runtime --- Event
Runtime --- Config
Runtime --- Storage
Runtime --- LogMem

Capability --- Automation
Capability --- DeviceOps
Capability --- Connectivity
Capability --- SecurityDelivery

Platform --- HAL
Platform --- Board
Platform --- ESPIDF

Resources --- ManagedCompute
Resources --- PowerCooling
Resources --- NetworkMux
Resources --- Visual
Resources --- Persistence

classDef boundary fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#172554;
classDef actor fill:#f8fafc,stroke:#94a3b8,stroke-width:1.2px,color:#0f172a;
classDef entry fill:#e0f2fe,stroke:#0284c7,stroke-width:1.35px,color:#082f49;
classDef runtime fill:#ecfdf5,stroke:#059669,stroke-width:1.35px,color:#064e3b;
classDef capability fill:#fff7ed,stroke:#ea580c,stroke-width:1.25px,color:#7c2d12;
classDef platform fill:#f5f3ff,stroke:#7c3aed,stroke-width:1.35px,color:#2e1065;
classDef hardware fill:#f1f5f9,stroke:#475569,stroke-width:1.2px,color:#0f172a;

class Boundary,Entry,CoreAPI,Runtime,Capability,Platform,Resources boundary;
class Browser,CLI,Services,Hosts actor;
class WebUI,Console,HTTPS entry;
class Service,Event,Config,Storage,LogMem runtime;
class Automation,DeviceOps,Connectivity,SecurityDelivery capability;
class HAL,Board,ESPIDF platform;
class ManagedCompute,PowerCooling,NetworkMux,Visual,Persistence hardware;
Binary file added assets/system-architecture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
56 changes: 55 additions & 1 deletion components/ts_api/src/ts_api_automation.c
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include "ts_variable.h"
#include "ts_rule_engine.h"
#include "ts_source_manager.h"
#include "ts_temp_source.h"
#include "ts_action_manager.h"
#include "ts_config_pack.h"
#include "ts_cert.h"
Expand All @@ -29,6 +30,7 @@
#include "esp_heap_caps.h"
#include "esp_crt_bundle.h"
#include "esp_log.h"
#include "esp_timer.h"
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
Expand Down Expand Up @@ -219,15 +221,53 @@ static esp_err_t api_automation_variables_list(const cJSON *params, ts_api_resul
{
result->data = cJSON_CreateObject();
cJSON *vars_array = cJSON_AddArrayToObject(result->data, "variables");
int64_t now_ms = esp_timer_get_time() / 1000;
const char *prefix = NULL;
const char *source_id = NULL;
size_t prefix_len = 0;
bool include_value = true;
bool include_meta = true;

if (params) {
cJSON *prefix_param = cJSON_GetObjectItem(params, "prefix");
if (prefix_param && cJSON_IsString(prefix_param) && prefix_param->valuestring) {
prefix = prefix_param->valuestring;
prefix_len = strlen(prefix);
}

cJSON *source_id_param = cJSON_GetObjectItem(params, "source_id");
if (source_id_param && cJSON_IsString(source_id_param) && source_id_param->valuestring) {
source_id = source_id_param->valuestring;
}

cJSON *include_value_param = cJSON_GetObjectItem(params, "include_value");
if (include_value_param && cJSON_IsBool(include_value_param)) {
include_value = cJSON_IsTrue(include_value_param);
}

cJSON *include_meta_param = cJSON_GetObjectItem(params, "include_meta");
if (include_meta_param && cJSON_IsBool(include_meta_param)) {
include_meta = cJSON_IsTrue(include_meta_param);
}
}

// 遍历所有变量(使用内部迭代器)
ts_variable_iterate_ctx_t ctx = {0};
ts_auto_variable_t var;

while (ts_variable_iterate(&ctx, &var) == ESP_OK) {
if (prefix_len > 0 && strncmp(var.name, prefix, prefix_len) != 0) {
continue;
}
if (source_id && strcmp(var.source_id, source_id) != 0) {
continue;
}

cJSON *var_obj = cJSON_CreateObject();
cJSON_AddStringToObject(var_obj, "name", var.name);
cJSON_AddItemToObject(var_obj, "value", value_to_json(&var.value));
if (include_value) {
cJSON_AddItemToObject(var_obj, "value", value_to_json(&var.value));
}

// 类型字符串
const char *type_str = "null";
Expand All @@ -241,6 +281,20 @@ static esp_err_t api_automation_variables_list(const cJSON *params, ts_api_resul
cJSON_AddStringToObject(var_obj, "type", type_str);
cJSON_AddBoolToObject(var_obj, "persistent", (var.flags & TS_AUTO_VAR_PERSISTENT) != 0);
cJSON_AddBoolToObject(var_obj, "readonly", (var.flags & TS_AUTO_VAR_READONLY) != 0);
if (include_meta) {
cJSON_AddNumberToObject(var_obj, "last_change_ms", (double)var.last_change_ms);
cJSON_AddNumberToObject(var_obj, "last_update_ms", (double)var.last_update_ms);
if (var.last_update_ms > 0) {
int64_t age_ms = now_ms - var.last_update_ms;
bool stale = (age_ms < 0 || age_ms > TS_TEMP_DATA_TIMEOUT_MS);
if (age_ms < 0) age_ms = 0;
cJSON_AddNumberToObject(var_obj, "age_ms", (double)age_ms);
cJSON_AddBoolToObject(var_obj, "stale", stale);
} else {
cJSON_AddNullToObject(var_obj, "age_ms");
cJSON_AddBoolToObject(var_obj, "stale", true);
}
}

if (var.source_id[0] != '\0') {
cJSON_AddStringToObject(var_obj, "source_id", var.source_id);
Expand Down
29 changes: 27 additions & 2 deletions components/ts_api/src/ts_api_device.c
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,19 @@ static const char *fan_mode_to_str(ts_fan_mode_t mode)
case TS_FAN_MODE_OFF: return "off";
case TS_FAN_MODE_MANUAL: return "manual";
case TS_FAN_MODE_AUTO: return "auto";
case TS_FAN_MODE_CURVE: return "curve";
default: return "unknown";
}
}

static const char *fan_auto_state_to_str(ts_fan_auto_state_t state)
{
switch (state) {
case TS_FAN_AUTO_STATE_IDLE: return "idle";
case TS_FAN_AUTO_STATE_BASELINE: return "baseline";
case TS_FAN_AUTO_STATE_ACTIVE: return "active";
case TS_FAN_AUTO_STATE_GUARD: return "guard";
case TS_FAN_AUTO_STATE_STALE: return "stale";
default: return "unknown";
}
}
Expand Down Expand Up @@ -215,6 +228,15 @@ static esp_err_t api_device_fan_status(const cJSON *params, ts_api_result_t *res
cJSON_AddNumberToObject(fan, "rpm", status.rpm);
cJSON_AddNumberToObject(fan, "temp", status.temp / 10.0);
cJSON_AddBoolToObject(fan, "running", status.is_running);
cJSON_AddNumberToObject(fan, "control_temperature", status.control_temp / 10.0);
cJSON_AddNumberToObject(fan, "guard_temperature", status.guard_temp / 10.0);
cJSON_AddNumberToObject(fan, "predicted_temperature", status.predicted_temp / 10.0);
cJSON_AddNumberToObject(fan, "slope_c_per_min", status.slope_c_per_min);
cJSON_AddNumberToObject(fan, "controller_gain", status.controller_gain);
cJSON_AddNumberToObject(fan, "cooling_response", status.cooling_response);
cJSON_AddStringToObject(fan, "auto_state", fan_auto_state_to_str(status.auto_state));
cJSON_AddBoolToObject(fan, "guard_active", status.guard_active);
cJSON_AddBoolToObject(fan, "temp_stale", status.temp_stale);
cJSON_AddItemToArray(fans, fan);
}
}
Expand All @@ -226,7 +248,7 @@ static esp_err_t api_device_fan_status(const cJSON *params, ts_api_result_t *res
/**
* @brief device.fan.set - Set fan parameters
* @param fan: fan id
* @param mode: "off", "manual", "auto"
* @param mode: "off", "manual", "auto", "curve"
* @param duty: duty cycle (0-100) for manual mode
*/
static esp_err_t api_device_fan_set(const cJSON *params, ts_api_result_t *result)
Expand Down Expand Up @@ -254,6 +276,8 @@ static esp_err_t api_device_fan_set(const cJSON *params, ts_api_result_t *result
fan_mode = TS_FAN_MODE_MANUAL;
} else if (strcmp(mode->valuestring, "auto") == 0) {
fan_mode = TS_FAN_MODE_AUTO;
} else if (strcmp(mode->valuestring, "curve") == 0) {
fan_mode = TS_FAN_MODE_CURVE;
} else {
ts_api_result_error(result, TS_API_ERR_INVALID_ARG, "Invalid mode");
return ESP_ERR_INVALID_ARG;
Expand All @@ -264,7 +288,8 @@ static esp_err_t api_device_fan_set(const cJSON *params, ts_api_result_t *result
cJSON *duty = cJSON_GetObjectItem(params, "duty");
if (ret == ESP_OK && duty && cJSON_IsNumber(duty)) {
int duty_val = (int)cJSON_GetNumberValue(duty);
if (duty_val >= 0 && duty_val <= 100) {
if ((!mode || !cJSON_IsString(mode) || strcmp(mode->valuestring, "manual") == 0) &&
duty_val >= 0 && duty_val <= 100) {
ret = ts_fan_set_duty(fan_id, duty_val);
}
}
Expand Down
26 changes: 23 additions & 3 deletions components/ts_api/src/ts_api_fan.c
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ static const char *mode_to_string(ts_fan_mode_t mode)
}
}

static const char *auto_state_to_string(ts_fan_auto_state_t state)
{
switch (state) {
case TS_FAN_AUTO_STATE_IDLE: return "idle";
case TS_FAN_AUTO_STATE_BASELINE: return "baseline";
case TS_FAN_AUTO_STATE_ACTIVE: return "active";
case TS_FAN_AUTO_STATE_GUARD: return "guard";
case TS_FAN_AUTO_STATE_STALE: return "stale";
default: return "unknown";
}
}

static ts_fan_mode_t string_to_mode(const char *str)
{
if (!str) return TS_FAN_MODE_MANUAL;
Expand All @@ -53,6 +65,15 @@ static cJSON *status_to_json(ts_fan_id_t fan_id, const ts_fan_status_t *status)
cJSON_AddBoolToObject(obj, "enabled", status->enabled);
cJSON_AddBoolToObject(obj, "running", status->is_running);
cJSON_AddBoolToObject(obj, "fault", status->fault);
cJSON_AddNumberToObject(obj, "control_temperature", status->control_temp / 10.0);
cJSON_AddNumberToObject(obj, "guard_temperature", status->guard_temp / 10.0);
cJSON_AddNumberToObject(obj, "predicted_temperature", status->predicted_temp / 10.0);
cJSON_AddNumberToObject(obj, "slope_c_per_min", status->slope_c_per_min);
cJSON_AddNumberToObject(obj, "controller_gain", status->controller_gain);
cJSON_AddNumberToObject(obj, "cooling_response", status->cooling_response);
cJSON_AddStringToObject(obj, "auto_state", auto_state_to_string(status->auto_state));
cJSON_AddBoolToObject(obj, "guard_active", status->guard_active);
cJSON_AddBoolToObject(obj, "temp_stale", status->temp_stale);
return obj;
}

Expand Down Expand Up @@ -102,9 +123,8 @@ static esp_err_t api_fan_status(const cJSON *params, ts_api_result_t *result)
/* 添加全局温度信息(从绑定变量读取) */
ts_temp_status_t temp_status;
if (ts_temp_get_status(&temp_status) == ESP_OK) {
float temp_c = temp_status.current_temp / 10.0f;
cJSON_AddNumberToObject(data, "temperature", temp_c);
cJSON_AddBoolToObject(data, "temp_valid", temp_status.current_temp > -400); // > -40°C 表示有效
cJSON_AddNumberToObject(data, "temperature", temp_status.current_temp / 10.0f);
cJSON_AddBoolToObject(data, "temp_valid", temp_status.current_valid);
if (temp_status.bound_variable[0] != '\0') {
cJSON_AddStringToObject(data, "temp_source", temp_status.bound_variable);
}
Expand Down
Loading
Loading