From 28013c3b19c9513e5516dbb0e4d42fd9974671fd Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Fri, 13 Nov 2026 15:34:52 +0800 Subject: [PATCH 1/2] feat(settings): add 12-hour clock option for the top bar Add a "12H Clock" toggle under Date & Time in the APPLaunch settings tree, backed by the shared CP0_CONFIG_KEY_CLOCK_12H config key so the setting and its presentation cannot drift apart. The preference is read with GetInt, written with SetInt plus Save, and rolled back when Save fails. cp0_time_str() keeps its documented 24-hour "HH:MM" contract because the Date & Time page parses that value. A new cp0::time::format_12_hour() helper only reformats the presentation to "h:MM AM/PM", and the top bar pill switches to a wider background asset to fit the longer text. Add unit tests for the formatter and contract tests for the setting, the shared config key, the persistence rollback, and the wide pill asset. --- .../cp0_status_time_background_wide.inc | 371 ++++++++++++++++++ .../cp0_lvgl/include/cp0_lvgl_app.h | 4 + .../include/cp0_lvgl_app_page_assets.h | 1 + .../cp0_lvgl/include/ui_app_page.hpp | 9 + .../cp0_lvgl/src/cp0_lvgl_app_page_assets.c | 15 + .../cp0_lvgl/src/cp0_time_format.cpp | 47 +++ .../cp0_lvgl/src/cp0_time_format.hpp | 30 ++ ext_components/cp0_lvgl/src/ui_app_page.cpp | 51 ++- ext_components/cp0_lvgl/tests/run_tests.sh | 6 + .../cp0_lvgl/tests/test_time_format.cpp | 99 +++++ .../main/ui/settings/settings_page.cpp | 71 ++++ projects/APPLaunch/tests/run_tests.sh | 1 + .../test_settings_clock_format_contract.py | 103 +++++ 13 files changed, 807 insertions(+), 1 deletion(-) create mode 100644 ext_components/cp0_lvgl/assets/cp0_status_time_background_wide.inc create mode 100644 ext_components/cp0_lvgl/src/cp0_time_format.cpp create mode 100644 ext_components/cp0_lvgl/src/cp0_time_format.hpp create mode 100644 ext_components/cp0_lvgl/tests/test_time_format.cpp create mode 100644 projects/APPLaunch/tests/test_settings_clock_format_contract.py diff --git a/ext_components/cp0_lvgl/assets/cp0_status_time_background_wide.inc b/ext_components/cp0_lvgl/assets/cp0_status_time_background_wide.inc new file mode 100644 index 00000000..80dc1ae1 --- /dev/null +++ b/ext_components/cp0_lvgl/assets/cp0_status_time_background_wide.inc @@ -0,0 +1,371 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + +static const uint8_t cp0_status_time_background_wide_map[] = { + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x98, 0xf6, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xf8, + 0x33, 0x65, 0x98, 0xc2, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x34, 0x66, 0x99, 0xc6, 0x32, 0x64, 0x98, 0x52, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x34, 0x67, 0x98, 0x54, 0x00, 0x00, 0x00, 0x00, 0x32, 0x66, 0x98, 0x8a, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x32, 0x66, 0x99, 0x89, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x65, 0x9a, 0x51, + 0x33, 0x65, 0x98, 0xc2, 0x33, 0x66, 0x98, 0xf6, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, 0x33, 0x66, 0x99, 0xff, + 0x33, 0x66, 0x98, 0xf6, 0x33, 0x65, 0x98, 0xc2, 0x32, 0x65, 0x97, 0x51, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; diff --git a/ext_components/cp0_lvgl/include/cp0_lvgl_app.h b/ext_components/cp0_lvgl/include/cp0_lvgl_app.h index a7760693..efb89b5b 100644 --- a/ext_components/cp0_lvgl/include/cp0_lvgl_app.h +++ b/ext_components/cp0_lvgl/include/cp0_lvgl_app.h @@ -213,6 +213,10 @@ cp0_battery_info_t cp0_battery_read(void); int cp0_backlight_read(void); int cp0_backlight_max(void); int cp0_backlight_write(int val); +/* Config key (cp0_signal_config_api GetInt/SetInt) selecting how clock values are + presented: 0 = 24-hour "HH:MM" (default), 1 = US 12-hour "h:MM AM/PM". + cp0_time_str() itself always returns the documented 24-hour "HH:MM" text. */ +#define CP0_CONFIG_KEY_CLOCK_12H "clock_12h" void cp0_time_str(char *buf, int buf_size); #ifdef __cplusplus diff --git a/ext_components/cp0_lvgl/include/cp0_lvgl_app_page_assets.h b/ext_components/cp0_lvgl/include/cp0_lvgl_app_page_assets.h index 1cdd9719..f2e19103 100644 --- a/ext_components/cp0_lvgl/include/cp0_lvgl_app_page_assets.h +++ b/ext_components/cp0_lvgl/include/cp0_lvgl_app_page_assets.h @@ -9,6 +9,7 @@ #include "lvgl/lvgl.h" LV_IMAGE_DECLARE(cp0_status_time_background); +LV_IMAGE_DECLARE(cp0_status_time_background_wide); LV_IMAGE_DECLARE(cp0_status_battery_background); LV_IMAGE_DECLARE(cp0_status_ethernet); LV_IMAGE_DECLARE(setting_cross); diff --git a/ext_components/cp0_lvgl/include/ui_app_page.hpp b/ext_components/cp0_lvgl/include/ui_app_page.hpp index aba9c310..721656e7 100644 --- a/ext_components/cp0_lvgl/include/ui_app_page.hpp +++ b/ext_components/cp0_lvgl/include/ui_app_page.hpp @@ -282,6 +282,11 @@ class AppTopBarTitleComponent final : public AppTopBarComponent class AppTopBarTimeComponent final : public AppTopBarComponent { public: + // Pill widths in pixels. The 24-hour pill fits "HH:MM"; the US 12-hour + // form ("h:MM AM/PM") needs room for its widest value, "10:44 AM". + static constexpr int kTwentyFourHourWidth = 40; + static constexpr int kTwelveHourWidth = 68; + AppTopBarTimeComponent(); ~AppTopBarTimeComponent() override; void refresh() override; @@ -293,7 +298,11 @@ class AppTopBarTimeComponent final : public AppTopBarComponent private: static void child_delete_cb(lv_event_t *event); + // Resizes the pill when the CP0_CONFIG_KEY_CLOCK_12H preference changes. + void apply_clock_format(bool twelve_hour); lv_obj_t *label_ = nullptr; + bool twelve_hour_ = false; + bool clock_format_applied_ = false; }; /** @brief Built-in top-bar component that displays battery state. */ diff --git a/ext_components/cp0_lvgl/src/cp0_lvgl_app_page_assets.c b/ext_components/cp0_lvgl/src/cp0_lvgl_app_page_assets.c index 8658b1ba..d1bc535b 100644 --- a/ext_components/cp0_lvgl/src/cp0_lvgl_app_page_assets.c +++ b/ext_components/cp0_lvgl/src/cp0_lvgl_app_page_assets.c @@ -7,6 +7,7 @@ #include "cp0_lvgl_app_page_assets.h" #include "../assets/cp0_status_time_background.inc" +#include "../assets/cp0_status_time_background_wide.inc" #include "../assets/cp0_status_battery_background.inc" #include "../assets/setting_cross.inc" #include "../assets/setting_ok.inc" @@ -27,6 +28,20 @@ const lv_image_dsc_t cp0_status_time_background = { .data = cp0_status_time_background_map, }; +/* Same pill, widened for the US 12-hour clock text ("h:MM AM/PM"). */ +const lv_image_dsc_t cp0_status_time_background_wide = { + .header = { + .magic = LV_IMAGE_HEADER_MAGIC, + .cf = LV_COLOR_FORMAT_ARGB8888, + .flags = 0, + .w = 68, + .h = 16, + .stride = 68 * 4, + }, + .data_size = sizeof(cp0_status_time_background_wide_map), + .data = cp0_status_time_background_wide_map, +}; + const lv_image_dsc_t cp0_status_battery_background = { .header = { .magic = LV_IMAGE_HEADER_MAGIC, diff --git a/ext_components/cp0_lvgl/src/cp0_time_format.cpp b/ext_components/cp0_lvgl/src/cp0_time_format.cpp new file mode 100644 index 00000000..15bf80fe --- /dev/null +++ b/ext_components/cp0_lvgl/src/cp0_time_format.cpp @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + +#include "cp0_time_format.hpp" + +#include + +namespace cp0::time { + +namespace { + +bool two_digits(std::string_view text, std::size_t offset, int &value) noexcept +{ + if (offset + 1 >= text.size()) return false; + const char high = text[offset]; + const char low = text[offset + 1]; + if (high < '0' || high > '9' || low < '0' || low > '9') return false; + value = (high - '0') * 10 + (low - '0'); + return true; +} + +} // namespace + +bool format_12_hour(std::string_view hh_mm, char *out, std::size_t out_size) noexcept +{ + // "12:59 PM" is the widest result; requiring room for it up front keeps a + // failed call from touching the caller's buffer at all. + constexpr std::size_t kMaxFormattedBytes = sizeof("12:59 PM"); + if (!out || out_size < kMaxFormattedBytes) return false; + + int hour = 0; + int minute = 0; + if (hh_mm.size() != 5 || hh_mm[2] != ':' || !two_digits(hh_mm, 0, hour) || + !two_digits(hh_mm, 3, minute) || hour > 23 || minute > 59) + return false; + + const char *meridiem = hour < 12 ? "AM" : "PM"; + const int display_hour = hour % 12 == 0 ? 12 : hour % 12; + const int written = + std::snprintf(out, out_size, "%d:%02d %s", display_hour, minute, meridiem); + return written > 0 && static_cast(written) < out_size; +} + +} // namespace cp0::time diff --git a/ext_components/cp0_lvgl/src/cp0_time_format.hpp b/ext_components/cp0_lvgl/src/cp0_time_format.hpp new file mode 100644 index 00000000..4d854f86 --- /dev/null +++ b/ext_components/cp0_lvgl/src/cp0_time_format.hpp @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include + +namespace cp0::time { + +/** + * @brief Presents 24-hour clock text as US 12-hour time. + * + * cp0_time_str() keeps its documented "HH:MM" contract because callers such as + * the Date & Time page parse that value. This helper only changes how the same + * value is presented, e.g. on the application top bar. + * + * @param hh_mm Exactly five characters: a two-digit hour 00..23, ':' and a + * two-digit minute 00..59. + * @param out Destination buffer. Untouched when the call fails. + * @param out_size Destination capacity in bytes, including the terminator. + * @return true when hh_mm was a valid 24-hour clock and out holds the 12-hour + * text ("h:MM AM" / "h:MM PM"); false otherwise. + */ +bool format_12_hour(std::string_view hh_mm, char *out, std::size_t out_size) noexcept; + +} // namespace cp0::time diff --git a/ext_components/cp0_lvgl/src/ui_app_page.cpp b/ext_components/cp0_lvgl/src/ui_app_page.cpp index fc9d9f1a..6c81a704 100644 --- a/ext_components/cp0_lvgl/src/ui_app_page.cpp +++ b/ext_components/cp0_lvgl/src/ui_app_page.cpp @@ -9,6 +9,11 @@ #include "cp0_pointer_lifecycle.hpp" #include "cp0_status_lifecycle.hpp" #include "cp0_status_component_contract.hpp" +#include "cp0_sync_signal.hpp" +#include "cp0_time_format.hpp" + +#include +#include namespace { @@ -397,10 +402,30 @@ lv_obj_t *create_centered_status_label(lv_obj_t *obj) return label; } +/* The clock presentation is a user preference stored in the shared config + service; an unavailable service keeps the 24-hour default. */ +bool clock_prefers_twelve_hour() +{ + int preference = 0; + cp0::signal::invoke_noexcept([&preference] { + cp0_signal_config_api( + {"GetInt", CP0_CONFIG_KEY_CLOCK_12H, "0"}, + [&preference](int code, std::string data) { + int parsed = 0; + const char *end = data.data() + data.size(); + const auto result = std::from_chars(data.data(), end, parsed); + if (code == 0 && result.ec == std::errc{} && result.ptr == end && + (parsed == 0 || parsed == 1)) + preference = parsed; + }); + }); + return preference != 0; +} + } // namespace AppTopBarTimeComponent::AppTopBarTimeComponent() - : AppTopBarComponent("cp0.time", 40) + : AppTopBarComponent("cp0.time", kTwentyFourHourWidth) { } @@ -427,11 +452,35 @@ void AppTopBarTimeComponent::child_delete_cb(lv_event_t *event) void AppTopBarTimeComponent::refresh() { if (!label_) return; + + // cp0_time_str() always yields the documented 24-hour "HH:MM" value; the + // preference only changes how the pill presents it. + const bool twelve_hour = clock_prefers_twelve_hour(); + if (!clock_format_applied_ || twelve_hour != twelve_hour_) apply_clock_format(twelve_hour); + char text[16] = "--:--"; cp0_time_str(text, sizeof(text)); + if (twelve_hour) { + char formatted[16] = {}; + if (cp0::time::format_12_hour(text, formatted, sizeof(formatted))) { + lv_label_set_text(label_, formatted); + return; + } + } lv_label_set_text(label_, text[0] ? text : "--:--"); } +void AppTopBarTimeComponent::apply_clock_format(bool twelve_hour) +{ + twelve_hour_ = twelve_hour; + clock_format_applied_ = true; + if (!obj()) return; + set_width(twelve_hour ? kTwelveHourWidth : kTwentyFourHourWidth); + configure_status_component( + obj(), twelve_hour ? &cp0_status_time_background_wide : &cp0_status_time_background); + if (label_) lv_obj_center(label_); +} + void AppTopBarTimeComponent::on_object_deleted() { label_ = nullptr; diff --git a/ext_components/cp0_lvgl/tests/run_tests.sh b/ext_components/cp0_lvgl/tests/run_tests.sh index 8d428cea..e3bbd325 100755 --- a/ext_components/cp0_lvgl/tests/run_tests.sh +++ b/ext_components/cp0_lvgl/tests/run_tests.sh @@ -419,6 +419,12 @@ fi "$root/tests/test_settings_policy.cpp" -o "$binary" "$binary" +"${CXX:-c++}" -std=c++17 -Wall -Wextra -Werror \ + -I"$root/src" \ + "$root/src/cp0_time_format.cpp" \ + "$root/tests/test_time_format.cpp" -o "$binary" +"$binary" + "${CXX:-c++}" -std=c++17 -Wall -Wextra -Werror \ -I"$root/src" \ "$root/src/cp0_pty_contract.cpp" \ diff --git a/ext_components/cp0_lvgl/tests/test_time_format.cpp b/ext_components/cp0_lvgl/tests/test_time_format.cpp new file mode 100644 index 00000000..97683c1d --- /dev/null +++ b/ext_components/cp0_lvgl/tests/test_time_format.cpp @@ -0,0 +1,99 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + +#include "cp0_time_format.hpp" + +#include +#include +#include + +using cp0::time::format_12_hour; + +namespace { + +std::string formatted(const char *text) +{ + char buffer[16] = {}; + if (!format_12_hour(text, buffer, sizeof(buffer))) return ""; + return buffer; +} + +void expect(const char *input, const char *expected) +{ + char buffer[16] = {}; + assert(format_12_hour(input, buffer, sizeof(buffer))); + assert(std::strcmp(buffer, expected) == 0); +} + +} // namespace + +int main() +{ + // Midnight and noon are the two cases that are easy to get wrong: neither + // maps to hour 0. + expect("00:00", "12:00 AM"); + expect("00:05", "12:05 AM"); + expect("00:59", "12:59 AM"); + expect("12:00", "12:00 PM"); + expect("12:34", "12:34 PM"); + + // Morning keeps the AM suffix; the leading hour zero is dropped. + expect("01:05", "1:05 AM"); + expect("09:09", "9:09 AM"); + expect("11:59", "11:59 AM"); + + // Afternoon/evening wrap back to a 1..12 hour. + expect("13:05", "1:05 PM"); + expect("19:44", "7:44 PM"); + expect("23:59", "11:59 PM"); + expect("10:44", "10:44 AM"); + + // Minutes are always two digits, even when they contain zeros. + expect("07:00", "7:00 AM"); + expect("21:07", "9:07 PM"); + + // Anything that is not a valid 24-hour clock is rejected, and the caller's + // buffer is left untouched so the previous text cannot be half-overwritten. + const char *invalid[] = { + "", + "1:05", + "01:5", + "0105", + "24:00", + "23:60", + "aa:bb", + " 1:05", + "-1:05", + "01-05", + "01:005", + }; + for (const char *input : invalid) { + char buffer[16] = "keep"; + assert(!format_12_hour(input, buffer, sizeof(buffer))); + assert(std::strcmp(buffer, "keep") == 0); + } + + // A too-small buffer fails instead of truncating the result. + char small[8] = "keep"; + assert(!format_12_hour("10:44", small, sizeof(small))); + assert(std::strcmp(small, "keep") == 0); + + // A one-byte buffer cannot hold even an empty string; make sure the helper + // does not write a truncated result. + char tiny[4] = "abc"; + assert(!format_12_hour("13:05", tiny, 1)); + assert(tiny[0] == 'a'); + + // Null/zero-size output is rejected rather than crashing. + char buffer[16] = {}; + assert(!format_12_hour("10:44", nullptr, sizeof(buffer))); + assert(!format_12_hour("10:44", buffer, 0)); + + assert(formatted("10:44") == "10:44 AM"); + assert(formatted("nope!") == ""); + + return 0; +} diff --git a/projects/APPLaunch/main/ui/settings/settings_page.cpp b/projects/APPLaunch/main/ui/settings/settings_page.cpp index 2ab89903..f017dc81 100644 --- a/projects/APPLaunch/main/ui/settings/settings_page.cpp +++ b/projects/APPLaunch/main/ui/settings/settings_page.cpp @@ -578,6 +578,72 @@ void bluetooth_named_only_api(int cmd, void *data) bluetooth_named_only_state = previous; } +// CP0_CONFIG_KEY_CLOCK_12H is the same key the platform top bar reads, so the +// setting and its presentation cannot drift apart. +constexpr const char *kClock12HourConfigKey = CP0_CONFIG_KEY_CLOCK_12H; + +bool read_clock_12h_config() +{ + int preference = 0; + try { + cp0_signal_config_api( + {"GetInt", kClock12HourConfigKey, "0"}, + [&preference](int code, std::string data) { + int parsed = 0; + const char *end = data.data() + data.size(); + const auto result = std::from_chars(data.data(), end, parsed); + if (code == 0 && result.ec == std::errc{} && result.ptr == end && + (parsed == 0 || parsed == 1)) + preference = parsed; + }); + } catch (...) { + } + return preference != 0; +} + +bool write_clock_12h_config(bool twelve_hour) +{ + bool succeeded = false; + try { + cp0_signal_config_api( + {"SetInt", kClock12HourConfigKey, twelve_hour ? "1" : "0"}, + [&succeeded](int code, std::string) { succeeded = code == 0; }); + } catch (...) { + } + return succeeded; +} + +bool save_clock_12h_config() +{ + bool succeeded = false; + try { + cp0_signal_config_api({"Save"}, [&succeeded](int code, std::string) { succeeded = code == 0; }); + } catch (...) { + } + return succeeded; +} + +void clock_12h_api(int cmd, void *data) +{ + if ((cmd == SettingApiReadFlag || cmd == SettingApiReadFlagTimeStart) && data) { + const bool twelve_hour = read_clock_12h_config(); + if (cmd == SettingApiReadFlag) + *static_cast(data) = twelve_hour; + else + std::get<0>(*static_cast(data)) = twelve_hour; + return; + } + if (cmd != SettingApiActivate) return; + + const bool previous = read_clock_12h_config(); + if (!write_clock_12h_config(!previous)) return; + if (save_clock_12h_config()) return; + + // SetInt mutates the live config immediately. Restore it when Save fails so + // a later page rebuild cannot observe an unpersisted value. + if (write_clock_12h_config(previous)) (void)save_clock_12h_config(); +} + static void append_numeric_options(Tree &tree, const NodeIter &parent, int first, int last) { for (int value = first; value <= last; ++value) tree.append_child(parent, SettingEntry{std::to_string(value)}); @@ -738,6 +804,11 @@ void UISettingTreePage::create_page_detail() mode_tree.append_child(write_rtc, SettingEntry{"Yes"}); mode_tree.append_child(write_rtc, SettingEntry{"No"}); } + // Presentation only: the stored clock stays 24-hour, the top bar shows + // "h:MM AM/PM" once this is on. + SettingEntry clock_format{"12H Clock", clock_12h_api, true}; + clock_format.status_read_policy = SettingStatusReadPolicy::Direct; + mode_tree.append_child(date_time, std::move(clock_format)); } { diff --git a/projects/APPLaunch/tests/run_tests.sh b/projects/APPLaunch/tests/run_tests.sh index b98b75fc..b745254b 100755 --- a/projects/APPLaunch/tests/run_tests.sh +++ b/projects/APPLaunch/tests/run_tests.sh @@ -12,6 +12,7 @@ python3 "$test_root/test_settings_menu_order.py" python3 "$test_root/test_settings_wifi_enable_copy_contract.py" python3 "$test_root/test_settings_ethernet_layout_contract.py" python3 "$test_root/test_settings_rtc_layout_contract.py" +python3 "$test_root/test_settings_clock_format_contract.py" python3 "$test_root/test_settings_volume_initial_selection.py" python3 "$test_root/test_preinstalled_app_packaging.py" python3 "$test_root/test_settings_storage_build_contract.py" diff --git a/projects/APPLaunch/tests/test_settings_clock_format_contract.py b/projects/APPLaunch/tests/test_settings_clock_format_contract.py new file mode 100644 index 00000000..f3194362 --- /dev/null +++ b/projects/APPLaunch/tests/test_settings_clock_format_contract.py @@ -0,0 +1,103 @@ +# SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +# +# SPDX-License-Identifier: MIT + +"""Contracts for the launcher's US 12-hour clock option. + +The setting lives in the APPLaunch settings tree, but the clock is drawn by the +cp0_lvgl top bar, so the two sides must not drift apart: + +* both read the same shared config key (``CP0_CONFIG_KEY_CLOCK_12H``) and + neither hard-codes the key string; +* ``cp0_time_str()`` keeps its documented 24-hour ``HH:MM`` contract, because + the Date & Time page parses that value; the option only changes presentation; +* the toggle persists through ``SetInt`` + ``Save`` with a rollback, like the + other config-backed toggles in the settings tree. + +The checks are static (regex over the sources) so they need no toolchain. +""" + +import re +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +REPO = Path(__file__).resolve().parents[3] +SETTINGS_PAGE = (ROOT / "main/ui/settings/settings_page.cpp").read_text(encoding="utf-8") +TOP_BAR = (REPO / "ext_components/cp0_lvgl/src/ui_app_page.cpp").read_text( + encoding="utf-8" +) +PUBLIC_API = (REPO / "ext_components/cp0_lvgl/include/cp0_lvgl_app.h").read_text( + encoding="utf-8" +) +TIME_FORMAT = (REPO / "ext_components/cp0_lvgl/src/cp0_time_format.cpp").read_text( + encoding="utf-8" +) +ASSETS_HEADER = ( + REPO / "ext_components/cp0_lvgl/include/cp0_lvgl_app_page_assets.h" +).read_text(encoding="utf-8") +TOP_BAR_HEADER = ( + REPO / "ext_components/cp0_lvgl/include/ui_app_page.hpp" +).read_text(encoding="utf-8") + + +def date_time_block() -> str: + start = SETTINGS_PAGE.index("NodeIter date_time =") + system = SETTINGS_PAGE.index("NodeIter system =", start) + return SETTINGS_PAGE[start:system] + + +def test_time_format_toggle_is_under_date_and_time(): + block = date_time_block() + assert 'SettingEntry clock_format{"12H Clock", clock_12h_api, true};' in block + assert "clock_format.status_read_policy = SettingStatusReadPolicy::Direct;" in block + assert "mode_tree.append_child(date_time, std::move(clock_format));" in block + # It must sit after the manual editor: the "Set Manually" workflow is + # guarded by its own activation gate and must not be reordered. + assert block.index('SettingEntry clock_format{"12H Clock"') > block.index( + "NodeIter write_rtc" + ) + + +def test_setting_and_top_bar_share_one_config_key(): + assert '#define CP0_CONFIG_KEY_CLOCK_12H "clock_12h"' in PUBLIC_API + # Neither side hard-codes the key string; both use the shared constant. + assert '"clock_12h"' not in SETTINGS_PAGE + assert '"clock_12h"' not in TOP_BAR + assert "CP0_CONFIG_KEY_CLOCK_12H" in SETTINGS_PAGE + assert "CP0_CONFIG_KEY_CLOCK_12H" in TOP_BAR + + +def test_toggle_persists_with_a_rollback(): + assert '{"GetInt", kClock12HourConfigKey, "0"}' in SETTINGS_PAGE + assert '{"SetInt", kClock12HourConfigKey, twelve_hour ? "1" : "0"}' in SETTINGS_PAGE + assert 'cp0_signal_config_api({"Save"}' in SETTINGS_PAGE + # SetInt mutates the live config immediately, so a failed Save must restore + # the previous value instead of leaving an unpersisted preference. + assert ( + "if (write_clock_12h_config(previous)) (void)save_clock_12h_config();" + in SETTINGS_PAGE + ) + + +def test_top_bar_only_changes_the_presentation(): + # The 24-hour value keeps coming from the documented cp0_time_str() API. + assert "cp0_time_str(text, sizeof(text));" in TOP_BAR + assert "cp0::time::format_12_hour(text, formatted, sizeof(formatted))" in TOP_BAR + assert "void cp0_time_str(char *buf, int buf_size);" in PUBLIC_API + # The formatter only reformats the five-character HH:MM text. + assert "hh_mm.size() != 5" in TIME_FORMAT + assert "hh_mm[2] != ':'" in TIME_FORMAT + + +def test_12_hour_pill_uses_the_wider_background(): + assert "LV_IMAGE_DECLARE(cp0_status_time_background_wide);" in ASSETS_HEADER + assert "cp0_status_time_background_wide" in TOP_BAR + assert "static constexpr int kTwelveHourWidth = 68;" in TOP_BAR_HEADER + assert "static constexpr int kTwentyFourHourWidth = 40;" in TOP_BAR_HEADER + + +if __name__ == "__main__": + for _name, _test in sorted(globals().items()): + if _name.startswith("test_") and callable(_test): + _test() From 9d6ad1b300575859e9c5c0d30726456c3c342a48 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Fri, 13 Nov 2026 16:02:22 +0800 Subject: [PATCH 2/2] chore: update APPLaunch deploy config and emulator sources - Point APPLaunch SSH deploy host to the new device IP - Use {password}/{remote_file_path} placeholders in deploy after_cmd - Add cp0_time_format.cpp to CardputerZero-Emulator sources --- projects/APPLaunch/setup.ini | 4 ++-- projects/CardputerZero-Emulator/CMakeLists.txt | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/projects/APPLaunch/setup.ini b/projects/APPLaunch/setup.ini index 30ec93d6..ea122e76 100644 --- a/projects/APPLaunch/setup.ini +++ b/projects/APPLaunch/setup.ini @@ -1,9 +1,9 @@ [ssh] local_file_path = dist remote_file_path = /home/pi/dist -remote_host = 192.168.28.237 +remote_host = 192.168.28.97 remote_port = 22 username = pi password = pi ; before_cmd = 'echo pi | sudo -S systemctl stop APPLaunch.service' -after_cmd = 'systemctl --user stop APPLaunch.service; echo pi | sudo -S mkdir -p /usr/share/APPLaunch/bin; echo pi | sudo -S cp -a /home/pi/dist/APPLaunch/. /usr/share/APPLaunch/; echo pi | sudo -S install -m 755 /home/pi/dist/M5CardputerZero-APPLaunch /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch; systemctl --user start APPLaunch.service' +after_cmd = 'systemctl --user stop APPLaunch.service; echo {password} | sudo -S mkdir -p /usr/share/APPLaunch/bin; echo {password} | sudo -S cp -a {remote_file_path}/APPLaunch/. /usr/share/APPLaunch/; echo {password} | sudo -S install -m 755 {remote_file_path}/M5CardputerZero-APPLaunch /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch; systemctl --user start APPLaunch.service' diff --git a/projects/CardputerZero-Emulator/CMakeLists.txt b/projects/CardputerZero-Emulator/CMakeLists.txt index 93bc95ef..d7c96ca4 100644 --- a/projects/CardputerZero-Emulator/CMakeLists.txt +++ b/projects/CardputerZero-Emulator/CMakeLists.txt @@ -159,6 +159,7 @@ set(CP0_LVGL_APP_COMMON_SRCS ${CP0_LVGL}/src/cp0_network_api_contract.cpp ${CP0_LVGL}/src/cp0_status_ethernet.c ${CP0_LVGL}/src/cp0_status_lifecycle.cpp + ${CP0_LVGL}/src/cp0_time_format.cpp ${CP0_LVGL}/src/ui_app_page.cpp )