Skip to content
Open
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
371 changes: 371 additions & 0 deletions ext_components/cp0_lvgl/assets/cp0_status_time_background_wide.inc

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions ext_components/cp0_lvgl/include/cp0_lvgl_app.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions ext_components/cp0_lvgl/include/cp0_lvgl_app_page_assets.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
9 changes: 9 additions & 0 deletions ext_components/cp0_lvgl/include/ui_app_page.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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. */
Expand Down
15 changes: 15 additions & 0 deletions ext_components/cp0_lvgl/src/cp0_lvgl_app_page_assets.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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,
Expand Down
47 changes: 47 additions & 0 deletions ext_components/cp0_lvgl/src/cp0_time_format.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/

#include "cp0_time_format.hpp"

#include <cstdio>

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<std::size_t>(written) < out_size;
}

} // namespace cp0::time
30 changes: 30 additions & 0 deletions ext_components/cp0_lvgl/src/cp0_time_format.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/

#pragma once

#include <cstddef>
#include <string_view>

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
51 changes: 50 additions & 1 deletion ext_components/cp0_lvgl/src/ui_app_page.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <charconv>
#include <system_error>

namespace {

Expand Down Expand Up @@ -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)
{
}

Expand All @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions ext_components/cp0_lvgl/tests/run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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" \
Expand Down
99 changes: 99 additions & 0 deletions ext_components/cp0_lvgl/tests/test_time_format.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/

#include "cp0_time_format.hpp"

#include <cassert>
#include <cstring>
#include <string>

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 "<invalid>";
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!") == "<invalid>");

return 0;
}
Loading
Loading