重构显示逻辑为 MVP 架构,进行显示模块的解耦

This commit is contained in:
2026-03-24 19:52:22 +08:00
parent a4bf0962b2
commit 0690d6a00e
42 changed files with 2207 additions and 1417 deletions

View File

@@ -6,8 +6,8 @@ set(DTU_TEST_COMMON_SOURCES
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/ascii.c"
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd.c"
"${CMAKE_SOURCE_DIR}/src/Drv/key.c"
"${CMAKE_SOURCE_DIR}/src/Drv/menu.c"
"${CMAKE_SOURCE_DIR}/src/Drv/display.c"
"${CMAKE_SOURCE_DIR}/src/Drv/menu/app/menu.c"
"${CMAKE_SOURCE_DIR}/src/Drv/menu/model/display.c"
"${CMAKE_SOURCE_DIR}/src/TCP/tcp.c"
"${CMAKE_SOURCE_DIR}/src/thread_utils.c"
"${CMAKE_SOURCE_DIR}/src/remoteDisplay.c"
@@ -20,12 +20,17 @@ set(DTU_TEST_COMMON_SOURCES
# - add_test: 注册到 CTest支持 ctest 统一执行
function(add_dtu_test test_name)
add_executable(${test_name} ${ARGN})
target_sources(${test_name} PRIVATE
"${CMAKE_SOURCE_DIR}/src/common/utf8.c"
)
# 测试目标可见的头文件目录
target_include_directories(${test_name} PRIVATE
"${CMAKE_SOURCE_DIR}/include"
"${CMAKE_SOURCE_DIR}/src"
"${CMAKE_SOURCE_DIR}/src/TCP"
"${CMAKE_SOURCE_DIR}/src/Drv"
"${CMAKE_SOURCE_DIR}/src/Drv/lcd"
"${CMAKE_SOURCE_DIR}/src/common"
)
# 平台差异Windows 需要 ws2_32非 Windows 使用 pthread
if(WIN32)
@@ -56,14 +61,6 @@ add_dtu_test(
"${CMAKE_SOURCE_DIR}/src/TCP/tcp.c"
"${CMAKE_SOURCE_DIR}/src/thread_utils.c"
)
add_dtu_test(
test_p0_utf8_next
test_p0_utf8_next.c
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd.c"
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_draw.c"
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_text.c"
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/ascii.c"
)
add_dtu_test(
test_p0_utf8_hz12_get
test_p0_utf8_hz12_get.c
@@ -89,12 +86,35 @@ add_dtu_test(
add_dtu_test(
test_p1_menu
test_p1_menu.c
"${CMAKE_SOURCE_DIR}/src/Drv/display.c"
"${CMAKE_SOURCE_DIR}/src/Drv/menu/model/menu_tree_builder.c"
"${CMAKE_SOURCE_DIR}/src/Drv/menu/view/menu_layout.c"
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_text.c"
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd.c"
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/ascii.c"
)
add_dtu_test(
test_p1_menu_nav_legacy
test_p1_menu_nav_legacy.c
"${CMAKE_SOURCE_DIR}/src/Drv/menu/presenter/menu_navigator.c"
)
add_dtu_test(
test_p1_menu_navigator
test_p1_menu_navigator.c
"${CMAKE_SOURCE_DIR}/src/Drv/menu/presenter/menu_navigator.c"
)
add_dtu_test(
test_p1_menu_tree_builder
test_p1_menu_tree_builder.c
"${CMAKE_SOURCE_DIR}/src/Drv/menu/model/menu_tree_builder.c"
)
add_dtu_test(
test_p1_menu_layout
test_p1_menu_layout.c
"${CMAKE_SOURCE_DIR}/src/Drv/menu/view/menu_layout.c"
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_text.c"
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd.c"
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_draw.c"
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_text.c"
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/ascii.c"
"${CMAKE_SOURCE_DIR}/src/Drv/key.c"
)
# ------------------------------------------------------------
@@ -106,3 +126,21 @@ add_dtu_test(
"${CMAKE_SOURCE_DIR}/src/TCP/tcp.c"
"${CMAKE_SOURCE_DIR}/src/thread_utils.c"
)
add_dtu_test(
test_p2_menu_runtime_startup
test_p2_menu_runtime_startup.c
"${CMAKE_SOURCE_DIR}/src/Drv/menu/app/menu.c"
"${CMAKE_SOURCE_DIR}/src/Drv/menu/model/display.c"
"${CMAKE_SOURCE_DIR}/src/Drv/menu/model/menu_model.c"
"${CMAKE_SOURCE_DIR}/src/Drv/menu/view/menu_view.c"
"${CMAKE_SOURCE_DIR}/src/Drv/menu/presenter/menu_presenter.c"
"${CMAKE_SOURCE_DIR}/src/Drv/menu/model/menu_tree_builder.c"
"${CMAKE_SOURCE_DIR}/src/Drv/menu/view/menu_layout.c"
"${CMAKE_SOURCE_DIR}/src/Drv/menu/presenter/menu_navigator.c"
"${CMAKE_SOURCE_DIR}/src/Drv/menu/view/menu_renderer_lcd.c"
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd.c"
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_draw.c"
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_text.c"
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/ascii.c"
"${CMAKE_SOURCE_DIR}/src/Drv/key.c"
)

View File

@@ -1,13 +1,42 @@
/* -------------------------------------------------------------------------
* 文件名: test_p0_utf8_hz12_get.c
* 作用:
* 验证中文点阵查询接口 utf8_hz12_get() 的基础行为是否正确。
*
* 被测接口:
* const uint8_t* utf8_hz12_get(uint32_t unicode);
*
* 测试目标:
* 1) 对于字库中存在的码点,应返回非 NULL 位图指针。
* 2) 对于字库范围外或不存在的码点,应返回 NULL。
*
* 说明:
* - 本测试只校验“是否命中”这一契约,不校验位图内容本身。
* - 若后续需要,可新增逐字节位图校验测试。
* ------------------------------------------------------------------------- */
#include "../src/Drv/lcd/ascii.h"
#include "test_common.h"
/* -------------------------------------------------------------------------
* 测试入口:
* 按“命中/低位未命中/高位未命中”三个场景依次断言。
*
* 返回值约定:
* 0 -> 测试通过
* 非0 -> 断言失败(由 ASSERT_* 宏返回)
* ------------------------------------------------------------------------- */
int main(void)
{
/* 命中用例U+4F60“你”通常在当前字库中存在 */
const uint8_t* hit = utf8_hz12_get(0x4F60u); /* 你 */
/* 低位未命中:非常低的控制区码点,预期不在中文字库 */
const uint8_t* miss_low = utf8_hz12_get(0x0001u);
/* 高位未命中:超高码点,预期不在当前 12 点阵中文表中 */
const uint8_t* miss_high = utf8_hz12_get(0x10FFFFu);
/* 命中必须返回有效指针 */
ASSERT_TRUE(hit != NULL);
/* 非命中必须返回 NULL */
ASSERT_TRUE(miss_low == NULL);
ASSERT_TRUE(miss_high == NULL);
return 0;

View File

@@ -1,27 +0,0 @@
#include "../src/Drv/lcd/lcd_text.h"
#include "test_common.h"
int main(void)
{
uint32_t unicode = 0;
const unsigned char ascii[] = "A";
const unsigned char u2[] = {0xC2, 0xA2, 0x00}; /* U+00A2 */
const unsigned char u3[] = {0xE4, 0xBD, 0xA0, 0x00}; /* U+4F60 */
const unsigned char invalid[] = {0xF0, 0x00}; /* 4-byte start not supported */
const unsigned char truncated2[] = {0xC2, 0x00};
const unsigned char truncated3[] = {0xE4, 0xBD, 0x00};
ASSERT_EQ_INT(1, utf8_next(ascii, &unicode));
ASSERT_EQ_U32(0x41u, unicode);
ASSERT_EQ_INT(2, utf8_next(u2, &unicode));
ASSERT_EQ_U32(0x00A2u, unicode);
ASSERT_EQ_INT(3, utf8_next(u3, &unicode));
ASSERT_EQ_U32(0x4F60u, unicode);
ASSERT_EQ_INT(0, utf8_next(invalid, &unicode));
ASSERT_EQ_INT(0, utf8_next(truncated2, &unicode));
ASSERT_EQ_INT(0, utf8_next(truncated3, &unicode));
return 0;
}

View File

@@ -1,28 +1,47 @@
#include <string.h>
#include "test_common.h"
#include "../src/Drv/menu.c"
#include "../src/Drv/menu/view/menu_layout.h"
#include "../src/Drv/menu/model/menu_tree_builder.h"
static int noop_proc(void)
{
return 0;
}
int main(void)
{
tagMenuCtrl ctrl;
tagMenuItem items[4];
uint8_t menu_num[4] = {0};
tagPMenuItem first[4] = {0};
tagPMenuItem index[4] = {0};
uint8_t max_len;
Menu_Init();
ASSERT_TRUE(g_tMenuCtrl.by0LevelNum > 0);
ASSERT_TRUE(g_tMenuCtrl.ptHead != NULL);
ASSERT_TRUE(g_tMenuCtrl.ptCurrent != NULL);
const tagMenuModel model[4] = {
{0, "Root", "", 0, 0, 0, (FUNCPTR)noop_proc},
{1, "设置", "", 0, 0, 0, (FUNCPTR)noop_proc},
{2, "子项", "", 0, 0, 0, (FUNCPTR)noop_proc},
{1, "查看", "", 0, 0, 0, (FUNCPTR)noop_proc},
};
ASSERT_EQ_INT(3, utf8_len_cal((uint8_t*)"ABC"));
ASSERT_EQ_INT(2, utf8_len_cal((uint8_t*)""));
memset(&ctrl, 0, sizeof(ctrl));
memset(items, 0, sizeof(items));
first[0] = g_tMenuCtrl.ptHead;
index[0] = g_tMenuCtrl.ptHead;
max_len = Menu_charLenCal(0, menu_num, first, index);
MenuTree_0LevelNumCal(&ctrl, model, 4);
ASSERT_EQ_INT(1, ctrl.by0LevelNum);
MenuTree_MainCreate(items, model, 4);
ASSERT_EQ_INT(3, MenuLayout_Utf8LenCal((uint8_t *)"ABC"));
ASSERT_EQ_INT(2, MenuLayout_Utf8LenCal((uint8_t *)""));
first[0] = &items[0];
index[0] = &items[0];
max_len = MenuLayout_CharLenCal(0, menu_num, first, index);
ASSERT_TRUE(max_len > 0);
ASSERT_TRUE(menu_num[1] > 0);
ASSERT_STREQ("设置", (const char *)items[1].byName);
return 0;
}

View File

@@ -0,0 +1,47 @@
#include "test_common.h"
#include <string.h>
#include "../src/Drv/lcd/lcd.h"
#include "../src/Drv/menu/app/menu.h"
#include "../src/Drv/menu/view/menu_layout.h"
int main(void)
{
tagMenuItem root;
tagMenuItem child;
MenuLayoutConfig config = {
LCD_SIZE_X,
LCD_SIZE_Y,
MENU_YMIN,
MENU_YMAX_FROM_LCD(LCD_SIZE_Y),
LINE_HEIGHT,
MENU_WITDTH,
MENU_XADD,
MENU_YADD};
memset(&root, 0, sizeof(root));
memset(&child, 0, sizeof(child));
memcpy(root.byName, "Root", 5);
memcpy(child.byName, "Child", 6);
root.byClass = 0;
child.byClass = 1;
root.ptLower = &child;
root.ptBehind = &root;
root.ptBefore = &root;
child.ptHigher = &root;
child.ptBehind = &child;
child.ptBefore = &child;
ASSERT_EQ_INT(3, MenuLayout_Utf8LenCal((uint8_t *)"ABC"));
ASSERT_EQ_INT(2, MenuLayout_Utf8LenCal((uint8_t *)""));
MenuLayout_PositionCal(&root, 1, &config);
ASSERT_TRUE(root.wEPosX > root.wSPosX);
ASSERT_TRUE(root.wEPosY > root.wSPosY);
ASSERT_TRUE(root.wEPosY <= LCD_SIZE_Y);
return 0;
}

View File

@@ -0,0 +1,93 @@
#include <string.h>
#include "test_common.h"
#include "../src/Drv/key.h"
#include "../src/Drv/menu/presenter/menu_navigator.h"
static int g_exec_count = 0;
static int on_exec(void)
{
g_exec_count++;
return 0;
}
static void build_legacy_like_tree(MenuNavState *nav, tagMenuItem *root, tagMenuItem *m1, tagMenuItem *m2, tagMenuItem *m1_sub)
{
memset(nav, 0, sizeof(*nav));
memset(root, 0, sizeof(*root));
memset(m1, 0, sizeof(*m1));
memset(m2, 0, sizeof(*m2));
memset(m1_sub, 0, sizeof(*m1_sub));
root->byClass = 0;
root->ptLower = m1;
root->ptBefore = root;
root->ptBehind = root;
root->wPos = 1;
m1->byClass = 1;
m1->wPos = 1;
m1->ptHigher = root;
m1->ptBefore = m2;
m1->ptBehind = m2;
m1->ptLower = m1_sub;
m1->pfnWinProc = on_exec;
m2->byClass = 1;
m2->wPos = 2;
m2->ptHigher = root;
m2->ptBefore = m1;
m2->ptBehind = m1;
m2->pfnWinProc = on_exec;
m1_sub->byClass = 2;
m1_sub->wPos = 1;
m1_sub->ptHigher = m1;
m1_sub->ptBefore = m1_sub;
m1_sub->ptBehind = m1_sub;
m1_sub->pfnWinProc = on_exec;
nav->ptHead = root;
nav->ptCurrent = m1;
nav->ptCurBak = m1;
nav->ptRoute[0] = root;
nav->ptRoute[1] = m1;
}
int main(void)
{
MenuNavState nav;
tagMenuItem root;
tagMenuItem m1;
tagMenuItem m2;
tagMenuItem m1_sub;
MenuNavResult result;
build_legacy_like_tree(&nav, &root, &m1, &m2, &m1_sub);
result = MenuNavigator_ProcessKey(&nav, KEY_D);
ASSERT_EQ_INT(1, result.needRefresh);
ASSERT_TRUE(nav.ptCurrent == &m2);
result = MenuNavigator_ProcessKey(&nav, KEY_U);
ASSERT_EQ_INT(1, result.needRefresh);
ASSERT_TRUE(nav.ptCurrent == &m1);
result = MenuNavigator_ProcessKey(&nav, KEY_ENT);
ASSERT_EQ_INT(1, result.needRefresh);
ASSERT_TRUE(nav.ptCurrent == &m1_sub);
result = MenuNavigator_ProcessKey(&nav, KEY_ESC);
ASSERT_EQ_INT(1, result.needRefresh);
ASSERT_TRUE(nav.ptCurrent == &m1);
g_exec_count = 0;
nav.ptCurrent = &m2;
result = MenuNavigator_ProcessKey(&nav, KEY_ENT);
ASSERT_EQ_INT(0, result.needRefresh);
ASSERT_EQ_INT(1, g_exec_count);
return 0;
}

View File

@@ -0,0 +1,79 @@
#include "test_common.h"
#include <string.h>
#include "../src/Drv/key.h"
#include "../src/Drv/menu/presenter/menu_navigator.h"
static int g_exec_count = 0;
static int on_exec(void)
{
g_exec_count++;
return 0;
}
static void build_two_level(MenuNavState *nav, tagMenuItem *root, tagMenuItem *child_a, tagMenuItem *child_b)
{
memset(nav, 0, sizeof(*nav));
memset(root, 0, sizeof(*root));
memset(child_a, 0, sizeof(*child_a));
memset(child_b, 0, sizeof(*child_b));
root->byClass = 0;
root->wPos = 1;
root->ptLower = child_a;
root->ptBefore = root;
root->ptBehind = root;
child_a->byClass = 1;
child_a->wPos = 1;
child_a->ptHigher = root;
child_a->ptBefore = child_b;
child_a->ptBehind = child_b;
child_a->pfnWinProc = on_exec;
child_b->byClass = 1;
child_b->wPos = 2;
child_b->ptHigher = root;
child_b->ptBefore = child_a;
child_b->ptBehind = child_a;
child_b->pfnWinProc = on_exec;
nav->ptHead = root;
nav->ptCurrent = child_a;
nav->ptCurBak = child_a;
nav->ptRoute[0] = root;
nav->ptRoute[1] = child_a;
}
int main(void)
{
MenuNavState nav;
tagMenuItem root;
tagMenuItem a;
tagMenuItem b;
MenuNavResult result;
build_two_level(&nav, &root, &a, &b);
result = MenuNavigator_ProcessKey(&nav, KEY_D);
ASSERT_EQ_INT(1, result.needRefresh);
ASSERT_TRUE(nav.ptCurrent == &b);
result = MenuNavigator_ProcessKey(&nav, KEY_U);
ASSERT_EQ_INT(1, result.needRefresh);
ASSERT_TRUE(nav.ptCurrent == &a);
g_exec_count = 0;
result = MenuNavigator_ProcessKey(&nav, KEY_ENT);
ASSERT_EQ_INT(0, result.needRefresh);
ASSERT_EQ_INT(1, g_exec_count);
result = MenuNavigator_ProcessKey(&nav, KEY_ESC);
ASSERT_EQ_INT(1, result.skipRenderThisRound);
ASSERT_TRUE(nav.ptCurrent == root.ptLower);
ASSERT_TRUE(nav.ptRoute[0] == &root);
return 0;
}

View File

@@ -0,0 +1,37 @@
#include "test_common.h"
#include <string.h>
#include "../src/Drv/menu/model/menu_tree_builder.h"
static int noop_proc(void)
{
return 0;
}
int main(void)
{
tagMenuCtrl ctrl;
tagMenuItem items[4];
const tagMenuModel model[4] = {
{0, "Root", "", 0, 0, 0, (FUNCPTR)noop_proc},
{1, "A", "", 0, 0, 0, (FUNCPTR)noop_proc},
{1, "B", "", 0, 0, 0, (FUNCPTR)noop_proc},
{2, "C", "", 0, 0, 0, (FUNCPTR)noop_proc},
};
memset(&ctrl, 0, sizeof(ctrl));
memset(items, 0, sizeof(items));
MenuTree_0LevelNumCal(&ctrl, model, 4);
ASSERT_EQ_INT(1, ctrl.by0LevelNum);
MenuTree_MainCreate(items, model, 4);
ASSERT_TRUE(items[0].ptLower == &items[1]);
ASSERT_TRUE(items[1].ptBehind == &items[2]);
ASSERT_TRUE(items[2].ptBefore == &items[1]);
ASSERT_TRUE(items[2].ptLower == &items[3]);
ASSERT_TRUE(items[3].ptHigher == &items[2]);
return 0;
}

View File

@@ -0,0 +1,39 @@
#include "test_common.h"
#include "../src/Drv/key.h"
#include "../src/Drv/menu/app/menu.h"
int main(void)
{
int decorated_found = 0;
uint16_t itemCount = 0;
const tagMenuItem *menuItems;
MenuApp_Init();
Key_Init();
menuItems = MenuApp_GetMenuItems(&itemCount);
for (uint16_t i = 0; i < itemCount; i++)
{
if (menuItems[i].ptLower != NULL)
{
uint8_t len = 0;
while ((len < 50) && (menuItems[i].byName[len] != '\0'))
{
len++;
}
ASSERT_TRUE(len > 0);
ASSERT_EQ_INT('\x10', menuItems[i].byName[len - 1]);
decorated_found = 1;
break;
}
}
ASSERT_TRUE(decorated_found == 1);
/* 首次路由应仅触发首帧绘制,不应崩溃 */
MenuApp_PollInput();
/* 二次刷新路径也不应崩溃 */
MenuApp_Render();
return 0;
}