feat(ui): add weather and lucky color commands
This commit is contained in:
@@ -26,7 +26,9 @@ typedef enum {
|
|||||||
CONTROL_PLANE_MODEL_COMMAND_UNKNOWN = 0,
|
CONTROL_PLANE_MODEL_COMMAND_UNKNOWN = 0,
|
||||||
CONTROL_PLANE_MODEL_COMMAND_DIRECTION,
|
CONTROL_PLANE_MODEL_COMMAND_DIRECTION,
|
||||||
CONTROL_PLANE_MODEL_COMMAND_CALIBRATE,
|
CONTROL_PLANE_MODEL_COMMAND_CALIBRATE,
|
||||||
CONTROL_PLANE_MODEL_COMMAND_CHOUQIAN
|
CONTROL_PLANE_MODEL_COMMAND_CHOUQIAN,
|
||||||
|
CONTROL_PLANE_MODEL_COMMAND_WEATHER,
|
||||||
|
CONTROL_PLANE_MODEL_COMMAND_LUCKY_COLOR,
|
||||||
} control_plane_model_command_type_t;
|
} control_plane_model_command_type_t;
|
||||||
|
|
||||||
typedef void (*control_plane_model_command_callback_t)(
|
typedef void (*control_plane_model_command_callback_t)(
|
||||||
|
|||||||
@@ -147,6 +147,8 @@ bool control_plane_extract_model_command(const char *text,
|
|||||||
cJSON *direction = cJSON_GetObjectItemCaseSensitive(cmd_json, "转向");
|
cJSON *direction = cJSON_GetObjectItemCaseSensitive(cmd_json, "转向");
|
||||||
cJSON *calibrate = cJSON_GetObjectItemCaseSensitive(cmd_json, "校准");
|
cJSON *calibrate = cJSON_GetObjectItemCaseSensitive(cmd_json, "校准");
|
||||||
cJSON *chouqian = cJSON_GetObjectItemCaseSensitive(cmd_json, "抽签");
|
cJSON *chouqian = cJSON_GetObjectItemCaseSensitive(cmd_json, "抽签");
|
||||||
|
cJSON *weather = cJSON_GetObjectItemCaseSensitive(cmd_json, "天气");
|
||||||
|
cJSON *lucky_color = cJSON_GetObjectItemCaseSensitive(cmd_json, "喜色");
|
||||||
|
|
||||||
if (cJSON_IsString(direction) && direction->valuestring != NULL) {
|
if (cJSON_IsString(direction) && direction->valuestring != NULL) {
|
||||||
if (is_supported_model_direction(direction->valuestring)) {
|
if (is_supported_model_direction(direction->valuestring)) {
|
||||||
@@ -173,6 +175,16 @@ bool control_plane_extract_model_command(const char *text,
|
|||||||
}
|
}
|
||||||
found = true;
|
found = true;
|
||||||
}
|
}
|
||||||
|
} else if (cJSON_IsString(weather) && weather->valuestring != NULL &&
|
||||||
|
weather->valuestring[0] != '\0') {
|
||||||
|
*out_command_type = CONTROL_PLANE_MODEL_COMMAND_WEATHER;
|
||||||
|
strlcpy(out_value, weather->valuestring, out_value_capacity);
|
||||||
|
found = true;
|
||||||
|
} else if (cJSON_IsString(lucky_color) && lucky_color->valuestring != NULL &&
|
||||||
|
lucky_color->valuestring[0] != '\0') {
|
||||||
|
*out_command_type = CONTROL_PLANE_MODEL_COMMAND_LUCKY_COLOR;
|
||||||
|
strlcpy(out_value, lucky_color->valuestring, out_value_capacity);
|
||||||
|
found = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
cJSON_Delete(cmd_json);
|
cJSON_Delete(cmd_json);
|
||||||
|
|||||||
@@ -1040,6 +1040,57 @@ static esp_err_t control_plane_run_model_calibration_motion(void)
|
|||||||
return control_plane_rotate_to_angle(start_angle);
|
return control_plane_rotate_to_angle(start_angle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const char *color_name;
|
||||||
|
domain_wuxing_element_t element;
|
||||||
|
} control_plane_lucky_color_mapping_t;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Lucky-color names are intentionally mapped in one place so product changes
|
||||||
|
* can replace colors without touching model-command parsing or LCD playback.
|
||||||
|
*/
|
||||||
|
static const control_plane_lucky_color_mapping_t s_lucky_color_mappings[] = {
|
||||||
|
{"金", DOMAIN_WUXING_ELEMENT_METAL},
|
||||||
|
{"棕", DOMAIN_WUXING_ELEMENT_WOOD},
|
||||||
|
{"蓝", DOMAIN_WUXING_ELEMENT_WATER},
|
||||||
|
{"红", DOMAIN_WUXING_ELEMENT_FIRE},
|
||||||
|
{"黄", DOMAIN_WUXING_ELEMENT_EARTH},
|
||||||
|
};
|
||||||
|
|
||||||
|
static bool control_plane_resolve_lucky_color(const char *color_name,
|
||||||
|
domain_wuxing_element_t *out_element)
|
||||||
|
{
|
||||||
|
if (color_name == NULL || out_element == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0;
|
||||||
|
i < sizeof(s_lucky_color_mappings) / sizeof(s_lucky_color_mappings[0]);
|
||||||
|
++i) {
|
||||||
|
if (strcmp(color_name, s_lucky_color_mappings[i].color_name) == 0) {
|
||||||
|
*out_element = s_lucky_color_mappings[i].element;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t control_plane_show_lucky_color_expression(const char *color_name)
|
||||||
|
{
|
||||||
|
domain_wuxing_element_t element = DOMAIN_WUXING_ELEMENT_WATER;
|
||||||
|
if (!control_plane_resolve_lucky_color(color_name, &element)) {
|
||||||
|
return ESP_ERR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
const domain_expression_request_t request = {
|
||||||
|
.id = DOMAIN_EXPRESSION_WUXING,
|
||||||
|
.trigger = DOMAIN_EXPRESSION_TRIGGER_MODEL_LUCKY_COLOR,
|
||||||
|
.wuxing_element = element,
|
||||||
|
.wuxing_element_valid = true,
|
||||||
|
};
|
||||||
|
return domain_runtime_request_expression(&request);
|
||||||
|
}
|
||||||
|
|
||||||
static esp_err_t control_plane_show_chouqian_expression(size_t result_index, bool result_index_valid)
|
static esp_err_t control_plane_show_chouqian_expression(size_t result_index, bool result_index_valid)
|
||||||
{
|
{
|
||||||
const domain_expression_request_t request = {
|
const domain_expression_request_t request = {
|
||||||
@@ -1107,6 +1158,22 @@ static void control_plane_model_command_task(void *arg)
|
|||||||
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
|
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
|
||||||
ESP_LOGW(TAG, "Chouqian expression failed: %s", esp_err_to_name(err));
|
ESP_LOGW(TAG, "Chouqian expression failed: %s", esp_err_to_name(err));
|
||||||
}
|
}
|
||||||
|
} else if (cmd.command_type == CONTROL_PLANE_MODEL_COMMAND_WEATHER) {
|
||||||
|
/*
|
||||||
|
* TODO: Request the weather LCD expression here after its assets
|
||||||
|
* and domain expression mapping are available. Parsing and
|
||||||
|
* dispatch stay active now to preserve the model-command contract.
|
||||||
|
*/
|
||||||
|
ESP_LOGI(TAG, "Weather UI pending, received model command: %s", cmd.value);
|
||||||
|
} else if (cmd.command_type == CONTROL_PLANE_MODEL_COMMAND_LUCKY_COLOR) {
|
||||||
|
ESP_LOGI(TAG, "Lucky color triggered by model command: %s", cmd.value);
|
||||||
|
esp_err_t err = control_plane_show_lucky_color_expression(cmd.value);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG,
|
||||||
|
"Show lucky-color expression failed for '%s': %s",
|
||||||
|
cmd.value,
|
||||||
|
esp_err_to_name(err));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -166,6 +166,15 @@ typedef enum {
|
|||||||
DOMAIN_EXPRESSION_OTA_UPDATE,
|
DOMAIN_EXPRESSION_OTA_UPDATE,
|
||||||
} domain_expression_id_t;
|
} domain_expression_id_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
DOMAIN_WUXING_ELEMENT_WATER = 0,
|
||||||
|
DOMAIN_WUXING_ELEMENT_WOOD,
|
||||||
|
DOMAIN_WUXING_ELEMENT_FIRE,
|
||||||
|
DOMAIN_WUXING_ELEMENT_EARTH,
|
||||||
|
DOMAIN_WUXING_ELEMENT_METAL,
|
||||||
|
DOMAIN_WUXING_ELEMENT_COUNT,
|
||||||
|
} domain_wuxing_element_t;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
DOMAIN_EXPRESSION_TRIGGER_UNSPECIFIED = 0,
|
DOMAIN_EXPRESSION_TRIGGER_UNSPECIFIED = 0,
|
||||||
DOMAIN_EXPRESSION_TRIGGER_SERVER_TASK_FAILED,
|
DOMAIN_EXPRESSION_TRIGGER_SERVER_TASK_FAILED,
|
||||||
@@ -181,6 +190,7 @@ typedef enum {
|
|||||||
DOMAIN_EXPRESSION_TRIGGER_SW2_DOUBLE_CLICK,
|
DOMAIN_EXPRESSION_TRIGGER_SW2_DOUBLE_CLICK,
|
||||||
DOMAIN_EXPRESSION_TRIGGER_LOCAL_WAKE,
|
DOMAIN_EXPRESSION_TRIGGER_LOCAL_WAKE,
|
||||||
DOMAIN_EXPRESSION_TRIGGER_OTA_UPDATE,
|
DOMAIN_EXPRESSION_TRIGGER_OTA_UPDATE,
|
||||||
|
DOMAIN_EXPRESSION_TRIGGER_MODEL_LUCKY_COLOR,
|
||||||
} domain_expression_trigger_t;
|
} domain_expression_trigger_t;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
@@ -189,6 +199,8 @@ typedef struct {
|
|||||||
size_t direction_index; /* Only used by DOMAIN_EXPRESSION_WEALTH_DIRECTION */
|
size_t direction_index; /* Only used by DOMAIN_EXPRESSION_WEALTH_DIRECTION */
|
||||||
size_t chouqian_result_index; /* Optional for DOMAIN_EXPRESSION_CHOUQIAN */
|
size_t chouqian_result_index; /* Optional for DOMAIN_EXPRESSION_CHOUQIAN */
|
||||||
bool chouqian_result_index_valid;
|
bool chouqian_result_index_valid;
|
||||||
|
domain_wuxing_element_t wuxing_element; /* Optional for DOMAIN_EXPRESSION_WUXING */
|
||||||
|
bool wuxing_element_valid;
|
||||||
} domain_expression_request_t;
|
} domain_expression_request_t;
|
||||||
|
|
||||||
esp_err_t domain_init(const domain_direction_axis_config_t *config,
|
esp_err_t domain_init(const domain_direction_axis_config_t *config,
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ typedef struct {
|
|||||||
uint16_t icon_y;
|
uint16_t icon_y;
|
||||||
} domain_wuxing_result_t;
|
} domain_wuxing_result_t;
|
||||||
|
|
||||||
|
/* Keep this table ordered by domain_wuxing_element_t. */
|
||||||
static const domain_wuxing_result_t s_wuxing_results[] = {
|
static const domain_wuxing_result_t s_wuxing_results[] = {
|
||||||
{.asset_dir = "shui", .icon_id = 0, .icon_x = 124, .icon_y = 8},
|
{.asset_dir = "shui", .icon_id = 0, .icon_x = 124, .icon_y = 8},
|
||||||
{.asset_dir = "mu", .icon_id = 1, .icon_x = 217, .icon_y = 92},
|
{.asset_dir = "mu", .icon_id = 1, .icon_x = 217, .icon_y = 92},
|
||||||
@@ -104,6 +105,10 @@ static const domain_wuxing_result_t s_wuxing_results[] = {
|
|||||||
{.asset_dir = "jin", .icon_id = 4, .icon_x = 18, .icon_y = 102},
|
{.asset_dir = "jin", .icon_id = 4, .icon_x = 18, .icon_y = 102},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_Static_assert(sizeof(s_wuxing_results) / sizeof(s_wuxing_results[0]) ==
|
||||||
|
DOMAIN_WUXING_ELEMENT_COUNT,
|
||||||
|
"Wuxing result assets must match domain_wuxing_element_t");
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Wealth-direction reveal asset directories, indexed parallel to the direction
|
* Wealth-direction reveal asset directories, indexed parallel to the direction
|
||||||
* catalog order in components/domain/src/direction/domain_direction_catalog.c
|
* catalog order in components/domain/src/direction/domain_direction_catalog.c
|
||||||
@@ -1461,7 +1466,18 @@ static esp_err_t domain_resolve_expression_platform_request(
|
|||||||
if (request->id == DOMAIN_EXPRESSION_WUXING) {
|
if (request->id == DOMAIN_EXPRESSION_WUXING) {
|
||||||
static platform_lcd_presentation_step_t s_wuxing_steps[3];
|
static platform_lcd_presentation_step_t s_wuxing_steps[3];
|
||||||
const size_t result_count = sizeof(s_wuxing_results) / sizeof(s_wuxing_results[0]);
|
const size_t result_count = sizeof(s_wuxing_results) / sizeof(s_wuxing_results[0]);
|
||||||
const domain_wuxing_result_t *result = &s_wuxing_results[esp_random() % result_count];
|
size_t result_index = esp_random() % result_count;
|
||||||
|
if (request->wuxing_element_valid) {
|
||||||
|
if ((size_t)request->wuxing_element >= result_count) {
|
||||||
|
ESP_LOGW(TAG,
|
||||||
|
"Wuxing element out of range: %u (max %u)",
|
||||||
|
(unsigned int)request->wuxing_element,
|
||||||
|
(unsigned int)result_count);
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
result_index = (size_t)request->wuxing_element;
|
||||||
|
}
|
||||||
|
const domain_wuxing_result_t *result = &s_wuxing_results[result_index];
|
||||||
|
|
||||||
s_wuxing_steps[0] = (platform_lcd_presentation_step_t) {
|
s_wuxing_steps[0] = (platform_lcd_presentation_step_t) {
|
||||||
.asset_dir = "qianzhiwuxing",
|
.asset_dir = "qianzhiwuxing",
|
||||||
|
|||||||
@@ -0,0 +1,343 @@
|
|||||||
|
# 天气与幸运色 UI 指令接入说明
|
||||||
|
|
||||||
|
## 1. 背景与目标
|
||||||
|
|
||||||
|
对话模型使用与财位指令相同的注释 JSON 格式返回天气和喜色指令:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/*{"天气":"晴"}*/
|
||||||
|
/*{"喜色":"红"}*/
|
||||||
|
```
|
||||||
|
|
||||||
|
本次开发目标如下:
|
||||||
|
|
||||||
|
1. 识别并分发“天气”指令。天气 UI 素材尚未完成,本阶段只预留处理入口,不改变 LCD。
|
||||||
|
2. 识别“喜色”指令,并根据喜色确定性播放项目已有的五行 UI。
|
||||||
|
3. 保持原有按键触发五行 UI 时的随机行为不变。
|
||||||
|
4. 集中管理喜色映射,方便后续更换颜色或素材。
|
||||||
|
|
||||||
|
## 2. 问题原因
|
||||||
|
|
||||||
|
### 2.1 原模型指令不支持天气和喜色
|
||||||
|
|
||||||
|
原有指令契约只包含转向、校准和抽签。模型即使返回“天气”或“喜色”JSON,指令提取器也不会识别,指令无法进入现有模型命令队列。
|
||||||
|
|
||||||
|
### 2.2 原五行 UI 只能随机显示
|
||||||
|
|
||||||
|
项目已有完整的五行 UI 和金木水火土资源,但原逻辑通过随机数选择结果:
|
||||||
|
|
||||||
|
```c
|
||||||
|
const domain_wuxing_result_t *result =
|
||||||
|
&s_wuxing_results[esp_random() % result_count];
|
||||||
|
```
|
||||||
|
|
||||||
|
这适合原有随机五行功能,却不能满足喜色的确定性要求。例如模型返回“红”时,必须固定显示“火”,不能继续随机。
|
||||||
|
|
||||||
|
### 2.3 天气 UI 没有可播放资源
|
||||||
|
|
||||||
|
目前尚未确定天气类型、图片资源、播放时序和领域表达式映射。如果直接加入空的 LCD 表达式,可能因资源不存在而回退到基础表情。因此本阶段先打通解析和调度链路,明确保留未来 UI 接入位置。
|
||||||
|
|
||||||
|
## 3. 解决方式与具体代码
|
||||||
|
|
||||||
|
### 3.1 扩展模型指令类型
|
||||||
|
|
||||||
|
在原枚举末尾追加天气和幸运色,已有枚举值保持不变:
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef enum {
|
||||||
|
CONTROL_PLANE_MODEL_COMMAND_UNKNOWN = 0,
|
||||||
|
CONTROL_PLANE_MODEL_COMMAND_DIRECTION,
|
||||||
|
CONTROL_PLANE_MODEL_COMMAND_CALIBRATE,
|
||||||
|
CONTROL_PLANE_MODEL_COMMAND_CHOUQIAN,
|
||||||
|
CONTROL_PLANE_MODEL_COMMAND_WEATHER,
|
||||||
|
CONTROL_PLANE_MODEL_COMMAND_LUCKY_COLOR,
|
||||||
|
} control_plane_model_command_type_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
新指令继续复用原有回调、队列和模型命令工作任务,没有新增常驻任务。
|
||||||
|
|
||||||
|
### 3.2 解析天气与喜色
|
||||||
|
|
||||||
|
指令提取器仍要求注释 JSON 只有一个字段。新增字段读取和非空校验:
|
||||||
|
|
||||||
|
```c
|
||||||
|
cJSON *weather = cJSON_GetObjectItemCaseSensitive(cmd_json, "天气");
|
||||||
|
cJSON *lucky_color = cJSON_GetObjectItemCaseSensitive(cmd_json, "喜色");
|
||||||
|
|
||||||
|
if (cJSON_IsString(weather) && weather->valuestring != NULL &&
|
||||||
|
weather->valuestring[0] != '\0') {
|
||||||
|
*out_command_type = CONTROL_PLANE_MODEL_COMMAND_WEATHER;
|
||||||
|
strlcpy(out_value, weather->valuestring, out_value_capacity);
|
||||||
|
found = true;
|
||||||
|
} else if (cJSON_IsString(lucky_color) && lucky_color->valuestring != NULL &&
|
||||||
|
lucky_color->valuestring[0] != '\0') {
|
||||||
|
*out_command_type = CONTROL_PLANE_MODEL_COMMAND_LUCKY_COLOR;
|
||||||
|
strlcpy(out_value, lucky_color->valuestring, out_value_capacity);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
支持的标准喜色指令为:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/*{"喜色":"金"}*/
|
||||||
|
/*{"喜色":"棕"}*/
|
||||||
|
/*{"喜色":"蓝"}*/
|
||||||
|
/*{"喜色":"红"}*/
|
||||||
|
/*{"喜色":"黄"}*/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 为原五行 UI 增加可选的确定性结果
|
||||||
|
|
||||||
|
五行表达式属于领域层,因此在领域契约中声明五行元素:
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef enum {
|
||||||
|
DOMAIN_WUXING_ELEMENT_WATER = 0,
|
||||||
|
DOMAIN_WUXING_ELEMENT_WOOD,
|
||||||
|
DOMAIN_WUXING_ELEMENT_FIRE,
|
||||||
|
DOMAIN_WUXING_ELEMENT_EARTH,
|
||||||
|
DOMAIN_WUXING_ELEMENT_METAL,
|
||||||
|
DOMAIN_WUXING_ELEMENT_COUNT,
|
||||||
|
} domain_wuxing_element_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
统一表达式请求增加可选的五行字段:
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct {
|
||||||
|
domain_expression_id_t id;
|
||||||
|
domain_expression_trigger_t trigger;
|
||||||
|
size_t direction_index;
|
||||||
|
size_t chouqian_result_index;
|
||||||
|
bool chouqian_result_index_valid;
|
||||||
|
domain_wuxing_element_t wuxing_element;
|
||||||
|
bool wuxing_element_valid;
|
||||||
|
} domain_expression_request_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
字段含义:
|
||||||
|
|
||||||
|
- `wuxing_element_valid == false`:继续随机选择五行,兼容原有调用。
|
||||||
|
- `wuxing_element_valid == true`:使用 `wuxing_element` 指定的结果。
|
||||||
|
|
||||||
|
### 3.4 复用原有五行资源和播放流程
|
||||||
|
|
||||||
|
本次没有复制五行素材,也没有新建第二套播放步骤。仍使用原资源表:
|
||||||
|
|
||||||
|
```c
|
||||||
|
/* Keep this table ordered by domain_wuxing_element_t. */
|
||||||
|
static const domain_wuxing_result_t s_wuxing_results[] = {
|
||||||
|
{.asset_dir = "shui", .icon_id = 0, .icon_x = 124, .icon_y = 8},
|
||||||
|
{.asset_dir = "mu", .icon_id = 1, .icon_x = 217, .icon_y = 92},
|
||||||
|
{.asset_dir = "huo", .icon_id = 2, .icon_x = 198, .icon_y = 171},
|
||||||
|
{.asset_dir = "tu", .icon_id = 3, .icon_x = 46, .icon_y = 178},
|
||||||
|
{.asset_dir = "jin", .icon_id = 4, .icon_x = 18, .icon_y = 102},
|
||||||
|
};
|
||||||
|
|
||||||
|
_Static_assert(sizeof(s_wuxing_results) / sizeof(s_wuxing_results[0]) ==
|
||||||
|
DOMAIN_WUXING_ELEMENT_COUNT,
|
||||||
|
"Wuxing result assets must match domain_wuxing_element_t");
|
||||||
|
```
|
||||||
|
|
||||||
|
编译期断言保证五行枚举和资源表数量一致,防止后续修改导致静默错位。
|
||||||
|
|
||||||
|
原表达式只扩展结果选择方式,前置画面、图标合成、结果揭示和返回基础表情继续走原流程:
|
||||||
|
|
||||||
|
```c
|
||||||
|
const size_t result_count =
|
||||||
|
sizeof(s_wuxing_results) / sizeof(s_wuxing_results[0]);
|
||||||
|
size_t result_index = esp_random() % result_count;
|
||||||
|
|
||||||
|
if (request->wuxing_element_valid) {
|
||||||
|
if ((size_t)request->wuxing_element >= result_count) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
result_index = (size_t)request->wuxing_element;
|
||||||
|
}
|
||||||
|
|
||||||
|
const domain_wuxing_result_t *result = &s_wuxing_results[result_index];
|
||||||
|
```
|
||||||
|
|
||||||
|
因此,原按键调用仍随机;喜色调用则固定显示指定元素。
|
||||||
|
|
||||||
|
### 3.5 集中管理喜色映射
|
||||||
|
|
||||||
|
当前映射关系如下:
|
||||||
|
|
||||||
|
| 喜色 | 五行 UI |
|
||||||
|
| --- | --- |
|
||||||
|
| 金 | 金 |
|
||||||
|
| 棕 | 木 |
|
||||||
|
| 蓝 | 水 |
|
||||||
|
| 红 | 火 |
|
||||||
|
| 黄 | 土 |
|
||||||
|
|
||||||
|
代码将映射集中在一个表中:
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct {
|
||||||
|
const char *color_name;
|
||||||
|
domain_wuxing_element_t element;
|
||||||
|
} control_plane_lucky_color_mapping_t;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Lucky-color names are intentionally mapped in one place so product changes
|
||||||
|
* can replace colors without touching model-command parsing or LCD playback.
|
||||||
|
*/
|
||||||
|
static const control_plane_lucky_color_mapping_t s_lucky_color_mappings[] = {
|
||||||
|
{"金", DOMAIN_WUXING_ELEMENT_METAL},
|
||||||
|
{"棕", DOMAIN_WUXING_ELEMENT_WOOD},
|
||||||
|
{"蓝", DOMAIN_WUXING_ELEMENT_WATER},
|
||||||
|
{"红", DOMAIN_WUXING_ELEMENT_FIRE},
|
||||||
|
{"黄", DOMAIN_WUXING_ELEMENT_EARTH},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
映射查找代码:
|
||||||
|
|
||||||
|
```c
|
||||||
|
static bool control_plane_resolve_lucky_color(
|
||||||
|
const char *color_name,
|
||||||
|
domain_wuxing_element_t *out_element)
|
||||||
|
{
|
||||||
|
if (color_name == NULL || out_element == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0;
|
||||||
|
i < sizeof(s_lucky_color_mappings) /
|
||||||
|
sizeof(s_lucky_color_mappings[0]);
|
||||||
|
++i) {
|
||||||
|
if (strcmp(color_name, s_lucky_color_mappings[i].color_name) == 0) {
|
||||||
|
*out_element = s_lucky_color_mappings[i].element;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
映射成功后仍请求原有五行表达式:
|
||||||
|
|
||||||
|
```c
|
||||||
|
const domain_expression_request_t request = {
|
||||||
|
.id = DOMAIN_EXPRESSION_WUXING,
|
||||||
|
.trigger = DOMAIN_EXPRESSION_TRIGGER_MODEL_LUCKY_COLOR,
|
||||||
|
.wuxing_element = element,
|
||||||
|
.wuxing_element_valid = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
return domain_runtime_request_expression(&request);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.6 预留天气 UI 接入点
|
||||||
|
|
||||||
|
天气指令到达模型命令工作任务后,目前仅记录指令值:
|
||||||
|
|
||||||
|
```c
|
||||||
|
} else if (cmd.command_type == CONTROL_PLANE_MODEL_COMMAND_WEATHER) {
|
||||||
|
/*
|
||||||
|
* TODO: Request the weather LCD expression here after its assets
|
||||||
|
* and domain expression mapping are available. Parsing and
|
||||||
|
* dispatch stay active now to preserve the model-command contract.
|
||||||
|
*/
|
||||||
|
ESP_LOGI(TAG,
|
||||||
|
"Weather UI pending, received model command: %s",
|
||||||
|
cmd.value);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
天气素材完成后,只需补充领域表达式和此处的请求,不需要再次修改模型协议、解析器、回调或队列。
|
||||||
|
|
||||||
|
## 4. 解决成果
|
||||||
|
|
||||||
|
1. 设备能够识别 `/*{"天气":"晴"}*/`,并将天气值分发至工作任务。
|
||||||
|
2. 天气指令暂不切换 LCD,避免缺少资源时出现错误或回退画面。
|
||||||
|
3. 五种标准喜色能够固定显示对应的金、木、水、火、土 UI。
|
||||||
|
4. 喜色功能完整复用已有五行资源、图标坐标、播放步骤和 LCD 调度。
|
||||||
|
5. 原按键触发五行 UI 的随机行为保持不变。
|
||||||
|
6. 不支持的喜色不会随机显示错误结果,而是记录映射失败日志。
|
||||||
|
7. ESP-IDF 5.5.4 全量构建通过,固件镜像生成成功。
|
||||||
|
|
||||||
|
## 5. 后续影响
|
||||||
|
|
||||||
|
### 5.1 对现有功能的影响
|
||||||
|
|
||||||
|
- 财位、校准、抽签等已有模型指令的枚举值未改变。
|
||||||
|
- 表达式请求结构增加字段,但现有请求均使用指定字段初始化,新增字段默认置零。
|
||||||
|
- 原五行调用的 `wuxing_element_valid` 默认为 `false`,继续随机显示。
|
||||||
|
- 未新增 LCD 图片,不增加 LittleFS 资源占用。
|
||||||
|
- 未新增任务、队列或锁,不改变任务生命周期和并发模型。
|
||||||
|
|
||||||
|
### 5.2 当前限制
|
||||||
|
|
||||||
|
- 当前只接受单字喜色:金、棕、蓝、红、黄;“红色”“蓝色”等别名尚未支持。
|
||||||
|
- 天气值当前只校验为非空字符串,尚未限制为晴、阴、雨等固定枚举。
|
||||||
|
- 指令 JSON 仍要求只有一个字段,同时包含天气和喜色会被拒绝。
|
||||||
|
- 沿用原有单次模型命令分发机制,同一次模型响应中的后续指令不会重复触发。
|
||||||
|
- 当前完成了编译验证,尚未烧录设备进行 LCD 实机验收。
|
||||||
|
|
||||||
|
## 6. 后续开发方式
|
||||||
|
|
||||||
|
### 6.1 接入天气 UI
|
||||||
|
|
||||||
|
天气素材完成后按以下顺序开发:
|
||||||
|
|
||||||
|
1. 确定允许的天气值,例如晴、多云、阴、雨、雪。
|
||||||
|
2. 将天气 PNG 加入平台 LCD 资源目录和构建白名单。
|
||||||
|
3. 在平台资源表注册天气素材名称和帧序列。
|
||||||
|
4. 在领域表达式枚举中增加天气表达式语义。
|
||||||
|
5. 在领域适配器中将天气类型映射为平台播放请求。
|
||||||
|
6. 在天气命令分支中构造表达式请求,替换现有 `TODO` 占位。
|
||||||
|
7. 验证 UI 优先级、打断策略、播放时长和返回基础表情行为。
|
||||||
|
|
||||||
|
调用方向应保持为:
|
||||||
|
|
||||||
|
```text
|
||||||
|
模型天气指令
|
||||||
|
-> 控制层决定显示时机
|
||||||
|
-> 领域层表达式请求
|
||||||
|
-> 平台层 LCD 播放
|
||||||
|
```
|
||||||
|
|
||||||
|
控制层不应直接调用平台 LCD API。
|
||||||
|
|
||||||
|
### 6.2 修改或扩展喜色
|
||||||
|
|
||||||
|
如果只调整颜色与五行的关系,修改集中映射表即可。例如将棕改成绿且仍对应木:
|
||||||
|
|
||||||
|
```c
|
||||||
|
{"绿", DOMAIN_WUXING_ELEMENT_WOOD},
|
||||||
|
```
|
||||||
|
|
||||||
|
如果要兼容颜色别名,可以将多个字符串映射到同一元素:
|
||||||
|
|
||||||
|
```c
|
||||||
|
{"红", DOMAIN_WUXING_ELEMENT_FIRE},
|
||||||
|
{"红色", DOMAIN_WUXING_ELEMENT_FIRE},
|
||||||
|
```
|
||||||
|
|
||||||
|
如果未来幸运色需要独立于五行 UI,应新增领域表达式和独立资源映射,不应修改原有五行素材含义。
|
||||||
|
|
||||||
|
### 6.3 建议实机验证
|
||||||
|
|
||||||
|
烧录设备后至少测试:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/*{"天气":"晴"}*/
|
||||||
|
/*{"喜色":"金"}*/
|
||||||
|
/*{"喜色":"棕"}*/
|
||||||
|
/*{"喜色":"蓝"}*/
|
||||||
|
/*{"喜色":"红"}*/
|
||||||
|
/*{"喜色":"黄"}*/
|
||||||
|
/*{"喜色":"紫"}*/
|
||||||
|
```
|
||||||
|
|
||||||
|
预期结果:
|
||||||
|
|
||||||
|
- 天气“晴”只输出预留日志,不改变 LCD。
|
||||||
|
- 五种标准喜色分别显示金、木、水、火、土 UI。
|
||||||
|
- 不支持的“紫”不播放随机五行 UI,并输出失败日志。
|
||||||
|
- 喜色 UI 播放完成后正常返回基础表情。
|
||||||
|
- 原按键五行功能继续随机显示结果。
|
||||||
Reference in New Issue
Block a user