From b66c5de719c2d48c0f5a84fc39a61d61cf8d446e Mon Sep 17 00:00:00 2001 From: Peter Reitinger Date: Sun, 19 Jul 2026 00:58:28 +0200 Subject: [PATCH 01/20] Fix incorrect BLEFS documentation (#2442) Version characteristic returns 16bit not 32bit as described. From copy & paste it was "To continue reading" instead of "To continue writing". --- doc/BLEFS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/BLEFS.md b/doc/BLEFS.md index 5374fd8787..3f620d7871 100644 --- a/doc/BLEFS.md +++ b/doc/BLEFS.md @@ -14,7 +14,7 @@ There are two relevant UUIDs in this protocol: the version characteristic, and t UUID: `adaf0100-4669-6c65-5472-616e73666572` -The version characteristic returns the version of the protocol to which the sender adheres. It returns a single unsigned 32-bit integer. The latest version at the time of writing this is 4. +The version characteristic returns the version of the protocol to which the sender adheres. It returns a single unsigned 16-bit integer. The latest version at the time of writing this is 4. ### Transfer @@ -71,7 +71,7 @@ To begin writing to a file, a header must first be sent. The header packet shoul - Unsigned 32-bit integer encoding the size of the file that will be sent - File path: UTF-8 encoded string that is _not_ null terminated. -To continue reading the file after this initial packet, the following packet should be sent until all the data has been sent and a response had been received with 0 free space. No close command is required after the data has been received. +To continue writing the file after this initial packet, the following packet should be sent until all the data has been sent and a response had been received with 0 free space. No close command is required after the data has been received. - Command (single byte): `0x22` - Status: `0x01` From 71d1f5b45bd2bc9499acb16a5b6893f5aae0a15f Mon Sep 17 00:00:00 2001 From: mark9064 <30447455+mark9064@users.noreply.github.com> Date: Sun, 19 Jul 2026 21:04:56 +0100 Subject: [PATCH 02/20] Keep external flash awake during AOD --- src/systemtask/SystemTask.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/systemtask/SystemTask.cpp b/src/systemtask/SystemTask.cpp index 9feeadedd9..826474a068 100644 --- a/src/systemtask/SystemTask.cpp +++ b/src/systemtask/SystemTask.cpp @@ -310,14 +310,14 @@ void SystemTask::Work() { if (state != SystemTaskState::GoingToSleep) { break; } - if (BootloaderVersion::IsValid()) { - // First versions of the bootloader do not expose their version and cannot initialize the SPI NOR FLASH - // if it's in sleep mode. Avoid bricked device by disabling sleep mode on these versions. - spiNorFlash.Sleep(); - } - // Must keep SPI awake when still updating the display for always on + // Must keep SPI and flash awake when still updating the display for always on if (msg == Messages::OnDisplayTaskSleeping) { + if (BootloaderVersion::IsValid()) { + // First versions of the bootloader do not expose their version and cannot initialize the SPI NOR FLASH + // if it's in sleep mode. Avoid bricked device by disabling sleep mode on these versions. + spiNorFlash.Sleep(); + } spi.Sleep(); } @@ -409,14 +409,13 @@ void SystemTask::GoToRunning() { // SPI only switched off when entering Sleeping, not AOD or GoingToSleep if (state == SystemTaskState::Sleeping) { spi.Wakeup(); + spiNorFlash.Wakeup(); } // Double Tap needs the touch screen to be in normal mode if (!settingsController.isWakeUpModeOn(Pinetime::Controllers::Settings::WakeUpMode::DoubleTap)) { touchPanel.Wakeup(); } - - spiNorFlash.Wakeup(); } displayApp.PushMessage(Pinetime::Applications::Display::Messages::GoToRunning); From 8d7a04e9d1a44041929d58c03b9eef2fba75b5cb Mon Sep 17 00:00:00 2001 From: Steveis Date: Fri, 7 Aug 2026 10:03:18 +0100 Subject: [PATCH 03/20] Clarify notification dismissal comment It actually dismisses to the previous screen which could be the watch face but could equally be an app or setting you were in. --- src/displayapp/screens/Notifications.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/displayapp/screens/Notifications.cpp b/src/displayapp/screens/Notifications.cpp index 837c4683aa..7d937c6d8a 100644 --- a/src/displayapp/screens/Notifications.cpp +++ b/src/displayapp/screens/Notifications.cpp @@ -173,7 +173,7 @@ bool Notifications::OnTouchEvent(Pinetime::Applications::TouchEvents event) { } else { // don't update id, notification manager will try to fetch // but not find it. Refresh will try to load latest message - // or dismiss to watchface + // or dismiss to previous screen } DismissToBlack(); return true; From bd5ceff215f8c23684fc1a7ade28430cf5591d49 Mon Sep 17 00:00:00 2001 From: mark9064 <30447455+mark9064@users.noreply.github.com> Date: Sun, 16 Aug 2026 16:49:04 +0000 Subject: [PATCH 04/20] Introduce AI policy based on CoMaps policy (#2472) --- AI_USAGE.md | 42 ++++++++++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 3 +++ 2 files changed, 45 insertions(+) create mode 100644 AI_USAGE.md diff --git a/AI_USAGE.md b/AI_USAGE.md new file mode 100644 index 0000000000..135f963a66 --- /dev/null +++ b/AI_USAGE.md @@ -0,0 +1,42 @@ + + +# AI usage in the InfiniTime project + +This document describes if and how _artificial intelligence_ can be used in the InfiniTime project by contributors, including agreements for how to use it in a transparent and responsible way. This agreement is applicable to all projects hosted under the [InfiniTime organisation](https://github.com/InfiniTimeOrg). + +All contributors are expected to be aware of this agreement and to apply it in their contributions. Contributions that do not honor these agreements will not be accepted and admins will close corresponding pull requests and issues. + +## Definitions & Motivations + +> [!NOTE] +>This does not necessarily reflect the official or commonly used definitions. + +Software and services that heavily rely on large language model technology to generate their outcomes are referred to as Artificial Intelligence (AI). Examples of products that fit this definition: GitHub Copilot, ChatGPT, Claude Sonnet, DeepSeek, Llama and Gemini. + +There's a distinction between general and narrow AI, all the aforementioned examples fall under general AI as they were not trained to execute a specific well-defined task. Narrow AI is trained to be used for specific well-defined tasks where the problem space is known in advance. + +Vibe coding is the practice where AI creates a code change (feature, bug fix, tests, refactor) with a human that describes what needs to be implemented. + +AI agents are AIs that are configured to perform interactions or make changes with little to no human supervision. + +"AI agents" and "vibe coding" typically result in the "authors" of code not having expertise in the changes they aim to contribute, thus not able to answer questions about which trade-offs were made and why. If authors were only minimally involved in how code was generated, they can't be accountable for AI's output, which is harmful to the long-term sustainability of open source projects like InfiniTime. + +## Agreement + +1. If content was made with the help of AI, you must convey that this is the case. This includes content that you authored but was motivated by a suggestion of AI. +2. If at any point you used AI's work in your contribution you must make an effort to verify that you can submit this under the license of the repository. +3. The accountability of using AI in a contribution lies with the person that makes that contribution. +4. All communication, that includes: **commit messages**, **pull request messages**, **documentation**, **code comments** and **issues** (and comments on issues/pull requests), that is intended to be read by people to understand your thoughts and work must not have been generated with AI. We exclude machine translation and tooling that helps with grammar and spelling check. +5. Using general AI for review is forbidden. Changes have to be approved by a human reviewer. +6. It is not allowed to use AI in an autonomous-looking way to contribute in InfiniTime (i.e. solely using 'agent mode' or 'vibe coding'). To ensure the long-term maintainability and sustainability of the project, we require authors to have read **and** understand the code they submit themselves. +7. Commit messages must not contain advertisements for AI services/agents. This includes Co-authored-by lines and similar statements within the message text. + +## License + +This agreement is a near verbatim copy of [the _AI Agreement_ that the _CoMaps_ project has passed in March 2026](https://codeberg.org/comaps/Governance/pulls/136) which is [licensed under a Creative Commons CC BY 4.0 license](https://creativecommons.org/licenses/by/4.0/legalcode). + +That agreement was in turn adapted from the _Forgejo_ project. The InfiniTime project extends its thanks to many upstream contributors to this policy, and also releases this version under the same CC BY 4.0 license with the hope that any further adopters of this policy would consider doing the same. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e894b5bcb7..d38b1c08fe 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,8 @@ # How to contribute? +> [!WARNING] +>All contributions must adhere to the [AI policy](AI_USAGE.md) + Here are things you can do to help. - [Report bugs or issues](https://github.com/InfiniTimeOrg/InfiniTime/issues/new/choose) From 4f7b430c01667965cf49ea6467c187d3abb8c44e Mon Sep 17 00:00:00 2001 From: mark9064 <30447455+mark9064@users.noreply.github.com> Date: Sat, 8 Aug 2026 23:51:57 +0100 Subject: [PATCH 05/20] Update GCC (in doc and docker) to GCC15.3 --- .vscode/c_cpp_properties.json | 2 +- .vscode/launch.json | 4 ++-- doc/buildAndProgram.md | 4 ++-- doc/buildWithVScode.md | 2 +- docker/build.sh | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index c5f88a827b..33919bdbd6 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -2,7 +2,7 @@ "env": { // TODO: This is a duplication of the configuration set in /docker/build.sh! "TOOLS_DIR": "/opt", - "GCC_ARM_PATH": "gcc-arm-none-eabi-10.3-2021.10" + "GCC_ARM_PATH": "arm-gnu-toolchain-15.3.rel1-x86_64-arm-none-eabi" }, "configurations": [ { diff --git a/.vscode/launch.json b/.vscode/launch.json index 7d3f17a11c..ace345de8f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -53,7 +53,7 @@ "continue" ], // Only use armToolchainPath if your arm-none-eabi-gdb is not in your path (some GCC packages does not contain arm-none-eabi-gdb) - "armToolchainPath": "${workspaceRoot}/../gcc-arm-none-eabi-10.3-2021.10/bin", + "armToolchainPath": "${workspaceRoot}/../arm-gnu-toolchain-15.3.rel1-x86_64-arm-none-eabi/bin", "svdFile": "${workspaceRoot}/nrf52.svd", "configFiles": [ "interface/stlink.cfg", @@ -68,7 +68,7 @@ "request": "launch", "servertype": "external", // FIXME: This is hardcoded. I have no idea how to use the values set in build.sh here - "gdbPath": "/opt/gcc-arm-none-eabi-10.3-2021.10/bin/arm-none-eabi-gdb", + "gdbPath": "/opt/arm-gnu-toolchain-15.3.rel1-x86_64-arm-none-eabi/bin/arm-none-eabi-gdb", // Connect to an already running OpenOCD instance "gdbTarget": "host.docker.internal:3333", "svdFile": "${workspaceRoot}/nrf52.svd", diff --git a/doc/buildAndProgram.md b/doc/buildAndProgram.md index 5d3af46ef0..889b105e34 100644 --- a/doc/buildAndProgram.md +++ b/doc/buildAndProgram.md @@ -4,7 +4,7 @@ To build this project, you'll need: -- A cross-compiler : [ARM-GCC (10.3-2021.10)](https://developer.arm.com/downloads/-/gnu-rm) +- A cross-compiler : [ARM-GCC 15.3-Rel1 (arm-none-eabi)](https://gitlab.arm.com/tooling/gnu-toolchains-for-arm/-/tree/releases/15.3.rel1) - The NRF52 SDK 15.3.0 : [nRF-SDK v15.3.0](https://nsscprodmedia.blob.core.windows.net/prod/software-and-other-downloads/sdks/nrf5/binaries/nrf5sdk153059ac345.zip) - The Python 3 modules `cbor`, `intelhex`, `click` and `cryptography` modules for the `mcuboot` tool (see [requirements.txt](../tools/mcuboot/requirements.txt)) - To keep the system clean, you can install python modules into a python virtual environment (`venv`) @@ -38,7 +38,7 @@ CMake configures the project according to variables you specify the command line Variable | Description | Example| ----------|-------------|--------| -**ARM_NONE_EABI_TOOLCHAIN_PATH**|path to the toolchain directory|`-DARM_NONE_EABI_TOOLCHAIN_PATH=/home/jf/nrf52/gcc-arm-none-eabi-10.3-2021.10/`| +**ARM_NONE_EABI_TOOLCHAIN_PATH**|path to the toolchain directory|`-DARM_NONE_EABI_TOOLCHAIN_PATH=/home/jf/nrf52/arm-gnu-toolchain-15.3.rel1-x86_64-arm-none-eabi/`| **NRF5_SDK_PATH**|path to the NRF52 SDK|`-DNRF5_SDK_PATH=/home/jf/nrf52/Pinetime/sdk`| **CMAKE_BUILD_TYPE (\*)**| Build type (Release or Debug). Release is applied by default if this variable is not specified.|`-DCMAKE_BUILD_TYPE=Debug` **BUILD_DFU (\*\*)**|Build DFU files while building (needs [adafruit-nrfutil](https://github.com/adafruit/Adafruit_nRF52_nrfutil)).|`-DBUILD_DFU=1` diff --git a/doc/buildWithVScode.md b/doc/buildWithVScode.md index 5f8724828a..89796caeeb 100644 --- a/doc/buildWithVScode.md +++ b/doc/buildWithVScode.md @@ -8,7 +8,7 @@ To support as many setups as possible the VS Code configuration files expect the Variable | Description | Example ----------|-------------|-------- -**ARM_NONE_EABI_TOOLCHAIN_PATH**|path to the toolchain directory|`export ARM_NONE_EABI_TOOLCHAIN_PATH=/opt/gcc-arm-none-eabi-10.3-2021.10` +**ARM_NONE_EABI_TOOLCHAIN_PATH**|path to the toolchain directory|`export ARM_NONE_EABI_TOOLCHAIN_PATH=/opt/arm-gnu-toolchain-15.3.rel1-x86_64-arm-none-eabi` **NRF5_SDK_PATH**|path to the NRF52 SDK|`export NRF5_SDK_PATH=/opt/nRF5_SDK_15.3.0_59ac345` ## VS Code Extensions diff --git a/docker/build.sh b/docker/build.sh index 04963ea0ab..4046215eee 100755 --- a/docker/build.sh +++ b/docker/build.sh @@ -16,7 +16,7 @@ export NPM_DIR="$BUILD_DIR/npm" export npm_config_cache="${NPM_DIR}" export BUILD_TYPE=${BUILD_TYPE:=Release} -export GCC_ARM_VER=${GCC_ARM_VER:="10.3-2021.10"} +export GCC_ARM_VER=${GCC_ARM_VER:="15.3.rel1"} export NRF_SDK_VER=${NRF_SDK_VER:="nRF5_SDK_15.3.0_59ac345"} # convert to lower case and remove _ and . character # the download URL uses the SLUG, but the extracted folder is named like the original value @@ -26,7 +26,7 @@ export NRF_SDK_VER_SLUG=${NRF_SDK_VER_SLUG//[_.]/} MACHINE="$(uname -m)" [ "$MACHINE" = "arm64" ] && MACHINE="aarch64" -export GCC_ARM_PATH="gcc-arm-none-eabi-$GCC_ARM_VER" +export GCC_ARM_PATH="arm-gnu-toolchain-$GCC_ARM_VER-$MACHINE-arm-none-eabi" main() { local target="$1" @@ -50,7 +50,7 @@ main() { } GetGcc() { - wget -q https://developer.arm.com/-/media/Files/downloads/gnu-rm/$GCC_ARM_VER/$GCC_ARM_PATH-$MACHINE-linux.tar.bz2 -O - | tar -xj -C $TOOLS_DIR/ + wget -q https://gitlab.arm.com/api/v4/projects/tooling%2Fgnu-toolchains-for-arm/packages/generic/gnu-toolchain/$GCC_ARM_VER/$GCC_ARM_PATH.tar.xz -O - | tar -xJ -C $TOOLS_DIR/ if [ ! -d "$TOOLS_DIR/$GCC_ARM_PATH" ]; then echo "missing GCC path: $TOOLS_DIR/$GCC_ARM_PATH" return 1 From 5907b1235bdbe75dd554dc5a27534d77d1f9a909 Mon Sep 17 00:00:00 2001 From: mark9064 <30447455+mark9064@users.noreply.github.com> Date: Sat, 8 Aug 2026 23:45:00 +0100 Subject: [PATCH 06/20] Implement required stubs instead of linking nosys --- src/CMakeLists.txt | 12 ++++++------ src/stdlib.c | 49 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e4a354df64..faef4160da 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -930,7 +930,7 @@ target_compile_options(${EXECUTABLE_NAME} PUBLIC set_target_properties(${EXECUTABLE_NAME} PROPERTIES SUFFIX ".out" - LINK_FLAGS "-mthumb -mabi=aapcs -L ${NRF5_SDK_PATH}/modules/nrfx/mdk -T${NRF5_LINKER_SCRIPT} --specs=nosys.specs -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Wl,--gc-sections -Wl,--print-memory-usage --specs=nano.specs -Wl,-Map=${EXECUTABLE_FILE_NAME}.map" + LINK_FLAGS "-mthumb -mabi=aapcs -L ${NRF5_SDK_PATH}/modules/nrfx/mdk -T${NRF5_LINKER_SCRIPT} -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Wl,--gc-sections -Wl,--print-memory-usage --specs=nano.specs -Wl,-Map=${EXECUTABLE_FILE_NAME}.map" ) add_custom_command(TARGET ${EXECUTABLE_NAME} @@ -965,7 +965,7 @@ target_compile_options(${EXECUTABLE_MCUBOOT_NAME} PUBLIC set_target_properties(${EXECUTABLE_MCUBOOT_NAME} PROPERTIES SUFFIX ".out" - LINK_FLAGS "-mthumb -mabi=aapcs -L ${NRF5_SDK_PATH}/modules/nrfx/mdk -T${NRF5_LINKER_SCRIPT_MCUBOOT} --specs=nosys.specs -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Wl,--gc-sections -Wl,--print-memory-usage --specs=nano.specs -Wl,-Map=${EXECUTABLE_MCUBOOT_FILE_NAME}.map" + LINK_FLAGS "-mthumb -mabi=aapcs -L ${NRF5_SDK_PATH}/modules/nrfx/mdk -T${NRF5_LINKER_SCRIPT_MCUBOOT} -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Wl,--gc-sections -Wl,--print-memory-usage --specs=nano.specs -Wl,-Map=${EXECUTABLE_MCUBOOT_FILE_NAME}.map" ) add_custom_command(TARGET ${EXECUTABLE_MCUBOOT_NAME} @@ -1008,7 +1008,7 @@ target_compile_options(${EXECUTABLE_RECOVERY_NAME} PUBLIC set_target_properties(${EXECUTABLE_RECOVERY_NAME} PROPERTIES SUFFIX ".out" - LINK_FLAGS "-mthumb -mabi=aapcs -L ${NRF5_SDK_PATH}/modules/nrfx/mdk -T${NRF5_LINKER_SCRIPT} --specs=nosys.specs -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Wl,--gc-sections -Wl,--print-memory-usage --specs=nano.specs -Wl,-Map=${EXECUTABLE_RECOVERY_FILE_NAME}.map" + LINK_FLAGS "-mthumb -mabi=aapcs -L ${NRF5_SDK_PATH}/modules/nrfx/mdk -T${NRF5_LINKER_SCRIPT} -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Wl,--gc-sections -Wl,--print-memory-usage --specs=nano.specs -Wl,-Map=${EXECUTABLE_RECOVERY_FILE_NAME}.map" ) add_custom_command(TARGET ${EXECUTABLE_RECOVERY_NAME} @@ -1040,7 +1040,7 @@ target_compile_options(${EXECUTABLE_RECOVERY_MCUBOOT_NAME} PUBLIC set_target_properties(${EXECUTABLE_RECOVERY_MCUBOOT_NAME} PROPERTIES SUFFIX ".out" - LINK_FLAGS "-mthumb -mabi=aapcs -L ${NRF5_SDK_PATH}/modules/nrfx/mdk -T${NRF5_LINKER_SCRIPT_MCUBOOT} --specs=nosys.specs -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Wl,--gc-sections -Wl,--print-memory-usage --specs=nano.specs -Wl,-Map=${EXECUTABLE_GRAPHICS_FILE_NAME}.map" + LINK_FLAGS "-mthumb -mabi=aapcs -L ${NRF5_SDK_PATH}/modules/nrfx/mdk -T${NRF5_LINKER_SCRIPT_MCUBOOT} -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Wl,--gc-sections -Wl,--print-memory-usage --specs=nano.specs -Wl,-Map=${EXECUTABLE_GRAPHICS_FILE_NAME}.map" ) add_custom_command(TARGET ${EXECUTABLE_RECOVERY_MCUBOOT_NAME} @@ -1083,7 +1083,7 @@ add_dependencies(${EXECUTABLE_RECOVERYLOADER_NAME} ${EXECUTABLE_RECOVERY_MCUBOOT set_target_properties(${EXECUTABLE_RECOVERYLOADER_NAME} PROPERTIES SUFFIX ".out" - LINK_FLAGS "-mthumb -mabi=aapcs -L ${NRF5_SDK_PATH}/modules/nrfx/mdk -T${NRF5_LINKER_SCRIPT} --specs=nosys.specs -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Wl,--gc-sections -Wl,--print-memory-usage --specs=nano.specs -Wl,-Map=${EXECUTABLE_RECOVERYLOADER_FILE_NAME}.map" + LINK_FLAGS "-mthumb -mabi=aapcs -L ${NRF5_SDK_PATH}/modules/nrfx/mdk -T${NRF5_LINKER_SCRIPT} -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Wl,--gc-sections -Wl,--print-memory-usage --specs=nano.specs -Wl,-Map=${EXECUTABLE_RECOVERYLOADER_FILE_NAME}.map" ) add_custom_command(TARGET ${EXECUTABLE_RECOVERYLOADER_NAME} @@ -1118,7 +1118,7 @@ add_dependencies(${EXECUTABLE_MCUBOOT_RECOVERYLOADER_NAME} ${EXECUTABLE_RECOVERY set_target_properties(${EXECUTABLE_MCUBOOT_RECOVERYLOADER_NAME} PROPERTIES SUFFIX ".out" - LINK_FLAGS "-mthumb -mabi=aapcs -std=gnu++98 -std=c99 -L ${NRF5_SDK_PATH}/modules/nrfx/mdk -T${NRF5_LINKER_SCRIPT_MCUBOOT} --specs=nosys.specs -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Wl,--gc-sections -Wl,--print-memory-usage --specs=nano.specs -Wl,-Map=${EXECUTABLE_MCUBOOT_RECOVERYLOADER_FILE_NAME}.map" + LINK_FLAGS "-mthumb -mabi=aapcs -std=gnu++98 -std=c99 -L ${NRF5_SDK_PATH}/modules/nrfx/mdk -T${NRF5_LINKER_SCRIPT_MCUBOOT} -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Wl,--gc-sections -Wl,--print-memory-usage --specs=nano.specs -Wl,-Map=${EXECUTABLE_MCUBOOT_RECOVERYLOADER_FILE_NAME}.map" ) add_custom_command(TARGET ${EXECUTABLE_MCUBOOT_RECOVERYLOADER_NAME} diff --git a/src/stdlib.c b/src/stdlib.c index 21b506a843..4b26a75008 100644 --- a/src/stdlib.c +++ b/src/stdlib.c @@ -1,6 +1,8 @@ #include #include #include +#include +#include // Override malloc() and free() to use the memory manager from FreeRTOS. // According to the documentation of libc, we also need to override @@ -29,7 +31,7 @@ void __wrap_free(void* ptr) { } void* calloc(size_t num, size_t size) { - void *ptr = malloc(num * size); + void* ptr = malloc(num * size); if (ptr) { memset(ptr, 0, num * size); } @@ -49,3 +51,48 @@ void* realloc(void* ptr, size_t newSize) { void* __wrap_realloc(void* ptr, size_t newSize) { return realloc(ptr, newSize); } + +// Implement functions required by libc as stubs +// These functions aren't linked into the final binary + +__attribute__((error("stub"))) void _close(__attribute__((unused)) int fp) { + __builtin_trap(); +} + +__attribute__((error("stub"))) void _fstat(__attribute__((unused)) int fildes, __attribute__((unused)) struct stat* buf) { + __builtin_trap(); +} + +__attribute__((error("stub"))) pid_t _getpid() { + __builtin_trap(); +} + +__attribute__((error("stub"))) int _isatty(__attribute__((unused)) int fd) { + __builtin_trap(); +} + +__attribute__((error("stub"))) int _kill(__attribute__((unused)) pid_t pid, __attribute__((unused)) int sig) { + __builtin_trap(); +} + +__attribute__((error("stub"))) off_t _lseek(__attribute__((unused)) int fd, + __attribute__((unused)) off_t offset, + __attribute__((unused)) int whence) { + __builtin_trap(); +} + +__attribute__((error("stub"))) ssize_t _read(__attribute__((unused)) int fd, + __attribute__((unused)) void* buf, + __attribute__((unused)) size_t count) { + __builtin_trap(); +} + +__attribute__((error("stub"))) ssize_t _write(__attribute__((unused)) int fd, + __attribute__((unused)) void* buf, + __attribute__((unused)) size_t count) { + __builtin_trap(); +} + +__attribute__((error("stub"))) void _exit(__attribute__((unused)) int status) { + __builtin_trap(); +} From 825056574f47a8187b410b860f326050566553e2 Mon Sep 17 00:00:00 2001 From: mark9064 <30447455+mark9064@users.noreply.github.com> Date: Wed, 19 Aug 2026 00:28:35 +0100 Subject: [PATCH 07/20] Remove redundant LFS logging information --- src/libs/lfs_config.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/lfs_config.h b/src/libs/lfs_config.h index eaeede0e63..9ba83312da 100644 --- a/src/libs/lfs_config.h +++ b/src/libs/lfs_config.h @@ -5,7 +5,7 @@ #ifndef LFS_TRACE #ifdef LFS_YES_TRACE #define LFS_TRACE_(fmt, ...) \ - NRF_LOG_DEBUG("[LFS] %s:%d:trace: " fmt "%s\n", __FILE__, __LINE__, __VA_ARGS__) + NRF_LOG_DEBUG("[LFS] trace: " fmt "%s\n", __VA_ARGS__) #define LFS_TRACE(...) LFS_TRACE_(__VA_ARGS__, "") #else #define LFS_TRACE(...) @@ -15,7 +15,7 @@ #ifndef LFS_DEBUG #ifndef LFS_NO_DEBUG #define LFS_DEBUG_(fmt, ...) \ - NRF_LOG_DEBUG("[LFS] %s:%d:debug: " fmt "%s\n", __FILE__, __LINE__, __VA_ARGS__) + NRF_LOG_DEBUG("[LFS] debug: " fmt "%s\n", __VA_ARGS__) #define LFS_DEBUG(...) LFS_DEBUG_(__VA_ARGS__, "") #else #define LFS_DEBUG(...) @@ -25,7 +25,7 @@ #ifndef LFS_WARN #ifndef LFS_NO_WARN #define LFS_WARN_(fmt, ...) \ - NRF_LOG_WARNING("[LFS] %s:%d:warn: " fmt "%s\n", __FILE__, __LINE__, __VA_ARGS__) + NRF_LOG_WARNING("[LFS] warn: " fmt "%s\n", __VA_ARGS__) #define LFS_WARN(...) LFS_WARN_(__VA_ARGS__, "") #else #define LFS_WARN(...) @@ -35,7 +35,7 @@ #ifndef LFS_ERROR #ifndef LFS_NO_ERROR #define LFS_ERROR_(fmt, ...) \ - NRF_LOG_ERROR("[LFS] %s:%d:error: " fmt "%s\n", __FILE__, __LINE__, __VA_ARGS__) + NRF_LOG_ERROR("[LFS] error: " fmt "%s\n", __VA_ARGS__) #define LFS_ERROR(...) LFS_ERROR_(__VA_ARGS__, "") #else #define LFS_ERROR(...) From 6c119eb52206b580b556b41633dddc1e1b66a8da Mon Sep 17 00:00:00 2001 From: mark9064 <30447455+mark9064@users.noreply.github.com> Date: Mon, 7 Sep 2026 22:27:08 +0100 Subject: [PATCH 08/20] Antialias smaller system fonts (#2399) --- src/displayapp/fonts/CMakeLists.txt | 4 ---- src/displayapp/fonts/fonts.json | 9 ++++----- src/displayapp/fonts/jetbrains_mono_bold_20.c_M.patch | 8 -------- src/displayapp/fonts/jetbrains_mono_bold_20.c_zero.patch | 6 ------ 4 files changed, 4 insertions(+), 23 deletions(-) delete mode 100644 src/displayapp/fonts/jetbrains_mono_bold_20.c_M.patch delete mode 100644 src/displayapp/fonts/jetbrains_mono_bold_20.c_zero.patch diff --git a/src/displayapp/fonts/CMakeLists.txt b/src/displayapp/fonts/CMakeLists.txt index 562f0801ad..73badff8d9 100644 --- a/src/displayapp/fonts/CMakeLists.txt +++ b/src/displayapp/fonts/CMakeLists.txt @@ -4,10 +4,6 @@ set(FONTS jetbrains_mono_42 jetbrains_mono_76 jetbrains_mono_bold_20 find_program(LV_FONT_CONV "lv_font_conv" NO_CACHE REQUIRED HINTS "${CMAKE_SOURCE_DIR}/node_modules/.bin") message(STATUS "Using ${LV_FONT_CONV} to generate font files") -configure_file(${CMAKE_CURRENT_LIST_DIR}/jetbrains_mono_bold_20.c_zero.patch - ${CMAKE_CURRENT_BINARY_DIR}/jetbrains_mono_bold_20.c_zero.patch COPYONLY) -configure_file(${CMAKE_CURRENT_LIST_DIR}/jetbrains_mono_bold_20.c_M.patch - ${CMAKE_CURRENT_BINARY_DIR}/jetbrains_mono_bold_20.c_M.patch COPYONLY) if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.12) # FindPython3 module introduces with CMake 3.12 # https://cmake.org/cmake/help/latest/module/FindPython3.html diff --git a/src/displayapp/fonts/fonts.json b/src/displayapp/fonts/fonts.json index 3221c2f171..3223b76b56 100644 --- a/src/displayapp/fonts/fonts.json +++ b/src/displayapp/fonts/fonts.json @@ -10,9 +10,8 @@ "range": "0xf294, 0xf242, 0xf54b, 0xf21e, 0xf1e6, 0xf017, 0xf129, 0xf03a, 0xf185, 0xf560, 0xf001, 0xf3fd, 0xf1fc, 0xf45d, 0xf59f, 0xf5a0, 0xf027, 0xf028, 0xf6a9, 0xf04b, 0xf04c, 0xf048, 0xf051, 0xf095, 0xf3dd, 0xf04d, 0xf2f2, 0xf024, 0xf252, 0xf569, 0xf06e, 0xf015, 0xf00c, 0xf0f3, 0xf522, 0xf743, 0xf1ec, 0xf55a, 0xf3ed" } ], - "bpp": 1, - "size": 20, - "patches": ["jetbrains_mono_bold_20.c_zero.patch", "jetbrains_mono_bold_20.c_M.patch"] + "bpp": 2, + "size": 20 }, "jetbrains_mono_42": { "sources": [ @@ -21,7 +20,7 @@ "range": "0x25, 0x2b, 0x2d, 0x2e, 0x30-0x3a, 0x43, 0x46, 0x4b-0x4d, 0x66, 0x69, 0x6b, 0x6d, 0x74, 0xb0" } ], - "bpp": 1, + "bpp": 2, "size": 42 }, "jetbrains_mono_76": { @@ -71,7 +70,7 @@ "range": "0xf185, 0xf186, 0xf6c3, 0xf6c4, 0xf73c, 0xf743, 0xf740, 0xf75f, 0xf0c2, 0xf05e, 0xf73b, 0xf0e7, 0xf2dc" } ], - "bpp": 1, + "bpp": 2, "size": 25 } } diff --git a/src/displayapp/fonts/jetbrains_mono_bold_20.c_M.patch b/src/displayapp/fonts/jetbrains_mono_bold_20.c_M.patch deleted file mode 100644 index 31f243ada8..0000000000 --- a/src/displayapp/fonts/jetbrains_mono_bold_20.c_M.patch +++ /dev/null @@ -1,8 +0,0 @@ -@@ -217,7 +217,7 @@ - 0xc0, 0xe0, 0x70, 0x38, 0x1c, 0xf, 0xff, 0xfc, - - /* U+004D "M" */ -- 0xf3, 0xfc, 0xfd, 0x3f, 0xcf, 0xff, 0xff, 0xfe, -+ 0xf3, 0xfc, 0xff, 0x3f, 0xcf, 0xff, 0xff, 0xfe, - 0xdf, 0xb7, 0xe1, 0xf8, 0x7e, 0x1f, 0x87, 0xe1, - 0xf8, 0x70, diff --git a/src/displayapp/fonts/jetbrains_mono_bold_20.c_zero.patch b/src/displayapp/fonts/jetbrains_mono_bold_20.c_zero.patch deleted file mode 100644 index ecb9966278..0000000000 --- a/src/displayapp/fonts/jetbrains_mono_bold_20.c_zero.patch +++ /dev/null @@ -1,6 +0,0 @@ -91,92c91,92 -< 0x3f, 0x1f, 0xef, 0x3f, 0x87, 0xe1, 0xf8, 0x7f, -< 0xdf, 0xf7, 0xe1, 0xf8, 0x7e, 0x1f, 0xcf, 0x7f, ---- -> 0x3f, 0x1f, 0xef, 0x3f, 0x87, 0xe1, 0xf8, 0x7e, -> 0xdf, 0xb7, 0xe1, 0xf8, 0x7e, 0x1f, 0xcf, 0x7f, From bbee39ea33d2a1f760e9164fac11844d758acc0d Mon Sep 17 00:00:00 2001 From: Margot Date: Sat, 17 Oct 2026 01:48:54 +0200 Subject: [PATCH 09/20] Add escaped quotes to LINK_FLAGS --- src/CMakeLists.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index faef4160da..9a4dc2b8c3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -930,7 +930,7 @@ target_compile_options(${EXECUTABLE_NAME} PUBLIC set_target_properties(${EXECUTABLE_NAME} PROPERTIES SUFFIX ".out" - LINK_FLAGS "-mthumb -mabi=aapcs -L ${NRF5_SDK_PATH}/modules/nrfx/mdk -T${NRF5_LINKER_SCRIPT} -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Wl,--gc-sections -Wl,--print-memory-usage --specs=nano.specs -Wl,-Map=${EXECUTABLE_FILE_NAME}.map" + LINK_FLAGS "-mthumb -mabi=aapcs -L ${NRF5_SDK_PATH}/modules/nrfx/mdk -T\"${NRF5_LINKER_SCRIPT}\" -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Wl,--gc-sections -Wl,--print-memory-usage --specs=nano.specs -Wl,-Map=${EXECUTABLE_FILE_NAME}.map" ) add_custom_command(TARGET ${EXECUTABLE_NAME} @@ -965,7 +965,7 @@ target_compile_options(${EXECUTABLE_MCUBOOT_NAME} PUBLIC set_target_properties(${EXECUTABLE_MCUBOOT_NAME} PROPERTIES SUFFIX ".out" - LINK_FLAGS "-mthumb -mabi=aapcs -L ${NRF5_SDK_PATH}/modules/nrfx/mdk -T${NRF5_LINKER_SCRIPT_MCUBOOT} -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Wl,--gc-sections -Wl,--print-memory-usage --specs=nano.specs -Wl,-Map=${EXECUTABLE_MCUBOOT_FILE_NAME}.map" + LINK_FLAGS "-mthumb -mabi=aapcs -L ${NRF5_SDK_PATH}/modules/nrfx/mdk -T\"${NRF5_LINKER_SCRIPT_MCUBOOT}\" -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Wl,--gc-sections -Wl,--print-memory-usage --specs=nano.specs -Wl,-Map=${EXECUTABLE_MCUBOOT_FILE_NAME}.map" ) add_custom_command(TARGET ${EXECUTABLE_MCUBOOT_NAME} @@ -1008,7 +1008,7 @@ target_compile_options(${EXECUTABLE_RECOVERY_NAME} PUBLIC set_target_properties(${EXECUTABLE_RECOVERY_NAME} PROPERTIES SUFFIX ".out" - LINK_FLAGS "-mthumb -mabi=aapcs -L ${NRF5_SDK_PATH}/modules/nrfx/mdk -T${NRF5_LINKER_SCRIPT} -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Wl,--gc-sections -Wl,--print-memory-usage --specs=nano.specs -Wl,-Map=${EXECUTABLE_RECOVERY_FILE_NAME}.map" + LINK_FLAGS "-mthumb -mabi=aapcs -L ${NRF5_SDK_PATH}/modules/nrfx/mdk -T\"${NRF5_LINKER_SCRIPT}\" -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Wl,--gc-sections -Wl,--print-memory-usage --specs=nano.specs -Wl,-Map=${EXECUTABLE_RECOVERY_FILE_NAME}.map" ) add_custom_command(TARGET ${EXECUTABLE_RECOVERY_NAME} @@ -1040,7 +1040,7 @@ target_compile_options(${EXECUTABLE_RECOVERY_MCUBOOT_NAME} PUBLIC set_target_properties(${EXECUTABLE_RECOVERY_MCUBOOT_NAME} PROPERTIES SUFFIX ".out" - LINK_FLAGS "-mthumb -mabi=aapcs -L ${NRF5_SDK_PATH}/modules/nrfx/mdk -T${NRF5_LINKER_SCRIPT_MCUBOOT} -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Wl,--gc-sections -Wl,--print-memory-usage --specs=nano.specs -Wl,-Map=${EXECUTABLE_GRAPHICS_FILE_NAME}.map" + LINK_FLAGS "-mthumb -mabi=aapcs -L ${NRF5_SDK_PATH}/modules/nrfx/mdk -T\"${NRF5_LINKER_SCRIPT_MCUBOOT}\" -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Wl,--gc-sections -Wl,--print-memory-usage --specs=nano.specs -Wl,-Map=${EXECUTABLE_GRAPHICS_FILE_NAME}.map" ) add_custom_command(TARGET ${EXECUTABLE_RECOVERY_MCUBOOT_NAME} @@ -1083,7 +1083,7 @@ add_dependencies(${EXECUTABLE_RECOVERYLOADER_NAME} ${EXECUTABLE_RECOVERY_MCUBOOT set_target_properties(${EXECUTABLE_RECOVERYLOADER_NAME} PROPERTIES SUFFIX ".out" - LINK_FLAGS "-mthumb -mabi=aapcs -L ${NRF5_SDK_PATH}/modules/nrfx/mdk -T${NRF5_LINKER_SCRIPT} -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Wl,--gc-sections -Wl,--print-memory-usage --specs=nano.specs -Wl,-Map=${EXECUTABLE_RECOVERYLOADER_FILE_NAME}.map" + LINK_FLAGS "-mthumb -mabi=aapcs -L ${NRF5_SDK_PATH}/modules/nrfx/mdk -T\"${NRF5_LINKER_SCRIPT}\" -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Wl,--gc-sections -Wl,--print-memory-usage --specs=nano.specs -Wl,-Map=${EXECUTABLE_RECOVERYLOADER_FILE_NAME}.map" ) add_custom_command(TARGET ${EXECUTABLE_RECOVERYLOADER_NAME} @@ -1118,7 +1118,7 @@ add_dependencies(${EXECUTABLE_MCUBOOT_RECOVERYLOADER_NAME} ${EXECUTABLE_RECOVERY set_target_properties(${EXECUTABLE_MCUBOOT_RECOVERYLOADER_NAME} PROPERTIES SUFFIX ".out" - LINK_FLAGS "-mthumb -mabi=aapcs -std=gnu++98 -std=c99 -L ${NRF5_SDK_PATH}/modules/nrfx/mdk -T${NRF5_LINKER_SCRIPT_MCUBOOT} -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Wl,--gc-sections -Wl,--print-memory-usage --specs=nano.specs -Wl,-Map=${EXECUTABLE_MCUBOOT_RECOVERYLOADER_FILE_NAME}.map" + LINK_FLAGS "-mthumb -mabi=aapcs -std=gnu++98 -std=c99 -L ${NRF5_SDK_PATH}/modules/nrfx/mdk -T\"${NRF5_LINKER_SCRIPT_MCUBOOT}\" -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Wl,--gc-sections -Wl,--print-memory-usage --specs=nano.specs -Wl,-Map=${EXECUTABLE_MCUBOOT_RECOVERYLOADER_FILE_NAME}.map" ) add_custom_command(TARGET ${EXECUTABLE_MCUBOOT_RECOVERYLOADER_NAME} From 1eac3fbf226f9442a350b4a53b48ea47d71d91ba Mon Sep 17 00:00:00 2001 From: Steveis Date: Sun, 18 Oct 2026 01:35:57 +0100 Subject: [PATCH 10/20] Improve notification preview UX (#2469) --- src/displayapp/DisplayApp.cpp | 4 ++-- src/displayapp/screens/Notifications.cpp | 11 +++++------ src/displayapp/screens/Notifications.h | 1 - 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/displayapp/DisplayApp.cpp b/src/displayapp/DisplayApp.cpp index 84fa603622..11ed9a263b 100644 --- a/src/displayapp/DisplayApp.cpp +++ b/src/displayapp/DisplayApp.cpp @@ -320,8 +320,8 @@ void DisplayApp::Refresh() { brightnessController.Set(Controllers::BrightnessController::Levels::Off); } // Since the active screen is not really an app, go back to Clock. - if (currentApp == Apps::Launcher || currentApp == Apps::Notifications || currentApp == Apps::QuickSettings || - currentApp == Apps::Settings) { + if (currentApp == Apps::Launcher || currentApp == Apps::Notifications || currentApp == Apps::NotificationsPreview || + currentApp == Apps::QuickSettings || currentApp == Apps::Settings) { LoadScreen(Apps::Clock, DisplayApp::FullRefreshDirections::None); // Wait for the clock app to load before moving on. while (!lv_task_handler()) { diff --git a/src/displayapp/screens/Notifications.cpp b/src/displayapp/screens/Notifications.cpp index 7d937c6d8a..16f261e252 100644 --- a/src/displayapp/screens/Notifications.cpp +++ b/src/displayapp/screens/Notifications.cpp @@ -55,7 +55,6 @@ Notifications::Notifications(DisplayApp* app, lv_line_set_points(timeoutLine, timeoutLinePoints, 2); timeoutTickCountStart = xTaskGetTickCount(); - interacted = false; } taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); @@ -121,6 +120,7 @@ void Notifications::Refresh() { void Notifications::OnPreviewInteraction() { wakeLock.Release(); motorController.StopRinging(); + mode = Modes::Normal; if (timeoutLine != nullptr) { lv_obj_del(timeoutLine); timeoutLine = nullptr; @@ -147,16 +147,15 @@ void Notifications::OnPreviewDismiss() { } bool Notifications::OnTouchEvent(Pinetime::Applications::TouchEvents event) { - if (mode != Modes::Normal) { - if (!interacted && event == TouchEvents::Tap) { - interacted = true; + if (mode == Modes::Preview) { + if (event == TouchEvents::Tap || event == TouchEvents::SwipeDown) { OnPreviewInteraction(); - return true; } else if (event == Pinetime::Applications::TouchEvents::SwipeRight) { OnPreviewDismiss(); return true; + } else { + return false; } - return false; } switch (event) { diff --git a/src/displayapp/screens/Notifications.h b/src/displayapp/screens/Notifications.h index 8488dc5bb2..a8caee6253 100644 --- a/src/displayapp/screens/Notifications.h +++ b/src/displayapp/screens/Notifications.h @@ -86,7 +86,6 @@ namespace Pinetime { TickType_t timeoutTickCountStart; static const TickType_t timeoutLength = pdMS_TO_TICKS(7000); - bool interacted = true; bool dismissingNotification = false; From b0d40a2ddb417c7b6093e743b3f0620619096906 Mon Sep 17 00:00:00 2001 From: Davis Mosenkovs Date: Sun, 19 Jul 2026 19:51:13 +0300 Subject: [PATCH 11/20] Refactor NotificationManager::Notification Refactor NotificationManager::Notification to use constructors. This reduces risk of coding errors (incl. buffer overflows) when creating NotificationManager::Notification and copying text to it. --- .../ble/AlertNotificationClient.cpp | 5 +--- .../ble/AlertNotificationService.cpp | 5 +--- src/components/ble/DfuService.cpp | 4 +-- src/components/ble/FSService.cpp | 4 +-- src/components/ble/ImmediateAlertService.cpp | 4 +-- src/components/ble/NotificationManager.cpp | 30 +++++++++++++++++++ src/components/ble/NotificationManager.h | 16 ++++++++-- 7 files changed, 49 insertions(+), 19 deletions(-) diff --git a/src/components/ble/AlertNotificationClient.cpp b/src/components/ble/AlertNotificationClient.cpp index e3bc924230..c50643e451 100644 --- a/src/components/ble/AlertNotificationClient.cpp +++ b/src/components/ble/AlertNotificationClient.cpp @@ -156,10 +156,7 @@ void AlertNotificationClient::OnNotification(ble_gap_event* event) { size_t bufferSize = std::min(packetLen + stringTerminatorSize, maxBufferSize); auto messageSize = std::min(maxMessageSize, (bufferSize - headerSize)); - NotificationManager::Notification notif; - os_mbuf_copydata(event->notify_rx.om, headerSize, messageSize - 1, notif.message.data()); - notif.message[messageSize - 1] = '\0'; - notif.size = messageSize; + NotificationManager::Notification notif(event->notify_rx.om, headerSize, messageSize); notif.category = Pinetime::Controllers::NotificationManager::Categories::SimpleAlert; notificationManager.Push(std::move(notif)); diff --git a/src/components/ble/AlertNotificationService.cpp b/src/components/ble/AlertNotificationService.cpp index d9f28698bc..7aaa3cca19 100644 --- a/src/components/ble/AlertNotificationService.cpp +++ b/src/components/ble/AlertNotificationService.cpp @@ -61,11 +61,8 @@ int AlertNotificationService::OnAlert(struct ble_gatt_access_ctxt* ctxt) { auto messageSize = std::min(maxMessageSize, (bufferSize - headerSize)); Categories category; - NotificationManager::Notification notif; - os_mbuf_copydata(ctxt->om, headerSize, messageSize - 1, notif.message.data()); + NotificationManager::Notification notif(ctxt->om, headerSize, messageSize); os_mbuf_copydata(ctxt->om, 0, 1, &category); - notif.message[messageSize - 1] = '\0'; - notif.size = messageSize; // TODO convert all ANS categories to NotificationController categories switch (category) { diff --git a/src/components/ble/DfuService.cpp b/src/components/ble/DfuService.cpp index ad9c99e9e0..a6affc854a 100644 --- a/src/components/ble/DfuService.cpp +++ b/src/components/ble/DfuService.cpp @@ -82,9 +82,7 @@ void DfuService::Init() { int DfuService::OnServiceData(uint16_t connectionHandle, uint16_t attributeHandle, ble_gatt_access_ctxt* context) { #ifndef PINETIME_IS_RECOVERY if (systemTask.GetSettings().GetDfuAndFsMode() == Pinetime::Controllers::Settings::DfuAndFsMode::Disabled) { - Pinetime::Controllers::NotificationManager::Notification notif; - memcpy(notif.message.data(), denyAlert, denyAlertLength); - notif.size = denyAlertLength; + Pinetime::Controllers::NotificationManager::Notification notif(denyAlert, denyAlertLength); notif.category = Pinetime::Controllers::NotificationManager::Categories::SimpleAlert; systemTask.GetNotificationManager().Push(std::move(notif)); systemTask.PushMessage(Pinetime::System::Messages::OnNewNotification); diff --git a/src/components/ble/FSService.cpp b/src/components/ble/FSService.cpp index 721ed297c5..82af3cd038 100644 --- a/src/components/ble/FSService.cpp +++ b/src/components/ble/FSService.cpp @@ -53,9 +53,7 @@ void FSService::Init() { int FSService::OnFSServiceRequested(uint16_t connectionHandle, uint16_t attributeHandle, ble_gatt_access_ctxt* context) { #ifndef PINETIME_IS_RECOVERY if (systemTask.GetSettings().GetDfuAndFsMode() == Pinetime::Controllers::Settings::DfuAndFsMode::Disabled) { - Pinetime::Controllers::NotificationManager::Notification notif; - memcpy(notif.message.data(), denyAlert, denyAlertLength); - notif.size = denyAlertLength; + Pinetime::Controllers::NotificationManager::Notification notif(denyAlert, denyAlertLength); notif.category = Pinetime::Controllers::NotificationManager::Categories::SimpleAlert; systemTask.GetNotificationManager().Push(std::move(notif)); systemTask.PushMessage(Pinetime::System::Messages::OnNewNotification); diff --git a/src/components/ble/ImmediateAlertService.cpp b/src/components/ble/ImmediateAlertService.cpp index e25e018f92..19f32465f5 100644 --- a/src/components/ble/ImmediateAlertService.cpp +++ b/src/components/ble/ImmediateAlertService.cpp @@ -62,9 +62,7 @@ int ImmediateAlertService::OnAlertLevelChanged(uint16_t attributeHandle, ble_gat auto alertLevel = static_cast(context->om->om_data[0]); auto* alertString = ToString(alertLevel); - NotificationManager::Notification notif; - std::memcpy(notif.message.data(), alertString, strlen(alertString) + 1); - notif.size = strlen(alertString) + 1; + NotificationManager::Notification notif(alertString, strlen(alertString) + 1); notif.category = Pinetime::Controllers::NotificationManager::Categories::SimpleAlert; notificationManager.Push(std::move(notif)); diff --git a/src/components/ble/NotificationManager.cpp b/src/components/ble/NotificationManager.cpp index 223f6221af..bbc7246824 100644 --- a/src/components/ble/NotificationManager.cpp +++ b/src/components/ble/NotificationManager.cpp @@ -132,6 +132,36 @@ size_t NotificationManager::NbNotifications() const { return size; } +NotificationManager::Notification::Notification() { +} + +/** + * Make a Notification from const char* buffer. + * + * @param message the const char* buffer with the message + * @param size the size of complete message, including the final 0x00 byte + */ +NotificationManager::Notification::Notification(const char* message, uint8_t size) { + uint8_t effectiveSize = std::min(std::max(size, uint8_t {1}), NotificationManager::MessageSize); + memcpy(this->message.data(), message, effectiveSize - 1); + this->message[effectiveSize - 1] = '\0'; + this->size = effectiveSize; +} + +/** + * Make a Notification from a value in const struct os_mbuf* buffer. + * + * @param om the const struct os_mbuf* buffer containing the message + * @param off the offset of the message - passed to os_mbuf_copydata() + * @param size the size of complete message, including the final 0x00 byte + */ +NotificationManager::Notification::Notification(const struct os_mbuf* om, int off, uint8_t size) { + uint8_t effectiveSize = std::min(std::max(size, uint8_t {1}), NotificationManager::MessageSize); + os_mbuf_copydata(om, off, effectiveSize - 1, this->message.data()); + this->message[effectiveSize - 1] = '\0'; + this->size = effectiveSize; +} + const char* NotificationManager::Notification::Message() const { const char* itField = std::find(message.begin(), message.begin() + size - 1, '\0'); if (itField != message.begin() + size - 1) { diff --git a/src/components/ble/NotificationManager.h b/src/components/ble/NotificationManager.h index 57a9c715d2..cc90fb5018 100644 --- a/src/components/ble/NotificationManager.h +++ b/src/components/ble/NotificationManager.h @@ -5,6 +5,12 @@ #include #include +#define min // workaround: nimble's min/max macros conflict with libstdc++ +#define max +#include +#undef max +#undef min + namespace Pinetime { namespace Controllers { class NotificationManager { @@ -25,17 +31,23 @@ namespace Pinetime { static constexpr uint8_t MessageSize {100}; struct Notification { + public: using Id = uint8_t; using Idx = uint8_t; - std::array message{}; - uint8_t size; Categories category = Categories::Unknown; Id id = 0; bool valid = false; + Notification(); + Notification(const char* message, uint8_t size); + Notification(const struct os_mbuf* om, int off, uint8_t size); const char* Message() const; const char* Title() const; + + private: + std::array message {}; + uint8_t size; }; void Push(Notification&& notif); From 85afe767dab437d39fb5e348844c59144b396dc4 Mon Sep 17 00:00:00 2001 From: Liam Willey Date: Sat, 17 Oct 2026 20:44:01 -0400 Subject: [PATCH 12/20] Watchface: Numerals (#2047) --- src/CMakeLists.txt | 1 + src/displayapp/UserApps.h | 1 + src/displayapp/apps/Apps.h.in | 1 + src/displayapp/apps/CMakeLists.txt | 1 + src/displayapp/screens/WatchFaceNumerals.cpp | 136 +++++++++++++++++++ src/displayapp/screens/WatchFaceNumerals.h | 75 ++++++++++ src/resources/fonts.json | 24 ++++ src/resources/fonts/BloggerSans-Bold.ttf | Bin 0 -> 167768 bytes 8 files changed, 239 insertions(+) create mode 100644 src/displayapp/screens/WatchFaceNumerals.cpp create mode 100644 src/displayapp/screens/WatchFaceNumerals.h create mode 100644 src/resources/fonts/BloggerSans-Bold.ttf diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9a4dc2b8c3..8a575884b3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -429,6 +429,7 @@ list(APPEND SOURCE_FILES displayapp/screens/WatchFaceTerminal.cpp displayapp/screens/WatchFacePineTimeStyle.cpp displayapp/screens/WatchFaceCasioStyleG7710.cpp + displayapp/screens/WatchFaceNumerals.cpp displayapp/screens/WatchFacePrideFlag.cpp ## diff --git a/src/displayapp/UserApps.h b/src/displayapp/UserApps.h index 25926edc40..731a4e0369 100644 --- a/src/displayapp/UserApps.h +++ b/src/displayapp/UserApps.h @@ -14,6 +14,7 @@ #include "displayapp/screens/WatchFaceInfineat.h" #include "displayapp/screens/WatchFacePineTimeStyle.h" #include "displayapp/screens/WatchFaceTerminal.h" +#include "displayapp/screens/WatchFaceNumerals.h" #include "displayapp/screens/WatchFacePrideFlag.h" namespace Pinetime { diff --git a/src/displayapp/apps/Apps.h.in b/src/displayapp/apps/Apps.h.in index d440b598d1..b8f90b2753 100644 --- a/src/displayapp/apps/Apps.h.in +++ b/src/displayapp/apps/Apps.h.in @@ -55,6 +55,7 @@ namespace Pinetime { Terminal, Infineat, CasioStyleG7710, + Numerals, PrideFlag, }; diff --git a/src/displayapp/apps/CMakeLists.txt b/src/displayapp/apps/CMakeLists.txt index 93196ed6a0..c00203128b 100644 --- a/src/displayapp/apps/CMakeLists.txt +++ b/src/displayapp/apps/CMakeLists.txt @@ -28,6 +28,7 @@ else() set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::Terminal") set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::Infineat") set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::CasioStyleG7710") + set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::Numerals") set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::PrideFlag") set(WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}" CACHE STRING "List of watch faces to build into the firmware") endif() diff --git a/src/displayapp/screens/WatchFaceNumerals.cpp b/src/displayapp/screens/WatchFaceNumerals.cpp new file mode 100644 index 0000000000..9b84de54e7 --- /dev/null +++ b/src/displayapp/screens/WatchFaceNumerals.cpp @@ -0,0 +1,136 @@ +#include "displayapp/screens/WatchFaceNumerals.h" + +#include +#include "components/ble/NotificationManager.h" +#include "components/settings/Settings.h" + +using namespace Pinetime::Applications::Screens; + +WatchFaceNumerals::WatchFaceNumerals(Controllers::DateTime& dateTimeController, + Controllers::NotificationManager& notificationManager, + Controllers::Settings& settingsController, + Controllers::FS& filesystem) + : currentDateTime {{}}, + dateTimeController {dateTimeController}, + notificationManager {notificationManager}, + settingsController {settingsController} { + + lfs_file f = {}; + if (filesystem.FileOpen(&f, "/fonts/rounded_large.bin", LFS_O_RDONLY) >= 0) { + filesystem.FileClose(&f); + fontLarge = lv_font_load("F:/fonts/rounded_large.bin"); + } + + if (filesystem.FileOpen(&f, "/fonts/rounded_small.bin", LFS_O_RDONLY) >= 0) { + filesystem.FileClose(&f); + fontSmall = lv_font_load("F:/fonts/rounded_small.bin"); + } + + notificationIcon = lv_obj_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_bg_color(notificationIcon, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_AQUA); + lv_obj_set_style_local_radius(notificationIcon, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_RADIUS_CIRCLE); + lv_obj_set_size(notificationIcon, 16, 16); + lv_obj_align(notificationIcon, lv_scr_act(), LV_ALIGN_IN_BOTTOM_LEFT, 4, -76); + lv_obj_set_hidden(notificationIcon, true); + + labelTimeHour = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_font(labelTimeHour, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, fontLarge); + lv_obj_set_style_local_text_color(labelTimeHour, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_GRAY); + lv_obj_align(labelTimeHour, lv_scr_act(), LV_ALIGN_IN_BOTTOM_RIGHT, -160, -125); + + labelTimeMinute = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_font(labelTimeMinute, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, fontLarge); + lv_obj_set_style_local_text_color(labelTimeMinute, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_AQUA); + lv_obj_align(labelTimeMinute, lv_scr_act(), LV_ALIGN_IN_BOTTOM_RIGHT, -160, 0); + + labelTimeAMPM1 = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_text_static(labelTimeAMPM1, ""); + lv_obj_set_style_local_text_font(labelTimeAMPM1, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, fontSmall); + lv_obj_set_style_local_text_color(labelTimeAMPM1, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, ampmColor); + lv_obj_align(labelTimeAMPM1, lv_scr_act(), LV_ALIGN_IN_BOTTOM_LEFT, 2, -34); + // We use two labels for more flexibility with alignment + labelTimeAMPM2 = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_text_static(labelTimeAMPM2, "M"); + lv_obj_set_style_local_text_font(labelTimeAMPM2, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, fontSmall); + lv_obj_set_style_local_text_color(labelTimeAMPM2, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, ampmColor); + lv_obj_align(labelTimeAMPM2, lv_scr_act(), LV_ALIGN_IN_BOTTOM_LEFT, 0, 0); + + dateDayOfWeek = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(dateDayOfWeek, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE); + lv_obj_set_style_local_text_font(dateDayOfWeek, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, fontSmall); + lv_obj_align(dateDayOfWeek, lv_scr_act(), LV_ALIGN_IN_TOP_LEFT, 0, 0); + + dateDay = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(dateDay, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE); + lv_obj_set_style_local_text_font(dateDay, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, fontSmall); + lv_obj_align(dateDay, lv_scr_act(), LV_ALIGN_IN_TOP_LEFT, 0, 36); + + Refresh(); + taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); +} + +WatchFaceNumerals::~WatchFaceNumerals() { + lv_task_del(taskRefresh); + + if (fontLarge != nullptr) { + lv_font_free(fontLarge); + } + if (fontSmall != nullptr) { + lv_font_free(fontSmall); + } + + lv_obj_clean(lv_scr_act()); +} + +void WatchFaceNumerals::Refresh() { + notificationState = notificationManager.AreNewNotificationsAvailable(); + if (notificationState.IsUpdated()) { + lv_obj_set_hidden(notificationIcon, !notificationState.Get()); + } + + currentDateTime = std::chrono::time_point_cast(dateTimeController.CurrentDateTime()); + if (currentDateTime.IsUpdated()) { + uint8_t hour = dateTimeController.Hours(); + uint8_t minute = dateTimeController.Minutes(); + + if (settingsController.GetClockType() == Controllers::Settings::ClockType::H12) { + char ampmChar[2] = "A"; + if (hour == 0) { + hour = 12; + } else if (hour == 12) { + ampmChar[0] = 'P'; + } else if (hour > 12) { + hour = hour - 12; + ampmChar[0] = 'P'; + } + lv_label_set_text(labelTimeAMPM1, ampmChar); + } else { + lv_obj_set_hidden(labelTimeAMPM2, true); + } + lv_label_set_text_fmt(labelTimeHour, "%02d", hour); + lv_label_set_text_fmt(labelTimeMinute, "%02d", minute); + + currentDate = std::chrono::time_point_cast(currentDateTime.Get()); + if (currentDate.IsUpdated()) { + lv_label_set_text_static(dateDayOfWeek, dateTimeController.DayOfWeekShortToString()); + lv_label_set_text_fmt(dateDay, "%d", dateTimeController.Day()); + lv_obj_realign(dateDay); + } + } +} + +bool WatchFaceNumerals::IsAvailable(Pinetime::Controllers::FS& filesystem) { + lfs_file file = {}; + + if (filesystem.FileOpen(&file, "/fonts/rounded_small.bin", LFS_O_RDONLY) < 0) { + return false; + } + + filesystem.FileClose(&file); + if (filesystem.FileOpen(&file, "/fonts/rounded_large.bin", LFS_O_RDONLY) < 0) { + return false; + } + + filesystem.FileClose(&file); + return true; +} diff --git a/src/displayapp/screens/WatchFaceNumerals.h b/src/displayapp/screens/WatchFaceNumerals.h new file mode 100644 index 0000000000..44a4a9a339 --- /dev/null +++ b/src/displayapp/screens/WatchFaceNumerals.h @@ -0,0 +1,75 @@ +#pragma once + +#include +#include +#include +#include "displayapp/screens/Screen.h" +#include "components/datetime/DateTimeController.h" +#include "utility/DirtyValue.h" +#include "displayapp/apps/Apps.h" + +namespace Pinetime { + namespace Controllers { + class Settings; + class NotificationManager; + } + + namespace Applications { + namespace Screens { + + class WatchFaceNumerals : public Screen { + public: + WatchFaceNumerals(Controllers::DateTime& dateTimeController, + Controllers::NotificationManager& notificationManager, + Controllers::Settings& settingsController, + Controllers::FS& filesystem); + ~WatchFaceNumerals() override; + + void Refresh() override; + + static bool IsAvailable(Pinetime::Controllers::FS& filesystem); + + private: + static constexpr lv_color_t ampmColor = LV_COLOR_MAKE(0x99, 0x99, 0x99); + + Utility::DirtyValue notificationState; + Utility::DirtyValue> currentDateTime; + Utility::DirtyValue> currentDate; + + lv_obj_t* labelTimeHour; + lv_obj_t* labelTimeMinute; + lv_obj_t* labelTimeAMPM1; + lv_obj_t* labelTimeAMPM2; + lv_obj_t* notificationIcon; + lv_obj_t* dateDay; + lv_obj_t* dateDayOfWeek; + + Controllers::DateTime& dateTimeController; + Controllers::NotificationManager& notificationManager; + Controllers::Settings& settingsController; + + lv_font_t* fontLarge = nullptr; + lv_font_t* fontSmall = nullptr; + + lv_task_t* taskRefresh; + }; + } + + template <> + struct WatchFaceTraits { + static constexpr WatchFace watchFace = WatchFace::Numerals; + static constexpr const char* name = "Numerals"; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::WatchFaceNumerals(controllers.dateTimeController, + controllers.notificationManager, + controllers.settingsController, + controllers.filesystem); + }; + + static bool IsAvailable(Pinetime::Controllers::FS& filesystem) { + return Screens::WatchFaceNumerals::IsAvailable(filesystem); + } + }; + } +} diff --git a/src/resources/fonts.json b/src/resources/fonts.json index c4a63349a1..2f8a9f6807 100644 --- a/src/resources/fonts.json +++ b/src/resources/fonts.json @@ -58,5 +58,29 @@ "size": 115, "format": "bin", "target_path": "/fonts/" + }, + "rounded_large": { + "sources": [ + { + "file": "fonts/BloggerSans-Bold.ttf", + "symbols": "0123456789" + } + ], + "bpp": 1, + "size": 150, + "format": "bin", + "target_path": "/fonts/" + }, + "rounded_small": { + "sources": [ + { + "file": "fonts/BloggerSans-Bold.ttf", + "symbols": "0123456789APMONTUEWDHRFIS" + } + ], + "bpp": 1, + "size": 30, + "format": "bin", + "target_path": "/fonts/" } } diff --git a/src/resources/fonts/BloggerSans-Bold.ttf b/src/resources/fonts/BloggerSans-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..d287255d9192f973bada4a97ddce3ce3a7d7adfc GIT binary patch literal 167768 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 Date: Sun, 18 Oct 2026 14:49:28 +0100 Subject: [PATCH 13/20] Remove Watchface Digital initialisation of variables no longer used (#2494) --- src/displayapp/screens/WatchFaceDigital.h | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/displayapp/screens/WatchFaceDigital.h b/src/displayapp/screens/WatchFaceDigital.h index e3a1ac649a..64894124a5 100644 --- a/src/displayapp/screens/WatchFaceDigital.h +++ b/src/displayapp/screens/WatchFaceDigital.h @@ -42,9 +42,6 @@ namespace Pinetime { void Refresh() override; private: - uint8_t displayedHour = -1; - uint8_t displayedMinute = -1; - Utility::DirtyValue> currentDateTime {}; Utility::DirtyValue stepCount {}; Utility::DirtyValue heartbeat {}; From add77f9e4a8d364edbe7d8b44f0fdece222f3f84 Mon Sep 17 00:00:00 2001 From: Davis Mosenkovs Date: Sat, 18 Jul 2026 21:40:11 +0300 Subject: [PATCH 14/20] Add Legend watch face An elegant and very practical digital watch face. Uses only fonts already in InfiniTime and no external resources. Has the standard functionality: * shows time, date (without year) and day of week * supports 24 and 12 hour time formats * BLE, new notification and alarm clock icons * battery percentage (green when charging) * steps and heart rate * temperature and weather icon Additionally: * visual indication (amber dot) for any notification(s) * visual indication (red or blue line) when notifications are muted * battery percentage turns red when 20% or less * stopwatch and timer icons (when active) * colorful sun, snow and thunderstorm weather icons * generally uses brighter colors for more urgent things --- src/CMakeLists.txt | 1 + src/displayapp/UserApps.h | 1 + src/displayapp/apps/Apps.h.in | 1 + src/displayapp/apps/CMakeLists.txt | 1 + src/displayapp/screens/WatchFaceLegend.cpp | 332 +++++++++++++++++++++ src/displayapp/screens/WatchFaceLegend.h | 143 +++++++++ 6 files changed, 479 insertions(+) create mode 100644 src/displayapp/screens/WatchFaceLegend.cpp create mode 100644 src/displayapp/screens/WatchFaceLegend.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8a575884b3..897db7efad 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -431,6 +431,7 @@ list(APPEND SOURCE_FILES displayapp/screens/WatchFaceCasioStyleG7710.cpp displayapp/screens/WatchFaceNumerals.cpp displayapp/screens/WatchFacePrideFlag.cpp + displayapp/screens/WatchFaceLegend.cpp ## diff --git a/src/displayapp/UserApps.h b/src/displayapp/UserApps.h index 731a4e0369..adc4fbb01e 100644 --- a/src/displayapp/UserApps.h +++ b/src/displayapp/UserApps.h @@ -16,6 +16,7 @@ #include "displayapp/screens/WatchFaceTerminal.h" #include "displayapp/screens/WatchFaceNumerals.h" #include "displayapp/screens/WatchFacePrideFlag.h" +#include "displayapp/screens/WatchFaceLegend.h" namespace Pinetime { namespace Applications { diff --git a/src/displayapp/apps/Apps.h.in b/src/displayapp/apps/Apps.h.in index b8f90b2753..bad2b909a9 100644 --- a/src/displayapp/apps/Apps.h.in +++ b/src/displayapp/apps/Apps.h.in @@ -57,6 +57,7 @@ namespace Pinetime { CasioStyleG7710, Numerals, PrideFlag, + Legend, }; template diff --git a/src/displayapp/apps/CMakeLists.txt b/src/displayapp/apps/CMakeLists.txt index c00203128b..4da2795f6e 100644 --- a/src/displayapp/apps/CMakeLists.txt +++ b/src/displayapp/apps/CMakeLists.txt @@ -30,6 +30,7 @@ else() set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::CasioStyleG7710") set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::Numerals") set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::PrideFlag") + set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::Legend") set(WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}" CACHE STRING "List of watch faces to build into the firmware") endif() diff --git a/src/displayapp/screens/WatchFaceLegend.cpp b/src/displayapp/screens/WatchFaceLegend.cpp new file mode 100644 index 0000000000..c43b395482 --- /dev/null +++ b/src/displayapp/screens/WatchFaceLegend.cpp @@ -0,0 +1,332 @@ +#include "displayapp/screens/WatchFaceLegend.h" + +#include +#include "displayapp/screens/BleIcon.h" +#include "displayapp/screens/NotificationIcon.h" +#include "displayapp/screens/Symbols.h" +#include "displayapp/screens/WeatherSymbols.h" +#include "components/battery/BatteryController.h" +#include "components/ble/BleController.h" +#include "components/ble/NotificationManager.h" +#include "components/heartrate/HeartRateController.h" +#include "components/motion/MotionController.h" +#include "components/settings/Settings.h" + +using namespace Pinetime::Applications::Screens; + +WatchFaceLegend::WatchFaceLegend(Controllers::DateTime& dateTimeController, + const Controllers::Battery& batteryController, + const Controllers::Ble& bleController, + const Controllers::AlarmController& alarmController, + Controllers::Timer& timerController, + Controllers::StopWatchController& stopWatchController, + Controllers::NotificationManager& notificationManager, + Controllers::Settings& settingsController, + Controllers::HeartRateController& heartRateController, + Controllers::MotionController& motionController, + Controllers::SimpleWeatherService& weatherService) + : dateTimeController {dateTimeController}, + batteryController {batteryController}, + bleController {bleController}, + alarmController {alarmController}, + timerController {timerController}, + stopWatchController {stopWatchController}, + notificationManager {notificationManager}, + settingsController {settingsController}, + heartRateController {heartRateController}, + motionController {motionController}, + weatherService {weatherService} { + + // ---- Top status strip ------------------------------------------------- + // Any notification indicator (top left). Hidden when notifications are empty. + notificationAnyDot = lv_obj_create(lv_scr_act(), nullptr); + lv_obj_set_size(notificationAnyDot, 7, 7); + lv_obj_set_style_local_bg_color(notificationAnyDot, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, colorAnyNotification); + lv_obj_set_style_local_radius(notificationAnyDot, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, LV_RADIUS_CIRCLE); + lv_obj_set_style_local_border_width(notificationAnyDot, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, 0); + lv_obj_align(notificationAnyDot, nullptr, LV_ALIGN_IN_TOP_LEFT, 18, 16); + lv_obj_set_hidden(notificationAnyDot, true); // hidden by default + + // Standard new notification indicator (used when GetNotificationStatus() is Off or Sleep). + notificationNewIcon = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(notificationNewIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorRed); + lv_label_set_text_static(notificationNewIcon, ""); + lv_obj_align(notificationNewIcon, notificationAnyDot, LV_ALIGN_OUT_RIGHT_MID, 10, 0); + lv_obj_set_auto_realign(notificationNewIcon, true); + + // Services icons. Empty string when nothing is active, can contain multiple icons. + servicesIcons = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(servicesIcons, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorSecondary); + lv_label_set_text_static(servicesIcons, ""); + lv_obj_align(servicesIcons, notificationNewIcon, LV_ALIGN_OUT_RIGHT_MID, 12, 1); + lv_obj_set_auto_realign(servicesIcons, true); + + // Numeric battery (top right). + batteryValue = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(batteryValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorSecondary); + lv_label_set_text_static(batteryValue, "--%"); + lv_obj_align(batteryValue, nullptr, LV_ALIGN_IN_TOP_RIGHT, -5, 10); + lv_obj_set_auto_realign(batteryValue, true); + + // BLE icon, left of the battery percentage. + bleIcon = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(bleIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorBlue); + lv_label_set_text_static(bleIcon, ""); + lv_obj_align(bleIcon, batteryValue, LV_ALIGN_OUT_LEFT_MID, -10, 0); + lv_obj_set_auto_realign(bleIcon, true); + + // Upper divider - displayed (red or blue) when GetNotificationStatus() is Off or Sleep + upperDivider = lv_line_create(lv_scr_act(), nullptr); + lv_line_set_points(upperDivider, dividerPoints, 2); + lv_obj_set_style_local_line_width(upperDivider, LV_LINE_PART_MAIN, LV_STATE_DEFAULT, 1); + lv_obj_align(upperDivider, nullptr, LV_ALIGN_IN_TOP_LEFT, 4, 45); + lv_obj_set_hidden(upperDivider, true); // hidden by default + + // ---- Center block ------------------------------------------------------ + labelDate = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(labelDate, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorSecondary); + lv_label_set_text_static(labelDate, ""); + lv_obj_align(labelDate, nullptr, LV_ALIGN_IN_TOP_LEFT, 4, 64); + + labelTime = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_font(labelTime, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_extrabold_compressed); + lv_obj_set_style_local_text_color(labelTime, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorForeground); + lv_label_set_text_static(labelTime, ""); + lv_obj_align(labelTime, nullptr, LV_ALIGN_IN_TOP_LEFT, 0, 94); + + labelAmPm = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(labelAmPm, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorForeground); + lv_label_set_text_static(labelAmPm, ""); + lv_obj_align(labelAmPm, nullptr, LV_ALIGN_IN_TOP_RIGHT, -4, 64); + lv_obj_set_auto_realign(labelAmPm, true); + + divider = lv_line_create(lv_scr_act(), nullptr); + lv_line_set_points(divider, dividerPoints, 2); + lv_obj_set_style_local_line_color(divider, LV_LINE_PART_MAIN, LV_STATE_DEFAULT, colorDivider); + lv_obj_set_style_local_line_width(divider, LV_LINE_PART_MAIN, LV_STATE_DEFAULT, 1); + lv_obj_align(divider, nullptr, LV_ALIGN_IN_TOP_LEFT, 4, 177); + + // ---- Bottom row -------------------------------------------------- + stepValue = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(stepValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorSecondary); + lv_label_set_text_static(stepValue, "0"); + lv_obj_align(stepValue, nullptr, LV_ALIGN_IN_TOP_LEFT, 4, 186); + + stepCaption = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(stepCaption, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorCaption); + lv_label_set_text_static(stepCaption, "steps"); + lv_obj_align(stepCaption, nullptr, LV_ALIGN_IN_TOP_LEFT, 4, 210); + + heartbeatValue = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(heartbeatValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorSecondary); + lv_label_set_text_static(heartbeatValue, "--"); + lv_obj_align(heartbeatValue, nullptr, LV_ALIGN_IN_TOP_LEFT, 100, 186); + + heartbeatCaption = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(heartbeatCaption, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorCaption); + lv_label_set_text_static(heartbeatCaption, "BPM"); + lv_obj_align(heartbeatCaption, nullptr, LV_ALIGN_IN_TOP_LEFT, 100, 210); + + weatherValue = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(weatherValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorSecondary); + lv_label_set_text_static(weatherValue, "--"); + lv_obj_align(weatherValue, nullptr, LV_ALIGN_IN_TOP_LEFT, 188, 186); + + weatherCaption = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(weatherCaption, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorCaption); + lv_label_set_text_static(weatherCaption, "\xC2\xB0"); + lv_obj_align(weatherCaption, nullptr, LV_ALIGN_IN_TOP_LEFT, 188, 210); + + weatherIcon = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(weatherIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, colorSecondary); + lv_obj_set_style_local_text_font(weatherIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &fontawesome_weathericons); + lv_label_set_text_static(weatherIcon, ""); + lv_obj_align(weatherIcon, weatherValue, LV_ALIGN_OUT_LEFT_BOTTOM, -5, 12); + lv_obj_set_auto_realign(weatherIcon, true); + + Refresh(); + taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); +} + +WatchFaceLegend::~WatchFaceLegend() { + lv_task_del(taskRefresh); + lv_obj_clean(lv_scr_act()); +} + +void WatchFaceLegend::Refresh() { + // ---- Notification indicators ---- + notificationState = notificationManager.AreNewNotificationsAvailable(); + notificationEmpty = notificationManager.IsEmpty(); + if (notificationState.IsUpdated() || notificationEmpty.IsUpdated()) { + bool state = notificationState.Get(); + bool empty = notificationEmpty.Get(); + lv_obj_set_hidden(notificationAnyDot, empty && !state); + lv_label_set_text_static(notificationNewIcon, NotificationIcon::GetIcon(state)); + lv_obj_align(servicesIcons, state ? notificationNewIcon : notificationAnyDot, LV_ALIGN_OUT_RIGHT_MID, empty && !state ? -18 : 12, 1); + } + + // ---- Services (Alarm, Timer, StopWatch) ---- + alarmEnabled = alarmController.IsEnabled(); + timerRunning = timerController.IsRunning(); + stopWatchRunning = stopWatchController.IsRunning(); + if (alarmEnabled.IsUpdated() || timerRunning.IsUpdated() || stopWatchRunning.IsUpdated()) { + lv_label_set_text_fmt(servicesIcons, + "%s%s%s%s%s", + stopWatchRunning.Get() ? Symbols::stopWatch : "", + stopWatchRunning.Get() ? " " : "", + alarmEnabled.Get() ? Symbols::bell : "", + alarmEnabled.Get() ? " " : "", + timerRunning.Get() ? Symbols::hourGlass : ""); + } + + // ---- BLE ---- + bleState = bleController.IsConnected(); + bleRadioEnabled = bleController.IsRadioEnabled(); + if (bleState.IsUpdated() || bleRadioEnabled.IsUpdated()) { + lv_label_set_text_static(bleIcon, BleIcon::GetIcon(bleState.Get())); + } + + // ---- Battery ---- + batteryPercentRemaining = batteryController.PercentRemaining(); + powerPresent = batteryController.IsPowerPresent(); + if (batteryPercentRemaining.IsUpdated() || powerPresent.IsUpdated()) { + uint8_t remaining = batteryPercentRemaining.Get(); + lv_label_set_text_fmt(batteryValue, "%d%%", remaining); + lv_obj_set_style_local_text_color(batteryValue, + LV_LABEL_PART_MAIN, + LV_STATE_DEFAULT, + powerPresent.Get() ? colorCharging + : remaining > 20 ? colorSecondary + : colorRed); + lv_obj_align(bleIcon, batteryValue, LV_ALIGN_OUT_LEFT_MID, -10, 0); + } + + // ---- Upper divider, based on notification config ---- + notificationConfig = settingsController.GetNotificationStatus(); + if (notificationConfig.IsUpdated()) { + switch (notificationConfig.Get()) { + case Controllers::Settings::Notification::Off: + lv_obj_set_style_local_line_color(upperDivider, LV_LINE_PART_MAIN, LV_STATE_DEFAULT, colorRed); + lv_obj_set_hidden(upperDivider, false); + break; + case Controllers::Settings::Notification::Sleep: + lv_obj_set_style_local_line_color(upperDivider, LV_LINE_PART_MAIN, LV_STATE_DEFAULT, colorBlue); + lv_obj_set_hidden(upperDivider, false); + break; + default: + lv_obj_set_hidden(upperDivider, true); + break; + } + } + + // ---- Time and date ---- + currentDateTime = std::chrono::time_point_cast(dateTimeController.CurrentDateTime()); + if (currentDateTime.IsUpdated()) { + uint8_t hour = dateTimeController.Hours(); + uint8_t minute = dateTimeController.Minutes(); + + if (settingsController.GetClockType() == Controllers::Settings::ClockType::H12) { + const char* ampm = "AM"; + if (hour == 0) { + hour = 12; + } else if (hour == 12) { + ampm = "PM"; + } else if (hour > 12) { + hour = hour - 12; + ampm = "PM"; + } + lv_label_set_text_static(labelAmPm, ampm); + } else { + lv_label_set_text_static(labelAmPm, ""); + } + lv_label_set_text_fmt(labelTime, "%02d:%02d", hour, minute); + + currentDate = std::chrono::time_point_cast(currentDateTime.Get()); + if (currentDate.IsUpdated()) { + lv_label_set_text_fmt(labelDate, + "%s %02d %s", + dateTimeController.DayOfWeekShortToString(), + dateTimeController.Day(), + dateTimeController.MonthShortToString()); + } + } + + // ---- Steps ---- + stepCount = motionController.NbSteps(); + if (stepCount.IsUpdated()) { + lv_label_set_text_fmt(stepValue, "%lu", stepCount.Get()); + } + + // ---- Heart rate ---- + heartbeat = heartRateController.HeartRate(); + heartbeatRunning = heartRateController.State() != Controllers::HeartRateController::States::Stopped; + if (heartbeat.IsUpdated() || heartbeatRunning.IsUpdated()) { + if (heartbeatRunning.Get()) { + lv_label_set_text_fmt(heartbeatValue, "%d", heartbeat.Get()); + } else { + lv_label_set_text_static(heartbeatValue, "--"); + } + } + + // ---- Weather ---- + currentWeather = weatherService.Current(); + if (currentWeather.IsUpdated()) { + if (settingsController.GetWeatherFormat() == Controllers::Settings::WeatherFormat::Imperial) { + lv_label_set_text_static(weatherCaption, + "\xC2\xB0" + "F"); + } else { + lv_label_set_text_static(weatherCaption, + "\xC2\xB0" + "C"); + } + + auto optCurrentWeather = currentWeather.Get(); + if (optCurrentWeather) { + int16_t temp; + if (settingsController.GetWeatherFormat() == Controllers::Settings::WeatherFormat::Imperial) { + temp = optCurrentWeather->temperature.Fahrenheit(); + } else { + temp = optCurrentWeather->temperature.Celsius(); + } + if (temp > -100) { + lv_label_set_text_fmt(weatherValue, "%d", temp); + } else { + lv_label_set_text_static(weatherValue, "--"); + } + + lv_color_t color; + switch (optCurrentWeather->iconId) { + case Controllers::SimpleWeatherService::Icons::Sun: + color = colorWeatherYellow; + break; + case Controllers::SimpleWeatherService::Icons::Snow: + case Controllers::SimpleWeatherService::Icons::Thunderstorm: + color = colorWeatherLightBlue; + break; + default: + color = colorWeatherDefault; + break; + } + lv_obj_set_style_local_text_color(weatherIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, color); + lv_label_set_text_static(weatherIcon, Symbols::GetSymbol(optCurrentWeather->iconId, weatherService.IsNight())); + + if (!weatherDisplayed) { // adjust the layout when necessary + lv_obj_align(weatherValue, nullptr, LV_ALIGN_IN_TOP_LEFT, 200, 186); + lv_obj_align(weatherCaption, nullptr, LV_ALIGN_IN_TOP_LEFT, 200, 210); + lv_obj_align(weatherIcon, weatherValue, LV_ALIGN_OUT_LEFT_BOTTOM, -5, 12); + weatherDisplayed = true; + } + } else { + lv_label_set_text_static(weatherValue, "--"); + lv_label_set_text_static(weatherIcon, ""); + + if (weatherDisplayed) { // adjust the layout when necessary + lv_obj_align(weatherValue, nullptr, LV_ALIGN_IN_TOP_LEFT, 188, 186); + lv_obj_align(weatherCaption, nullptr, LV_ALIGN_IN_TOP_LEFT, 188, 210); + lv_obj_align(weatherIcon, weatherValue, LV_ALIGN_OUT_LEFT_BOTTOM, -5, 12); + weatherDisplayed = false; + } + } + } +} diff --git a/src/displayapp/screens/WatchFaceLegend.h b/src/displayapp/screens/WatchFaceLegend.h new file mode 100644 index 0000000000..b4e64e5e63 --- /dev/null +++ b/src/displayapp/screens/WatchFaceLegend.h @@ -0,0 +1,143 @@ +#pragma once + +#include +#include +#include +#include +#include "displayapp/apps/Apps.h" +#include "displayapp/screens/Screen.h" +#include "displayapp/Controllers.h" +#include "components/datetime/DateTimeController.h" +#include "components/ble/SimpleWeatherService.h" +#include "utility/DirtyValue.h" + +namespace Pinetime { + namespace Controllers { + class Settings; + class Battery; + class Ble; + class NotificationManager; + class HeartRateController; + class MotionController; + } + + namespace Applications { + namespace Screens { + + class WatchFaceLegend : public Screen { + public: + WatchFaceLegend(Controllers::DateTime& dateTimeController, + const Controllers::Battery& batteryController, + const Controllers::Ble& bleController, + const Controllers::AlarmController& alarmController, + Controllers::Timer& timerController, + Controllers::StopWatchController& stopWatchController, + Controllers::NotificationManager& notificationManager, + Controllers::Settings& settingsController, + Controllers::HeartRateController& heartRateController, + Controllers::MotionController& motionController, + Controllers::SimpleWeatherService& weatherService); + ~WatchFaceLegend() override; + + void Refresh() override; + + private: + Utility::DirtyValue batteryPercentRemaining {}; + Utility::DirtyValue powerPresent {}; + Utility::DirtyValue bleState {}; + Utility::DirtyValue bleRadioEnabled {}; + Utility::DirtyValue alarmEnabled {}; + Utility::DirtyValue timerRunning {}; + Utility::DirtyValue stopWatchRunning {}; + Utility::DirtyValue notificationConfig {}; + Utility::DirtyValue notificationState {}; + Utility::DirtyValue notificationEmpty {}; + Utility::DirtyValue stepCount {}; + Utility::DirtyValue heartbeat {}; + Utility::DirtyValue heartbeatRunning {}; + Utility::DirtyValue> currentDateTime {}; + Utility::DirtyValue> currentWeather {}; + + Utility::DirtyValue> currentDate; + + // Tracks whether weather is displayed to adjust the layout only when necessary + bool weatherDisplayed = false; + + // Top status strip + lv_obj_t* notificationAnyDot; + lv_obj_t* notificationNewIcon; + lv_obj_t* servicesIcons; + lv_obj_t* bleIcon; + lv_obj_t* batteryValue; + lv_obj_t* upperDivider; + + // Center block + lv_obj_t* labelDate; + lv_obj_t* labelTime; + lv_obj_t* labelAmPm; + lv_obj_t* divider; + + // Bottom row + lv_obj_t* stepValue; + lv_obj_t* stepCaption; + lv_obj_t* heartbeatValue; + lv_obj_t* heartbeatCaption; + lv_obj_t* weatherValue; + lv_obj_t* weatherCaption; + lv_obj_t* weatherIcon; + + static constexpr lv_color_t colorForeground = LV_COLOR_MAKE(0xF5, 0xF4, 0xEF); + static constexpr lv_color_t colorSecondary = LV_COLOR_MAKE(0x8A, 0x8A, 0x85); + static constexpr lv_color_t colorCaption = LV_COLOR_MAKE(0x4A, 0x4A, 0x46); + static constexpr lv_color_t colorDivider = LV_COLOR_MAKE(0x2A, 0x2A, 0x2A); + static constexpr lv_color_t colorAnyNotification = LV_COLOR_MAKE(0xEF, 0x9D, 0x3A); + static constexpr lv_color_t colorRed = LV_COLOR_MAKE(0xE8, 0x43, 0x3C); + static constexpr lv_color_t colorBlue = LV_COLOR_MAKE(0x37, 0x8A, 0xDD); + static constexpr lv_color_t colorCharging = LV_COLOR_MAKE(0x0A, 0xAD, 0x50); + static constexpr lv_color_t colorWeatherDefault = LV_COLOR_MAKE(0xA6, 0xAC, 0xB0); + static constexpr lv_color_t colorWeatherYellow = LV_COLOR_MAKE(0xF4, 0xC5, 0x42); + static constexpr lv_color_t colorWeatherLightBlue = LV_COLOR_MAKE(0x82, 0xD3, 0xE8); + + static constexpr lv_point_t dividerPoints[2] {{0, 0}, {232, 0}}; + + Controllers::DateTime& dateTimeController; + const Controllers::Battery& batteryController; + const Controllers::Ble& bleController; + const Controllers::AlarmController& alarmController; + Controllers::Timer& timerController; + Controllers::StopWatchController& stopWatchController; + Controllers::NotificationManager& notificationManager; + Controllers::Settings& settingsController; + Controllers::HeartRateController& heartRateController; + Controllers::MotionController& motionController; + Controllers::SimpleWeatherService& weatherService; + + lv_task_t* taskRefresh; + }; + } + + template <> + struct WatchFaceTraits { + static constexpr WatchFace watchFace = WatchFace::Legend; + static constexpr const char* name = "Legend"; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::WatchFaceLegend(controllers.dateTimeController, + controllers.batteryController, + controllers.bleController, + controllers.alarmController, + controllers.timer, + controllers.stopWatchController, + controllers.notificationManager, + controllers.settingsController, + controllers.heartRateController, + controllers.motionController, + *controllers.weatherController); + } + + static bool IsAvailable(Pinetime::Controllers::FS& /*filesystem*/) { + return true; + } + }; + } +} From 55812d501a2db876902e9bfd6347dd7e43308873 Mon Sep 17 00:00:00 2001 From: JoeStrout Date: Fri, 23 Oct 2026 16:17:27 -0700 Subject: [PATCH 15/20] Fix outdated ITD link (#2455) --- doc/gettingStarted/updating-software.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/gettingStarted/updating-software.md b/doc/gettingStarted/updating-software.md index 951499758e..3a66a0a0c7 100644 --- a/doc/gettingStarted/updating-software.md +++ b/doc/gettingStarted/updating-software.md @@ -49,7 +49,7 @@ Since those resources are not part of the firmware, they need to be flashed and Resources are packaged into a single .zip file named `infinitime-resources-x.y.z.zip` (where `x`, `y` and `z` are the version numbers of InfiniTime). You can use the companion app of your choice to flash the resources. -**Note: at the time of writing this page, [Amazfish](https://github.com/piggz/harbour-amazfish) and [ITD](https://gitea.arsenm.dev/Arsen6331/itd) have already integrated this functionality. Other companion apps will hopefully implement it soon!* +**Note: at the time of writing this page, [Amazfish](https://github.com/piggz/harbour-amazfish) and [ITD](https://git.elara.ws/Elara6331/itd) have already integrated this functionality. Other companion apps will hopefully implement it soon!* ## Amazfish Use the `Download file` functionality of Amazfish. From 6a0cbc6d31df90776bd3e3818e75a4d48f02bdfd Mon Sep 17 00:00:00 2001 From: Nick Garnsworthy Date: Fri, 23 Oct 2026 18:35:46 -0500 Subject: [PATCH 16/20] More pride watch faces (#2438) --- doc/gettingStarted/Watchfaces.md | 4 ++ doc/gettingStarted/Watchfaces/Pride.png | Bin 0 -> 2413 bytes src/components/settings/Settings.h | 2 +- src/displayapp/screens/WatchFacePrideFlag.cpp | 37 +++++++++++++++++- 4 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 doc/gettingStarted/Watchfaces/Pride.png diff --git a/doc/gettingStarted/Watchfaces.md b/doc/gettingStarted/Watchfaces.md index 9edff0bb51..a1e820f9ba 100644 --- a/doc/gettingStarted/Watchfaces.md +++ b/doc/gettingStarted/Watchfaces.md @@ -29,3 +29,7 @@ InfiniTime has 6 apps on the `main` branch at the time of writing. ### Casio G7710 ![Casio G7710 face](/doc/gettingStarted/Watchfaces/CasioG7710.png) + +### Pride Flag +![Pride flag face](/doc/gettingStarted/Watchfaces/Pride.png) + - You can long-press on the display to change flags. (Gay, Lesbian, Bi, Trans, Non-binary, Aromantic, Asexual, AroAce, Pansexual) \ No newline at end of file diff --git a/doc/gettingStarted/Watchfaces/Pride.png b/doc/gettingStarted/Watchfaces/Pride.png new file mode 100644 index 0000000000000000000000000000000000000000..7e97295cea6c28e2eea095b92bb1d2b1b79069dc GIT binary patch literal 2413 zcmcImc~lbk76!KwCQCET9F-c=%F-;;v{6c}7`4Po9CDcqY*7Rk$}K=G*EDgQ!d90V z(cDmS0Ys74B9)mw8(h&;GIK%DR1`!Wo%jBD=bh>O*B`(8opbNG_uO;tz2EoC^znx2 zfQ`Wb06@p%=cA`pDEacVwNw_0n9KtJKnFaIy86aF6b4Q6uL>V2d0uNBMu zaakjNkZA^^Kjv0^d^(-*wxPPyVW%0;??XlDyVsN85)6B;(f6E_26D*pibkzdpgJQY zbl1sci&Mz~Bj5?X#b`CafT;y_eaJp;+1#xiIZc56>PFX&)Nym?!H)AsjG6W{>SqDZ zOH~vH>TTg~<4(1;pQp5Sj2a{VxoloUd!VeYq*JH7Bo|I4BVH`O%%IjgbNWnmk85hB z=mj4!0{#v3jZUuA35bANJp&`%<4M5t8s7$DUJfO1Bsg7L{PZ3kpLA-)qEdZ}Vj5)9-m}V{R@L%)|93n`R zcv-#;%P5+}jNG?$eKYMg9o0Vo+H%}FVPl!7&8U#NR3?vn zREsjJ%qz|B>S_BudQV#jz9EX3Xvgk!QJf94OKVsv>tuT)YYJBfLNY)StsZ@9Nv7lG z0*h^m4cNrh)d5hYrbeHShwDs}WGx&N;UQuq0kZtr7qk00FQD2HCLP16!(j$}fO8?yjb~YZxJSfD0ndbnUPlfgeodH_ ztzBHQce2FO=FS?Y8y*NqPB!*Q{x*|W*sfc>pmk)EYPl#PvAUEfCJ3TkZHLOH#>|(Z z-n4AqH}JM&%%yW*>_iAi(w$NsT4;zeONR&Wu$yj#&lTi5kn_G{-Uj+S%(V%~LAPHk zGWlTMe#g`1mDbC1{gy-JwXxVJjkeZOyJwc04 za<@EjQ<1-4Qe2gTt~Px!chlaFH9;ePfS8Z3tEoWp=vPs2#_sq)Yq_6)9NT8@WR*ZY zn65OrG${}=@603b9Qx>C6`6xc+vGLDs%sgCxlp}J>?_cS?nPQwd=lP-@XD)Mx4{Gk zHbf($Bgyxd5uT|BpTewpzq##3BZ87J<^Ery)?~z0V8Ih-{d58;tfA(vjH6uB55S@k zH^Y^7H&SyS7}0TYB}^yEm}9AEfe2Cbg>9+K^F5P{IeVNRZL-VO&puQE!EJz1O;*`Lti-{DSD^!l-cQxpK05yx_K+lTOg2jkg_Db)tq|JObq& zX;)EMRTE~!ABX!(RkGgM&-dTV@M>={niS;e{iV@tzWNGaqEyo20efcaJXIqW|HN1@ zJrjm>6X%WvSXNap@~7lX6yv>>n&573%}pwb(n$cBCZtubh!<{WdoG)&vfAwMgw-1N;MSS0MA?1z27m(!q@!)lO|n&6_f0_9 znPqJJ7XFZ&KQDdUrr1q|YrAVY?B6`xFp;FI(Z{WSVw(7|5!i?VJ@QdbR@yP~Uj0;y z*#)P+c69WgBh5=pY9^e|Y)`Amx|+l3?0!^9t4n6xDV+>oY! z^70@JTgGG6`)=s}ZuM01_tKTwA(XAOU?;&{Tz6~QzDE&ZYJhynL{%dW6ux$3bj;0l z#)=IqLq1`8WaLN<$3KK6C+G)!&ygymt(!EE8RMWIj~zP^e;uO<%z24IHQ_RL5!)(< zmEC&5K(E+ay|gooS@Y6OiU@<)V$E`R+~Qh(3fSZoGw*qG!5g|Dn+2z|H_9#@Zru_c z>(=j&L~Csgfq2z?Rygjc59-AP&S{{=oSR}%mL literal 0 HcmV?d00001 diff --git a/src/components/settings/Settings.h b/src/components/settings/Settings.h index 9133d3fea1..93b484336d 100644 --- a/src/components/settings/Settings.h +++ b/src/components/settings/Settings.h @@ -39,7 +39,7 @@ namespace Pinetime { }; enum class PTSGaugeStyle : uint8_t { Full, Half, Numeric }; enum class PTSWeather : uint8_t { On, Off }; - enum class PrideFlag : uint8_t { Gay, Trans, Bi, Lesbian }; + enum class PrideFlag : uint8_t { Gay, Trans, Bi, Lesbian, Nonbinary, Ace, Aro, AroAce, Pan }; enum class DfuAndFsMode : uint8_t { Disabled, Enabled, EnabledTillReboot }; struct PineTimeStyle { diff --git a/src/displayapp/screens/WatchFacePrideFlag.cpp b/src/displayapp/screens/WatchFacePrideFlag.cpp index e029c076f4..d035337444 100644 --- a/src/displayapp/screens/WatchFacePrideFlag.cpp +++ b/src/displayapp/screens/WatchFacePrideFlag.cpp @@ -3,6 +3,7 @@ #include "components/battery/BatteryController.h" #include "components/ble/BleController.h" #include "displayapp/screens/Symbols.h" +#include "lvgl/src/lv_misc/lv_color.h" using namespace Pinetime::Applications::Screens; @@ -15,7 +16,7 @@ namespace { Pinetime::Controllers::Settings::PrideFlag GetNext(Pinetime::Controllers::Settings::PrideFlag prideFlag) { const auto prideFlagAsInt = static_cast(prideFlag); Pinetime::Controllers::Settings::PrideFlag nextFlag; - if (prideFlagAsInt < 3) { + if (prideFlagAsInt < 8) { nextFlag = static_cast(prideFlagAsInt + 1); } else { nextFlag = static_cast(0); @@ -29,7 +30,7 @@ namespace { if (prideFlagAsInt > 0) { prevFlag = static_cast(prideFlagAsInt - 1); } else { - prevFlag = static_cast(3); + prevFlag = static_cast(8); } return prevFlag; } @@ -62,6 +63,8 @@ namespace { constexpr lv_color_t grayPurple = LV_COLOR_MAKE(0x9b, 0x4f, 0x96); constexpr lv_color_t darkBlue = LV_COLOR_MAKE(0x00, 0x38, 0xa8); constexpr lv_color_t orange = LV_COLOR_MAKE(0xef, 0x76, 0x27); + constexpr lv_color_t sunOrange = LV_COLOR_MAKE(0xe2, 0x8c, 0x00); + constexpr lv_color_t brightYellow = LV_COLOR_MAKE(0xec, 0xcd, 0x00); constexpr lv_color_t lightOrange = LV_COLOR_MAKE(0xff, 0x9b, 0x55); constexpr lv_color_t lightPurple = LV_COLOR_MAKE(0xd4, 0x61, 0xa6); constexpr lv_color_t darkPurple = LV_COLOR_MAKE(0xb5, 0x56, 0x90); @@ -71,14 +74,29 @@ namespace { constexpr lv_color_t lightGreen = LV_COLOR_MAKE(0x98, 0xe8, 0xc1); constexpr lv_color_t indigo = LV_COLOR_MAKE(0x50, 0x49, 0xcc); constexpr lv_color_t steelBlue = LV_COLOR_MAKE(0x3d, 0x1a, 0x78); + constexpr lv_color_t skyBlue = LV_COLOR_MAKE(0x4a, 0x91, 0xe8); constexpr std::array gayColours {darkGreen, cyan, lightGreen, LV_COLOR_WHITE, lightBlue, indigo, steelBlue}; constexpr std::array transColours {lightBlue, lightPink, LV_COLOR_WHITE, lightPink, lightBlue}; constexpr std::array biColours {hotPink, hotPink, grayPurple, darkBlue, darkBlue}; constexpr std::array lesbianColours {LV_COLOR_RED, orange, lightOrange, LV_COLOR_WHITE, lightPurple, darkPurple, magenta}; + constexpr std::array nonbinaryColours {LV_COLOR_YELLOW, LV_COLOR_WHITE, LV_COLOR_PURPLE, LV_COLOR_BLACK}; + constexpr std::array aceColours {LV_COLOR_BLACK, LV_COLOR_GRAY, LV_COLOR_WHITE, LV_COLOR_PURPLE}; + constexpr std::array aroColours {LV_COLOR_GREEN, lightGreen, LV_COLOR_WHITE, LV_COLOR_GRAY, LV_COLOR_BLACK}; + constexpr std::array aroaceColours {sunOrange, + brightYellow, + LV_COLOR_WHITE, + LV_COLOR_MAKE(0x62, 0xae, 0xdc), + LV_COLOR_MAKE(0x20, 0x38, 0x56)}; + constexpr std::array panColours {hotPink, LV_COLOR_YELLOW, skyBlue}; constexpr PrideFlagData gayFlagData(gayColours, LV_COLOR_BLACK, LV_COLOR_BLACK, LV_COLOR_WHITE); constexpr PrideFlagData transFlagData(transColours, LV_COLOR_WHITE, LV_COLOR_BLACK, LV_COLOR_WHITE); constexpr PrideFlagData biFlagData(biColours, LV_COLOR_BLACK, LV_COLOR_WHITE, LV_COLOR_BLACK); constexpr PrideFlagData lesbianFlagData(lesbianColours, LV_COLOR_WHITE, LV_COLOR_BLACK, LV_COLOR_WHITE); + constexpr PrideFlagData nonbinaryFlagData(nonbinaryColours, LV_COLOR_BLACK, LV_COLOR_BLACK, LV_COLOR_WHITE); + constexpr PrideFlagData aceFlagData(aceColours, LV_COLOR_WHITE, LV_COLOR_BLACK, LV_COLOR_BLACK); + constexpr PrideFlagData aroFlagData(aroColours, LV_COLOR_WHITE, LV_COLOR_BLACK, LV_COLOR_WHITE); + constexpr PrideFlagData aroaceFlagData(aroaceColours, LV_COLOR_WHITE, LV_COLOR_BLACK, LV_COLOR_WHITE); + constexpr PrideFlagData panFlagData(panColours, LV_COLOR_BLACK, LV_COLOR_BLACK, LV_COLOR_BLACK); } WatchFacePrideFlag::WatchFacePrideFlag(Controllers::DateTime& dateTimeController, @@ -333,5 +351,20 @@ void WatchFacePrideFlag::UpdateScreen(const Pinetime::Controllers::Settings::Pri case Pinetime::Controllers::Settings::PrideFlag::Lesbian: UseFlagData(lesbianFlagData); break; + case Pinetime::Controllers::Settings::PrideFlag::Nonbinary: + UseFlagData(nonbinaryFlagData); + break; + case Pinetime::Controllers::Settings::PrideFlag::Ace: + UseFlagData(aceFlagData); + break; + case Pinetime::Controllers::Settings::PrideFlag::Aro: + UseFlagData(aroFlagData); + break; + case Pinetime::Controllers::Settings::PrideFlag::AroAce: + UseFlagData(aroaceFlagData); + break; + case Pinetime::Controllers::Settings::PrideFlag::Pan: + UseFlagData(panFlagData); + break; } } From 9e69b86f0479723298416539e204ed71c0b01070 Mon Sep 17 00:00:00 2001 From: SteveAmor Date: Fri, 31 Jul 2026 16:06:47 +0100 Subject: [PATCH 17/20] Resets the appStack when the display is automatically moved back to the watch face --- src/displayapp/DisplayApp.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/displayapp/DisplayApp.cpp b/src/displayapp/DisplayApp.cpp index 11ed9a263b..ae6a668259 100644 --- a/src/displayapp/DisplayApp.cpp +++ b/src/displayapp/DisplayApp.cpp @@ -323,6 +323,8 @@ void DisplayApp::Refresh() { if (currentApp == Apps::Launcher || currentApp == Apps::Notifications || currentApp == Apps::NotificationsPreview || currentApp == Apps::QuickSettings || currentApp == Apps::Settings) { LoadScreen(Apps::Clock, DisplayApp::FullRefreshDirections::None); + appStackDirections.Reset(); + returnAppStack.Reset(); // Wait for the clock app to load before moving on. while (!lv_task_handler()) { }; From 2d1aa29c70e00a1a661eaa83c3829d8746e26cd1 Mon Sep 17 00:00:00 2001 From: Steveis Date: Sat, 24 Oct 2026 19:21:06 +0100 Subject: [PATCH 18/20] Show selected watchface page first (#1872) --- src/displayapp/screens/settings/SettingWatchFace.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/displayapp/screens/settings/SettingWatchFace.cpp b/src/displayapp/screens/settings/SettingWatchFace.cpp index 0d5168d26f..6cb90b3300 100644 --- a/src/displayapp/screens/settings/SettingWatchFace.cpp +++ b/src/displayapp/screens/settings/SettingWatchFace.cpp @@ -57,7 +57,10 @@ SettingWatchFace::SettingWatchFace(Pinetime::Applications::DisplayApp* app, : watchfaceItems {std::move(watchfaceItems)}, settingsController {settingsController}, filesystem {filesystem}, - screens {app, 0, CreateScreenList(), Screens::ScreenListModes::UpDown} { + screens {app, + static_cast(static_cast(settingsController.GetWatchFace()) / settingsPerScreen), + CreateScreenList(), + Screens::ScreenListModes::UpDown} { } SettingWatchFace::~SettingWatchFace() { From 91a0a622c77b8aed80f1351f8190a02dbc84aac3 Mon Sep 17 00:00:00 2001 From: Steveis Date: Sat, 24 Oct 2026 19:46:01 +0100 Subject: [PATCH 19/20] Implement DirtyValues on Tile, Quick Settings, Steps and Batt Info screens (#2471) --- src/displayapp/screens/BatteryInfo.cpp | 60 +++++++++---------- src/displayapp/screens/BatteryInfo.h | 6 +- src/displayapp/screens/Steps.cpp | 56 ++++++++--------- src/displayapp/screens/Steps.h | 8 ++- src/displayapp/screens/Tile.cpp | 19 +++--- src/displayapp/screens/Tile.h | 6 +- .../screens/settings/QuickSettings.cpp | 19 +++--- .../screens/settings/QuickSettings.h | 8 ++- 8 files changed, 89 insertions(+), 93 deletions(-) diff --git a/src/displayapp/screens/BatteryInfo.cpp b/src/displayapp/screens/BatteryInfo.cpp index 16845d53e7..8ce3090370 100644 --- a/src/displayapp/screens/BatteryInfo.cpp +++ b/src/displayapp/screens/BatteryInfo.cpp @@ -7,16 +7,12 @@ using namespace Pinetime::Applications::Screens; BatteryInfo::BatteryInfo(const Pinetime::Controllers::Battery& batteryController) : batteryController {batteryController} { - batteryPercent = batteryController.PercentRemaining(); - batteryVoltage = batteryController.Voltage(); - chargingArc = lv_arc_create(lv_scr_act(), nullptr); lv_arc_set_rotation(chargingArc, 270); lv_arc_set_bg_angles(chargingArc, 0, 360); lv_arc_set_adjustable(chargingArc, false); lv_obj_set_size(chargingArc, 180, 180); lv_obj_align(chargingArc, nullptr, LV_ALIGN_CENTER, 0, -30); - lv_arc_set_value(chargingArc, batteryPercent); lv_obj_set_style_local_bg_opa(chargingArc, LV_ARC_PART_BG, LV_STATE_DEFAULT, LV_OPA_0); lv_obj_set_style_local_line_color(chargingArc, LV_ARC_PART_BG, LV_STATE_DEFAULT, Colors::bgAlt); lv_obj_set_style_local_border_width(chargingArc, LV_ARC_PART_BG, LV_STATE_DEFAULT, 2); @@ -24,24 +20,24 @@ BatteryInfo::BatteryInfo(const Pinetime::Controllers::Battery& batteryController lv_obj_set_style_local_line_color(chargingArc, LV_ARC_PART_INDIC, LV_STATE_DEFAULT, LV_COLOR_LIME); status = lv_label_create(lv_scr_act(), nullptr); - lv_label_set_text_static(status, "Reading Battery status"); lv_label_set_align(status, LV_LABEL_ALIGN_CENTER); - lv_obj_align(status, nullptr, LV_ALIGN_IN_BOTTOM_MID, 0, -17); + lv_obj_align(status, nullptr, LV_ALIGN_IN_BOTTOM_MID, 0, -33); + lv_obj_set_auto_realign(status, true); percent = lv_label_create(lv_scr_act(), nullptr); lv_obj_set_style_local_text_font(percent, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_42); - lv_label_set_text_fmt(percent, "%i%%", batteryPercent); lv_label_set_align(percent, LV_LABEL_ALIGN_LEFT); lv_obj_align(percent, chargingArc, LV_ALIGN_CENTER, 0, 0); + lv_obj_set_auto_realign(percent, true); voltage = lv_label_create(lv_scr_act(), nullptr); lv_obj_set_style_local_text_color(voltage, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::orange); - lv_label_set_text_fmt(voltage, "%1i.%02i volts", batteryVoltage / 1000, batteryVoltage % 1000 / 10); lv_label_set_align(voltage, LV_LABEL_ALIGN_CENTER); lv_obj_align(voltage, nullptr, LV_ALIGN_IN_BOTTOM_MID, 0, -7); + lv_obj_set_auto_realign(voltage, true); - taskRefresh = lv_task_create(RefreshTaskCallback, 5000, LV_TASK_PRIO_MID, this); Refresh(); + taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); } BatteryInfo::~BatteryInfo() { @@ -53,28 +49,32 @@ void BatteryInfo::Refresh() { batteryPercent = batteryController.PercentRemaining(); batteryVoltage = batteryController.Voltage(); + batteryCharging = batteryController.IsCharging(); - if (batteryController.IsCharging()) { - lv_obj_set_style_local_line_color(chargingArc, LV_ARC_PART_INDIC, LV_STATE_DEFAULT, LV_COLOR_LIME); - lv_label_set_text_static(status, "Charging"); - } else if (batteryPercent == 100) { - lv_obj_set_style_local_line_color(chargingArc, LV_ARC_PART_INDIC, LV_STATE_DEFAULT, LV_COLOR_BLUE); - lv_label_set_text_static(status, "Fully charged"); - } else if (batteryPercent > 15) { - lv_obj_set_style_local_line_color(chargingArc, LV_ARC_PART_INDIC, LV_STATE_DEFAULT, LV_COLOR_GREEN); - lv_label_set_text_static(status, "Discharging"); - } else if (batteryPercent > 5) { - lv_obj_set_style_local_line_color(chargingArc, LV_ARC_PART_INDIC, LV_STATE_DEFAULT, LV_COLOR_ORANGE); - lv_label_set_text_static(status, "Battery low"); - } else { - lv_obj_set_style_local_line_color(chargingArc, LV_ARC_PART_INDIC, LV_STATE_DEFAULT, Colors::deepOrange); - lv_label_set_text_static(status, "Battery critical"); + if (batteryPercent.IsUpdated() || batteryCharging.IsUpdated()) { + uint8_t battPercent = batteryPercent.Get(); + if (batteryCharging.Get()) { + lv_obj_set_style_local_line_color(chargingArc, LV_ARC_PART_INDIC, LV_STATE_DEFAULT, LV_COLOR_LIME); + lv_label_set_text_static(status, "Charging"); + } else if (battPercent == 100) { + lv_obj_set_style_local_line_color(chargingArc, LV_ARC_PART_INDIC, LV_STATE_DEFAULT, LV_COLOR_BLUE); + lv_label_set_text_static(status, "Fully charged"); + } else if (battPercent > 15) { + lv_obj_set_style_local_line_color(chargingArc, LV_ARC_PART_INDIC, LV_STATE_DEFAULT, LV_COLOR_GREEN); + lv_label_set_text_static(status, "Discharging"); + } else if (battPercent > 5) { + lv_obj_set_style_local_line_color(chargingArc, LV_ARC_PART_INDIC, LV_STATE_DEFAULT, LV_COLOR_ORANGE); + lv_label_set_text_static(status, "Battery low"); + } else { + lv_obj_set_style_local_line_color(chargingArc, LV_ARC_PART_INDIC, LV_STATE_DEFAULT, Colors::deepOrange); + lv_label_set_text_static(status, "Battery critical"); + } + lv_label_set_text_fmt(percent, "%i%%", battPercent); + lv_arc_set_value(chargingArc, battPercent); } - lv_label_set_text_fmt(percent, "%i%%", batteryPercent); - lv_obj_align(percent, chargingArc, LV_ALIGN_CENTER, 0, 0); - - lv_obj_align(status, voltage, LV_ALIGN_IN_BOTTOM_MID, 0, -27); - lv_label_set_text_fmt(voltage, "%1i.%02i volts", batteryVoltage / 1000, batteryVoltage % 1000 / 10); - lv_arc_set_value(chargingArc, batteryPercent); + if (batteryVoltage.IsUpdated()) { + uint16_t battVolts = batteryVoltage.Get(); + lv_label_set_text_fmt(voltage, "%1i.%02i volts", battVolts / 1000, battVolts % 1000 / 10); + } } diff --git a/src/displayapp/screens/BatteryInfo.h b/src/displayapp/screens/BatteryInfo.h index 27bbaa00c0..a58cfc33b9 100644 --- a/src/displayapp/screens/BatteryInfo.h +++ b/src/displayapp/screens/BatteryInfo.h @@ -3,6 +3,7 @@ #include #include "displayapp/screens/Screen.h" #include +#include "utility/DirtyValue.h" namespace Pinetime { namespace Controllers { @@ -29,8 +30,9 @@ namespace Pinetime { lv_task_t* taskRefresh; - uint8_t batteryPercent = 0; - uint16_t batteryVoltage = 0; + Utility::DirtyValue batteryPercent {}; + Utility::DirtyValue batteryVoltage {}; + Utility::DirtyValue batteryCharging {}; }; } } diff --git a/src/displayapp/screens/Steps.cpp b/src/displayapp/screens/Steps.cpp index 2e73dab512..6d0e1cde39 100644 --- a/src/displayapp/screens/Steps.cpp +++ b/src/displayapp/screens/Steps.cpp @@ -7,10 +7,6 @@ using namespace Pinetime::Applications::Screens; using Days = Pinetime::Controllers::MotionController::Days; -namespace { - constexpr const char* yesterdayStr = "Yest: %5lu"; -} - static void lap_event_handler(lv_obj_t* obj, lv_event_t event) { auto* steps = static_cast(obj->user_data); steps->lapBtnEventHandler(event); @@ -31,16 +27,11 @@ Steps::Steps(Controllers::MotionController& motionController, Controllers::Setti lv_arc_set_range(stepsArc, 0, 500); lv_obj_align(stepsArc, nullptr, LV_ALIGN_CENTER, 0, 0); - stepsCount = motionController.NbSteps(); - currentTripSteps = stepsCount - motionController.GetTripSteps(); - - lv_arc_set_value(stepsArc, int16_t(500 * stepsCount / settingsController.GetStepsGoal())); - lSteps = lv_label_create(lv_scr_act(), nullptr); lv_obj_set_style_local_text_color(lSteps, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_LIME); lv_obj_set_style_local_text_font(lSteps, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_42); - lv_label_set_text_fmt(lSteps, "%lu", stepsCount); lv_obj_align(lSteps, nullptr, LV_ALIGN_CENTER, 0, -40); + lv_obj_set_auto_realign(lSteps, true); lv_obj_t* lstepsL = lv_label_create(lv_scr_act(), nullptr); lv_obj_set_style_local_text_color(lstepsL, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::lightGray); @@ -49,15 +40,15 @@ Steps::Steps(Controllers::MotionController& motionController, Controllers::Setti lStepsYesterday = lv_label_create(lv_scr_act(), nullptr); lv_obj_set_style_local_text_color(lStepsYesterday, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::lightGray); - lv_label_set_text_fmt(lStepsYesterday, yesterdayStr, motionController.NbSteps(Days::Yesterday)); lv_label_set_align(lStepsYesterday, LV_LABEL_ALIGN_CENTER); lv_obj_align(lStepsYesterday, lSteps, LV_ALIGN_OUT_BOTTOM_MID, 0, 20); + lv_obj_set_auto_realign(lStepsYesterday, true); - lv_obj_t* lstepsGoal = lv_label_create(lv_scr_act(), nullptr); - lv_obj_set_style_local_text_color(lstepsGoal, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_CYAN); - lv_label_set_text_fmt(lstepsGoal, "Goal: %5lu", settingsController.GetStepsGoal()); - lv_label_set_align(lstepsGoal, LV_LABEL_ALIGN_CENTER); - lv_obj_align(lstepsGoal, lSteps, LV_ALIGN_OUT_BOTTOM_MID, 0, 40); + lStepsGoal = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(lStepsGoal, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_CYAN); + lv_label_set_align(lStepsGoal, LV_LABEL_ALIGN_CENTER); + lv_obj_align(lStepsGoal, lSteps, LV_ALIGN_OUT_BOTTOM_MID, 0, 40); + lv_obj_set_auto_realign(lStepsGoal, true); resetBtn = lv_btn_create(lv_scr_act(), nullptr); resetBtn->user_data = this; @@ -69,14 +60,13 @@ Steps::Steps(Controllers::MotionController& motionController, Controllers::Setti resetButtonLabel = lv_label_create(resetBtn, nullptr); lv_label_set_text_static(resetButtonLabel, "Reset"); - currentTripSteps = motionController.GetTripSteps(); - tripLabel = lv_label_create(lv_scr_act(), nullptr); lv_obj_set_style_local_text_color(tripLabel, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_YELLOW); - lv_label_set_text_fmt(tripLabel, "Trip: %5li", currentTripSteps); - lv_obj_align(tripLabel, lstepsGoal, LV_ALIGN_IN_LEFT_MID, 0, 20); + lv_obj_align(tripLabel, lStepsGoal, LV_ALIGN_IN_LEFT_MID, 0, 20); + lv_obj_set_auto_realign(tripLabel, true); - taskRefresh = lv_task_create(RefreshTaskCallback, 100, LV_TASK_PRIO_MID, this); + Refresh(); + taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); } Steps::~Steps() { @@ -87,26 +77,28 @@ Steps::~Steps() { void Steps::Refresh() { stepsCount = motionController.NbSteps(); currentTripSteps = motionController.GetTripSteps(); + stepsGoal = settingsController.GetStepsGoal(); - lv_label_set_text_fmt(lSteps, "%lu", stepsCount); - lv_obj_align(lSteps, nullptr, LV_ALIGN_CENTER, 0, -40); - - lv_label_set_text_fmt(lStepsYesterday, yesterdayStr, motionController.NbSteps(Days::Yesterday)); - lv_obj_align(lSteps, nullptr, LV_ALIGN_CENTER, 0, -40); + if (stepsCount.IsUpdated() || stepsGoal.IsUpdated()) { + lv_label_set_text_fmt(lSteps, "%lu", stepsCount.Get()); + lv_label_set_text_fmt(lStepsGoal, "Goal: %5lu", stepsGoal.Get()); + lv_label_set_text_fmt(lStepsYesterday, "Yest: %5lu", motionController.NbSteps(Days::Yesterday)); + lv_arc_set_value(stepsArc, int16_t(500 * stepsCount.Get() / stepsGoal.Get())); + } - if (currentTripSteps < 100000) { - lv_label_set_text_fmt(tripLabel, "Trip: %5li", currentTripSteps); - } else { - lv_label_set_text_fmt(tripLabel, "Trip: 99999+"); + if (currentTripSteps.IsUpdated()) { + if (currentTripSteps.Get() < 100000) { + lv_label_set_text_fmt(tripLabel, "Trip: %5li", currentTripSteps.Get()); + } else { + lv_label_set_text_fmt(tripLabel, "Trip: 99999+"); + } } - lv_arc_set_value(stepsArc, int16_t(500 * stepsCount / settingsController.GetStepsGoal())); } void Steps::lapBtnEventHandler(lv_event_t event) { if (event != LV_EVENT_CLICKED) { return; } - stepsCount = motionController.NbSteps(); motionController.ResetTrip(); Refresh(); } diff --git a/src/displayapp/screens/Steps.h b/src/displayapp/screens/Steps.h index 4824be6aad..5e8648452d 100644 --- a/src/displayapp/screens/Steps.h +++ b/src/displayapp/screens/Steps.h @@ -7,6 +7,7 @@ #include "displayapp/apps/Apps.h" #include "displayapp/Controllers.h" #include "Symbols.h" +#include "utility/DirtyValue.h" namespace Pinetime { @@ -29,17 +30,18 @@ namespace Pinetime { Controllers::MotionController& motionController; Controllers::Settings& settingsController; - uint32_t currentTripSteps = 0; + Utility::DirtyValue currentTripSteps {}; + Utility::DirtyValue stepsCount {}; + Utility::DirtyValue stepsGoal {}; lv_obj_t* lSteps; lv_obj_t* lStepsYesterday; + lv_obj_t* lStepsGoal; lv_obj_t* stepsArc; lv_obj_t* resetBtn; lv_obj_t* resetButtonLabel; lv_obj_t* tripLabel; - uint32_t stepsCount; - lv_task_t* taskRefresh; }; } diff --git a/src/displayapp/screens/Tile.cpp b/src/displayapp/screens/Tile.cpp index 45f715b55a..8d9890c501 100644 --- a/src/displayapp/screens/Tile.cpp +++ b/src/displayapp/screens/Tile.cpp @@ -6,11 +6,6 @@ using namespace Pinetime::Applications::Screens; namespace { - void lv_update_task(struct _lv_task_t* task) { - auto* user_data = static_cast(task->user_data); - user_data->UpdateScreen(); - } - void event_handler(lv_obj_t* obj, lv_event_t event) { if (event != LV_EVENT_VALUE_CHANGED) { return; @@ -86,19 +81,21 @@ Tile::Tile(uint8_t screenID, btnm1->user_data = this; lv_obj_set_event_cb(btnm1, event_handler); - taskUpdate = lv_task_create(lv_update_task, 5000, LV_TASK_PRIO_MID, this); - - UpdateScreen(); + Refresh(); + taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); } Tile::~Tile() { - lv_task_del(taskUpdate); + lv_task_del(taskRefresh); lv_obj_clean(lv_scr_act()); } -void Tile::UpdateScreen() { - lv_label_set_text(label_time, dateTimeController.FormattedTime().c_str()); +void Tile::Refresh() { statusIcons.Update(); + currentDateTime = std::chrono::time_point_cast(dateTimeController.CurrentDateTime()); + if (currentDateTime.IsUpdated()) { + lv_label_set_text(label_time, dateTimeController.FormattedTime().c_str()); + } } void Tile::OnValueChangedEvent(lv_obj_t* obj, uint32_t buttonId) { diff --git a/src/displayapp/screens/Tile.h b/src/displayapp/screens/Tile.h index c16151d0e1..ed0a4036b6 100644 --- a/src/displayapp/screens/Tile.h +++ b/src/displayapp/screens/Tile.h @@ -10,6 +10,7 @@ #include "components/battery/BatteryController.h" #include "displayapp/widgets/PageIndicator.h" #include "displayapp/widgets/StatusIcons.h" +#include "utility/DirtyValue.h" namespace Pinetime { namespace Applications { @@ -34,14 +35,15 @@ namespace Pinetime { ~Tile() override; - void UpdateScreen(); + void Refresh() override; void OnValueChangedEvent(lv_obj_t* obj, uint32_t buttonId); private: DisplayApp* app; + Utility::DirtyValue> currentDateTime {}; Controllers::DateTime& dateTimeController; - lv_task_t* taskUpdate; + lv_task_t* taskRefresh; lv_obj_t* label_time; lv_obj_t* btnm1; diff --git a/src/displayapp/screens/settings/QuickSettings.cpp b/src/displayapp/screens/settings/QuickSettings.cpp index c5c3071aef..d473240fce 100644 --- a/src/displayapp/screens/settings/QuickSettings.cpp +++ b/src/displayapp/screens/settings/QuickSettings.cpp @@ -15,11 +15,6 @@ namespace { } } - void lv_update_task(struct _lv_task_t* task) { - auto* user_data = static_cast(task->user_data); - user_data->UpdateScreen(); - } - enum class ButtonState : lv_state_t { NotificationsOn = LV_STATE_CHECKED, NotificationsOff = LV_STATE_DEFAULT, @@ -119,21 +114,23 @@ QuickSettings::QuickSettings(Pinetime::Applications::DisplayApp* app, lv_obj_set_style_local_text_font(lbl_btn, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &lv_font_sys_48); lv_label_set_text_static(lbl_btn, Symbols::settings); - taskUpdate = lv_task_create(lv_update_task, 5000, LV_TASK_PRIO_MID, this); - - UpdateScreen(); + Refresh(); + taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); } QuickSettings::~QuickSettings() { lv_style_reset(&btn_style); - lv_task_del(taskUpdate); + lv_task_del(taskRefresh); lv_obj_clean(lv_scr_act()); settingsController.SaveSettings(); } -void QuickSettings::UpdateScreen() { - lv_label_set_text(label_time, dateTimeController.FormattedTime().c_str()); +void QuickSettings::Refresh() { statusIcons.Update(); + currentDateTime = std::chrono::time_point_cast(dateTimeController.CurrentDateTime()); + if (currentDateTime.IsUpdated()) { + lv_label_set_text(label_time, dateTimeController.FormattedTime().c_str()); + } } void QuickSettings::OnButtonEvent(lv_obj_t* object) { diff --git a/src/displayapp/screens/settings/QuickSettings.h b/src/displayapp/screens/settings/QuickSettings.h index 87c126b7fa..ac514130dd 100644 --- a/src/displayapp/screens/settings/QuickSettings.h +++ b/src/displayapp/screens/settings/QuickSettings.h @@ -9,6 +9,7 @@ #include "components/settings/Settings.h" #include "components/battery/BatteryController.h" #include "displayapp/widgets/StatusIcons.h" +#include "utility/DirtyValue.h" namespace Pinetime { @@ -30,16 +31,19 @@ namespace Pinetime { void OnButtonEvent(lv_obj_t* object); - void UpdateScreen(); + void Refresh() override; private: DisplayApp* app; + + Utility::DirtyValue> currentDateTime {}; + Controllers::DateTime& dateTimeController; Controllers::BrightnessController& brightness; Controllers::MotorController& motorController; Controllers::Settings& settingsController; - lv_task_t* taskUpdate; + lv_task_t* taskRefresh; lv_obj_t* label_time; lv_style_t btn_style; From a1d39baa5d61c2bd221e9388ef7b25c5b10acaca Mon Sep 17 00:00:00 2001 From: JustScott Date: Sun, 14 Jan 2024 15:34:48 -0600 Subject: [PATCH 20/20] Add Timer's Time Remaining to the StatusIcons bar When a timer is active the time remaining will be displayed in the StatusIcons bar along with an hour glass symbol, and will be set to hidden when the timer's off. --- src/components/timer/Timer.cpp | 4 +- src/components/timer/Timer.h | 4 +- src/displayapp/DisplayApp.cpp | 4 +- src/displayapp/screens/ApplicationList.cpp | 3 + src/displayapp/screens/ApplicationList.h | 2 + src/displayapp/screens/Tile.cpp | 3 +- src/displayapp/screens/Tile.h | 1 + src/displayapp/screens/WatchFaceDigital.cpp | 3 +- src/displayapp/screens/WatchFaceDigital.h | 3 + .../screens/settings/QuickSettings.cpp | 5 +- .../screens/settings/QuickSettings.h | 3 +- src/displayapp/widgets/StatusIcons.cpp | 84 ++++++++++++++++++- src/displayapp/widgets/StatusIcons.h | 15 +++- 13 files changed, 119 insertions(+), 15 deletions(-) diff --git a/src/components/timer/Timer.cpp b/src/components/timer/Timer.cpp index 3ccffc7c04..1136321df3 100644 --- a/src/components/timer/Timer.cpp +++ b/src/components/timer/Timer.cpp @@ -15,7 +15,7 @@ void Timer::StartTimer(std::chrono::milliseconds duration) { // nullopt if timer stopped (StopTimer called / StartTimer not yet called) // otherwise TimerStatus with the ticks until/since expiry (depending on state of expired flag) -std::optional Timer::GetTimerState() { +std::optional Timer::GetTimerState() const { if (IsRunning()) { TickType_t remainingTime = expiry - xTaskGetTickCount(); return std::make_optional( @@ -38,7 +38,7 @@ void Timer::StopTimer() { triggered = false; } -bool Timer::IsRunning() { +bool Timer::IsRunning() const { return (xTimerIsTimerActive(timer) == pdTRUE); } diff --git a/src/components/timer/Timer.h b/src/components/timer/Timer.h index d43c7970e7..2391b06e48 100644 --- a/src/components/timer/Timer.h +++ b/src/components/timer/Timer.h @@ -21,9 +21,9 @@ namespace Pinetime { void StopTimer(); - std::optional GetTimerState(); + std::optional GetTimerState() const; - bool IsRunning(); + bool IsRunning() const; void ResetExpiredTime(); diff --git a/src/displayapp/DisplayApp.cpp b/src/displayapp/DisplayApp.cpp index ae6a668259..97c4279bea 100644 --- a/src/displayapp/DisplayApp.cpp +++ b/src/displayapp/DisplayApp.cpp @@ -535,6 +535,7 @@ void DisplayApp::LoadScreen(Apps app, DisplayApp::FullRefreshDirections directio batteryController, bleController, alarmController, + timer, dateTimeController, filesystem, std::move(apps)); @@ -589,7 +590,8 @@ void DisplayApp::LoadScreen(Apps app, DisplayApp::FullRefreshDirections directio motorController, settingsController, bleController, - alarmController); + alarmController, + timer); break; case Apps::Settings: currentScreen = std::make_unique(this, settingsController); diff --git a/src/displayapp/screens/ApplicationList.cpp b/src/displayapp/screens/ApplicationList.cpp index fb46b41384..90ea40ee9b 100644 --- a/src/displayapp/screens/ApplicationList.cpp +++ b/src/displayapp/screens/ApplicationList.cpp @@ -22,6 +22,7 @@ ApplicationList::ApplicationList(DisplayApp* app, const Pinetime::Controllers::Battery& batteryController, const Pinetime::Controllers::Ble& bleController, const Pinetime::Controllers::AlarmController& alarmController, + const Controllers::Timer& timer, Controllers::DateTime& dateTimeController, Pinetime::Controllers::FS& filesystem, std::array&& apps) @@ -30,6 +31,7 @@ ApplicationList::ApplicationList(DisplayApp* app, batteryController {batteryController}, bleController {bleController}, alarmController {alarmController}, + timer {timer}, dateTimeController {dateTimeController}, filesystem {filesystem}, apps {std::move(apps)}, @@ -62,6 +64,7 @@ std::unique_ptr ApplicationList::CreateScreen(unsigned int screenNum) co batteryController, bleController, alarmController, + timer, dateTimeController, pageApps); } diff --git a/src/displayapp/screens/ApplicationList.h b/src/displayapp/screens/ApplicationList.h index 4a57d7c034..457c4bb1ed 100644 --- a/src/displayapp/screens/ApplicationList.h +++ b/src/displayapp/screens/ApplicationList.h @@ -19,6 +19,7 @@ namespace Pinetime { const Pinetime::Controllers::Battery& batteryController, const Pinetime::Controllers::Ble& bleController, const Pinetime::Controllers::AlarmController& alarmController, + const Pinetime::Controllers::Timer& timer, Controllers::DateTime& dateTimeController, Pinetime::Controllers::FS& filesystem, std::array&& apps); @@ -34,6 +35,7 @@ namespace Pinetime { const Pinetime::Controllers::Battery& batteryController; const Pinetime::Controllers::Ble& bleController; const Pinetime::Controllers::AlarmController& alarmController; + const Pinetime::Controllers::Timer& timer; Controllers::DateTime& dateTimeController; Pinetime::Controllers::FS& filesystem; std::array apps; diff --git a/src/displayapp/screens/Tile.cpp b/src/displayapp/screens/Tile.cpp index 8d9890c501..31467b6a73 100644 --- a/src/displayapp/screens/Tile.cpp +++ b/src/displayapp/screens/Tile.cpp @@ -25,12 +25,13 @@ Tile::Tile(uint8_t screenID, const Controllers::Battery& batteryController, const Controllers::Ble& bleController, const Controllers::AlarmController& alarmController, + const Controllers::Timer& timer, Controllers::DateTime& dateTimeController, std::array& applications) : app {app}, dateTimeController {dateTimeController}, pageIndicator(screenID, numScreens), - statusIcons(batteryController, bleController, alarmController) { + statusIcons(batteryController, bleController, alarmController, timer, settingsController) { settingsController.SetAppMenu(screenID); diff --git a/src/displayapp/screens/Tile.h b/src/displayapp/screens/Tile.h index ed0a4036b6..e32447e3fb 100644 --- a/src/displayapp/screens/Tile.h +++ b/src/displayapp/screens/Tile.h @@ -30,6 +30,7 @@ namespace Pinetime { const Controllers::Battery& batteryController, const Controllers::Ble& bleController, const Controllers::AlarmController& alarmController, + const Controllers::Timer& timer, Controllers::DateTime& dateTimeController, std::array& applications); diff --git a/src/displayapp/screens/WatchFaceDigital.cpp b/src/displayapp/screens/WatchFaceDigital.cpp index a037abfe16..ca108d184e 100644 --- a/src/displayapp/screens/WatchFaceDigital.cpp +++ b/src/displayapp/screens/WatchFaceDigital.cpp @@ -20,6 +20,7 @@ WatchFaceDigital::WatchFaceDigital(Controllers::DateTime& dateTimeController, const Controllers::Battery& batteryController, const Controllers::Ble& bleController, const Controllers::AlarmController& alarmController, + const Controllers::Timer& timer, Controllers::NotificationManager& notificationManager, Controllers::Settings& settingsController, Controllers::HeartRateController& heartRateController, @@ -32,7 +33,7 @@ WatchFaceDigital::WatchFaceDigital(Controllers::DateTime& dateTimeController, heartRateController {heartRateController}, motionController {motionController}, weatherService {weatherService}, - statusIcons(batteryController, bleController, alarmController) { + statusIcons(batteryController, bleController, alarmController, timer, settingsController) { statusIcons.Create(); diff --git a/src/displayapp/screens/WatchFaceDigital.h b/src/displayapp/screens/WatchFaceDigital.h index 64894124a5..b73aab9527 100644 --- a/src/displayapp/screens/WatchFaceDigital.h +++ b/src/displayapp/screens/WatchFaceDigital.h @@ -21,6 +21,7 @@ namespace Pinetime { class NotificationManager; class HeartRateController; class MotionController; + class Timer; } namespace Applications { @@ -32,6 +33,7 @@ namespace Pinetime { const Controllers::Battery& batteryController, const Controllers::Ble& bleController, const Controllers::AlarmController& alarmController, + const Controllers::Timer& timer, Controllers::NotificationManager& notificationManager, Controllers::Settings& settingsController, Controllers::HeartRateController& heartRateController, @@ -84,6 +86,7 @@ namespace Pinetime { controllers.batteryController, controllers.bleController, controllers.alarmController, + controllers.timer, controllers.notificationManager, controllers.settingsController, controllers.heartRateController, diff --git a/src/displayapp/screens/settings/QuickSettings.cpp b/src/displayapp/screens/settings/QuickSettings.cpp index d473240fce..5b38f30830 100644 --- a/src/displayapp/screens/settings/QuickSettings.cpp +++ b/src/displayapp/screens/settings/QuickSettings.cpp @@ -29,13 +29,14 @@ QuickSettings::QuickSettings(Pinetime::Applications::DisplayApp* app, Controllers::MotorController& motorController, Pinetime::Controllers::Settings& settingsController, const Controllers::Ble& bleController, - const Controllers::AlarmController& alarmController) + const Controllers::AlarmController& alarmController, + const Controllers::Timer& timer) : app {app}, dateTimeController {dateTimeController}, brightness {brightness}, motorController {motorController}, settingsController {settingsController}, - statusIcons(batteryController, bleController, alarmController) { + statusIcons(batteryController, bleController, alarmController, timer, settingsController) { statusIcons.Create(); diff --git a/src/displayapp/screens/settings/QuickSettings.h b/src/displayapp/screens/settings/QuickSettings.h index ac514130dd..434d87c03c 100644 --- a/src/displayapp/screens/settings/QuickSettings.h +++ b/src/displayapp/screens/settings/QuickSettings.h @@ -25,7 +25,8 @@ namespace Pinetime { Controllers::MotorController& motorController, Pinetime::Controllers::Settings& settingsController, const Controllers::Ble& bleController, - const Controllers::AlarmController& alarmController); + const Controllers::AlarmController& alarmController, + const Controllers::Timer& timer); ~QuickSettings() override; diff --git a/src/displayapp/widgets/StatusIcons.cpp b/src/displayapp/widgets/StatusIcons.cpp index 777731a59f..ce6af588f0 100644 --- a/src/displayapp/widgets/StatusIcons.cpp +++ b/src/displayapp/widgets/StatusIcons.cpp @@ -6,8 +6,15 @@ using namespace Pinetime::Applications::Widgets; StatusIcons::StatusIcons(const Controllers::Battery& batteryController, const Controllers::Ble& bleController, - const Controllers::AlarmController& alarmController) - : batteryIcon(true), batteryController {batteryController}, bleController {bleController}, alarmController {alarmController} { + const Controllers::AlarmController& alarmController, + const Controllers::Timer& timer, + const Controllers::Settings& settingsController) + : batteryIcon(true), + batteryController {batteryController}, + bleController {bleController}, + alarmController {alarmController}, + timer {timer}, + settingsController {settingsController} { } void StatusIcons::Create() { @@ -17,12 +24,29 @@ void StatusIcons::Create() { lv_obj_set_style_local_pad_inner(container, LV_CONT_PART_MAIN, LV_STATE_DEFAULT, 5); lv_obj_set_style_local_bg_opa(container, LV_CONT_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_TRANSP); + timerContainer = lv_cont_create(container, nullptr); + lv_cont_set_layout(timerContainer, LV_LAYOUT_ROW_TOP); + lv_cont_set_fit(timerContainer, LV_FIT_TIGHT); + lv_obj_set_style_local_pad_inner(timerContainer, LV_CONT_PART_MAIN, LV_STATE_DEFAULT, 1); + lv_obj_set_style_local_bg_opa(timerContainer, LV_CONT_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_TRANSP); + + timerIcon = lv_label_create(timerContainer, nullptr); + lv_label_set_text_static(timerIcon, Screens::Symbols::hourGlass); + lv_obj_set_style_local_text_color(timerIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x999999)); + + timeRemaining = lv_label_create(timerContainer, nullptr); + lv_obj_set_style_local_text_color(timeRemaining, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x999999)); + lv_obj_set_style_local_text_letter_space(timeRemaining, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, -1); + bleIcon = lv_label_create(container, nullptr); lv_label_set_text_static(bleIcon, Screens::Symbols::bluetooth); batteryPlug = lv_label_create(container, nullptr); lv_label_set_text_static(batteryPlug, Screens::Symbols::plug); + soloTimerIcon = lv_label_create(container, nullptr); + lv_label_set_text_static(soloTimerIcon, Screens::Symbols::hourGlass); + alarmIcon = lv_label_create(container, nullptr); lv_label_set_text_static(alarmIcon, Screens::Symbols::bell); @@ -54,5 +78,61 @@ void StatusIcons::Update() { lv_obj_set_hidden(bleIcon, !bleState.Get()); } + if (timer.IsRunning()) { + uint8_t activeIconCounter = 0; + uint8_t maxIcons = 0; + lv_obj_t* child = lv_obj_get_child(container, NULL); + + while (child != NULL) { + if (!lv_obj_get_hidden(child)) { + activeIconCounter++; + } + child = lv_obj_get_child(container, child); + } + + // Subtract batteryIcon as it doesn't count (always active) + activeIconCounter--; + + // Subtract individually because switching pages initially sets all icons + // to visible. This causes soloTimerIcon to briefly appear before the + // timerContainer is set to visible. + if (!lv_obj_get_hidden(timerContainer)) { + activeIconCounter--; + } + if (!lv_obj_get_hidden(soloTimerIcon)) { + activeIconCounter--; + } + + std::chrono::seconds secondsRemaining = + std::chrono::duration_cast(timer.GetTimerState().value_or(Controllers::Timer::TimerStatus {}).distanceToExpiry); + uint8_t timerMinutes = (secondsRemaining.count() % 3600) / 60; + uint8_t timerSeconds = secondsRemaining.count() % 60; + if (timerMinutes > 0) { + maxIcons = 2; + lv_label_set_text_fmt(timeRemaining, "%02d:%02d", timerMinutes, timerSeconds); + } else { + maxIcons = 4; + lv_label_set_text_fmt(timeRemaining, "%02d", timerSeconds); + } + + if (settingsController.GetClockType() == Controllers::Settings::ClockType::H24) { + maxIcons++; + if (timerMinutes > 0) { + maxIcons++; + } + } + + if (activeIconCounter > maxIcons) { + lv_obj_set_hidden(timerContainer, true); + lv_obj_set_hidden(soloTimerIcon, false); + } else { + lv_obj_set_hidden(soloTimerIcon, true); + lv_obj_set_hidden(timerContainer, false); + } + } else { + lv_obj_set_hidden(soloTimerIcon, true); + lv_obj_set_hidden(timerContainer, true); + } + lv_obj_realign(container); } diff --git a/src/displayapp/widgets/StatusIcons.h b/src/displayapp/widgets/StatusIcons.h index 5524e996c5..343562b21d 100644 --- a/src/displayapp/widgets/StatusIcons.h +++ b/src/displayapp/widgets/StatusIcons.h @@ -6,6 +6,7 @@ #include "components/battery/BatteryController.h" #include "components/ble/BleController.h" #include "components/alarm/AlarmController.h" +#include "components/settings/Settings.h" #include "displayapp/screens/BatteryIcon.h" #include "utility/DirtyValue.h" @@ -16,7 +17,9 @@ namespace Pinetime { public: StatusIcons(const Controllers::Battery& batteryController, const Controllers::Ble& bleController, - const Controllers::AlarmController& alarmController); + const Controllers::AlarmController& alarmController, + const Controllers::Timer& timer, + const Controllers::Settings& settingsController); void Align(); void Create(); @@ -31,6 +34,8 @@ namespace Pinetime { const Pinetime::Controllers::Battery& batteryController; const Controllers::Ble& bleController; const Controllers::AlarmController& alarmController; + const Controllers::Timer& timer; + const Controllers::Settings& settingsController; Utility::DirtyValue batteryPercentRemaining {}; Utility::DirtyValue powerPresent {}; @@ -38,10 +43,14 @@ namespace Pinetime { Utility::DirtyValue bleRadioEnabled {}; Utility::DirtyValue alarmEnabled {}; + lv_obj_t* container; + lv_obj_t* timerContainer; + lv_obj_t* timerIcon; + lv_obj_t* timeRemaining; lv_obj_t* bleIcon; - lv_obj_t* alarmIcon; lv_obj_t* batteryPlug; - lv_obj_t* container; + lv_obj_t* soloTimerIcon; + lv_obj_t* alarmIcon; }; } }