diff --git a/.devcontainer.json b/.devcontainer.json new file mode 100644 index 00000000..95a27dac --- /dev/null +++ b/.devcontainer.json @@ -0,0 +1,32 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.154.2/containers/cpp +{ + "build": { + "dockerfile": "docker/Dockerfile" + }, + "customizations": { + "vscode": { + "settings": { + // Set *default* container specific settings.json values on container create. + "terminal.integrated.profiles.linux": { + "bash": { + "path": "/bin/bash" + } + }, + "terminal.integrated.defaultProfile.linux": "bash", + "editor.formatOnSave": true, + // FIXME: This and the Dockerfile might get out of sync + "clang-format.executable": "clang-format-14" + }, + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "ms-vscode.cpptools", + "ms-vscode.cmake-tools", + "marus25.cortex-debug", + "notskm.clang-tidy", + "mjohns.clang-format" + ] + } + }, + "remoteUser": "infinitime" +} \ No newline at end of file diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index e4ad5c4f..00000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,66 +0,0 @@ -FROM ubuntu:latest - -ARG DEBIAN_FRONTEND=noninteractive -RUN apt-get update -qq \ - && apt-get install -y \ -# x86_64 / generic packages - bash \ - build-essential \ - cmake \ - git \ - make \ - python3 \ - python3-pip \ - python3-pil \ - tar \ - unzip \ - wget \ - curl \ - dos2unix \ - clang-format-12 \ - clang-tidy \ - locales \ - libncurses5 \ -# aarch64 packages - libffi-dev \ - libssl-dev \ - python3-dev \ - rustc \ - && rm -rf /var/cache/apt/* /var/lib/apt/lists/*; - -#SET LOCALE -RUN locale-gen en_US.UTF-8 -ENV LANG en_US.UTF-8 -ENV LANGUAGE en_US:en -ENV LC_ALL en_US.UTF-8 - -RUN pip3 install adafruit-nrfutil -# required for McuBoot -RUN pip3 install setuptools_rust - -WORKDIR /opt/ -# build.sh knows how to compile but it problimatic on Win10 -COPY build.sh . -RUN chmod +x build.sh -# create_build_openocd.sh uses cmake to crate to build directory -COPY create_build_openocd.sh . -RUN chmod +x create_build_openocd.sh -# Lets get each in a separate docker layer for better downloads -# GCC -# RUN bash -c "source /opt/build.sh; GetGcc;" -RUN wget https://developer.arm.com/-/media/Files/downloads/gnu-rm/9-2020q2/gcc-arm-none-eabi-9-2020-q2-update-x86_64-linux.tar.bz2 -O - | tar -xj -C /opt -# NrfSdk -# RUN bash -c "source /opt/build.sh; GetNrfSdk;" -RUN wget -q "https://developer.nordicsemi.com/nRF5_SDK/nRF5_SDK_v15.x.x/nRF5_SDK_15.3.0_59ac345.zip" -O /tmp/nRF5_SDK_15.3.0_59ac345 -RUN unzip -q /tmp/nRF5_SDK_15.3.0_59ac345 -d /opt -RUN rm /tmp/nRF5_SDK_15.3.0_59ac345 -# McuBoot -# RUN bash -c "source /opt/build.sh; GetMcuBoot;" -RUN git clone https://github.com/mcu-tools/mcuboot.git -RUN pip3 install -r ./mcuboot/scripts/requirements.txt - -RUN adduser infinitime - -ENV NRF5_SDK_PATH /opt/nRF5_SDK_15.3.0_59ac345 -ENV ARM_NONE_EABI_TOOLCHAIN_PATH /opt/gcc-arm-none-eabi-9-2020-q2-update -ENV SOURCES_DIR /workspaces/InfiniTime diff --git a/.devcontainer/build.sh b/.devcontainer/build.sh deleted file mode 100644 index b4f080dd..00000000 --- a/.devcontainer/build.sh +++ /dev/null @@ -1,87 +0,0 @@ -#!/bin/bash -(return 0 2>/dev/null) && SOURCED="true" || SOURCED="false" -export LC_ALL=C.UTF-8 -export LANG=C.UTF-8 -set -x -set -e - -# Default locations if the var isn't already set -export TOOLS_DIR="${TOOLS_DIR:=/opt}" -export SOURCES_DIR="${SOURCES_DIR:=/sources}" -export BUILD_DIR="${BUILD_DIR:=$SOURCES_DIR/build}" -export OUTPUT_DIR="${OUTPUT_DIR:=$BUILD_DIR/output}" - -export BUILD_TYPE=${BUILD_TYPE:=Release} -export GCC_ARM_VER=${GCC_ARM_VER:="gcc-arm-none-eabi-9-2020-q2-update"} -export NRF_SDK_VER=${NRF_SDK_VER:="nRF5_SDK_15.3.0_59ac345"} - -MACHINE="$(uname -m)" -[[ "$MACHINE" == "arm64" ]] && MACHINE="aarch64" - -main() { - local target="$1" - - mkdir -p "$TOOLS_DIR" - - [[ ! -d "$TOOLS_DIR/$GCC_ARM_VER" ]] && GetGcc - [[ ! -d "$TOOLS_DIR/$NRF_SDK_VER" ]] && GetNrfSdk - [[ ! -d "$TOOLS_DIR/mcuboot" ]] && GetMcuBoot - - mkdir -p "$BUILD_DIR" - - CmakeGenerate - CmakeBuild $target - BUILD_RESULT=$? - if [ "$DISABLE_POSTBUILD" != "true" -a "$BUILD_RESULT" == 0 ]; then - source "$BUILD_DIR/post_build.sh" - fi - # assuming post_build.sh will never fail on a successful build - return $BUILD_RESULT -} - -GetGcc() { - GCC_SRC="$GCC_ARM_VER-$MACHINE-linux.tar.bz" - wget -q https://developer.arm.com/-/media/Files/downloads/gnu-rm/9-2020q2/$GCC_SRC -O - | tar -xj -C $TOOLS_DIR/ -} - -GetMcuBoot() { - git clone https://github.com/mcu-tools/mcuboot.git "$TOOLS_DIR/mcuboot" - pip3 install -r "$TOOLS_DIR/mcuboot/scripts/requirements.txt" -} - -GetNrfSdk() { - wget -q "https://developer.nordicsemi.com/nRF5_SDK/nRF5_SDK_v15.x.x/$NRF_SDK_VER.zip" -O /tmp/$NRF_SDK_VER - unzip -q /tmp/$NRF_SDK_VER -d "$TOOLS_DIR/" - rm /tmp/$NRF_SDK_VER -} - -CmakeGenerate() { - # We can swap the CD and trailing SOURCES_DIR for -B and -S respectively - # once we go to newer CMake (Ubuntu 18.10 gives us CMake 3.10) - cd "$BUILD_DIR" - - cmake -G "Unix Makefiles" \ - -DCMAKE_BUILD_TYPE=$BUILD_TYPE \ - -DARM_NONE_EABI_TOOLCHAIN_PATH="$TOOLS_DIR/$GCC_ARM_VER" \ - -DNRF5_SDK_PATH="$TOOLS_DIR/$NRF_SDK_VER" \ - "$SOURCES_DIR" - cmake -L -N . -} - -CmakeBuild() { - local target="$1" - [[ -n "$target" ]] && target="--target $target" - if cmake --build "$BUILD_DIR" --config $BUILD_TYPE $target -- -j$(nproc) - then return 0; else return 1; - fi -} - -if [[ $SOURCED == "false" ]]; then - # It is important to return exit code of main - # To be future-proof, this is handled explicitely - main "$@" - BUILD_RESULT=$? - exit $BUILD_RESULT -else - echo "Sourced!" -fi diff --git a/.devcontainer/build_app.sh b/.devcontainer/build_app.sh deleted file mode 100644 index 0f578cc6..00000000 --- a/.devcontainer/build_app.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -cmake --build /workspaces/Pinetime/build --config Release -- -j6 pinetime-app \ No newline at end of file diff --git a/.devcontainer/create_build_openocd.sh b/.devcontainer/create_build_openocd.sh deleted file mode 100644 index c5bff5c8..00000000 --- a/.devcontainer/create_build_openocd.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -rm -rf build/ -cmake -G 'Unix Makefiles' -DCMAKE_BUILD_TYPE=Release -DUSE_OPENOCD=1 -DARM_NONE_EABI_TOOLCHAIN_PATH=/opt/gcc-arm-none-eabi-9-2020-q2-update -DNRF5_SDK_PATH=/opt/nRF5_SDK_15.3.0_59ac345 -S . -Bbuild \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 1bb315f7..00000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,38 +0,0 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: -// https://github.com/microsoft/vscode-dev-containers/tree/v0.154.2/containers/cpp -{ - // "name": "Pinetime", - // "image": "feabhas/pinetime-dev" - "build": { - "dockerfile": "Dockerfile", - // Update 'VARIANT' to pick an Debian / Ubuntu OS version: debian-10, debian-9, ubuntu-20.04, ubuntu-18.04 - // "args": { "VARIANT": "ubuntu-20.04" } - }, - "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined"], - - // Set *default* container specific settings.json values on container create. - "settings": { - "terminal.integrated.shell.linux": "/bin/bash", - "editor.formatOnSave": true, - "clang-format.executable": "clang-format-12" - }, - - // Add the IDs of extensions you want installed when the container is created. - "extensions": [ - "ms-vscode.cpptools", - "ms-vscode.cmake-tools", - "marus25.cortex-debug", - "notskm.clang-tidy", - "mjohns.clang-format" - ], - - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], - - // Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "bash /opt/create_build_openocd.sh", - - // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. - // "remoteUser": "vscode" - "remoteUser": "infinitime" -} diff --git a/.devcontainer/make_build_dir.sh b/.devcontainer/make_build_dir.sh deleted file mode 100644 index 76240037..00000000 --- a/.devcontainer/make_build_dir.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -cmake -G 'Unix Makefiles' -DCMAKE_BUILD_TYPE=Release -DUSE_OPENOCD=1 -DARM_NONE_EABI_TOOLCHAIN_PATH=/opt/gcc-arm-none-eabi-9-2020-q2-update -DNRF5_SDK_PATH=/opt/nRF5_SDK_15.3.0_59ac345 ${SOURCES_DIR} diff --git a/.gitignore b/.gitignore index 81e49ae0..b61095df 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,5 @@ src/arm-none-eabi # clangd .cache/ + +nRF5_SDK/ diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index 392f4151..c5f88a82 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -1,4 +1,9 @@ { + "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" + }, "configurations": [ { "name": "nrfCC", @@ -14,7 +19,22 @@ "intelliSenseMode": "linux-gcc-arm", "configurationProvider": "ms-vscode.cpp-tools", "compileCommands": "${workspaceFolder}/build/compile_commands.json" + }, + { + "name": "nrfCC Devcontainer", + "includePath": [ + "${workspaceFolder}/**", + "${workspaceFolder}/src/**", + "${workspaceFolder}/src" + ], + "defines": [], + "compilerPath": "${TOOLS_DIR}/${GCC_ARM_PATH}/bin/arm-none-eabi-gcc", + "cStandard": "c99", + "cppStandard": "c++20", + "intelliSenseMode": "linux-gcc-arm", + "configurationProvider": "ms-vscode.cpp-tools", + "compileCommands": "${workspaceFolder}/build/compile_commands.json" } ], "version": 4 -} \ No newline at end of file +} diff --git a/.vscode/cmake-kits.json b/.vscode/cmake-kits.json new file mode 100644 index 00000000..95bb600b --- /dev/null +++ b/.vscode/cmake-kits.json @@ -0,0 +1,6 @@ +[ + { + "name": "InfiniTime Compiler", + "environmentSetupScript": "${workspaceFolder}/docker/build.sh" + } +] diff --git a/.vscode/launch.json b/.vscode/launch.json index a50270d2..7d3f17a1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,20 +1,18 @@ - { +{ "version": "0.1.0", "configurations": [ { "name": "Debug - Openocd docker Remote", - "type":"cortex-debug", - "cortex-debug.armToolchainPath":"${env:ARM_NONE_EABI_TOOLCHAIN_PATH}/bin", + "type": "cortex-debug", "cwd": "${workspaceRoot}", "executable": "${command:cmake.launchTargetPath}", "request": "launch", "servertype": "external", - // This may need to be arm-none-eabi-gdb depending on your system - "gdbPath" : "${env:ARM_NONE_EABI_TOOLCHAIN_PATH}/bin/arm-none-eabi-gdb", + "gdbPath": "${env:ARM_NONE_EABI_TOOLCHAIN_PATH}/bin/arm-none-eabi-gdb", // Connect to an already running OpenOCD instance "gdbTarget": "host.docker.internal:3333", "svdFile": "${workspaceRoot}/nrf52.svd", - "runToMain": true, + "runToEntryPoint": "main", // Work around for stopping at main on restart "postRestartCommands": [ "break main", @@ -23,18 +21,16 @@ }, { "name": "Debug - Openocd Local", - "type":"cortex-debug", - "cortex-debug.armToolchainPath":"${env:ARM_NONE_EABI_TOOLCHAIN_PATH}/bin", + "type": "cortex-debug", "cwd": "${workspaceRoot}", "executable": "${command:cmake.launchTargetPath}", "request": "launch", "servertype": "openocd", - // This may need to be arm-none-eabi-gdb depending on your system - "gdbPath" : "${env:ARM_NONE_EABI_TOOLCHAIN_PATH}/bin/arm-none-eabi-gdb", + "gdbPath": "${env:ARM_NONE_EABI_TOOLCHAIN_PATH}/bin/arm-none-eabi-gdb", // Connect to an already running OpenOCD instance "gdbTarget": "localhost:3333", "svdFile": "${workspaceRoot}/nrf52.svd", - "runToMain": true, + "runToEntryPoint": "main", // Work around for stopping at main on restart "postRestartCommands": [ "break main", @@ -51,6 +47,11 @@ "showDevDebugOutput": false, "servertype": "openocd", "runToMain": true, + // Work around for stopping at main on restart + "postRestartCommands": [ + "break main", + "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", "svdFile": "${workspaceRoot}/nrf52.svd", @@ -58,7 +59,25 @@ "interface/stlink.cfg", "target/nrf52.cfg" ], - } - + }, + { + "name": "Debug - Openocd Devcontainer", + "type": "cortex-debug", + "cwd": "${workspaceRoot}", + "executable": "${command:cmake.launchTargetPath}", + "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", + // Connect to an already running OpenOCD instance + "gdbTarget": "host.docker.internal:3333", + "svdFile": "${workspaceRoot}/nrf52.svd", + "runToEntryPoint": "main", + // Work around for stopping at main on restart + "postRestartCommands": [ + "break main", + "continue" + ] + }, ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index f1cc3a81..a7b04eea 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,20 @@ { "C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools", "cmake.configureArgs": [ - "-DARM_NONE_EABI_TOOLCHAIN_PATH=${env:ARM_NONE_EABI_TOOLCHAIN_PATH}", - "-DNRF5_SDK_PATH=${env:NRF5_SDK_PATH}", + "-DARM_NONE_EABI_TOOLCHAIN_PATH=${env:TOOLS_DIR}/${env:GCC_ARM_PATH}", + "-DNRF5_SDK_PATH=${env:TOOLS_DIR}/${env:NRF_SDK_VER}", ], + "cmake.statusbar.advanced": { + "launch": { + "visibility": "hidden" + }, + "launchTarget": { + "visibility": "hidden" + }, + "debug": { + "visibility": "hidden" + } + }, "cmake.generator": "Unix Makefiles", "clang-tidy.buildPath": "build/compile_commands.json", "files.associations": { diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 17f51f5e..06a08bfc 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,20 +1,6 @@ { "version": "2.0.0", "tasks": [ - { - "label": "create openocd build", - "type": "shell", - "command": "/opt/create_build_openocd.sh", - "group": { - "kind": "build", - "isDefault": true - }, - "presentation": { - "reveal": "always", - "panel": "shared" - }, - "problemMatcher": [] - }, { "label": "update submodules", "type": "shell", @@ -31,14 +17,6 @@ "panel": "shared" }, "problemMatcher": [] - }, - { - "label": "BuildInit", - "dependsOn": [ - "update submodules", - "create openocd build" - ], - "problemMatcher": [] } ] } \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 2e3a033d..9b5669b0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.10) set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose Debug or Release") -project(pinetime VERSION 1.13.0 LANGUAGES C CXX ASM) +project(pinetime VERSION 1.14.0 LANGUAGES C CXX ASM) set(CMAKE_C_STANDARD 99) set(CMAKE_CXX_STANDARD 20) diff --git a/README.md b/README.md index dc8dc2d7..97fe94b9 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![InfiniTime logo](doc/logo/infinitime-logo-small.jpg "InfiniTime Logo") -Fast open-source firmware for the [PineTime smartwatch](https://www.pine64.org/pinetime/) with many features, written in modern C++. +Fast open-source firmware for the [PineTime smartwatch](https://pine64.org/devices/pinetime/) with many features, written in modern C++. ## Wadokei fork diff --git a/bootloader/ota-dfu-python/README.md b/bootloader/ota-dfu-python/README.md index c38e597a..c18da8bb 100644 --- a/bootloader/ota-dfu-python/README.md +++ b/bootloader/ota-dfu-python/README.md @@ -7,9 +7,9 @@ My own contribution is little more than a brute force conversion to python3. It is sparsely tested so there are likely to be a few remaining bytes versus string bugs remaining in the places I didn't test . I used it primarily as part of -[wasp-os](https://github.com/daniel-thompson/wasp-os) as a way to +[wasp-os](https://github.com/wasp-os/wasp-os) as a way to deliver OTA updates to nRF52-based smart watches, especially the -[Pine64 PineTime](https://www.pine64.org/pinetime/). +[Pine64 PineTime](https://pine64.org/devices/pinetime/). ## What does it do? diff --git a/doc/MotionService.md b/doc/MotionService.md index 429794ac..58c7e836 100644 --- a/doc/MotionService.md +++ b/doc/MotionService.md @@ -21,3 +21,5 @@ The current raw motion values. This is a 3 `int16_t` array: - [0] : X - [1] : Y - [2] : Z + +The three motion values are in units of "binary milli-g", where 1g is represented by a value of 1024. diff --git a/doc/buildWithVScode.md b/doc/buildWithVScode.md index 9d0a5bdf..5f872482 100644 --- a/doc/buildWithVScode.md +++ b/doc/buildWithVScode.md @@ -32,7 +32,7 @@ The .devcontainer folder contains the configuration and scripts for using a Dock Using the [Remote-Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension is recommended. It will handle configuring the Docker virtual machine and setting everything up. -More documentation is available in the [readme in .devcontainer](../.devcontainer/README.md) +More documentation is available in the [readme in .devcontainer](usingDevcontainers.md) ### DevContainer on Ubuntu diff --git a/doc/code/Apps.md b/doc/code/Apps.md index 6ca84481..44c4ed65 100644 --- a/doc/code/Apps.md +++ b/doc/code/Apps.md @@ -35,18 +35,20 @@ that will call the method `Refresh()` periodically. ## App types -There are basically 2 types of applications : **system** apps and **user** apps. +There are basically 3 types of applications : **system** apps and **user** apps and **watch faces**. **System** applications are always built into InfiniTime, and InfiniTime cannot work properly without those apps. -The watchfaces, settings, notifications and the application launcher are examples of such system applications. +Settings, notifications and the application launcher are examples of such system applications. **User** applications are optionally built into the firmware. They extend the functionalities of the system. -The distinction between **system** and **user** applications allows for more flexibility and customization. -This allows to easily select which user applications must be built into the firmware +**Watch faces** are very similar to the **user** apps, they are optional, but at least one must be built into the firmware. + +The distinction between **system** apps, **user** apps and watch faces allows for more flexibility and customization. +This allows to easily select which user applications and watch faces must be built into the firmware without overflowing the system memory. -## Apps initialization +## Apps and watch faces initialization Apps are created by `DisplayApp` in `DisplayApp::LoadScreen()`. This method simply call the creates an instance of the class that corresponds to the app specified in parameters. @@ -55,6 +57,8 @@ The constructor of **system** apps is called directly. If the application is a * the corresponding `AppDescription` is first retrieved from `userApps` and then the function `create` is called to create an instance of the app. +Watch faces are handled in a very similar way as the **user** apps : they are created by `DisplayApp` in the method `DisplayApp::LoadScreen()` when the application type is `Apps::Clock`. + ## User application selection at build time The list of user applications is generated at build time by the `consteval` function `CreateAppDescriptions()` @@ -85,6 +89,32 @@ struct AppTraits { This array `userApps` is used by `DisplayApp` to create the applications and the `AppLauncher` to list all available applications. +## Watch face selection at build time + +The list of available watch faces is also generated at build time by the `consteval` +function `CreateWatchFaceDescriptions()` in `UserApps.h` in the same way as the **user** apps. +Watch faces must declare a `WatchFaceTraits` so that the corresponding `WatchFaceDescription` can be generated. +Here is an example of `WatchFaceTraits`: +```c++ + template <> + struct WatchFaceTraits { + static constexpr WatchFace watchFace = WatchFace::Analog; + static constexpr const char* name = "Analog face"; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::WatchFaceAnalog(controllers.dateTimeController, + controllers.batteryController, + controllers.bleController, + controllers.notificationManager, + controllers.settingsController); + }; + + static bool IsAvailable(Pinetime::Controllers::FS& /*filesystem*/) { + return true; + } + }; +``` + ## Creating your own app A minimal user app could look like this: @@ -110,10 +140,10 @@ namespace Pinetime { } template <> - struct AppTraits { + struct AppTraits { static constexpr Apps app = Apps::MyApp; - static constexpr const char* icon = Screens::Symbol::myApp; - static Screens::Screens* Create(AppController& controllers) { + static constexpr const char* icon = Screens::Symbols::myApp; + static Screens::Screen* Create(AppControllers& controllers) { return new Screens::MyApp(); } }; @@ -146,7 +176,7 @@ Now we have our very own app, but InfiniTime does not know about it yet. The first step is to include your `MyApp.cpp` (or any new cpp files for that matter) in the compilation by adding it to [CMakeLists.txt](/CMakeLists.txt). The next step to making it launch-able is to give your app an id. -To do this, add an entry in the enum class `Pinetime::Applications::Apps` ([displayapp/Apps.h](/src/displayapp/Apps.h)). +To do this, add an entry in the enum class `Pinetime::Applications::Apps` ([displayapp/apps/Apps.h](/src/displayapp/apps/Apps.h.in)). Name this entry after your app. Add `#include "displayapp/screens/MyApp.h"` to the file [displayapp/DisplayApp.cpp](/src/displayapp/DisplayApp.cpp). @@ -168,6 +198,15 @@ Ex : build the firmware with 3 user application : Alarm, Timer and MyApp (the ap $ cmake ... -DENABLE_USERAPPS="Apps::Alarm, Apps::Timer, Apps::MyApp" ... ``` +Similarly, the list of watch faces is also generated by CMake, so you need to add the variable `ENABLE_WATCHFACES` to the command line of CMake. +It must be set with the comma separated list of watch faces that will be built into the firmware. + +Ex: build the firmware with 3 watch faces : Analog, PineTimeStyle and Infineat: + +```cmake +$ cmake ... -DENABLE_WATCHFACES="WatchFace::Analog,WatchFace::PineTimeStyle,WatchFace::Infineat" ... +``` + You should now be able to [build](../buildAndProgram.md) the firmware and flash it to your PineTime. Yay! diff --git a/.devcontainer/README.md b/doc/usingDevcontainers.md similarity index 100% rename from .devcontainer/README.md rename to doc/usingDevcontainers.md diff --git a/docker/Dockerfile b/docker/Dockerfile index 22bf7bd7..bb5d5f65 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -37,6 +37,13 @@ RUN apt-get update -qq \ libpangocairo-1.0-0 \ && rm -rf /var/cache/apt/* /var/lib/apt/lists/*; +# Add the necessary apt-gets for the devcontainer +RUN apt-get update -qq \ + && apt-get install -y \ + clang-format-14 \ + clang-tidy \ + libncurses5 + # Git needed for PROJECT_GIT_COMMIT_HASH variable setting RUN pip3 install adafruit-nrfutil @@ -55,5 +62,8 @@ RUN bash -c "source /opt/build.sh; GetNrfSdk;" # McuBoot RUN bash -c "source /opt/build.sh; GetMcuBoot;" +# Add the infinitime user for connecting devcontainer +RUN adduser infinitime + ENV SOURCES_DIR /sources CMD ["/opt/build.sh"] diff --git a/hooks/pre-commit b/hooks/pre-commit index 5e10aa19..60a01e34 100755 --- a/hooks/pre-commit +++ b/hooks/pre-commit @@ -1,25 +1,33 @@ -#!/bin/bash -if clang-format --version | grep -q 'version 11\.'; then - CLANG_FORMAT_EXECUTABLE="clang-format" -else - CLANG_FORMAT_EXECUTABLE="clang-format-11" +#!/bin/sh + +name="clang-format" + +if [ -z "$(command -v "git-$name")" ]; then + name="$(basename -a $(find $(echo "$PATH" | tr ':' ' ') -maxdepth 1 -type f -executable -name 'git-clang-format*') | sort | tail -n 1 | sed 's/^git-//')" fi -if ! command -v $CLANG_FORMAT_EXECUTABLE &> /dev/null -then - echo $CLANG_FORMAT_EXECUTABLE does not exist, make sure to install it - exit 1 -fi +minVersion="14.0.0" -for FILE in $(git diff --cached --name-only) -do - if [[ "$FILE" =~ src/[A-Za-z0-9\ \-]+*\.(c|h|cpp|cc)$ ]]; then - echo Autoformatting $FILE with $CLANG_FORMAT_EXECUTABLE - $CLANG_FORMAT_EXECUTABLE -style=file -i -- $FILE - git add -- $FILE - elif [[ "$FILE" =~ src/(components|displayapp|drivers|heartratetask|logging|systemtask)/.*\.(c|h|cpp|cc)$ ]]; then - echo Autoformatting $FILE with $CLANG_FORMAT_EXECUTABLE - $CLANG_FORMAT_EXECUTABLE -style=file -i -- $FILE - git add -- $FILE - fi +for file in $(find $(echo "$PATH" | tr ':' ' ') -maxdepth 1 -type f -executable -name 'clang-format*'); do + curBin="$file" + curVersion="$("$curBin" --version | cut -d ' ' -f 3)" + + if [ "$(printf '%s\n' "$curVersion" "$version" "$minVersion" | sort -V | tail -n 1)" = "$curVersion" ]; then + bin="$curBin" + version="$curVersion" + fi +done + +if [ -z "$name" ] || [ -z "$bin" ]; then + echo "Could not find a suitable clang-format installation. Install clang-format that includes the git-clang-format script, with at least version $minVersion" + exit 1 +fi + +args="--binary $bin -q --extensions cpp,h --style file --staged -- :!src/FreeRTOS :!src/libs" + +changedFiles="$(git "$name" --diffstat $args)" +git "$name" $args + +echo "$changedFiles" | head -n -1 | cut -d ' ' -f 2 | while read -r file; do + git add -- "$file" done diff --git a/shell.nix b/shell.nix new file mode 100644 index 00000000..2cd26898 --- /dev/null +++ b/shell.nix @@ -0,0 +1,50 @@ +{ pkgs ? import {} }: + +with pkgs; let + py4McuBoot = python3.withPackages (ps: with ps; [ + cbor + intelhex + click + cryptography + pillow + ]); + lv_img_convWrapper = pkgs.writeScriptBin "lv_img_conv" '' + npm install lv_img_conv + nodejs node_modules/lv_img_conv/lv_img_conv.js + ''; + buildInfinitime = pkgs.writeScriptBin "build-infinitime" '' + mkdir -p build/ + cmake -DARM_NONE_EABI_TOOLCHAIN_PATH=$ARM_NONE_EABI_TOOLCHAIN_PATH \ + -DNRF5_SDK_PATH=$NRF5_SDK_PATH \ + -DCMAKE_BUILD_TYPE=$CMAKE_BUILD_TYPE \ + -DBUILD_DFU=$BUILD_DFU \ + -DBUILD_RESOURCES=$BUILD_RESOURCES \ + -DTARGET_DEVICE=$TARGET_DEVICE \ + -S . -B build + cmake --build build -j6 + ''; +in mkShell { + packages = [ + gcc-arm-embedded-10 + nrf5-sdk + cmake + nodePackages.lv_font_conv + lv_img_convWrapper + # lv_img_conv + nodejs + py4McuBoot + clang-tools + SDL2 + libpng + adafruit-nrfutil + buildInfinitime + # watchmate # wish this worked -- use flatpak run io.gitlab.azymohliad.WatchMate + ]; + + ARM_NONE_EABI_TOOLCHAIN_PATH="${gcc-arm-embedded-10}"; + NRF5_SDK_PATH="${nrf5-sdk}/share/nRF5_SDK"; + CMAKE_BUILD_TYPE="Release"; + BUILD_DFU=1; + BUILD_RESOURCES=1; + TARGET_DEVICE="PINETIME"; +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4fd693ef..551993d8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -371,8 +371,6 @@ list(APPEND SOURCE_FILES displayapp/DisplayApp.cpp displayapp/screens/Screen.cpp displayapp/screens/Tile.cpp - displayapp/screens/InfiniPaint.cpp - displayapp/screens/Paddle.cpp displayapp/screens/StopWatch.cpp displayapp/screens/BatteryIcon.cpp displayapp/screens/BleIcon.cpp @@ -381,13 +379,9 @@ list(APPEND SOURCE_FILES displayapp/screens/Label.cpp displayapp/screens/FirmwareUpdate.cpp displayapp/screens/Music.cpp - displayapp/screens/Navigation.cpp - displayapp/screens/Metronome.cpp - displayapp/screens/Motion.cpp displayapp/screens/FirmwareValidation.cpp displayapp/screens/ApplicationList.cpp displayapp/screens/Notifications.cpp - displayapp/screens/Twos.cpp displayapp/screens/HeartRate.cpp displayapp/screens/FlashLight.cpp displayapp/screens/List.cpp @@ -395,10 +389,12 @@ list(APPEND SOURCE_FILES displayapp/screens/BatteryInfo.cpp displayapp/screens/Steps.cpp displayapp/screens/Timer.cpp + displayapp/screens/Dice.cpp displayapp/screens/PassKey.cpp displayapp/screens/Error.cpp displayapp/screens/Alarm.cpp displayapp/screens/Styles.cpp + displayapp/screens/WeatherSymbols.cpp displayapp/Colors.cpp displayapp/widgets/Counter.cpp displayapp/widgets/PageIndicator.cpp @@ -421,6 +417,7 @@ list(APPEND SOURCE_FILES displayapp/screens/settings/SettingChimes.cpp displayapp/screens/settings/SettingShakeThreshold.cpp displayapp/screens/settings/SettingBluetooth.cpp + displayapp/screens/settings/SettingLocation.cpp ## Watch faces displayapp/screens/WatchFaceAnalog.cpp @@ -429,6 +426,8 @@ list(APPEND SOURCE_FILES #displayapp/screens/WatchFaceTerminal.cpp displayapp/screens/WatchFacePineTimeStyle.cpp #displayapp/screens/WatchFaceCasioStyleG7710.cpp + #displayapp/screens/WatchFaceFuzzy.cpp + #displayapp/screens/WatchFaceSundial.cpp ## @@ -546,7 +545,6 @@ list(APPEND RECOVERY_SOURCE_FILES systemtask/SystemTask.cpp systemtask/SystemMonitor.cpp drivers/TwiMaster.cpp - components/gfx/Gfx.cpp components/rle/RleDecoder.cpp components/heartrate/HeartRateController.cpp heartratetask/HeartRateTask.cpp @@ -576,7 +574,6 @@ list(APPEND RECOVERYLOADER_SOURCE_FILES components/rle/RleDecoder.cpp - components/gfx/Gfx.cpp drivers/St7789.cpp components/brightness/BrightnessController.cpp @@ -595,9 +592,7 @@ set(INCLUDE_FILES displayapp/TouchEvents.h displayapp/screens/Screen.h displayapp/screens/Tile.h - displayapp/screens/InfiniPaint.h displayapp/screens/StopWatch.h - displayapp/screens/Paddle.h displayapp/screens/BatteryIcon.h displayapp/screens/BleIcon.h displayapp/screens/NotificationIcon.h @@ -611,10 +606,7 @@ set(INCLUDE_FILES displayapp/Apps.h displayapp/screens/Notifications.h displayapp/screens/HeartRate.h - displayapp/screens/Metronome.h - displayapp/screens/Motion.h displayapp/screens/Timer.h - displayapp/screens/Alarm.h displayapp/Colors.h displayapp/widgets/Counter.h displayapp/widgets/PageIndicator.h @@ -915,6 +907,18 @@ target_compile_options(sunset PRIVATE $<$: ${ASM_FLAGS}> ) +# SUNSET_SRC +add_library(sunset STATIC ${SUNSET_SRC}) +target_include_directories(sunset SYSTEM PUBLIC . ../) +target_include_directories(sunset SYSTEM PUBLIC ${INCLUDES_FROM_LIBS}) +target_compile_options(sunset PRIVATE + ${COMMON_FLAGS} + $<$: ${DEBUG_FLAGS}> + $<$: ${RELEASE_FLAGS}> + $<$: ${CXX_FLAGS}> + $<$: ${ASM_FLAGS}> + ) + # Build autonomous binary (without support for bootloader) set(EXECUTABLE_NAME "pinetime-app") set(EXECUTABLE_FILE_NAME ${EXECUTABLE_NAME}-${pinetime_VERSION_MAJOR}.${pinetime_VERSION_MINOR}.${pinetime_VERSION_PATCH}) diff --git a/src/FreeRTOSConfig.h b/src/FreeRTOSConfig.h index cf18f418..cdb248b9 100644 --- a/src/FreeRTOSConfig.h +++ b/src/FreeRTOSConfig.h @@ -62,7 +62,8 @@ #define configTICK_RATE_HZ 1024 #define configMAX_PRIORITIES (3) #define configMINIMAL_STACK_SIZE (120) -#define configTOTAL_HEAP_SIZE (1024 * 40) +// how much heap can one smartwatch need, michael, 40kb? +#define configTOTAL_HEAP_SIZE (1024 * 39) #define configMAX_TASK_NAME_LEN (4) #define configUSE_16_BIT_TICKS 0 #define configIDLE_SHOULD_YIELD 1 @@ -75,6 +76,7 @@ #define configUSE_TIME_SLICING 0 #define configUSE_NEWLIB_REENTRANT 0 #define configENABLE_BACKWARD_COMPATIBILITY 1 +#define configUSE_TASK_NOTIFICATIONS 0 /* Hook function related definitions. */ #define configUSE_IDLE_HOOK 0 diff --git a/src/components/ble/DfuService.cpp b/src/components/ble/DfuService.cpp index 1f06b69e..b3f2ff10 100644 --- a/src/components/ble/DfuService.cpp +++ b/src/components/ble/DfuService.cpp @@ -357,6 +357,8 @@ void DfuService::DfuImage::Init(size_t chunkSize, size_t totalSize, uint16_t exp this->totalSize = totalSize; this->expectedCrc = expectedCrc; this->ready = true; + totalWriteIndex = 0; + bufferWriteIndex = 0; } void DfuService::DfuImage::Append(uint8_t* data, size_t size) { diff --git a/src/components/ble/HeartRateService.h b/src/components/ble/HeartRateService.h index 72632c96..3f32fd09 100644 --- a/src/components/ble/HeartRateService.h +++ b/src/components/ble/HeartRateService.h @@ -2,9 +2,9 @@ #define min // workaround: nimble's min/max macros conflict with libstdc++ #define max #include -#include #undef max #undef min +#include namespace Pinetime { namespace Controllers { diff --git a/src/components/ble/MotionService.cpp b/src/components/ble/MotionService.cpp index 029be894..1626a5bf 100644 --- a/src/components/ble/MotionService.cpp +++ b/src/components/ble/MotionService.cpp @@ -120,3 +120,7 @@ void MotionService::UnsubscribeNotification(uint16_t attributeHandle) { else if (attributeHandle == motionValuesHandle) motionValuesNoficationEnabled = false; } + +bool MotionService::IsMotionNotificationSubscribed() const { + return motionValuesNoficationEnabled; +} diff --git a/src/components/ble/MotionService.h b/src/components/ble/MotionService.h index e15cffe3..acc91e8d 100644 --- a/src/components/ble/MotionService.h +++ b/src/components/ble/MotionService.h @@ -21,6 +21,7 @@ namespace Pinetime { void SubscribeNotification(uint16_t attributeHandle); void UnsubscribeNotification(uint16_t attributeHandle); + bool IsMotionNotificationSubscribed() const; private: NimbleController& nimble; diff --git a/src/components/ble/SimpleWeatherService.cpp b/src/components/ble/SimpleWeatherService.cpp index d545d45b..146152f8 100644 --- a/src/components/ble/SimpleWeatherService.cpp +++ b/src/components/ble/SimpleWeatherService.cpp @@ -158,3 +158,16 @@ bool SimpleWeatherService::CurrentWeather::operator==(const SimpleWeatherService this->maxTemperature == other.maxTemperature && this->minTemperature == other.maxTemperature && std::strcmp(this->location.data(), other.location.data()) == 0; } + +bool SimpleWeatherService::Forecast::Day::operator==(const SimpleWeatherService::Forecast::Day& other) const { + return this->iconId == other.iconId && this->maxTemperature == other.maxTemperature && this->minTemperature == other.maxTemperature; +} + +bool SimpleWeatherService::Forecast::operator==(const SimpleWeatherService::Forecast& other) const { + for (int i = 0; i < this->nbDays; i++) { + if (this->days[i] != other.days[i]) { + return false; + } + } + return this->timestamp == other.timestamp && this->nbDays == other.nbDays; +} diff --git a/src/components/ble/SimpleWeatherService.h b/src/components/ble/SimpleWeatherService.h index cec2bb8b..4bbefcfc 100644 --- a/src/components/ble/SimpleWeatherService.h +++ b/src/components/ble/SimpleWeatherService.h @@ -96,9 +96,13 @@ namespace Pinetime { int16_t minTemperature; int16_t maxTemperature; Icons iconId; + + bool operator==(const Day& other) const; }; std::array days; + + bool operator==(const Forecast& other) const; }; std::optional Current() const; diff --git a/src/components/datetime/DateTimeController.cpp b/src/components/datetime/DateTimeController.cpp index 8d4a834e..5335a398 100644 --- a/src/components/datetime/DateTimeController.cpp +++ b/src/components/datetime/DateTimeController.cpp @@ -115,8 +115,8 @@ const char* DateTime::MonthShortToStringLow(Months month) { return MonthsStringLow[static_cast(month)]; } -const char* DateTime::DayOfWeekShortToStringLow() const { - return DaysStringShortLow[static_cast(DayOfWeek())]; +const char* DateTime::DayOfWeekShortToStringLow(Days day) { + return DaysStringShortLow[static_cast(day)]; } void DateTime::Register(Pinetime::System::SystemTask* systemTask) { @@ -129,7 +129,7 @@ std::string DateTime::FormattedTime() { auto hour = Hours(); auto minute = Minutes(); // Return time as a string in 12- or 24-hour format - char buff[9]; + char buff[11]; if (settingsController.GetClockType() == ClockType::H12) { uint8_t hour12; const char* amPmStr; diff --git a/src/components/datetime/DateTimeController.h b/src/components/datetime/DateTimeController.h index 0bf6ac2a..f719df7d 100644 --- a/src/components/datetime/DateTimeController.h +++ b/src/components/datetime/DateTimeController.h @@ -122,7 +122,7 @@ namespace Pinetime { const char* MonthShortToString() const; const char* DayOfWeekShortToString() const; static const char* MonthShortToStringLow(Months month); - const char* DayOfWeekShortToStringLow() const; + static const char* DayOfWeekShortToStringLow(Days day); std::chrono::time_point CurrentDateTime() const { return currentDateTime; diff --git a/src/components/gfx/Gfx.cpp b/src/components/gfx/Gfx.cpp deleted file mode 100644 index baa6486a..00000000 --- a/src/components/gfx/Gfx.cpp +++ /dev/null @@ -1,196 +0,0 @@ -#include "components/gfx/Gfx.h" -#include "drivers/St7789.h" -using namespace Pinetime::Components; - -Gfx::Gfx(Pinetime::Drivers::St7789& lcd) : lcd {lcd} { -} - -void Gfx::Init() { -} - -void Gfx::ClearScreen() { - SetBackgroundColor(0x0000); - - state.remainingIterations = 240 + 1; - state.currentIteration = 0; - state.busy = true; - state.action = Action::FillRectangle; - state.taskToNotify = xTaskGetCurrentTaskHandle(); - - lcd.DrawBuffer(0, 0, width, height, reinterpret_cast(buffer), width * 2); - WaitTransferFinished(); -} - -void Gfx::FillRectangle(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint16_t color) { - SetBackgroundColor(color); - - state.remainingIterations = h; - state.currentIteration = 0; - state.busy = true; - state.action = Action::FillRectangle; - state.color = color; - state.taskToNotify = xTaskGetCurrentTaskHandle(); - - lcd.DrawBuffer(x, y, w, h, reinterpret_cast(buffer), width * 2); - - WaitTransferFinished(); -} - -void Gfx::FillRectangle(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t* b) { - state.remainingIterations = h; - state.currentIteration = 0; - state.busy = true; - state.action = Action::FillRectangle; - state.color = 0x00; - state.taskToNotify = xTaskGetCurrentTaskHandle(); - - lcd.DrawBuffer(x, y, w, h, reinterpret_cast(b), width * 2); - - WaitTransferFinished(); -} - -void Gfx::DrawString(uint8_t x, uint8_t y, uint16_t color, const char* text, const FONT_INFO* p_font, bool wrap) { - if (y > (height - p_font->height)) { - // Not enough space to write even single char. - return; - } - - uint8_t current_x = x; - uint8_t current_y = y; - - for (size_t i = 0; text[i] != '\0'; i++) { - if (text[i] == '\n') { - current_x = x; - current_y += p_font->height + p_font->height / 10; - } else { - DrawChar(p_font, (uint8_t) text[i], ¤t_x, current_y, color); - } - - uint8_t char_idx = text[i] - p_font->startChar; - uint16_t char_width = text[i] == ' ' ? (p_font->height / 2) : p_font->charInfo[char_idx].widthBits; - - if (current_x > (width - char_width)) { - if (wrap) { - current_x = x; - current_y += p_font->height + p_font->height / 10; - } else { - break; - } - - if (y > (height - p_font->height)) { - break; - } - } - } -} - -void Gfx::DrawChar(const FONT_INFO* font, uint8_t c, uint8_t* x, uint8_t y, uint16_t color) { - uint8_t char_idx = c - font->startChar; - uint16_t bytes_in_line = CEIL_DIV(font->charInfo[char_idx].widthBits, 8); - uint16_t bg = 0x0000; - - if (c == ' ') { - *x += font->height / 2; - return; - } - - // Build first line - for (uint16_t j = 0; j < bytes_in_line; j++) { - for (uint8_t k = 0; k < 8; k++) { - if ((1 << (7 - k)) & font->data[font->charInfo[char_idx].offset + j]) { - buffer[(j * 8) + k] = color; - } else { - buffer[(j * 8) + k] = bg; - } - } - } - - state.remainingIterations = font->height + 0; - state.currentIteration = 0; - state.busy = true; - state.action = Action::DrawChar; - state.font = const_cast(font); - state.character = c; - state.color = color; - state.taskToNotify = xTaskGetCurrentTaskHandle(); - - lcd.DrawBuffer(*x, y, bytes_in_line * 8, font->height, reinterpret_cast(&buffer), bytes_in_line * 8 * 2); - WaitTransferFinished(); - - *x += font->charInfo[char_idx].widthBits + font->spacePixels; -} - -void Gfx::pixel_draw(uint8_t x, uint8_t y, uint16_t color) { - lcd.DrawPixel(x, y, color); -} - -void Gfx::Sleep() { - lcd.Sleep(); -} - -void Gfx::Wakeup() { - lcd.Wakeup(); -} - -void Gfx::SetBackgroundColor(uint16_t color) { - for (int i = 0; i < width; i++) { - buffer[i] = color; - } -} - -bool Gfx::GetNextBuffer(uint8_t** data, size_t& size) { - if (!state.busy) - return false; - state.remainingIterations = state.remainingIterations - 1; - if (state.remainingIterations == 0) { - state.busy = false; - NotifyEndOfTransfer(state.taskToNotify); - return false; - } - - if (state.action == Action::FillRectangle) { - *data = reinterpret_cast(buffer); - size = width * 2; - } else if (state.action == Action::DrawChar) { - uint16_t bg = 0x0000; - uint8_t char_idx = state.character - state.font->startChar; - uint16_t bytes_in_line = CEIL_DIV(state.font->charInfo[char_idx].widthBits, 8); - - for (uint16_t j = 0; j < bytes_in_line; j++) { - for (uint8_t k = 0; k < 8; k++) { - if ((1 << (7 - k)) & state.font->data[state.font->charInfo[char_idx].offset + ((state.currentIteration + 1) * bytes_in_line) + j]) { - buffer[(j * 8) + k] = state.color; - } else { - buffer[(j * 8) + k] = bg; - } - } - } - - *data = reinterpret_cast(buffer); - size = bytes_in_line * 8 * 2; - } - - state.currentIteration = state.currentIteration + 1; - - return true; -} - -void Gfx::NotifyEndOfTransfer(TaskHandle_t task) { - if (task != nullptr) { - BaseType_t xHigherPriorityTaskWoken = pdFALSE; - vTaskNotifyGiveFromISR(task, &xHigherPriorityTaskWoken); - portYIELD_FROM_ISR(xHigherPriorityTaskWoken); - } -} - -void Gfx::WaitTransferFinished() const { - ulTaskNotifyTake(pdTRUE, 500); -} - -void Gfx::SetScrollArea(uint16_t topFixedLines, uint16_t scrollLines, uint16_t bottomFixedLines) { - lcd.VerticalScrollDefinition(topFixedLines, scrollLines, bottomFixedLines); -} - -void Gfx::SetScrollStartLine(uint16_t line) { - lcd.VerticalScrollStartAddress(line); -} diff --git a/src/components/gfx/Gfx.h b/src/components/gfx/Gfx.h deleted file mode 100644 index 17c248f7..00000000 --- a/src/components/gfx/Gfx.h +++ /dev/null @@ -1,62 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -#include "drivers/BufferProvider.h" - -namespace Pinetime { - namespace Drivers { - class St7789; - } - - namespace Components { - class Gfx : public Pinetime::Drivers::BufferProvider { - public: - explicit Gfx(Drivers::St7789& lcd); - void Init(); - void ClearScreen(); - void DrawString(uint8_t x, uint8_t y, uint16_t color, const char* text, const FONT_INFO* p_font, bool wrap); - void DrawChar(const FONT_INFO* font, uint8_t c, uint8_t* x, uint8_t y, uint16_t color); - void FillRectangle(uint8_t x, uint8_t y, uint8_t width, uint8_t height, uint16_t color); - void FillRectangle(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t* b); - void SetScrollArea(uint16_t topFixedLines, uint16_t scrollLines, uint16_t bottomFixedLines); - void SetScrollStartLine(uint16_t line); - - void Sleep(); - void Wakeup(); - bool GetNextBuffer(uint8_t** buffer, size_t& size) override; - void pixel_draw(uint8_t x, uint8_t y, uint16_t color); - - private: - static constexpr uint8_t width = 240; - static constexpr uint8_t height = 240; - - enum class Action { None, FillRectangle, DrawChar }; - - struct State { - State() : busy {false}, action {Action::None}, remainingIterations {0}, currentIteration {0} { - } - - volatile bool busy; - volatile Action action; - volatile uint16_t remainingIterations; - volatile uint16_t currentIteration; - volatile FONT_INFO* font; - volatile uint16_t color; - volatile uint8_t character; - volatile TaskHandle_t taskToNotify = nullptr; - }; - - volatile State state; - - uint16_t buffer[width]; // 1 line buffer - Drivers::St7789& lcd; - - void SetBackgroundColor(uint16_t color); - void WaitTransferFinished() const; - void NotifyEndOfTransfer(TaskHandle_t task); - }; - } -} diff --git a/src/components/motion/MotionController.cpp b/src/components/motion/MotionController.cpp index d28378d5..72507ac5 100644 --- a/src/components/motion/MotionController.cpp +++ b/src/components/motion/MotionController.cpp @@ -40,15 +40,15 @@ void MotionController::Update(int16_t x, int16_t y, int16_t z, uint32_t nbSteps) service->OnNewStepCountValue(nbSteps); } - if (service != nullptr && (this->x != x || yHistory[0] != y || zHistory[0] != z)) { + if (service != nullptr && (xHistory[0] != x || yHistory[0] != y || zHistory[0] != z)) { service->OnNewMotionValues(x, y, z); } lastTime = time; time = xTaskGetTickCount(); - lastX = this->x; - this->x = x; + xHistory++; + xHistory[0] = x; yHistory++; yHistory[0] = y; zHistory++; @@ -67,20 +67,26 @@ MotionController::AccelStats MotionController::GetAccelStats() const { AccelStats stats; for (uint8_t i = 0; i < AccelStats::numHistory; i++) { + stats.xMean += xHistory[histSize - i]; stats.yMean += yHistory[histSize - i]; stats.zMean += zHistory[histSize - i]; + stats.prevXMean += xHistory[1 + i]; stats.prevYMean += yHistory[1 + i]; stats.prevZMean += zHistory[1 + i]; } + stats.xMean /= AccelStats::numHistory; stats.yMean /= AccelStats::numHistory; stats.zMean /= AccelStats::numHistory; + stats.prevXMean /= AccelStats::numHistory; stats.prevYMean /= AccelStats::numHistory; stats.prevZMean /= AccelStats::numHistory; for (uint8_t i = 0; i < AccelStats::numHistory; i++) { + stats.xVariance += (xHistory[histSize - i] - stats.xMean) * (xHistory[histSize - i] - stats.xMean); stats.yVariance += (yHistory[histSize - i] - stats.yMean) * (yHistory[histSize - i] - stats.yMean); stats.zVariance += (zHistory[histSize - i] - stats.zMean) * (zHistory[histSize - i] - stats.zMean); } + stats.xVariance /= AccelStats::numHistory; stats.yVariance /= AccelStats::numHistory; stats.zVariance /= AccelStats::numHistory; @@ -93,7 +99,7 @@ bool MotionController::ShouldRaiseWake() const { constexpr int16_t yThresh = -64; constexpr int16_t rollDegreesThresh = -45; - if (x < -xThresh || x > xThresh) { + if (std::abs(stats.xMean) > xThresh) { return false; } @@ -107,8 +113,9 @@ bool MotionController::ShouldRaiseWake() const { bool MotionController::ShouldShakeWake(uint16_t thresh) { /* Currently Polling at 10hz, If this ever goes faster scalar and EMA might need adjusting */ - int32_t speed = - std::abs(zHistory[0] - zHistory[histSize - 1] + (yHistory[0] - yHistory[histSize - 1]) / 2 + (x - lastX) / 4) * 100 / (time - lastTime); + int32_t speed = std::abs(zHistory[0] - zHistory[histSize - 1] + (yHistory[0] - yHistory[histSize - 1]) / 2 + + (xHistory[0] - xHistory[histSize - 1]) / 4) * + 100 / (time - lastTime); // (.2 * speed) + ((1 - .2) * accumulatedSpeed); accumulatedSpeed = speed / 5 + accumulatedSpeed * 4 / 5; @@ -116,6 +123,11 @@ bool MotionController::ShouldShakeWake(uint16_t thresh) { } bool MotionController::ShouldLowerSleep() const { + if ((stats.xMean > 887 && DegreesRolled(stats.xMean, stats.zMean, stats.prevXMean, stats.prevZMean) > 30) || + (stats.xMean < -887 && DegreesRolled(stats.xMean, stats.zMean, stats.prevXMean, stats.prevZMean) < -30)) { + return true; + } + if (stats.yMean < 724 || DegreesRolled(stats.yMean, stats.zMean, stats.prevYMean, stats.prevZMean) < 30) { return false; } diff --git a/src/components/motion/MotionController.h b/src/components/motion/MotionController.h index 0aa7823e..be0241d3 100644 --- a/src/components/motion/MotionController.h +++ b/src/components/motion/MotionController.h @@ -21,7 +21,7 @@ namespace Pinetime { void Update(int16_t x, int16_t y, int16_t z, uint32_t nbSteps); int16_t X() const { - return x; + return xHistory[0]; } int16_t Y() const { @@ -62,6 +62,10 @@ namespace Pinetime { this->service = service; } + Pinetime::Controllers::MotionService* GetService() const { + return service; + } + private: uint32_t nbSteps = 0; uint32_t currentTripSteps = 0; @@ -72,11 +76,14 @@ namespace Pinetime { struct AccelStats { static constexpr uint8_t numHistory = 2; + int16_t xMean = 0; int16_t yMean = 0; int16_t zMean = 0; + int16_t prevXMean = 0; int16_t prevYMean = 0; int16_t prevZMean = 0; + uint32_t xVariance = 0; uint32_t yVariance = 0; uint32_t zVariance = 0; }; @@ -85,9 +92,8 @@ namespace Pinetime { AccelStats stats = {}; - int16_t lastX = 0; - int16_t x = 0; static constexpr uint8_t histSize = 8; + Utility::CircularBuffer xHistory = {}; Utility::CircularBuffer yHistory = {}; Utility::CircularBuffer zHistory = {}; int32_t accumulatedSpeed = 0; diff --git a/src/components/settings/Settings.h b/src/components/settings/Settings.h index 509441e4..50cad268 100644 --- a/src/components/settings/Settings.h +++ b/src/components/settings/Settings.h @@ -9,7 +9,7 @@ namespace Pinetime { namespace Controllers { class Settings { public: - enum class ClockType : uint8_t { H24, H12, Fuzzy }; + enum class ClockType : uint8_t { H24, H12, FUZZY }; enum class WeatherFormat : uint8_t { Metric, Imperial }; enum class Notification : uint8_t { On, Off, Sleep }; enum class ChimesOption : uint8_t { None, Hours, HalfHours }; @@ -329,7 +329,7 @@ namespace Pinetime { uint16_t shakeWakeThreshold = 150; Controllers::BrightnessController::Levels brightLevel = Controllers::BrightnessController::Levels::Medium; - Location location = {(int16_t)0,(int16_t)0,(int8_t)0}; + Location location = {(int16_t)44,(int16_t)-123,(int8_t)-8}; }; SettingsData settings; diff --git a/src/displayapp/DisplayApp.cpp b/src/displayapp/DisplayApp.cpp index b425a35e..b421cab3 100644 --- a/src/displayapp/DisplayApp.cpp +++ b/src/displayapp/DisplayApp.cpp @@ -26,6 +26,8 @@ #include "displayapp/screens/FlashLight.h" #include "displayapp/screens/BatteryInfo.h" #include "displayapp/screens/Steps.h" +#include "displayapp/screens/Dice.h" +#include "displayapp/screens/Weather.h" #include "displayapp/screens/PassKey.h" #include "displayapp/screens/Error.h" @@ -48,6 +50,7 @@ #include "displayapp/screens/settings/SettingChimes.h" #include "displayapp/screens/settings/SettingShakeThreshold.h" #include "displayapp/screens/settings/SettingBluetooth.h" +#include "displayapp/screens/settings/SettingLocation.h" #include "libs/lv_conf.h" #include "UserApps.h" @@ -124,6 +127,7 @@ void DisplayApp::Start(System::BootErrors error) { bootError = error; lvgl.Init(); + motorController.Init(); if (error == System::BootErrors::TouchController) { LoadNewScreen(Apps::Error, DisplayApp::FullRefreshDirections::None); @@ -141,9 +145,6 @@ void DisplayApp::Process(void* instance) { NRF_LOG_INFO("displayapp task started!"); app->InitHw(); - // Send a dummy notification to unlock the lvgl display driver for the first iteration - xTaskNotifyGive(xTaskGetCurrentTaskHandle()); - while (true) { app->Refresh(); } @@ -152,7 +153,6 @@ void DisplayApp::Process(void* instance) { void DisplayApp::InitHw() { brightnessController.Init(); ApplyBrightness(); - motorController.Init(); lcd.Init(); } @@ -482,6 +482,7 @@ void DisplayApp::LoadScreen(Apps app, DisplayApp::FullRefreshDirections directio else { currentScreen.reset(userWatchFaces[0].create(controllers)); } + settingsController.SetAppMenu(0); } break; case Apps::Error: currentScreen = std::make_unique(bootError); @@ -527,10 +528,11 @@ void DisplayApp::LoadScreen(Apps app, DisplayApp::FullRefreshDirections directio currentScreen = std::make_unique(this, settingsController); break; case Apps::SettingWatchFace: { - std::array items; + std::array items; int i = 0; for (const auto& userWatchFace : userWatchFaces) { - items[i++] = Screens::CheckboxList::Item {userWatchFace.name, userWatchFace.isAvailable(controllers.filesystem)}; + items[i++] = + Screens::SettingWatchFace::Item {userWatchFace.name, userWatchFace.watchFace, userWatchFace.isAvailable(controllers.filesystem)}; } currentScreen = std::make_unique(this, std::move(items), settingsController, filesystem); } break; @@ -599,9 +601,7 @@ void DisplayApp::PushMessage(Messages msg) { if (in_isr()) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; xQueueSendFromISR(msgQueue, &msg, &xHigherPriorityTaskWoken); - if (xHigherPriorityTaskWoken == pdTRUE) { - portYIELD_FROM_ISR(xHigherPriorityTaskWoken); - } + portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } else { TickType_t timeout = portMAX_DELAY; // Make xQueueSend() non-blocking if the message is a Notification message. We do this to avoid diff --git a/src/displayapp/DisplayAppRecovery.cpp b/src/displayapp/DisplayAppRecovery.cpp index c4bd5766..28892723 100644 --- a/src/displayapp/DisplayAppRecovery.cpp +++ b/src/displayapp/DisplayAppRecovery.cpp @@ -38,9 +38,6 @@ void DisplayApp::Process(void* instance) { auto* app = static_cast(instance); NRF_LOG_INFO("displayapp task started!"); - // Send a dummy notification to unlock the lvgl display driver for the first iteration - xTaskNotifyGive(xTaskGetCurrentTaskHandle()); - app->InitHw(); while (true) { app->Refresh(); @@ -94,7 +91,6 @@ void DisplayApp::DisplayLogo(uint16_t color) { Pinetime::Tools::RleDecoder rleDecoder(infinitime_nb, sizeof(infinitime_nb), color, colorBlack); for (int i = 0; i < displayWidth; i++) { rleDecoder.DecodeNext(displayBuffer, displayWidth * bytesPerPixel); - ulTaskNotifyTake(pdTRUE, 500); lcd.DrawBuffer(0, i, displayWidth, 1, reinterpret_cast(displayBuffer), displayWidth * bytesPerPixel); } } @@ -103,20 +99,15 @@ void DisplayApp::DisplayOtaProgress(uint8_t percent, uint16_t color) { const uint8_t barHeight = 20; std::fill(displayBuffer, displayBuffer + (displayWidth * bytesPerPixel), color); for (int i = 0; i < barHeight; i++) { - ulTaskNotifyTake(pdTRUE, 500); uint16_t barWidth = std::min(static_cast(percent) * 2.4f, static_cast(displayWidth)); lcd.DrawBuffer(0, displayWidth - barHeight + i, barWidth, 1, reinterpret_cast(displayBuffer), barWidth * bytesPerPixel); } } void DisplayApp::PushMessage(Display::Messages msg) { - BaseType_t xHigherPriorityTaskWoken; - xHigherPriorityTaskWoken = pdFALSE; + BaseType_t xHigherPriorityTaskWoken = pdFALSE; xQueueSendFromISR(msgQueue, &msg, &xHigherPriorityTaskWoken); - if (xHigherPriorityTaskWoken) { - /* Actual macro used here is port specific. */ - // TODO : should I do something here? - } + portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } void DisplayApp::Register(Pinetime::System::SystemTask* /*systemTask*/) { diff --git a/src/displayapp/DisplayAppRecovery.h b/src/displayapp/DisplayAppRecovery.h index 3a5c78d9..c1bf6243 100644 --- a/src/displayapp/DisplayAppRecovery.h +++ b/src/displayapp/DisplayAppRecovery.h @@ -5,7 +5,6 @@ #include #include #include -#include "components/gfx/Gfx.h" #include "drivers/Cst816s.h" #include #include diff --git a/src/displayapp/LittleVgl.cpp b/src/displayapp/LittleVgl.cpp index 89893cf7..c70a0856 100644 --- a/src/displayapp/LittleVgl.cpp +++ b/src/displayapp/LittleVgl.cpp @@ -152,10 +152,6 @@ void LittleVgl::SetFullRefresh(FullRefreshDirections direction) { void LittleVgl::FlushDisplay(const lv_area_t* area, lv_color_t* color_p) { uint16_t y1, y2, width, height = 0; - ulTaskNotifyTake(pdTRUE, 200); - // Notification is still needed (even if there is a mutex on SPI) because of the DataCommand pin - // which cannot be set/clear during a transfer. - if ((scrollDirection == LittleVgl::FullRefreshDirections::Down) && (area->y2 == visibleNbLines - 1)) { writeOffset = ((writeOffset + totalNbLines) - visibleNbLines) % totalNbLines; } else if ((scrollDirection == FullRefreshDirections::Up) && (area->y1 == 0)) { @@ -219,7 +215,6 @@ void LittleVgl::FlushDisplay(const lv_area_t* area, lv_color_t* color_p) { if (height > 0) { lcd.DrawBuffer(area->x1, y1, width, height, reinterpret_cast(color_p), width * height * 2); - ulTaskNotifyTake(pdTRUE, 100); } uint16_t pixOffset = width * height; diff --git a/src/displayapp/UserApps.h b/src/displayapp/UserApps.h index 641f71b3..073df2f0 100644 --- a/src/displayapp/UserApps.h +++ b/src/displayapp/UserApps.h @@ -3,6 +3,7 @@ #include "Controllers.h" #include "displayapp/screens/Alarm.h" +#include "displayapp/screens/Dice.h" #include "displayapp/screens/Timer.h" #include "displayapp/screens/Twos.h" #include "displayapp/screens/Tile.h" @@ -13,6 +14,8 @@ // #include "displayapp/screens/WatchFaceInfineat.h" #include "displayapp/screens/WatchFacePineTimeStyle.h" // #include "displayapp/screens/WatchFaceTerminal.h" +// #include "displayapp/screens/WatchFaceFuzzy.h" +// #include "displayapp/screens/WatchFaceSundial.h" namespace Pinetime { namespace Applications { diff --git a/src/displayapp/apps/Apps.h.in b/src/displayapp/apps/Apps.h.in index 81e2e819..5e073efe 100644 --- a/src/displayapp/apps/Apps.h.in +++ b/src/displayapp/apps/Apps.h.in @@ -27,6 +27,8 @@ namespace Pinetime { Metronome, Motion, Steps, + Dice, + Weather, PassKey, QuickSettings, Settings, @@ -41,13 +43,17 @@ namespace Pinetime { SettingChimes, SettingShakeThreshold, SettingBluetooth, - Error, - Weather + Error }; enum class WatchFace : uint8_t { Analog, PineTimeStyle, + #Terminal, + #Fuzzy, + #Sundial, + #Infineat, + #CasioStyleG7710, }; template @@ -68,8 +74,12 @@ namespace Pinetime { static constexpr size_t Count = sizeof...(Ws); }; +<<<<<<< HEAD using UserWatchFaceTypes = WatchFaceTypeList; +======= + using UserWatchFaceTypes = WatchFaceTypeList<@WATCHFACE_TYPES@>; +>>>>>>> main static_assert(UserWatchFaceTypes::Count >= 1); } diff --git a/src/displayapp/apps/CMakeLists.txt b/src/displayapp/apps/CMakeLists.txt index 13b731aa..4187d87e 100644 --- a/src/displayapp/apps/CMakeLists.txt +++ b/src/displayapp/apps/CMakeLists.txt @@ -1,9 +1,25 @@ -#if(DEFINED ENABLE_USERAPPS) -# set(USERAPP_TYPES ${ENABLE_USERAPPS} CACHE STRING "List of user apps to build into the firmware") -#else () - set(USERAPP_TYPES "Apps::Navigation, Apps::StopWatch, Apps::Timer, Apps::Alarm, Apps::Steps, Apps::HeartRate, Apps::Music, Apps::Twos" CACHE STRING "List of user apps to build into the firmware") - #Apps::Paint, Apps::Metronome, Apps::Paddle -#endif () +#"Apps::Navigation, Apps::StopWatch, Apps::Timer, Apps::Alarm, Apps::Steps, Apps::HeartRate, Apps::Music, Apps::Twos" +#Apps::Paint, Apps::Metronome, Apps::Paddle +if(DEFINED ENABLE_USERAPPS) + set(USERAPP_TYPES ${ENABLE_USERAPPS} CACHE STRING "List of user apps to build into the firmware") +else () + set(DEFAULT_USER_APP_TYPES "Apps::StopWatch") + set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Timer") + set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Steps") + set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::HeartRate") + set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Music") + set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Dice") + set(USERAPP_TYPES "${DEFAULT_USER_APP_TYPES}" CACHE STRING "List of user apps to build into the firmware") +endif () + +if(DEFINED ENABLE_WATCHFACES) + set(WATCHFACE_TYPES ${ENABLE_WATCHFACES} CACHE STRING "List of watch faces to build into the firmware") +else() + set(DEFAULT_WATCHFACE_TYPES "WatchFace::Digital") + set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::Fuzzy") + set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::Sundial") + set(WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}" CACHE STRING "List of watch faces to build into the firmware") +endif() add_library(infinitime_apps INTERFACE) target_sources(infinitime_apps INTERFACE "${CMAKE_CURRENT_BINARY_DIR}/Apps.h") diff --git a/src/displayapp/fonts/README.md b/src/displayapp/fonts/README.md index bcc2d49c..20fb4a43 100644 --- a/src/displayapp/fonts/README.md +++ b/src/displayapp/fonts/README.md @@ -16,7 +16,7 @@ - Define the new symbols in `src/displayapp/screens/Symbols.h`: ``` -static constexpr const char* newSymbol = "\xEF\x86\x85"; +static constexpr const char* newSymbol = "\xEF\x99\x81"; ``` ### the config file format: diff --git a/src/displayapp/fonts/fonts.json b/src/displayapp/fonts/fonts.json index ad6e906f..f8397a83 100644 --- a/src/displayapp/fonts/fonts.json +++ b/src/displayapp/fonts/fonts.json @@ -2,17 +2,30 @@ "jetbrains_mono_bold_20": { "sources": [ { - "file": "JetBrainsMono-Bold.ttf", - "range": "0x20-0x7e, 0x410-0x44f, 0xB0" + "file": "Vulf_Mono-Italic.woff", + "range": "0x20-0x7e, 0xB0" }, { "file": "FontAwesome5-Solid+Brands+Regular.woff", - "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, 0xf743" + "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" } ], "bpp": 1, - "size": 20, - "patches": ["jetbrains_mono_bold_20.c_zero.patch", "jetbrains_mono_bold_20.c_M.patch"] + "size": 20 + }, + "jetbrains_mono_bold_24": { + "sources": [ + { + "file": "Vulf_Mono-Italic.woff", + "range": "0x20-0x7e, 0xB0" + }, + { + "file": "FontAwesome5-Solid+Brands+Regular.woff", + "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" + } + ], + "bpp": 1, + "size": 26 }, "noto_serif_cjk_20": { "sources": [ @@ -39,7 +52,8 @@ { "file": "JetBrainsMono-Regular.ttf", "disabledfile": "Vulf Mono Light Italic.ttf", - "range": "0x20, 0x25, 0x27, 0x2b, 0x2d, 0x30-0x3a, 0x4b-0x4d, 0x61-0x7a" + "range": "0x20, 0x25, 0x27, 0x2b, 0x2d, 0x30-0x3a, 0x4b-0x4d, 0x61-0x7a", + "disabledrange": "0x25, 0x2b, 0x2d, 0x2e, 0x30-0x3a, 0x43, 0x46, 0x4b-0x4d, 0x66, 0x69, 0x6b, 0x6d, 0x74, 0xb0" } ], "bpp": 1, @@ -49,7 +63,7 @@ "sources": [ { "file": "JetBrainsMono-Light.ttf", - "range": "0x25, 0x2D, 0x2F, 0x30-0x3a" + "range": "0x25, 0x2D, 0x2F, 0x30-0x3a, 0x43, 0x46, 0xb0" } ], "bpp": 1, diff --git a/src/displayapp/screens/Alarm.h b/src/displayapp/screens/Alarm.h index 993d65d1..444102cb 100644 --- a/src/displayapp/screens/Alarm.h +++ b/src/displayapp/screens/Alarm.h @@ -69,7 +69,7 @@ namespace Pinetime { template <> struct AppTraits { static constexpr Apps app = Apps::Alarm; - static constexpr const char* icon = Screens::Symbols::clock; + static constexpr const char* icon = Screens::Symbols::bell; static Screens::Screen* Create(AppControllers& controllers) { return new Screens::Alarm(controllers.alarmController, diff --git a/src/displayapp/screens/Dice.cpp b/src/displayapp/screens/Dice.cpp new file mode 100644 index 00000000..302c5f3f --- /dev/null +++ b/src/displayapp/screens/Dice.cpp @@ -0,0 +1,199 @@ +#include "displayapp/screens/Dice.h" +#include "displayapp/screens/Screen.h" +#include "displayapp/screens/Symbols.h" +#include "components/settings/Settings.h" +#include "components/motor/MotorController.h" +#include "components/motion/MotionController.h" + +using namespace Pinetime::Applications::Screens; + +namespace { + lv_obj_t* MakeLabel(lv_font_t* font, + lv_color_t color, + lv_label_long_mode_t longMode, + uint8_t width, + lv_label_align_t labelAlignment, + const char* text, + lv_obj_t* reference, + lv_align_t alignment, + int8_t x, + int8_t y) { + lv_obj_t* label = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_font(label, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, font); + lv_obj_set_style_local_text_color(label, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, color); + lv_label_set_long_mode(label, longMode); + if (width != 0) { + lv_obj_set_width(label, width); + } + lv_label_set_align(label, labelAlignment); + lv_label_set_text(label, text); + lv_obj_align(label, reference, alignment, x, y); + return label; + } + + void btnRollEventHandler(lv_obj_t* obj, lv_event_t event) { + auto* screen = static_cast(obj->user_data); + if (event == LV_EVENT_CLICKED) { + screen->Roll(); + } + } +} + +Dice::Dice(Controllers::MotionController& motionController, + Controllers::MotorController& motorController, + Controllers::Settings& settingsController) + : motorController {motorController}, motionController {motionController}, settingsController {settingsController} { + std::seed_seq sseq {static_cast(xTaskGetTickCount()), + static_cast(motionController.X()), + static_cast(motionController.Y()), + static_cast(motionController.Z())}; + gen.seed(sseq); + + lv_obj_t* nCounterLabel = MakeLabel(&jetbrains_mono_bold_20, + LV_COLOR_WHITE, + LV_LABEL_LONG_EXPAND, + 0, + LV_LABEL_ALIGN_CENTER, + "count", + lv_scr_act(), + LV_ALIGN_IN_TOP_LEFT, + 0, + 0); + + lv_obj_t* dCounterLabel = MakeLabel(&jetbrains_mono_bold_20, + LV_COLOR_WHITE, + LV_LABEL_LONG_EXPAND, + 0, + LV_LABEL_ALIGN_CENTER, + "sides", + nCounterLabel, + LV_ALIGN_OUT_RIGHT_MID, + 20, + 0); + + nCounter.Create(); + lv_obj_align(nCounter.GetObject(), nCounterLabel, LV_ALIGN_OUT_BOTTOM_MID, 0, 10); + nCounter.SetValue(1); + + dCounter.Create(); + lv_obj_align(dCounter.GetObject(), dCounterLabel, LV_ALIGN_OUT_BOTTOM_MID, 0, 10); + dCounter.SetValue(6); + + std::uniform_int_distribution<> distrib(0, resultColors.size() - 1); + currentColorIndex = distrib(gen); + + resultTotalLabel = MakeLabel(&jetbrains_mono_42, + resultColors[currentColorIndex], + LV_LABEL_LONG_BREAK, + 120, + LV_LABEL_ALIGN_CENTER, + "", + lv_scr_act(), + LV_ALIGN_IN_TOP_RIGHT, + 11, + 38); + resultIndividualLabel = MakeLabel(&jetbrains_mono_bold_20, + resultColors[currentColorIndex], + LV_LABEL_LONG_BREAK, + 90, + LV_LABEL_ALIGN_CENTER, + "", + resultTotalLabel, + LV_ALIGN_OUT_BOTTOM_MID, + 0, + 10); + + Roll(); + openingRoll = false; + + btnRoll = lv_btn_create(lv_scr_act(), nullptr); + btnRoll->user_data = this; + lv_obj_set_event_cb(btnRoll, btnRollEventHandler); + lv_obj_set_size(btnRoll, 240, 50); + lv_obj_align(btnRoll, lv_scr_act(), LV_ALIGN_IN_BOTTOM_MID, 0, 0); + + btnRollLabel = MakeLabel(&jetbrains_mono_bold_20, + LV_COLOR_WHITE, + LV_LABEL_LONG_EXPAND, + 0, + LV_LABEL_ALIGN_CENTER, + Symbols::dice, + btnRoll, + LV_ALIGN_CENTER, + 0, + 0); + + // Spagetti code in motion controller: it only updates the shake speed when shake to wake is on... + enableShakeForDice = !settingsController.isWakeUpModeOn(Pinetime::Controllers::Settings::WakeUpMode::Shake); + if (enableShakeForDice) { + settingsController.setWakeUpMode(Pinetime::Controllers::Settings::WakeUpMode::Shake, true); + } + refreshTask = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); +} + +Dice::~Dice() { + // reset the shake to wake mode. + if (enableShakeForDice) { + settingsController.setWakeUpMode(Pinetime::Controllers::Settings::WakeUpMode::Shake, false); + enableShakeForDice = false; + } + lv_task_del(refreshTask); + lv_obj_clean(lv_scr_act()); +} + +void Dice::Refresh() { + // we only reset the hysteresis when at rest + if (motionController.CurrentShakeSpeed() >= settingsController.GetShakeThreshold()) { + if (currentRollHysteresis <= 0) { + // this timestamp is used for the screen timeout + lv_disp_get_next(NULL)->last_activity_time = lv_tick_get(); + + Roll(); + } + } else if (currentRollHysteresis > 0) + --currentRollHysteresis; +} + +void Dice::Roll() { + uint8_t resultIndividual; + uint16_t resultTotal = 0; + std::uniform_int_distribution<> distrib(1, dCounter.GetValue()); + + lv_label_set_text(resultIndividualLabel, ""); + + if (nCounter.GetValue() == 1) { + resultTotal = distrib(gen); + if (dCounter.GetValue() == 2) { + switch (resultTotal) { + case 1: + lv_label_set_text(resultIndividualLabel, "HEADS"); + break; + case 2: + lv_label_set_text(resultIndividualLabel, "TAILS"); + break; + } + } + } else { + for (uint8_t i = 0; i < nCounter.GetValue(); i++) { + resultIndividual = distrib(gen); + resultTotal += resultIndividual; + lv_label_ins_text(resultIndividualLabel, LV_LABEL_POS_LAST, std::to_string(resultIndividual).c_str()); + if (i < (nCounter.GetValue() - 1)) { + lv_label_ins_text(resultIndividualLabel, LV_LABEL_POS_LAST, "+"); + } + } + } + + lv_label_set_text_fmt(resultTotalLabel, "%d", resultTotal); + if (openingRoll == false) { + motorController.RunForDuration(30); + NextColor(); + currentRollHysteresis = rollHysteresis; + } +} + +void Dice::NextColor() { + currentColorIndex = (currentColorIndex + 1) % resultColors.size(); + lv_obj_set_style_local_text_color(resultTotalLabel, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, resultColors[currentColorIndex]); + lv_obj_set_style_local_text_color(resultIndividualLabel, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, resultColors[currentColorIndex]); +} diff --git a/src/displayapp/screens/Dice.h b/src/displayapp/screens/Dice.h new file mode 100644 index 00000000..da91657d --- /dev/null +++ b/src/displayapp/screens/Dice.h @@ -0,0 +1,61 @@ +#pragma once + +#include "displayapp/apps/Apps.h" +#include "displayapp/screens/Screen.h" +#include "displayapp/widgets/Counter.h" +#include "displayapp/Controllers.h" +#include "Symbols.h" + +#include +#include + +namespace Pinetime { + namespace Applications { + namespace Screens { + class Dice : public Screen { + public: + Dice(Controllers::MotionController& motionController, + Controllers::MotorController& motorController, + Controllers::Settings& settingsController); + ~Dice() override; + void Roll(); + void Refresh() override; + + private: + lv_obj_t* btnRoll; + lv_obj_t* btnRollLabel; + lv_obj_t* resultTotalLabel; + lv_obj_t* resultIndividualLabel; + lv_task_t* refreshTask; + bool enableShakeForDice = false; + + std::mt19937 gen; + + std::array resultColors = {LV_COLOR_YELLOW, LV_COLOR_MAGENTA, LV_COLOR_AQUA}; + uint8_t currentColorIndex; + void NextColor(); + + Widgets::Counter nCounter = Widgets::Counter(1, 9, jetbrains_mono_42); + Widgets::Counter dCounter = Widgets::Counter(2, 99, jetbrains_mono_42); + + bool openingRoll = true; + uint8_t currentRollHysteresis = 0; + static constexpr uint8_t rollHysteresis = 10; + + Controllers::MotorController& motorController; + Controllers::MotionController& motionController; + Controllers::Settings& settingsController; + }; + } + + template <> + struct AppTraits { + static constexpr Apps app = Apps::Dice; + static constexpr const char* icon = Screens::Symbols::dice; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::Dice(controllers.motionController, controllers.motorController, controllers.settingsController); + }; + }; + } +} diff --git a/src/displayapp/screens/HeartRate.cpp b/src/displayapp/screens/HeartRate.cpp index f611fa26..9677be3b 100644 --- a/src/displayapp/screens/HeartRate.cpp +++ b/src/displayapp/screens/HeartRate.cpp @@ -41,7 +41,7 @@ HeartRate::HeartRate(Controllers::HeartRateController& heartRateController, Syst lv_obj_set_style_local_text_color(label_hr, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::lightGray); } - lv_label_set_text_static(label_hr, "000"); + lv_label_set_text_static(label_hr, "---"); lv_obj_align(label_hr, nullptr, LV_ALIGN_CENTER, 0, -40); label_bpm = lv_label_create(lv_scr_act(), nullptr); @@ -82,10 +82,14 @@ void HeartRate::Refresh() { case Controllers::HeartRateController::States::NoTouch: case Controllers::HeartRateController::States::NotEnoughData: // case Controllers::HeartRateController::States::Stopped: - lv_label_set_text_static(label_hr, "000"); + lv_label_set_text_static(label_hr, "---"); break; default: - lv_label_set_text_fmt(label_hr, "%03d", heartRateController.HeartRate()); + if (heartRateController.HeartRate() == 0) { + lv_label_set_text_static(label_hr, "---"); + } else { + lv_label_set_text_fmt(label_hr, "%03d", heartRateController.HeartRate()); + } } lv_label_set_text_static(label_status, ToString(state)); diff --git a/src/displayapp/screens/Motion.cpp b/src/displayapp/screens/Motion.cpp index 87c55eea..ecbed317 100644 --- a/src/displayapp/screens/Motion.cpp +++ b/src/displayapp/screens/Motion.cpp @@ -53,9 +53,9 @@ void Motion::Refresh() { lv_label_set_text_fmt(labelStep, "Steps %lu", motionController.NbSteps()); lv_label_set_text_fmt(label, - "X #FF0000 %d# Y #00B000 %d# Z #FFFF00 %d#", - motionController.X() / 0x10, - motionController.Y() / 0x10, - motionController.Z() / 0x10); + "X #FF0000 %d# Y #00B000 %d# Z #FFFF00 %d# mg", + motionController.X(), + motionController.Y(), + motionController.Z()); lv_obj_align(label, nullptr, LV_ALIGN_IN_TOP_MID, 0, 10); } diff --git a/src/displayapp/screens/Symbols.h b/src/displayapp/screens/Symbols.h index 549ea04f..bd958b28 100644 --- a/src/displayapp/screens/Symbols.h +++ b/src/displayapp/screens/Symbols.h @@ -11,6 +11,7 @@ namespace Pinetime { static constexpr const char* plug = "\xEF\x87\xA6"; static constexpr const char* shoe = "\xEF\x95\x8B"; static constexpr const char* clock = "\xEF\x80\x97"; + static constexpr const char* bell = "\xEF\x83\xB3"; static constexpr const char* info = "\xEF\x84\xA9"; static constexpr const char* list = "\xEF\x80\xBA"; static constexpr const char* sun = "\xEF\x86\x85"; @@ -34,6 +35,7 @@ namespace Pinetime { static constexpr const char* hourGlass = "\xEF\x89\x92"; static constexpr const char* lapsFlag = "\xEF\x80\xA4"; static constexpr const char* drum = "\xEF\x95\xA9"; + static constexpr const char* dice = "\xEF\x94\xA2"; static constexpr const char* eye = "\xEF\x81\xAE"; static constexpr const char* home = "\xEF\x80\x95"; static constexpr const char* sleep = "\xEE\xBD\x84"; diff --git a/src/displayapp/screens/WatchFaceAnalog.cpp b/src/displayapp/screens/WatchFaceAnalog.cpp index e2419341..dbca8c5e 100644 --- a/src/displayapp/screens/WatchFaceAnalog.cpp +++ b/src/displayapp/screens/WatchFaceAnalog.cpp @@ -407,7 +407,7 @@ void WatchFaceAnalog::Refresh() { if (currentDateTime.IsUpdated()) { UpdateClock(); - currentDate = std::chrono::time_point_cast(currentDateTime.Get()); + currentDate = std::chrono::time_point_cast(currentDateTime.Get()); if (currentDate.IsUpdated()) { if (settingsController.GetClockType() == Controllers::Settings::ClockType::Fuzzy) { /*char const* MonthsString[] = {"--", "IANUARIUS","FEBRUARIUS","MARTIUS","APRILIS","MARTIUSIUNIUS","QUINTILIS","SEXTILIS","SEPTEMBER","OCTOBER","NOVEMBER","DECEMBER"}; diff --git a/src/displayapp/screens/WatchFaceAnalog.h b/src/displayapp/screens/WatchFaceAnalog.h index 143eb4aa..7c7b2e14 100644 --- a/src/displayapp/screens/WatchFaceAnalog.h +++ b/src/displayapp/screens/WatchFaceAnalog.h @@ -44,8 +44,7 @@ namespace Pinetime { Utility::DirtyValue bleState {}; Utility::DirtyValue> currentDateTime; Utility::DirtyValue notificationState {false}; - using days = std::chrono::duration>; // TODO: days is standard in c++20 - Utility::DirtyValue> currentDate; + Utility::DirtyValue> currentDate; lv_obj_t* minor_scales; lv_obj_t* major_scales; diff --git a/src/displayapp/screens/WatchFaceCasioStyleG7710.cpp b/src/displayapp/screens/WatchFaceCasioStyleG7710.cpp new file mode 100644 index 00000000..c695f852 --- /dev/null +++ b/src/displayapp/screens/WatchFaceCasioStyleG7710.cpp @@ -0,0 +1,334 @@ +#include "displayapp/screens/WatchFaceCasioStyleG7710.h" + +#include +#include +#include "displayapp/screens/BatteryIcon.h" +#include "displayapp/screens/BleIcon.h" +#include "displayapp/screens/NotificationIcon.h" +#include "displayapp/screens/Symbols.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; + +WatchFaceCasioStyleG7710::WatchFaceCasioStyleG7710(Controllers::DateTime& dateTimeController, + const Controllers::Battery& batteryController, + const Controllers::Ble& bleController, + Controllers::NotificationManager& notificatioManager, + Controllers::Settings& settingsController, + Controllers::HeartRateController& heartRateController, + Controllers::MotionController& motionController, + Controllers::FS& filesystem) + : currentDateTime {{}}, + batteryIcon(false), + dateTimeController {dateTimeController}, + batteryController {batteryController}, + bleController {bleController}, + notificatioManager {notificatioManager}, + settingsController {settingsController}, + heartRateController {heartRateController}, + motionController {motionController} { + + lfs_file f = {}; + if (filesystem.FileOpen(&f, "/fonts/lv_font_dots_40.bin", LFS_O_RDONLY) >= 0) { + filesystem.FileClose(&f); + font_dot40 = lv_font_load("F:/fonts/lv_font_dots_40.bin"); + } + + if (filesystem.FileOpen(&f, "/fonts/7segments_40.bin", LFS_O_RDONLY) >= 0) { + filesystem.FileClose(&f); + font_segment40 = lv_font_load("F:/fonts/7segments_40.bin"); + } + + if (filesystem.FileOpen(&f, "/fonts/7segments_115.bin", LFS_O_RDONLY) >= 0) { + filesystem.FileClose(&f); + font_segment115 = lv_font_load("F:/fonts/7segments_115.bin"); + } + + label_battery_value = lv_label_create(lv_scr_act(), nullptr); + lv_obj_align(label_battery_value, lv_scr_act(), LV_ALIGN_IN_TOP_RIGHT, 0, 0); + lv_obj_set_style_local_text_color(label_battery_value, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, color_text); + lv_label_set_text_static(label_battery_value, "00%"); + + batteryIcon.Create(lv_scr_act()); + batteryIcon.SetColor(color_text); + lv_obj_align(batteryIcon.GetObject(), label_battery_value, LV_ALIGN_OUT_LEFT_MID, -5, 0); + + batteryPlug = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(batteryPlug, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, color_text); + lv_label_set_text_static(batteryPlug, Symbols::plug); + lv_obj_align(batteryPlug, batteryIcon.GetObject(), LV_ALIGN_OUT_LEFT_MID, -5, 0); + + bleIcon = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(bleIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, color_text); + lv_label_set_text_static(bleIcon, Symbols::bluetooth); + lv_obj_align(bleIcon, batteryPlug, LV_ALIGN_OUT_LEFT_MID, -5, 0); + + notificationIcon = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(notificationIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, color_text); + lv_label_set_text_static(notificationIcon, NotificationIcon::GetIcon(false)); + lv_obj_align(notificationIcon, bleIcon, LV_ALIGN_OUT_LEFT_MID, -5, 0); + + label_day_of_week = lv_label_create(lv_scr_act(), nullptr); + lv_obj_align(label_day_of_week, lv_scr_act(), LV_ALIGN_IN_TOP_LEFT, 10, 64); + lv_obj_set_style_local_text_color(label_day_of_week, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, color_text); + lv_obj_set_style_local_text_font(label_day_of_week, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, font_dot40); + lv_label_set_text_static(label_day_of_week, "SUN"); + + label_week_number = lv_label_create(lv_scr_act(), nullptr); + lv_obj_align(label_week_number, lv_scr_act(), LV_ALIGN_IN_TOP_LEFT, 5, 22); + lv_obj_set_style_local_text_color(label_week_number, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, color_text); + lv_obj_set_style_local_text_font(label_week_number, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, font_dot40); + lv_label_set_text_static(label_week_number, "WK26"); + + label_day_of_year = lv_label_create(lv_scr_act(), nullptr); + lv_obj_align(label_day_of_year, lv_scr_act(), LV_ALIGN_IN_TOP_LEFT, 100, 30); + lv_obj_set_style_local_text_color(label_day_of_year, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, color_text); + lv_obj_set_style_local_text_font(label_day_of_year, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, font_segment40); + lv_label_set_text_static(label_day_of_year, "181-184"); + + lv_style_init(&style_line); + lv_style_set_line_width(&style_line, LV_STATE_DEFAULT, 2); + lv_style_set_line_color(&style_line, LV_STATE_DEFAULT, color_text); + lv_style_set_line_rounded(&style_line, LV_STATE_DEFAULT, true); + + lv_style_init(&style_border); + lv_style_set_line_width(&style_border, LV_STATE_DEFAULT, 6); + lv_style_set_line_color(&style_border, LV_STATE_DEFAULT, color_text); + lv_style_set_line_rounded(&style_border, LV_STATE_DEFAULT, true); + + line_icons = lv_line_create(lv_scr_act(), nullptr); + lv_line_set_points(line_icons, line_icons_points, 3); + lv_obj_add_style(line_icons, LV_LINE_PART_MAIN, &style_line); + lv_obj_align(line_icons, nullptr, LV_ALIGN_IN_TOP_RIGHT, -10, 18); + + line_day_of_week_number = lv_line_create(lv_scr_act(), nullptr); + lv_line_set_points(line_day_of_week_number, line_day_of_week_number_points, 4); + lv_obj_add_style(line_day_of_week_number, LV_LINE_PART_MAIN, &style_border); + lv_obj_align(line_day_of_week_number, nullptr, LV_ALIGN_IN_TOP_LEFT, 0, 8); + + line_day_of_year = lv_line_create(lv_scr_act(), nullptr); + lv_line_set_points(line_day_of_year, line_day_of_year_points, 3); + lv_obj_add_style(line_day_of_year, LV_LINE_PART_MAIN, &style_line); + lv_obj_align(line_day_of_year, nullptr, LV_ALIGN_IN_TOP_RIGHT, 0, 60); + + label_date = lv_label_create(lv_scr_act(), nullptr); + lv_obj_align(label_date, lv_scr_act(), LV_ALIGN_IN_TOP_LEFT, 100, 70); + lv_obj_set_style_local_text_color(label_date, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, color_text); + lv_obj_set_style_local_text_font(label_date, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, font_segment40); + lv_label_set_text_static(label_date, "6-30"); + + line_date = lv_line_create(lv_scr_act(), nullptr); + lv_line_set_points(line_date, line_date_points, 3); + lv_obj_add_style(line_date, LV_LINE_PART_MAIN, &style_line); + lv_obj_align(line_date, nullptr, LV_ALIGN_IN_TOP_RIGHT, 0, 100); + + label_time = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(label_time, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, color_text); + lv_obj_set_style_local_text_font(label_time, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, font_segment115); + lv_obj_align(label_time, lv_scr_act(), LV_ALIGN_CENTER, 0, 40); + + line_time = lv_line_create(lv_scr_act(), nullptr); + lv_line_set_points(line_time, line_time_points, 3); + lv_obj_add_style(line_time, LV_LINE_PART_MAIN, &style_line); + lv_obj_align(line_time, nullptr, LV_ALIGN_IN_BOTTOM_RIGHT, 0, -25); + + label_time_ampm = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(label_time_ampm, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, color_text); + lv_label_set_text_static(label_time_ampm, ""); + lv_obj_align(label_time_ampm, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 5, -5); + + backgroundLabel = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_click(backgroundLabel, true); + lv_label_set_long_mode(backgroundLabel, LV_LABEL_LONG_CROP); + lv_obj_set_size(backgroundLabel, 240, 240); + lv_obj_set_pos(backgroundLabel, 0, 0); + lv_label_set_text_static(backgroundLabel, ""); + + heartbeatIcon = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_text_static(heartbeatIcon, Symbols::heartBeat); + lv_obj_set_style_local_text_color(heartbeatIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, color_text); + lv_obj_align(heartbeatIcon, lv_scr_act(), LV_ALIGN_IN_BOTTOM_LEFT, 5, -2); + + heartbeatValue = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(heartbeatValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, color_text); + lv_label_set_text_static(heartbeatValue, ""); + lv_obj_align(heartbeatValue, heartbeatIcon, LV_ALIGN_OUT_RIGHT_MID, 5, 0); + + stepValue = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(stepValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, color_text); + lv_label_set_text_static(stepValue, "0"); + lv_obj_align(stepValue, lv_scr_act(), LV_ALIGN_IN_BOTTOM_RIGHT, -5, -2); + + stepIcon = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(stepIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, color_text); + lv_label_set_text_static(stepIcon, Symbols::shoe); + lv_obj_align(stepIcon, stepValue, LV_ALIGN_OUT_LEFT_MID, -5, 0); + + taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); + Refresh(); +} + +WatchFaceCasioStyleG7710::~WatchFaceCasioStyleG7710() { + lv_task_del(taskRefresh); + + lv_style_reset(&style_line); + lv_style_reset(&style_border); + + if (font_dot40 != nullptr) { + lv_font_free(font_dot40); + } + + if (font_segment40 != nullptr) { + lv_font_free(font_segment40); + } + + if (font_segment115 != nullptr) { + lv_font_free(font_segment115); + } + + lv_obj_clean(lv_scr_act()); +} + +void WatchFaceCasioStyleG7710::Refresh() { + powerPresent = batteryController.IsPowerPresent(); + if (powerPresent.IsUpdated()) { + lv_label_set_text_static(batteryPlug, BatteryIcon::GetPlugIcon(powerPresent.Get())); + } + + batteryPercentRemaining = batteryController.PercentRemaining(); + if (batteryPercentRemaining.IsUpdated()) { + auto batteryPercent = batteryPercentRemaining.Get(); + batteryIcon.SetBatteryPercentage(batteryPercent); + lv_label_set_text_fmt(label_battery_value, "%d%%", batteryPercent); + } + + bleState = bleController.IsConnected(); + bleRadioEnabled = bleController.IsRadioEnabled(); + if (bleState.IsUpdated() || bleRadioEnabled.IsUpdated()) { + lv_label_set_text_static(bleIcon, BleIcon::GetIcon(bleState.Get())); + } + lv_obj_realign(label_battery_value); + lv_obj_realign(batteryIcon.GetObject()); + lv_obj_realign(batteryPlug); + lv_obj_realign(bleIcon); + lv_obj_realign(notificationIcon); + + notificationState = notificatioManager.AreNewNotificationsAvailable(); + if (notificationState.IsUpdated()) { + lv_label_set_text_static(notificationIcon, NotificationIcon::GetIcon(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(label_time_ampm, ampmChar); + lv_label_set_text_fmt(label_time, "%2d:%02d", hour, minute); + } else { + lv_label_set_text_fmt(label_time, "%02d:%02d", hour, minute); + } + lv_obj_realign(label_time); + + currentDate = std::chrono::time_point_cast(currentDateTime.Get()); + if (currentDate.IsUpdated()) { + const char* weekNumberFormat = "%V"; + + uint16_t year = dateTimeController.Year(); + Controllers::DateTime::Months month = dateTimeController.Month(); + uint8_t day = dateTimeController.Day(); + int dayOfYear = dateTimeController.DayOfYear(); + if (settingsController.GetClockType() == Controllers::Settings::ClockType::H24) { + // 24h mode: ddmmyyyy, first DOW=Monday; + lv_label_set_text_fmt(label_date, "%3d-%2d", day, month); + weekNumberFormat = "%V"; // Replaced by the week number of the year (Monday as the first day of the week) as a decimal number + // [01,53]. If the week containing 1 January has four or more days in the new year, then it is considered + // week 1. Otherwise, it is the last week of the previous year, and the next week is week 1. Both January + // 4th and the first Thursday of January are always in week 1. [ tm_year, tm_wday, tm_yday] + } else { + // 12h mode: mmddyyyy, first DOW=Sunday; + lv_label_set_text_fmt(label_date, "%3d-%2d", month, day); + weekNumberFormat = "%U"; // Replaced by the week number of the year as a decimal number [00,53]. The first Sunday of January is the + // first day of week 1; days in the new year before this are in week 0. [ tm_year, tm_wday, tm_yday] + } + + time_t ttTime = + std::chrono::system_clock::to_time_t(std::chrono::time_point_cast(currentDateTime.Get())); + tm* tmTime = std::localtime(&ttTime); + + // TODO: When we start using C++20, use std::chrono::year::is_leap + int daysInCurrentYear = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 ? 366 : 365; + uint16_t daysTillEndOfYearNumber = daysInCurrentYear - dayOfYear; + + char buffer[8]; + strftime(buffer, 8, weekNumberFormat, tmTime); + uint8_t weekNumber = atoi(buffer); + + lv_label_set_text_fmt(label_day_of_week, "%s", dateTimeController.DayOfWeekShortToString()); + lv_label_set_text_fmt(label_day_of_year, "%3d-%3d", dayOfYear, daysTillEndOfYearNumber); + lv_label_set_text_fmt(label_week_number, "WK%02d", weekNumber); + + lv_obj_realign(label_day_of_week); + lv_obj_realign(label_day_of_year); + lv_obj_realign(label_week_number); + lv_obj_realign(label_date); + } + } + + heartbeat = heartRateController.HeartRate(); + heartbeatRunning = heartRateController.State() != Controllers::HeartRateController::States::Stopped; + if (heartbeat.IsUpdated() || heartbeatRunning.IsUpdated()) { + if (heartbeatRunning.Get()) { + lv_obj_set_style_local_text_color(heartbeatIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, color_text); + lv_label_set_text_fmt(heartbeatValue, "%d", heartbeat.Get()); + } else { + lv_obj_set_style_local_text_color(heartbeatIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x1B1B1B)); + lv_label_set_text_static(heartbeatValue, ""); + } + + lv_obj_realign(heartbeatIcon); + lv_obj_realign(heartbeatValue); + } + + stepCount = motionController.NbSteps(); + if (stepCount.IsUpdated()) { + lv_label_set_text_fmt(stepValue, "%lu", stepCount.Get()); + lv_obj_realign(stepValue); + lv_obj_realign(stepIcon); + } +} + +bool WatchFaceCasioStyleG7710::IsAvailable(Pinetime::Controllers::FS& filesystem) { + lfs_file file = {}; + + if (filesystem.FileOpen(&file, "/fonts/lv_font_dots_40.bin", LFS_O_RDONLY) < 0) { + return false; + } + + filesystem.FileClose(&file); + if (filesystem.FileOpen(&file, "/fonts/7segments_40.bin", LFS_O_RDONLY) < 0) { + return false; + } + + filesystem.FileClose(&file); + if (filesystem.FileOpen(&file, "/fonts/7segments_115.bin", LFS_O_RDONLY) < 0) { + return false; + } + + filesystem.FileClose(&file); + return true; +} diff --git a/src/displayapp/screens/WatchFaceCasioStyleG7710.h b/src/displayapp/screens/WatchFaceCasioStyleG7710.h new file mode 100644 index 00000000..0f46a692 --- /dev/null +++ b/src/displayapp/screens/WatchFaceCasioStyleG7710.h @@ -0,0 +1,126 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include "displayapp/screens/Screen.h" +#include "components/datetime/DateTimeController.h" +#include "components/ble/BleController.h" +#include "utility/DirtyValue.h" +#include "displayapp/apps/Apps.h" + +namespace Pinetime { + namespace Controllers { + class Settings; + class Battery; + class Ble; + class NotificationManager; + class HeartRateController; + class MotionController; + } + + namespace Applications { + namespace Screens { + + class WatchFaceCasioStyleG7710 : public Screen { + public: + WatchFaceCasioStyleG7710(Controllers::DateTime& dateTimeController, + const Controllers::Battery& batteryController, + const Controllers::Ble& bleController, + Controllers::NotificationManager& notificatioManager, + Controllers::Settings& settingsController, + Controllers::HeartRateController& heartRateController, + Controllers::MotionController& motionController, + Controllers::FS& filesystem); + ~WatchFaceCasioStyleG7710() override; + + void Refresh() override; + + static bool IsAvailable(Pinetime::Controllers::FS& filesystem); + + private: + Utility::DirtyValue batteryPercentRemaining {}; + Utility::DirtyValue powerPresent {}; + Utility::DirtyValue bleState {}; + Utility::DirtyValue bleRadioEnabled {}; + Utility::DirtyValue> currentDateTime {}; + Utility::DirtyValue stepCount {}; + Utility::DirtyValue heartbeat {}; + Utility::DirtyValue heartbeatRunning {}; + Utility::DirtyValue notificationState {}; + Utility::DirtyValue> currentDate; + + lv_point_t line_icons_points[3] {{0, 5}, {117, 5}, {122, 0}}; + lv_point_t line_day_of_week_number_points[4] {{0, 0}, {100, 0}, {95, 95}, {0, 95}}; + lv_point_t line_day_of_year_points[3] {{0, 5}, {130, 5}, {135, 0}}; + lv_point_t line_date_points[3] {{0, 5}, {135, 5}, {140, 0}}; + lv_point_t line_time_points[3] {{0, 0}, {230, 0}, {235, 5}}; + + lv_color_t color_text = lv_color_hex(0x98B69A); + + lv_style_t style_line; + lv_style_t style_border; + + lv_obj_t* label_time; + lv_obj_t* line_time; + lv_obj_t* label_time_ampm; + lv_obj_t* label_date; + lv_obj_t* line_date; + lv_obj_t* label_day_of_week; + lv_obj_t* label_week_number; + lv_obj_t* line_day_of_week_number; + lv_obj_t* label_day_of_year; + lv_obj_t* line_day_of_year; + lv_obj_t* backgroundLabel; + lv_obj_t* bleIcon; + lv_obj_t* batteryPlug; + lv_obj_t* label_battery_value; + lv_obj_t* heartbeatIcon; + lv_obj_t* heartbeatValue; + lv_obj_t* stepIcon; + lv_obj_t* stepValue; + lv_obj_t* notificationIcon; + lv_obj_t* line_icons; + + BatteryIcon batteryIcon; + + Controllers::DateTime& dateTimeController; + const Controllers::Battery& batteryController; + const Controllers::Ble& bleController; + Controllers::NotificationManager& notificatioManager; + Controllers::Settings& settingsController; + Controllers::HeartRateController& heartRateController; + Controllers::MotionController& motionController; + + lv_task_t* taskRefresh; + lv_font_t* font_dot40 = nullptr; + lv_font_t* font_segment40 = nullptr; + lv_font_t* font_segment115 = nullptr; + }; + } + + template <> + struct WatchFaceTraits { + static constexpr WatchFace watchFace = WatchFace::CasioStyleG7710; + static constexpr const char* name = "Casio G7710"; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::WatchFaceCasioStyleG7710(controllers.dateTimeController, + controllers.batteryController, + controllers.bleController, + controllers.notificationManager, + controllers.settingsController, + controllers.heartRateController, + controllers.motionController, + controllers.filesystem); + }; + + static bool IsAvailable(Pinetime::Controllers::FS& filesystem) { + return Screens::WatchFaceCasioStyleG7710::IsAvailable(filesystem); + } + }; + } +} diff --git a/src/displayapp/screens/WatchFaceDigital.cpp b/src/displayapp/screens/WatchFaceDigital.cpp new file mode 100644 index 00000000..2e00ee98 --- /dev/null +++ b/src/displayapp/screens/WatchFaceDigital.cpp @@ -0,0 +1,193 @@ +#include "displayapp/screens/WatchFaceDigital.h" + +#include +#include +#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/ble/SimpleWeatherService.h" +#include "components/settings/Settings.h" + +using namespace Pinetime::Applications::Screens; + +WatchFaceDigital::WatchFaceDigital(Controllers::DateTime& dateTimeController, + const Controllers::Battery& batteryController, + const Controllers::Ble& bleController, + Controllers::NotificationManager& notificationManager, + Controllers::Settings& settingsController, + Controllers::HeartRateController& heartRateController, + Controllers::MotionController& motionController, + Controllers::SimpleWeatherService& weatherService) + : currentDateTime {{}}, + dateTimeController {dateTimeController}, + notificationManager {notificationManager}, + settingsController {settingsController}, + heartRateController {heartRateController}, + motionController {motionController}, + weatherService {weatherService}, + statusIcons(batteryController, bleController) { + + statusIcons.Create(); + + notificationIcon = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(notificationIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_LIME); + lv_label_set_text_static(notificationIcon, NotificationIcon::GetIcon(false)); + lv_obj_align(notificationIcon, nullptr, LV_ALIGN_IN_TOP_LEFT, 0, 0); + + weatherIcon = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(weatherIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x999999)); + lv_obj_set_style_local_text_font(weatherIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &fontawesome_weathericons); + lv_label_set_text(weatherIcon, ""); + lv_obj_align(weatherIcon, nullptr, LV_ALIGN_IN_TOP_MID, -20, 50); + lv_obj_set_auto_realign(weatherIcon, true); + + temperature = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(temperature, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x999999)); + lv_label_set_text(temperature, ""); + lv_obj_align(temperature, nullptr, LV_ALIGN_IN_TOP_MID, 20, 50); + + label_date = lv_label_create(lv_scr_act(), nullptr); + lv_obj_align(label_date, lv_scr_act(), LV_ALIGN_CENTER, 0, 60); + lv_obj_set_style_local_text_color(label_date, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x999999)); + + label_time = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_font(label_time, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_extrabold_compressed); + + lv_obj_align(label_time, lv_scr_act(), LV_ALIGN_IN_RIGHT_MID, 0, 0); + + label_time_ampm = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_text_static(label_time_ampm, ""); + lv_obj_align(label_time_ampm, lv_scr_act(), LV_ALIGN_IN_RIGHT_MID, -30, -55); + + heartbeatIcon = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_text_static(heartbeatIcon, Symbols::heartBeat); + lv_obj_set_style_local_text_color(heartbeatIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0xCE1B1B)); + lv_obj_align(heartbeatIcon, lv_scr_act(), LV_ALIGN_IN_BOTTOM_LEFT, 0, 0); + + heartbeatValue = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(heartbeatValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0xCE1B1B)); + lv_label_set_text_static(heartbeatValue, ""); + lv_obj_align(heartbeatValue, heartbeatIcon, LV_ALIGN_OUT_RIGHT_MID, 5, 0); + + stepValue = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(stepValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x00FFE7)); + lv_label_set_text_static(stepValue, "0"); + lv_obj_align(stepValue, lv_scr_act(), LV_ALIGN_IN_BOTTOM_RIGHT, 0, 0); + + stepIcon = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(stepIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x00FFE7)); + lv_label_set_text_static(stepIcon, Symbols::shoe); + lv_obj_align(stepIcon, stepValue, LV_ALIGN_OUT_LEFT_MID, -5, 0); + + taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); + Refresh(); +} + +WatchFaceDigital::~WatchFaceDigital() { + lv_task_del(taskRefresh); + lv_obj_clean(lv_scr_act()); +} + +void WatchFaceDigital::Refresh() { + statusIcons.Update(); + + notificationState = notificationManager.AreNewNotificationsAvailable(); + if (notificationState.IsUpdated()) { + lv_label_set_text_static(notificationIcon, NotificationIcon::GetIcon(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[3] = "AM"; + 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(label_time_ampm, ampmChar); + lv_label_set_text_fmt(label_time, "%2d:%02d", hour, minute); + lv_obj_align(label_time, lv_scr_act(), LV_ALIGN_IN_RIGHT_MID, 0, 0); + } else { + lv_label_set_text_fmt(label_time, "%02d:%02d", hour, minute); + lv_obj_align(label_time, lv_scr_act(), LV_ALIGN_CENTER, 0, 0); + } + + currentDate = std::chrono::time_point_cast(currentDateTime.Get()); + if (currentDate.IsUpdated()) { + uint16_t year = dateTimeController.Year(); + uint8_t day = dateTimeController.Day(); + if (settingsController.GetClockType() == Controllers::Settings::ClockType::H24) { + lv_label_set_text_fmt(label_date, + "%s %d %s %d", + dateTimeController.DayOfWeekShortToString(), + day, + dateTimeController.MonthShortToString(), + year); + } else { + lv_label_set_text_fmt(label_date, + "%s %s %d %d", + dateTimeController.DayOfWeekShortToString(), + dateTimeController.MonthShortToString(), + day, + year); + } + lv_obj_realign(label_date); + } + } + + heartbeat = heartRateController.HeartRate(); + heartbeatRunning = heartRateController.State() != Controllers::HeartRateController::States::Stopped; + if (heartbeat.IsUpdated() || heartbeatRunning.IsUpdated()) { + if (heartbeatRunning.Get()) { + lv_obj_set_style_local_text_color(heartbeatIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0xCE1B1B)); + lv_label_set_text_fmt(heartbeatValue, "%d", heartbeat.Get()); + } else { + lv_obj_set_style_local_text_color(heartbeatIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x1B1B1B)); + lv_label_set_text_static(heartbeatValue, ""); + } + + lv_obj_realign(heartbeatIcon); + lv_obj_realign(heartbeatValue); + } + + stepCount = motionController.NbSteps(); + if (stepCount.IsUpdated()) { + lv_label_set_text_fmt(stepValue, "%lu", stepCount.Get()); + lv_obj_realign(stepValue); + lv_obj_realign(stepIcon); + } + + currentWeather = weatherService.Current(); + if (currentWeather.IsUpdated()) { + auto optCurrentWeather = currentWeather.Get(); + if (optCurrentWeather) { + int16_t temp = optCurrentWeather->temperature; + char tempUnit = 'C'; + if (settingsController.GetWeatherFormat() == Controllers::Settings::WeatherFormat::Imperial) { + temp = Controllers::SimpleWeatherService::CelsiusToFahrenheit(temp); + tempUnit = 'F'; + } + temp = temp / 100 + (temp % 100 >= 50 ? 1 : 0); + lv_label_set_text_fmt(temperature, "%d°%c", temp, tempUnit); + lv_label_set_text(weatherIcon, Symbols::GetSymbol(optCurrentWeather->iconId)); + } else { + lv_label_set_text_static(temperature, ""); + lv_label_set_text(weatherIcon, ""); + } + lv_obj_realign(temperature); + lv_obj_realign(weatherIcon); + } +} diff --git a/src/displayapp/screens/WatchFaceDigital.h b/src/displayapp/screens/WatchFaceDigital.h new file mode 100644 index 00000000..7bb713cb --- /dev/null +++ b/src/displayapp/screens/WatchFaceDigital.h @@ -0,0 +1,99 @@ +#pragma once + +#include +#include +#include +#include +#include "displayapp/screens/Screen.h" +#include "components/datetime/DateTimeController.h" +#include "components/ble/SimpleWeatherService.h" +#include "components/ble/BleController.h" +#include "displayapp/widgets/StatusIcons.h" +#include "utility/DirtyValue.h" +#include "displayapp/apps/Apps.h" + +namespace Pinetime { + namespace Controllers { + class Settings; + class Battery; + class Ble; + class NotificationManager; + class HeartRateController; + class MotionController; + } + + namespace Applications { + namespace Screens { + + class WatchFaceDigital : public Screen { + public: + WatchFaceDigital(Controllers::DateTime& dateTimeController, + const Controllers::Battery& batteryController, + const Controllers::Ble& bleController, + Controllers::NotificationManager& notificationManager, + Controllers::Settings& settingsController, + Controllers::HeartRateController& heartRateController, + Controllers::MotionController& motionController, + Controllers::SimpleWeatherService& weather); + ~WatchFaceDigital() override; + + void Refresh() override; + + private: + uint8_t displayedHour = -1; + uint8_t displayedMinute = -1; + + Utility::DirtyValue> currentDateTime {}; + Utility::DirtyValue stepCount {}; + Utility::DirtyValue heartbeat {}; + Utility::DirtyValue heartbeatRunning {}; + Utility::DirtyValue notificationState {}; + Utility::DirtyValue> currentWeather {}; + + Utility::DirtyValue> currentDate; + + lv_obj_t* label_time; + lv_obj_t* label_time_ampm; + lv_obj_t* label_date; + lv_obj_t* heartbeatIcon; + lv_obj_t* heartbeatValue; + lv_obj_t* stepIcon; + lv_obj_t* stepValue; + lv_obj_t* notificationIcon; + lv_obj_t* weatherIcon; + lv_obj_t* temperature; + + Controllers::DateTime& dateTimeController; + Controllers::NotificationManager& notificationManager; + Controllers::Settings& settingsController; + Controllers::HeartRateController& heartRateController; + Controllers::MotionController& motionController; + Controllers::SimpleWeatherService& weatherService; + + lv_task_t* taskRefresh; + Widgets::StatusIcons statusIcons; + }; + } + + template <> + struct WatchFaceTraits { + static constexpr WatchFace watchFace = WatchFace::Digital; + static constexpr const char* name = "Digital face"; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::WatchFaceDigital(controllers.dateTimeController, + controllers.batteryController, + controllers.bleController, + controllers.notificationManager, + controllers.settingsController, + controllers.heartRateController, + controllers.motionController, + *controllers.weatherController); + }; + + static bool IsAvailable(Pinetime::Controllers::FS& /*filesystem*/) { + return true; + } + }; + } +} diff --git a/src/displayapp/screens/WatchFaceFuzzy.cpp b/src/displayapp/screens/WatchFaceFuzzy.cpp new file mode 100644 index 00000000..ff75de80 --- /dev/null +++ b/src/displayapp/screens/WatchFaceFuzzy.cpp @@ -0,0 +1,189 @@ +#include "displayapp/screens/WatchFaceFuzzy.h" + +#include +#include +#include "displayapp/screens/NotificationIcon.h" +#include "displayapp/screens/Symbols.h" +#include "displayapp/screens/WeatherSymbols.h" +#include "components/ble/NotificationManager.h" +#include "components/heartrate/HeartRateController.h" +#include "components/motion/MotionController.h" +#include "components/ble/SimpleWeatherService.h" +#include "components/settings/Settings.h" + +using namespace Pinetime::Applications::Screens; + +WatchFaceFuzzy::WatchFaceFuzzy(Controllers::DateTime& dateTimeController, + Controllers::NotificationManager& notificationManager, + Controllers::Settings& settingsController, + Controllers::HeartRateController& heartRateController, + Controllers::MotionController& motionController, + Controllers::SimpleWeatherService& weatherService) + : currentDateTime {{}}, + dateTimeController {dateTimeController}, + notificationManager {notificationManager}, + settingsController {settingsController}, + heartRateController {heartRateController}, + motionController {motionController}, + weatherService {weatherService} +{ + + notificationIcon = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(notificationIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_LIME); + lv_label_set_text_static(notificationIcon, NotificationIcon::GetIcon(false)); + lv_obj_align(notificationIcon, nullptr, LV_ALIGN_IN_TOP_LEFT, 0, 0); + + weatherIcon = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(weatherIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x999999)); + lv_obj_set_style_local_text_font(weatherIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &fontawesome_weathericons); + lv_label_set_text(weatherIcon, ""); + lv_obj_align(weatherIcon, nullptr, LV_ALIGN_IN_TOP_MID, -20, 50); + lv_obj_set_auto_realign(weatherIcon, true); + + temperature = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(temperature, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x999999)); + lv_label_set_text(temperature, ""); + lv_obj_align(temperature, nullptr, LV_ALIGN_IN_TOP_MID, 20, 50); + + label_date = lv_label_create(lv_scr_act(), nullptr); + lv_obj_align(label_date, lv_scr_act(), LV_ALIGN_CENTER, 0, 60); + lv_obj_set_style_local_text_color(label_date, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x999999)); + + label_time = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_font(label_time, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_bold_24); + lv_label_set_recolor(label_time, true); + lv_obj_align(label_time, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 0, 0); + + heartbeatIcon = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_text_static(heartbeatIcon, Symbols::heartBeat); + lv_obj_set_style_local_text_color(heartbeatIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0xCE1B1B)); + lv_obj_align(heartbeatIcon, lv_scr_act(), LV_ALIGN_IN_BOTTOM_LEFT, 0, 0); + + heartbeatValue = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(heartbeatValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0xCE1B1B)); + lv_label_set_text_static(heartbeatValue, ""); + lv_obj_align(heartbeatValue, heartbeatIcon, LV_ALIGN_OUT_RIGHT_MID, 5, 0); + + stepValue = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(stepValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x00FFE7)); + lv_label_set_text_static(stepValue, "0"); + lv_obj_align(stepValue, lv_scr_act(), LV_ALIGN_IN_BOTTOM_RIGHT, 0, 0); + + stepIcon = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(stepIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x00FFE7)); + lv_label_set_text_static(stepIcon, Symbols::shoe); + lv_obj_align(stepIcon, stepValue, LV_ALIGN_OUT_LEFT_MID, -5, 0); + + taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); + Refresh(); +} + +WatchFaceFuzzy::~WatchFaceFuzzy() { + lv_task_del(taskRefresh); + lv_obj_clean(lv_scr_act()); +} + +void WatchFaceFuzzy::Refresh() { + notificationState = notificationManager.AreNewNotificationsAvailable(); + if (notificationState.IsUpdated()) { + lv_label_set_text_static(notificationIcon, NotificationIcon::GetIcon(notificationState.Get())); + } + + currentDateTime = std::chrono::time_point_cast(dateTimeController.CurrentDateTime()); + + if (currentDateTime.IsUpdated()) { + uint8_t hour = dateTimeController.Hours(); + uint8_t minute = dateTimeController.Minutes(); + + printTimeWords(hour, minute); + + currentDateTime = std::chrono::time_point_cast(dateTimeController.CurrentDateTime()); + if (currentDate.IsUpdated()) { + uint16_t year = dateTimeController.Year(); + uint8_t day = dateTimeController.Day(); + lv_label_set_text_fmt(label_date, + "%s %d %s %d", + dateTimeController.DayOfWeekShortToString(), + day, + dateTimeController.MonthShortToString(), + year); + lv_obj_realign(label_date); + } + } + + heartbeat = heartRateController.HeartRate(); + heartbeatRunning = heartRateController.State() != Controllers::HeartRateController::States::Stopped; + if (heartbeat.IsUpdated() || heartbeatRunning.IsUpdated()) { + if (heartbeatRunning.Get()) { + lv_obj_set_style_local_text_color(heartbeatIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0xCE1B1B)); + lv_label_set_text_fmt(heartbeatValue, "%d", heartbeat.Get()); + } else { + lv_obj_set_style_local_text_color(heartbeatIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x1B1B1B)); + lv_label_set_text_static(heartbeatValue, ""); + } + + lv_obj_realign(heartbeatIcon); + lv_obj_realign(heartbeatValue); + } + + stepCount = motionController.NbSteps(); + if (stepCount.IsUpdated()) { + lv_label_set_text_fmt(stepValue, "%lu", stepCount.Get()); + lv_obj_realign(stepValue); + lv_obj_realign(stepIcon); + } + + currentWeather = weatherService.Current(); + if (currentWeather.IsUpdated()) { + auto optCurrentWeather = currentWeather.Get(); + if (optCurrentWeather) { + int16_t temp = optCurrentWeather->temperature; + char tempUnit = 'C'; + if (settingsController.GetWeatherFormat() == Controllers::Settings::WeatherFormat::Imperial) { + temp = Controllers::SimpleWeatherService::CelsiusToFahrenheit(temp); + tempUnit = 'F'; + } + temp = temp / 100 + (temp % 100 >= 50 ? 1 : 0); + lv_label_set_text_fmt(temperature, "%d°%c", temp, tempUnit); + lv_label_set_text(weatherIcon, Symbols::GetSymbol(optCurrentWeather->iconId)); + } else { + lv_label_set_text_static(temperature, ""); + lv_label_set_text(weatherIcon, ""); + } + lv_obj_realign(temperature); + lv_obj_realign(weatherIcon); + } +} + +char const* WatchFaceFuzzy::mods[] = {"", "five", "ten", "quarter", "twenty", "twenty five", "half"}; +char const* WatchFaceFuzzy::nums[] = {"twelve", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve"}; + +void WatchFaceFuzzy::printTimeWords(int h, int m) { + const char* mod; + + if (m <= 30) { + mod = mods[m / 5]; + } else { + mod = mods[(60-m) / 5]; + } + h = (h % 12); + + if (m >= 57) { + sprintf(timeStr, "#ffffff nearly %s#\n#808080 o' clock#", nums[(h+1) % 12]); + } + else if (m == 0 || m <= 4) { + sprintf(timeStr, "#ffffff %s#\n#808080 o' clock#", nums[h]); + } + + else if (m <= 32) { + sprintf(timeStr, "#ffffff %s#\n#808080 past# #FFFFFF %s#", mod, nums[h]); + } + + else if (m > 32) { + sprintf(timeStr, "#ffffff %s#\n#808080 to# #FFFFFF %s#", mod, nums[(h+1) % 12]); + } + + printf("%s\n", timeStr); + lv_label_set_text(label_time, timeStr); + lv_obj_realign(label_time); +} diff --git a/src/displayapp/screens/WatchFaceFuzzy.h b/src/displayapp/screens/WatchFaceFuzzy.h new file mode 100644 index 00000000..1d30fbad --- /dev/null +++ b/src/displayapp/screens/WatchFaceFuzzy.h @@ -0,0 +1,100 @@ +#pragma once + +#include +#include +#include +#include +#include "displayapp/screens/Screen.h" +#include "components/datetime/DateTimeController.h" +#include "components/ble/SimpleWeatherService.h" +#include "components/ble/BleController.h" +#include "displayapp/widgets/StatusIcons.h" +#include "utility/DirtyValue.h" +#include "displayapp/apps/Apps.h" + +extern lv_font_t jetbrains_mono_bold_24; + +namespace Pinetime { + namespace Controllers { + class Settings; + class Battery; + class Ble; + class NotificationManager; + class HeartRateController; + class MotionController; + } + + namespace Applications { + namespace Screens { + + class WatchFaceFuzzy : public Screen { + public: + WatchFaceFuzzy(Controllers::DateTime& dateTimeController, + Controllers::NotificationManager& notificationManager, + Controllers::Settings& settingsController, + Controllers::HeartRateController& heartRateController, + Controllers::MotionController& motionController, + Controllers::SimpleWeatherService& weather); + ~WatchFaceFuzzy() override; + + void Refresh() override; + + private: + uint8_t displayedHour = -1; + uint8_t displayedMinute = -1; + + Utility::DirtyValue> currentDateTime {}; + Utility::DirtyValue stepCount {}; + Utility::DirtyValue heartbeat {}; + Utility::DirtyValue heartbeatRunning {}; + Utility::DirtyValue notificationState {}; + Utility::DirtyValue> currentWeather {}; + + Utility::DirtyValue> currentDate; + + lv_obj_t* label_time; + lv_obj_t* label_date; + lv_obj_t* heartbeatIcon; + lv_obj_t* heartbeatValue; + lv_obj_t* stepIcon; + lv_obj_t* stepValue; + lv_obj_t* notificationIcon; + lv_obj_t* weatherIcon; + lv_obj_t* temperature; + + Controllers::DateTime& dateTimeController; + Controllers::NotificationManager& notificationManager; + Controllers::Settings& settingsController; + Controllers::HeartRateController& heartRateController; + Controllers::MotionController& motionController; + Controllers::SimpleWeatherService& weatherService; + + lv_task_t* taskRefresh; + + static char const *nums[]; + static char const *mods[]; + char timeStr[64]; + void printTimeWords(int hour, int minute); + }; + } + + template <> + struct WatchFaceTraits { + static constexpr WatchFace watchFace = WatchFace::Fuzzy; + static constexpr const char* name = "Fuzzy face"; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::WatchFaceFuzzy(controllers.dateTimeController, + controllers.notificationManager, + controllers.settingsController, + controllers.heartRateController, + controllers.motionController, + *controllers.weatherController); + }; + + static bool IsAvailable(Pinetime::Controllers::FS& /*filesystem*/) { + return true; + } + }; + } +} diff --git a/src/displayapp/screens/WatchFaceInfineat.cpp b/src/displayapp/screens/WatchFaceInfineat.cpp new file mode 100644 index 00000000..4c6fc196 --- /dev/null +++ b/src/displayapp/screens/WatchFaceInfineat.cpp @@ -0,0 +1,509 @@ +#include "displayapp/screens/WatchFaceInfineat.h" + +#include +#include +#include "displayapp/screens/Symbols.h" +#include "displayapp/screens/BleIcon.h" +#include "components/settings/Settings.h" +#include "components/battery/BatteryController.h" +#include "components/ble/BleController.h" +#include "components/ble/NotificationManager.h" +#include "components/motion/MotionController.h" + +using namespace Pinetime::Applications::Screens; + +namespace { + void event_handler(lv_obj_t* obj, lv_event_t event) { + auto* screen = static_cast(obj->user_data); + screen->UpdateSelected(obj, event); + } + + enum class colors { + orange, + blue, + green, + rainbow, + gray, + nordBlue, + nordGreen, + }; + + constexpr int nColors = 7; // must match number of colors in InfineatColors + + constexpr int nLines = WatchFaceInfineat::nLines; + + constexpr std::array orangeColors = {LV_COLOR_MAKE(0xfd, 0x87, 0x2b), + LV_COLOR_MAKE(0xdb, 0x33, 0x16), + LV_COLOR_MAKE(0x6f, 0x10, 0x00), + LV_COLOR_MAKE(0xfd, 0x7a, 0x0a), + LV_COLOR_MAKE(0xff, 0xff, 0xff), + LV_COLOR_MAKE(0xff, 0xff, 0xff), + LV_COLOR_MAKE(0xff, 0xff, 0xff), + LV_COLOR_MAKE(0xe8, 0x51, 0x02), + LV_COLOR_MAKE(0xea, 0x1c, 0x00)}; + constexpr std::array blueColors = {LV_COLOR_MAKE(0xe7, 0xf8, 0xff), + LV_COLOR_MAKE(0x22, 0x32, 0xd0), + LV_COLOR_MAKE(0x18, 0x2a, 0x8b), + LV_COLOR_MAKE(0xe7, 0xf8, 0xff), + LV_COLOR_MAKE(0xff, 0xff, 0xff), + LV_COLOR_MAKE(0xff, 0xff, 0xff), + LV_COLOR_MAKE(0xff, 0xff, 0xff), + LV_COLOR_MAKE(0x59, 0x91, 0xff), + LV_COLOR_MAKE(0x16, 0x36, 0xff)}; + constexpr std::array greenColors = {LV_COLOR_MAKE(0xb8, 0xff, 0x9b), + LV_COLOR_MAKE(0x08, 0x86, 0x08), + LV_COLOR_MAKE(0x00, 0x4a, 0x00), + LV_COLOR_MAKE(0xb8, 0xff, 0x9b), + LV_COLOR_MAKE(0xff, 0xff, 0xff), + LV_COLOR_MAKE(0xff, 0xff, 0xff), + LV_COLOR_MAKE(0xff, 0xff, 0xff), + LV_COLOR_MAKE(0x62, 0xd5, 0x15), + LV_COLOR_MAKE(0x00, 0x74, 0x00)}; + constexpr std::array rainbowColors = {LV_COLOR_MAKE(0x2d, 0xa4, 0x00), + LV_COLOR_MAKE(0xac, 0x09, 0xc4), + LV_COLOR_MAKE(0xfe, 0x03, 0x03), + LV_COLOR_MAKE(0x0d, 0x57, 0xff), + LV_COLOR_MAKE(0xff, 0xff, 0xff), + LV_COLOR_MAKE(0xff, 0xff, 0xff), + LV_COLOR_MAKE(0xff, 0xff, 0xff), + LV_COLOR_MAKE(0xe0, 0xb9, 0x00), + LV_COLOR_MAKE(0xe8, 0x51, 0x02)}; + constexpr std::array grayColors = {LV_COLOR_MAKE(0xee, 0xee, 0xee), + LV_COLOR_MAKE(0x98, 0x95, 0x9b), + LV_COLOR_MAKE(0x19, 0x19, 0x19), + LV_COLOR_MAKE(0xee, 0xee, 0xee), + LV_COLOR_MAKE(0xff, 0xff, 0xff), + LV_COLOR_MAKE(0xff, 0xff, 0xff), + LV_COLOR_MAKE(0xff, 0xff, 0xff), + LV_COLOR_MAKE(0x91, 0x91, 0x91), + LV_COLOR_MAKE(0x3a, 0x3a, 0x3a)}; + constexpr std::array nordBlueColors = {LV_COLOR_MAKE(0xc3, 0xda, 0xf2), + LV_COLOR_MAKE(0x4d, 0x78, 0xce), + LV_COLOR_MAKE(0x15, 0x34, 0x51), + LV_COLOR_MAKE(0xc3, 0xda, 0xf2), + LV_COLOR_MAKE(0xff, 0xff, 0xff), + LV_COLOR_MAKE(0xff, 0xff, 0xff), + LV_COLOR_MAKE(0xff, 0xff, 0xff), + LV_COLOR_MAKE(0x5d, 0x8a, 0xd2), + LV_COLOR_MAKE(0x21, 0x51, 0x8a)}; + constexpr std::array nordGreenColors = {LV_COLOR_MAKE(0xd5, 0xf0, 0xe9), + LV_COLOR_MAKE(0x23, 0x83, 0x73), + LV_COLOR_MAKE(0x1d, 0x41, 0x3f), + LV_COLOR_MAKE(0xd5, 0xf0, 0xe9), + LV_COLOR_MAKE(0xff, 0xff, 0xff), + LV_COLOR_MAKE(0xff, 0xff, 0xff), + LV_COLOR_MAKE(0xff, 0xff, 0xff), + LV_COLOR_MAKE(0x2f, 0xb8, 0xa2), + LV_COLOR_MAKE(0x11, 0x70, 0x5a)}; + + constexpr const std::array* returnColor(colors color) { + if (color == colors::orange) { + return &orangeColors; + } + if (color == colors::blue) { + return &blueColors; + } + if (color == colors::green) { + return &greenColors; + } + if (color == colors::rainbow) { + return &rainbowColors; + } + if (color == colors::gray) { + return &grayColors; + } + if (color == colors::nordBlue) { + return &nordBlueColors; + } + return &nordGreenColors; + } +} + +WatchFaceInfineat::WatchFaceInfineat(Controllers::DateTime& dateTimeController, + const Controllers::Battery& batteryController, + const Controllers::Ble& bleController, + Controllers::NotificationManager& notificationManager, + Controllers::Settings& settingsController, + Controllers::MotionController& motionController, + Controllers::FS& filesystem) + : currentDateTime {{}}, + dateTimeController {dateTimeController}, + batteryController {batteryController}, + bleController {bleController}, + notificationManager {notificationManager}, + settingsController {settingsController}, + motionController {motionController} { + lfs_file f = {}; + if (filesystem.FileOpen(&f, "/fonts/teko.bin", LFS_O_RDONLY) >= 0) { + filesystem.FileClose(&f); + font_teko = lv_font_load("F:/fonts/teko.bin"); + } + + if (filesystem.FileOpen(&f, "/fonts/bebas.bin", LFS_O_RDONLY) >= 0) { + filesystem.FileClose(&f); + font_bebas = lv_font_load("F:/fonts/bebas.bin"); + } + + // Side Cover + static constexpr lv_point_t linePoints[nLines][2] = {{{30, 25}, {68, -8}}, + {{26, 167}, {43, 216}}, + {{27, 40}, {27, 196}}, + {{12, 182}, {65, 249}}, + {{17, 99}, {17, 144}}, + {{14, 81}, {40, 127}}, + {{14, 163}, {40, 118}}, + {{-20, 124}, {25, -11}}, + {{-29, 89}, {27, 254}}}; + + static constexpr lv_style_int_t lineWidths[nLines] = {18, 15, 14, 22, 20, 18, 18, 52, 48}; + + const std::array* colors = returnColor(static_cast(settingsController.GetInfineatColorIndex())); + for (int i = 0; i < nLines; i++) { + lines[i] = lv_line_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_line_width(lines[i], LV_LINE_PART_MAIN, LV_STATE_DEFAULT, lineWidths[i]); + lv_color_t color = (*colors)[i]; + lv_obj_set_style_local_line_color(lines[i], LV_LINE_PART_MAIN, LV_STATE_DEFAULT, color); + lv_line_set_points(lines[i], linePoints[i], 2); + } + + logoPine = lv_img_create(lv_scr_act(), nullptr); + lv_img_set_src(logoPine, "F:/images/pine_small.bin"); + lv_obj_set_pos(logoPine, 15, 106); + + lineBattery = lv_line_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_line_width(lineBattery, LV_LINE_PART_MAIN, LV_STATE_DEFAULT, 24); + lv_obj_set_style_local_line_color(lineBattery, LV_LINE_PART_MAIN, LV_STATE_DEFAULT, (*colors)[4]); + lv_obj_set_style_local_line_opa(lineBattery, LV_LINE_PART_MAIN, LV_STATE_DEFAULT, 190); + lineBatteryPoints[0] = {27, 105}; + lineBatteryPoints[1] = {27, 106}; + lv_line_set_points(lineBattery, lineBatteryPoints, 2); + lv_obj_move_foreground(lineBattery); + + notificationIcon = lv_obj_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_bg_color(notificationIcon, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, (*colors)[7]); + lv_obj_set_style_local_radius(notificationIcon, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_RADIUS_CIRCLE); + lv_obj_set_size(notificationIcon, 13, 13); + lv_obj_set_hidden(notificationIcon, true); + + if (!settingsController.GetInfineatShowSideCover()) { + ToggleBatteryIndicatorColor(false); + for (auto& line : lines) { + lv_obj_set_hidden(line, true); + } + } + + timeContainer = lv_obj_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_bg_opa(timeContainer, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_TRANSP); + lv_obj_set_size(timeContainer, 185, 185); + lv_obj_align(timeContainer, lv_scr_act(), LV_ALIGN_CENTER, 0, -10); + + labelHour = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_text_static(labelHour, "01"); + lv_obj_set_style_local_text_font(labelHour, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, font_bebas); + lv_obj_align(labelHour, timeContainer, LV_ALIGN_IN_TOP_MID, 0, 0); + + labelMinutes = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_font(labelMinutes, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, font_bebas); + lv_label_set_text_static(labelMinutes, "00"); + lv_obj_align(labelMinutes, timeContainer, LV_ALIGN_IN_BOTTOM_MID, 0, 0); + + labelTimeAmPm = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_font(labelTimeAmPm, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, font_teko); + + lv_label_set_text_static(labelTimeAmPm, ""); + lv_obj_align(labelTimeAmPm, timeContainer, LV_ALIGN_OUT_RIGHT_TOP, 0, 15); + + dateContainer = lv_obj_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_bg_opa(dateContainer, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_TRANSP); + lv_obj_set_size(dateContainer, 60, 30); + lv_obj_align(dateContainer, lv_scr_act(), LV_ALIGN_IN_RIGHT_MID, 0, 5); + + static constexpr lv_color_t grayColor = LV_COLOR_MAKE(0x99, 0x99, 0x99); + labelDate = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(labelDate, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, grayColor); + lv_obj_set_style_local_text_font(labelDate, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, font_teko); + lv_obj_align(labelDate, dateContainer, LV_ALIGN_IN_TOP_MID, 0, 0); + lv_label_set_text_static(labelDate, "Mon 01"); + + bleIcon = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(bleIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, grayColor); + lv_label_set_text_static(bleIcon, Symbols::bluetooth); + lv_obj_align(bleIcon, dateContainer, LV_ALIGN_OUT_BOTTOM_MID, 0, 0); + + stepValue = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(stepValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, grayColor); + lv_obj_set_style_local_text_font(stepValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, font_teko); + lv_obj_align(stepValue, lv_scr_act(), LV_ALIGN_IN_BOTTOM_RIGHT, 10, 0); + lv_label_set_text_static(stepValue, "0"); + + stepIcon = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(stepIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, grayColor); + lv_label_set_text_static(stepIcon, Symbols::shoe); + lv_obj_align(stepIcon, stepValue, LV_ALIGN_OUT_LEFT_MID, -5, 0); + + // Setting buttons + btnClose = lv_btn_create(lv_scr_act(), nullptr); + btnClose->user_data = this; + lv_obj_set_size(btnClose, 60, 60); + lv_obj_align(btnClose, lv_scr_act(), LV_ALIGN_CENTER, 0, -80); + lv_obj_set_style_local_bg_opa(btnClose, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_70); + lv_obj_t* lblClose = lv_label_create(btnClose, nullptr); + lv_label_set_text_static(lblClose, "X"); + lv_obj_set_event_cb(btnClose, event_handler); + lv_obj_set_hidden(btnClose, true); + + btnNextColor = lv_btn_create(lv_scr_act(), nullptr); + btnNextColor->user_data = this; + lv_obj_set_size(btnNextColor, 60, 60); + lv_obj_align(btnNextColor, lv_scr_act(), LV_ALIGN_IN_RIGHT_MID, -15, 0); + lv_obj_set_style_local_bg_opa(btnNextColor, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_70); + lv_obj_t* lblNextColor = lv_label_create(btnNextColor, nullptr); + lv_label_set_text_static(lblNextColor, ">"); + lv_obj_set_event_cb(btnNextColor, event_handler); + lv_obj_set_hidden(btnNextColor, true); + + btnPrevColor = lv_btn_create(lv_scr_act(), nullptr); + btnPrevColor->user_data = this; + lv_obj_set_size(btnPrevColor, 60, 60); + lv_obj_align(btnPrevColor, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 15, 0); + lv_obj_set_style_local_bg_opa(btnPrevColor, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_70); + lv_obj_t* lblPrevColor = lv_label_create(btnPrevColor, nullptr); + lv_label_set_text_static(lblPrevColor, "<"); + lv_obj_set_event_cb(btnPrevColor, event_handler); + lv_obj_set_hidden(btnPrevColor, true); + + btnToggleCover = lv_btn_create(lv_scr_act(), nullptr); + btnToggleCover->user_data = this; + lv_obj_set_size(btnToggleCover, 60, 60); + lv_obj_align(btnToggleCover, lv_scr_act(), LV_ALIGN_CENTER, 0, 80); + lv_obj_set_style_local_bg_opa(btnToggleCover, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_70); + const char* labelToggle = settingsController.GetInfineatShowSideCover() ? "ON" : "OFF"; + lblToggle = lv_label_create(btnToggleCover, nullptr); + lv_label_set_text_static(lblToggle, labelToggle); + lv_obj_set_event_cb(btnToggleCover, event_handler); + lv_obj_set_hidden(btnToggleCover, true); + + // Button to access the settings + btnSettings = lv_btn_create(lv_scr_act(), nullptr); + btnSettings->user_data = this; + lv_obj_set_size(btnSettings, 150, 150); + lv_obj_align(btnSettings, lv_scr_act(), LV_ALIGN_CENTER, 0, 0); + lv_obj_set_style_local_radius(btnSettings, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, 30); + lv_obj_set_style_local_bg_opa(btnSettings, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_70); + lv_obj_set_event_cb(btnSettings, event_handler); + labelBtnSettings = lv_label_create(btnSettings, nullptr); + lv_obj_set_style_local_text_font(labelBtnSettings, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &lv_font_sys_48); + lv_label_set_text_static(labelBtnSettings, Symbols::settings); + lv_obj_set_hidden(btnSettings, true); + + taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); + Refresh(); +} + +WatchFaceInfineat::~WatchFaceInfineat() { + lv_task_del(taskRefresh); + + if (font_bebas != nullptr) { + lv_font_free(font_bebas); + } + if (font_teko != nullptr) { + lv_font_free(font_teko); + } + + lv_obj_clean(lv_scr_act()); +} + +bool WatchFaceInfineat::OnTouchEvent(Pinetime::Applications::TouchEvents event) { + if ((event == Pinetime::Applications::TouchEvents::LongTap) && lv_obj_get_hidden(btnSettings)) { + lv_obj_set_hidden(btnSettings, false); + savedTick = lv_tick_get(); + return true; + } + // Prevent screen from sleeping when double tapping with settings on + if ((event == Pinetime::Applications::TouchEvents::DoubleTap) && !lv_obj_get_hidden(btnClose)) { + return true; + } + return false; +} + +void WatchFaceInfineat::CloseMenu() { + settingsController.SaveSettings(); + lv_obj_set_hidden(btnClose, true); + lv_obj_set_hidden(btnNextColor, true); + lv_obj_set_hidden(btnPrevColor, true); + lv_obj_set_hidden(btnToggleCover, true); +} + +bool WatchFaceInfineat::OnButtonPushed() { + if (!lv_obj_get_hidden(btnClose)) { + CloseMenu(); + return true; + } + return false; +} + +void WatchFaceInfineat::UpdateSelected(lv_obj_t* object, lv_event_t event) { + if (event == LV_EVENT_CLICKED) { + bool showSideCover = settingsController.GetInfineatShowSideCover(); + int colorIndex = settingsController.GetInfineatColorIndex(); + + if (object == btnSettings) { + lv_obj_set_hidden(btnSettings, true); + lv_obj_set_hidden(btnClose, false); + lv_obj_set_hidden(btnNextColor, !showSideCover); + lv_obj_set_hidden(btnPrevColor, !showSideCover); + lv_obj_set_hidden(btnToggleCover, false); + } + if (object == btnClose) { + CloseMenu(); + } + if (object == btnToggleCover) { + settingsController.SetInfineatShowSideCover(!showSideCover); + ToggleBatteryIndicatorColor(!showSideCover); + for (auto& line : lines) { + lv_obj_set_hidden(line, showSideCover); + } + lv_obj_set_hidden(btnNextColor, showSideCover); + lv_obj_set_hidden(btnPrevColor, showSideCover); + const char* labelToggle = showSideCover ? "OFF" : "ON"; + lv_label_set_text_static(lblToggle, labelToggle); + } + if (object == btnNextColor) { + colorIndex = (colorIndex + 1) % nColors; + settingsController.SetInfineatColorIndex(colorIndex); + } + if (object == btnPrevColor) { + colorIndex -= 1; + if (colorIndex < 0) + colorIndex = nColors - 1; + settingsController.SetInfineatColorIndex(colorIndex); + } + if (object == btnNextColor || object == btnPrevColor) { + const std::array* colors = returnColor(static_cast(settingsController.GetInfineatColorIndex())); + for (int i = 0; i < nLines; i++) { + lv_color_t color = (*colors)[i]; + lv_obj_set_style_local_line_color(lines[i], LV_LINE_PART_MAIN, LV_STATE_DEFAULT, color); + } + lv_obj_set_style_local_line_color(lineBattery, LV_LINE_PART_MAIN, LV_STATE_DEFAULT, (*colors)[4]); + lv_obj_set_style_local_bg_color(notificationIcon, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, (*colors)[7]); + } + } +} + +void WatchFaceInfineat::Refresh() { + notificationState = notificationManager.AreNewNotificationsAvailable(); + if (notificationState.IsUpdated()) { + lv_obj_set_hidden(notificationIcon, !notificationState.Get()); + lv_obj_align(notificationIcon, lv_scr_act(), LV_ALIGN_IN_TOP_RIGHT, 0, 0); + } + + 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[3] = "AM"; + 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(labelTimeAmPm, ampmChar); + } + lv_label_set_text_fmt(labelHour, "%02d", hour); + lv_label_set_text_fmt(labelMinutes, "%02d", minute); + + if (settingsController.GetClockType() == Controllers::Settings::ClockType::H12) { + lv_obj_align(labelTimeAmPm, timeContainer, LV_ALIGN_OUT_RIGHT_TOP, 0, 10); + lv_obj_align(labelHour, timeContainer, LV_ALIGN_IN_TOP_MID, 0, 5); + lv_obj_align(labelMinutes, timeContainer, LV_ALIGN_IN_BOTTOM_MID, 0, 0); + } + + currentDate = std::chrono::time_point_cast(currentDateTime.Get()); + if (currentDate.IsUpdated()) { + uint8_t day = dateTimeController.Day(); + Controllers::DateTime::Days dayOfWeek = dateTimeController.DayOfWeek(); + lv_label_set_text_fmt(labelDate, "%s %02d", dateTimeController.DayOfWeekShortToStringLow(dayOfWeek), day); + lv_obj_realign(labelDate); + } + } + + batteryPercentRemaining = batteryController.PercentRemaining(); + isCharging = batteryController.IsCharging(); + if (batteryController.IsCharging()) { // Charging battery animation + chargingBatteryPercent += 1; + if (chargingBatteryPercent > 100) { + chargingBatteryPercent = batteryPercentRemaining.Get(); + } + SetBatteryLevel(chargingBatteryPercent); + } else if (isCharging.IsUpdated() || batteryPercentRemaining.IsUpdated()) { + chargingBatteryPercent = batteryPercentRemaining.Get(); + SetBatteryLevel(chargingBatteryPercent); + } + + bleState = bleController.IsConnected(); + bleRadioEnabled = bleController.IsRadioEnabled(); + if (bleState.IsUpdated()) { + lv_label_set_text_static(bleIcon, BleIcon::GetIcon(bleState.Get())); + lv_obj_align(bleIcon, dateContainer, LV_ALIGN_OUT_BOTTOM_MID, 0, 3); + } + + stepCount = motionController.NbSteps(); + if (stepCount.IsUpdated()) { + lv_label_set_text_fmt(stepValue, "%lu", stepCount.Get()); + lv_obj_align(stepValue, lv_scr_act(), LV_ALIGN_IN_BOTTOM_MID, 10, 0); + lv_obj_align(stepIcon, stepValue, LV_ALIGN_OUT_LEFT_MID, -5, 0); + } + + if (!lv_obj_get_hidden(btnSettings)) { + if ((savedTick > 0) && (lv_tick_get() - savedTick > 3000)) { + lv_obj_set_hidden(btnSettings, true); + savedTick = 0; + } + } +} + +void WatchFaceInfineat::SetBatteryLevel(uint8_t batteryPercent) { + // starting point (y) + Pine64 logo height * (100 - batteryPercent) / 100 + lineBatteryPoints[1] = {27, static_cast(105 + 32 * (100 - batteryPercent) / 100)}; + lv_line_set_points(lineBattery, lineBatteryPoints, 2); +} + +void WatchFaceInfineat::ToggleBatteryIndicatorColor(bool showSideCover) { + if (!showSideCover) { // make indicator and notification icon color white + lv_obj_set_style_local_image_recolor_opa(logoPine, LV_IMG_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_100); + lv_obj_set_style_local_image_recolor(logoPine, LV_IMG_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE); + lv_obj_set_style_local_line_color(lineBattery, LV_LINE_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_BLACK); + lv_obj_set_style_local_bg_color(notificationIcon, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE); + } else { + lv_obj_set_style_local_image_recolor_opa(logoPine, LV_IMG_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_0); + const std::array* colors = returnColor(static_cast(settingsController.GetInfineatColorIndex())); + lv_obj_set_style_local_line_color(lineBattery, LV_LINE_PART_MAIN, LV_STATE_DEFAULT, (*colors)[4]); + lv_obj_set_style_local_bg_color(notificationIcon, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, (*colors)[7]); + } +} + +bool WatchFaceInfineat::IsAvailable(Pinetime::Controllers::FS& filesystem) { + lfs_file file = {}; + + if (filesystem.FileOpen(&file, "/fonts/teko.bin", LFS_O_RDONLY) < 0) { + return false; + } + + filesystem.FileClose(&file); + if (filesystem.FileOpen(&file, "/fonts/bebas.bin", LFS_O_RDONLY) < 0) { + return false; + } + + filesystem.FileClose(&file); + if (filesystem.FileOpen(&file, "/images/pine_small.bin", LFS_O_RDONLY) < 0) { + return false; + } + + filesystem.FileClose(&file); + return true; +} diff --git a/src/displayapp/screens/WatchFaceInfineat.h b/src/displayapp/screens/WatchFaceInfineat.h new file mode 100644 index 00000000..55c43f98 --- /dev/null +++ b/src/displayapp/screens/WatchFaceInfineat.h @@ -0,0 +1,123 @@ +#pragma once + +#include +#include +#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 Battery; + class Ble; + class NotificationManager; + class MotionController; + } + + namespace Applications { + namespace Screens { + + class WatchFaceInfineat : public Screen { + public: + static constexpr int nLines = 9; + WatchFaceInfineat(Controllers::DateTime& dateTimeController, + const Controllers::Battery& batteryController, + const Controllers::Ble& bleController, + Controllers::NotificationManager& notificationManager, + Controllers::Settings& settingsController, + Controllers::MotionController& motionController, + Controllers::FS& fs); + + ~WatchFaceInfineat() override; + + bool OnTouchEvent(TouchEvents event) override; + bool OnButtonPushed() override; + void UpdateSelected(lv_obj_t* object, lv_event_t event); + void CloseMenu(); + + void Refresh() override; + + static bool IsAvailable(Pinetime::Controllers::FS& filesystem); + + private: + uint32_t savedTick = 0; + uint8_t chargingBatteryPercent = 101; // not a mistake ;) + + Utility::DirtyValue batteryPercentRemaining {}; + Utility::DirtyValue isCharging {}; + Utility::DirtyValue bleState {}; + Utility::DirtyValue bleRadioEnabled {}; + Utility::DirtyValue> currentDateTime {}; + Utility::DirtyValue stepCount {}; + Utility::DirtyValue notificationState {}; + Utility::DirtyValue> currentDate; + + // Lines making up the side cover + lv_obj_t* lineBattery; + + lv_point_t lineBatteryPoints[2]; + + lv_obj_t* logoPine; + + lv_obj_t* timeContainer; + lv_obj_t* labelHour; + lv_obj_t* labelMinutes; + lv_obj_t* labelTimeAmPm; + lv_obj_t* dateContainer; + lv_obj_t* labelDate; + lv_obj_t* bleIcon; + lv_obj_t* stepIcon; + lv_obj_t* stepValue; + lv_obj_t* notificationIcon; + lv_obj_t* btnClose; + lv_obj_t* btnNextColor; + lv_obj_t* btnToggleCover; + lv_obj_t* btnPrevColor; + lv_obj_t* btnSettings; + lv_obj_t* labelBtnSettings; + lv_obj_t* lblToggle; + + lv_obj_t* lines[nLines]; + + Controllers::DateTime& dateTimeController; + const Controllers::Battery& batteryController; + const Controllers::Ble& bleController; + Controllers::NotificationManager& notificationManager; + Controllers::Settings& settingsController; + Controllers::MotionController& motionController; + + void SetBatteryLevel(uint8_t batteryPercent); + void ToggleBatteryIndicatorColor(bool showSideCover); + + lv_task_t* taskRefresh; + lv_font_t* font_teko = nullptr; + lv_font_t* font_bebas = nullptr; + }; + } + + template <> + struct WatchFaceTraits { + static constexpr WatchFace watchFace = WatchFace::Infineat; + static constexpr const char* name = "Infineat face"; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::WatchFaceInfineat(controllers.dateTimeController, + controllers.batteryController, + controllers.bleController, + controllers.notificationManager, + controllers.settingsController, + controllers.motionController, + controllers.filesystem); + }; + + static bool IsAvailable(Pinetime::Controllers::FS& filesystem) { + return Screens::WatchFaceInfineat::IsAvailable(filesystem); + } + }; + } +} diff --git a/src/displayapp/screens/WatchFacePineTimeStyle.cpp b/src/displayapp/screens/WatchFacePineTimeStyle.cpp index 296323d3..e56031f7 100644 --- a/src/displayapp/screens/WatchFacePineTimeStyle.cpp +++ b/src/displayapp/screens/WatchFacePineTimeStyle.cpp @@ -540,7 +540,6 @@ void WatchFacePineTimeStyle::Refresh() { } currentWeather = weatherService.Current(); - if (currentWeather.IsUpdated()) { auto optCurrentWeather = currentWeather.Get(); if (optCurrentWeather) { @@ -551,12 +550,10 @@ void WatchFacePineTimeStyle::Refresh() { temp = temp / 100 + (temp % 100 >= 50 ? 1 : 0); lv_label_set_text_fmt(temperature, "%d°", temp); lv_label_set_text(weatherIcon, Symbols::GetSymbol(optCurrentWeather->iconId)); - lv_obj_realign(temperature); - lv_obj_realign(weatherIcon); + } else { + lv_label_set_text(temperature, "--"); + lv_label_set_text(weatherIcon, Symbols::ban); } - } else { - lv_label_set_text(temperature, "--"); - lv_label_set_text(weatherIcon, Symbols::ban); lv_obj_realign(temperature); lv_obj_realign(weatherIcon); } diff --git a/src/displayapp/screens/WatchFaceSundial.cpp b/src/displayapp/screens/WatchFaceSundial.cpp new file mode 100644 index 00000000..7bac1ff9 --- /dev/null +++ b/src/displayapp/screens/WatchFaceSundial.cpp @@ -0,0 +1,259 @@ +#include "displayapp/screens/WatchFaceSundial.h" + +#include +#include +// #include +#include "displayapp/screens/NotificationIcon.h" +#include "displayapp/screens/Symbols.h" +#include "displayapp/screens/WeatherSymbols.h" +#include "displayapp/InfiniTimeTheme.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/ble/SimpleWeatherService.h" +#include "components/settings/Settings.h" +#include "sunset/src/sunset.h" + +using namespace Pinetime::Applications::Screens; + +namespace { + int16_t HourLength = 70; + constexpr int16_t MinuteLength = 90; + constexpr int16_t SecondLength = 110; + constexpr int16_t SunDialVerticalOffset = 40; + + // sin(90) = 1 so the value of _lv_trigo_sin(90) is the scaling factor + const auto LV_TRIG_SCALE = _lv_trigo_sin(90); + const lv_color_t DARK_GRAY = lv_color_make(48, 48, 48); + const lv_color_t DARK_ORANGE = lv_color_make(48, 26, 0); + + int16_t Cosine(int16_t angle) { + return _lv_trigo_sin(angle + 90); + } + + int16_t Sine(int16_t angle) { + return _lv_trigo_sin(angle); + } + + int16_t CoordinateXRelocate(int16_t x) { + return (x + LV_HOR_RES / 2); + } + + + int16_t CoordinateYRelocateSundial(int16_t y) { + return std::abs(y - SunDialVerticalOffset); + } + + lv_point_t CoordinateRelocateSundial(int16_t radius, int16_t angle) { + return lv_point_t {.x = CoordinateXRelocate(radius * static_cast(Sine(angle)) / LV_TRIG_SCALE), + .y = CoordinateYRelocateSundial(radius * static_cast(Cosine(angle)) / LV_TRIG_SCALE)}; + } + +} + +WatchFaceSundial::WatchFaceSundial(Controllers::DateTime& dateTimeController, + Controllers::NotificationManager& notificationManager, + Controllers::Settings& settingsController) + : currentDateTime {{}}, + dateTimeController {dateTimeController}, + notificationManager {notificationManager}, + settingsController {settingsController} +{ + // minor_scales = lv_linemeter_create(lv_scr_act(), nullptr); + // lv_linemeter_set_scale(minor_scales, 300, 51); + // lv_linemeter_set_angle_offset(minor_scales, 180); + // lv_obj_set_size(minor_scales, 240, 240); + // lv_obj_align(minor_scales, nullptr, LV_ALIGN_CENTER, 0, 0); + // lv_obj_set_style_local_bg_opa(minor_scales, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_TRANSP); + // lv_obj_set_style_local_scale_width(minor_scales, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, 4); + // lv_obj_set_style_local_scale_end_line_width(minor_scales, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, 1); + // lv_obj_set_style_local_scale_end_color(minor_scales, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_GRAY); + + // major_scales = lv_linemeter_create(lv_scr_act(), nullptr); + // lv_linemeter_set_scale(major_scales, 300, 11); + // lv_linemeter_set_angle_offset(major_scales, 180); + // lv_obj_set_size(major_scales, 240, 240); + // lv_obj_align(major_scales, nullptr, LV_ALIGN_CENTER, 0, 0); + // lv_obj_set_style_local_bg_opa(major_scales, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_TRANSP); + // lv_obj_set_style_local_scale_width(major_scales, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, 6); + // lv_obj_set_style_local_scale_end_line_width(major_scales, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, 4); + // lv_obj_set_style_local_scale_end_color(major_scales, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE); + + // large_scales = lv_linemeter_create(lv_scr_act(), nullptr); + // lv_linemeter_set_scale(large_scales, 180, 3); + // lv_linemeter_set_angle_offset(large_scales, 180); + // lv_obj_set_size(large_scales, 240, 240); + // lv_obj_align(large_scales, nullptr, LV_ALIGN_CENTER, 0, 0); + // lv_obj_set_style_local_bg_opa(large_scales, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_TRANSP); + // lv_obj_set_style_local_scale_width(large_scales, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, 20); + // lv_obj_set_style_local_scale_end_line_width(large_scales, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, 4); + // lv_obj_set_style_local_scale_end_color(large_scales, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_AQUA); + + twelve = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_align(twelve, LV_LABEL_ALIGN_RIGHT); + lv_label_set_text_static(twelve, "XII"); + lv_obj_align(twelve, NULL, LV_ALIGN_IN_TOP_RIGHT, -20, SunDialVerticalOffset-20); + lv_obj_set_style_local_text_color(twelve, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE); + + one = lv_label_create(lv_scr_act(), NULL); + lv_label_set_align(one, LV_LABEL_ALIGN_LEFT); + lv_label_set_text(one, "I"); + lv_obj_align(one, NULL, LV_ALIGN_IN_TOP_LEFT, 20, SunDialVerticalOffset-20); + lv_obj_set_style_local_text_color(one, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE); + + notificationIcon = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(notificationIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_LIME); + lv_label_set_text_static(notificationIcon, NotificationIcon::GetIcon(false)); + lv_obj_align(notificationIcon, nullptr, LV_ALIGN_IN_TOP_LEFT, 0, 0); + + // Date - Day / Week day + label_date_day = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(label_date_day, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, DARK_ORANGE); + lv_label_set_text_fmt(label_date_day, "%s\n%02i", dateTimeController.DayOfWeekShortToString(), dateTimeController.Day()); + lv_label_set_align(label_date_day, LV_LABEL_ALIGN_CENTER); + lv_obj_align(label_date_day, nullptr, LV_ALIGN_CENTER, 50, 0); + + minute_body = lv_line_create(lv_scr_act(), nullptr); + minute_body_trace = lv_line_create(lv_scr_act(), nullptr); + hour_body = lv_line_create(lv_scr_act(), nullptr); + hour_body_trace = lv_line_create(lv_scr_act(), nullptr); + second_body = lv_line_create(lv_scr_act(), nullptr); + + lv_style_init(&second_line_style); + lv_style_set_line_width(&second_line_style, LV_STATE_DEFAULT, 3); + lv_style_set_line_color(&second_line_style, LV_STATE_DEFAULT, LV_COLOR_RED); + lv_style_set_line_rounded(&second_line_style, LV_STATE_DEFAULT, true); + lv_obj_add_style(second_body, LV_LINE_PART_MAIN, &second_line_style); + + lv_style_init(&minute_line_style); + lv_style_set_line_width(&minute_line_style, LV_STATE_DEFAULT, 7); + lv_style_set_line_color(&minute_line_style, LV_STATE_DEFAULT, LV_COLOR_WHITE); + lv_style_set_line_rounded(&minute_line_style, LV_STATE_DEFAULT, true); + lv_obj_add_style(minute_body, LV_LINE_PART_MAIN, &minute_line_style); + + lv_style_init(&minute_line_style_trace); + lv_style_set_line_width(&minute_line_style_trace, LV_STATE_DEFAULT, 3); + lv_style_set_line_color(&minute_line_style_trace, LV_STATE_DEFAULT, LV_COLOR_WHITE); + lv_style_set_line_rounded(&minute_line_style_trace, LV_STATE_DEFAULT, false); + lv_obj_add_style(minute_body_trace, LV_LINE_PART_MAIN, &minute_line_style_trace); + + lv_style_init(&hour_line_style); + lv_style_set_line_width(&hour_line_style, LV_STATE_DEFAULT, 7); + lv_style_set_line_color(&hour_line_style, LV_STATE_DEFAULT, LV_COLOR_WHITE); + lv_style_set_line_rounded(&hour_line_style, LV_STATE_DEFAULT, true); + lv_obj_add_style(hour_body, LV_LINE_PART_MAIN, &hour_line_style); + + lv_style_init(&hour_line_style_trace); + lv_style_set_line_width(&hour_line_style_trace, LV_STATE_DEFAULT, 3); + lv_style_set_line_color(&hour_line_style_trace, LV_STATE_DEFAULT, LV_COLOR_WHITE); + lv_style_set_line_rounded(&hour_line_style_trace, LV_STATE_DEFAULT, false); + lv_obj_add_style(hour_body_trace, LV_LINE_PART_MAIN, &hour_line_style_trace); + + taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); + UpdateClock(); + Refresh(); +} + +WatchFaceSundial::~WatchFaceSundial() { + lv_task_del(taskRefresh); + + lv_style_reset(&hour_line_style); + lv_style_reset(&hour_line_style_trace); + lv_style_reset(&minute_line_style); + lv_style_reset(&minute_line_style_trace); + lv_style_reset(&second_line_style); + + lv_obj_clean(lv_scr_act()); +} + +void WatchFaceSundial::Refresh() { + currentDateTime = std::chrono::time_point_cast(dateTimeController.CurrentDateTime()); + if (currentDateTime.IsUpdated()) { + currentDateTime = std::chrono::time_point_cast(dateTimeController.CurrentDateTime()); + UpdateClock(); + if (currentDate.IsUpdated()) { + char const* MonthsString[] = {"--", "IANUARIUS","FEBRUARIUS","MARTIUS","APRILIS","MARTIUSIUNIUS","QUINTILIS","SEXTILIS","SEPTEMBER","OCTOBER","NOVEMBER","DECEMBER"}; + char const* DaysString[] = {"--", "LUNAE", "MARTIS", "MERCURII", "IOVIS", "VENERIS", "SATURNI", "SOLIS"}; + char const* RomanNumeralsString[] = {"--", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII"}; + lv_label_set_text_fmt(label_date_day, "%s\n%s %s", + DaysString[static_cast(dateTimeController.DayOfWeek())], + RomanNumeralsString[static_cast(dateTimeController.Day())], + MonthsString[static_cast(dateTimeController.Month())]); + lv_obj_align(label_date_day, nullptr, LV_ALIGN_IN_BOTTOM_MID, 0, -20); + } + } + +} + +void WatchFaceSundial::UpdateClock() { + uint8_t hour = dateTimeController.Hours(); + uint8_t minute = dateTimeController.Minutes(); + location = settingsController.GetLocation(); + + if (sHour != hour || sMinute != minute) { + // sun.setPosition(settings.lat.toFloat(), settings.lon.toFloat(), settings.gmtOffset / 3600); + sun.setPosition((float)location.latitude, (float)location.longitude, location.tzOffset); + + //from minutes past midnight + sun.setCurrentDate(dateTimeController.Year(), static_cast(dateTimeController.Month())+1, dateTimeController.Day()); + sun.setTZOffset(location.tzOffset); + + minutesSunrise = sun.calcSunrise(); //360; + minutesSunset = sun.calcSunset(); //1080; + minutesDaytime = (minutesSunset - minutesSunrise); + minutesNighttime = (1440 - minutesDaytime); + + minutesBeforeSunset = minutesSunset - (hour * 60 + minute); // i.e.zero degrees + HourLength = 90; // sundial hand length + + int16_t hourAngle; + + if(minutesBeforeSunset > 0 && minutesBeforeSunset < minutesDaytime) { // day (after sunrise) + hourAngle = 180.0 * minutesBeforeSunset / minutesDaytime + 90; + lv_style_set_line_color(&hour_line_style, LV_STATE_DEFAULT, LV_COLOR_WHITE); + lv_style_set_line_color(&hour_line_style_trace, LV_STATE_DEFAULT, LV_COLOR_WHITE); + lv_obj_set_style_local_text_color(label_date_day, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_ORANGE); + lv_obj_set_style_local_text_color(one, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE); + lv_obj_set_style_local_text_color(twelve, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE); + } else { // night (before sunrise or after sunset) + lv_style_set_line_color(&hour_line_style, LV_STATE_DEFAULT, DARK_GRAY); + lv_style_set_line_color(&hour_line_style_trace, LV_STATE_DEFAULT, DARK_GRAY); + lv_obj_set_style_local_text_color(label_date_day, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, DARK_ORANGE); + lv_obj_set_style_local_text_color(one, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, DARK_GRAY); + lv_obj_set_style_local_text_color(twelve, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, DARK_GRAY); + + if(minutesBeforeSunset > minutesDaytime) { // before sunrise + hourAngle = 180.0 * (minutesBeforeSunset - minutesDaytime) / minutesNighttime + 90; + } else { // after sunset + hourAngle = 180 + 180.0 * minutesBeforeSunset / minutesNighttime + 90; + } + } + /*NRF_LOG_INFO("a: %d, la: %f, lo: %f, ri: %d, se: %d, be: %d", + hourAngle, + (float)location.latitude, + (float)location.longitude, + minutesSunrise, + minutesSunset, + minutesBeforeSunset);*/ + + sHour = hour; + sMinute = minute; + + printf("H%d:%d lat%f lng%f z%d\n", hour, minute, (float)location.latitude, (float)location.longitude, location.tzOffset); + printf("%d before sunset, sunrise at %d:%d sunset at %d:%d angle %d\n", + minutesBeforeSunset, + minutesSunrise/60, minutesSunrise % 60, + minutesSunset/60, minutesSunset % 60, hourAngle); + + hour_point_trace[0] = CoordinateRelocateSundial(HourLength*.75, hourAngle); + hour_point_trace[1] = CoordinateRelocateSundial(HourLength, hourAngle); + + hour_point[0] = CoordinateRelocateSundial(0, hourAngle); + hour_point[1] = CoordinateRelocateSundial(HourLength*.75, hourAngle); + + lv_line_set_points(hour_body, hour_point, 2); + lv_line_set_points(hour_body_trace, hour_point_trace, 2); + } +} diff --git a/src/displayapp/screens/WatchFaceSundial.h b/src/displayapp/screens/WatchFaceSundial.h new file mode 100644 index 00000000..b5721585 --- /dev/null +++ b/src/displayapp/screens/WatchFaceSundial.h @@ -0,0 +1,111 @@ +#pragma once + +#include +#include +#include +#include +#include "displayapp/screens/Screen.h" +#include "components/datetime/DateTimeController.h" +#include "components/battery/BatteryController.h" +#include "components/ble/BleController.h" +#include "components/ble/NotificationManager.h" +#include "displayapp/screens/BatteryIcon.h" +#include "utility/DirtyValue.h" +#include "sunset/src/sunset.h" + +namespace Pinetime { + namespace Controllers { + class Settings; + class Battery; + class Ble; + class NotificationManager; + } + + namespace Applications { + namespace Screens { + + class WatchFaceSundial : public Screen { + public: + WatchFaceSundial(Controllers::DateTime& dateTimeController, + Controllers::NotificationManager& notificationManager, + Controllers::Settings& settingsController); + + ~WatchFaceSundial() override; + + void Refresh() override; + + private: + uint8_t sHour, sMinute, sSecond; + + Utility::DirtyValue batteryPercentRemaining {0}; + Utility::DirtyValue isCharging {}; + Utility::DirtyValue bleState {}; + Utility::DirtyValue> currentDateTime; + Utility::DirtyValue notificationState {false}; + using days = std::chrono::duration>; // TODO: days is standard in c++20 + Utility::DirtyValue> currentDate; + + lv_obj_t* major_scales; + lv_obj_t* one; + lv_obj_t* twelve; + + lv_obj_t* hour_body; + lv_obj_t* hour_body_trace; + lv_obj_t* minute_body; + lv_obj_t* minute_body_trace; + lv_obj_t* second_body; + + lv_point_t hour_point[2]; + lv_point_t hour_point_trace[2]; + lv_point_t minute_point[2]; + lv_point_t minute_point_trace[2]; + lv_point_t second_point[2]; + + lv_style_t hour_line_style; + lv_style_t hour_line_style_trace; + lv_style_t minute_line_style; + lv_style_t minute_line_style_trace; + lv_style_t second_line_style; + + lv_obj_t* label_date_day; + lv_obj_t* plugIcon; + lv_obj_t* notificationIcon; + lv_obj_t* bleIcon; + + Controllers::Settings::Location location; + SunSet sun; + int16_t minutesSunrise; + int16_t minutesSunset; + int16_t minutesDaytime; + int16_t minutesNighttime; + int16_t minutesBeforeSunset; + + const Controllers::DateTime& dateTimeController; + Controllers::NotificationManager& notificationManager; + Controllers::Settings& settingsController; + + void drawWatchFaceModeNight(); + void UpdateClock(); + void SetBatteryIcon(); + + lv_task_t* taskRefresh; + }; + } + + template <> + struct WatchFaceTraits { + static constexpr WatchFace watchFace = WatchFace::Sundial; + static constexpr const char* name = "Sundial face"; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::WatchFaceSundial(controllers.dateTimeController, + controllers.notificationManager, + controllers.settingsController); + }; + + static bool IsAvailable(Pinetime::Controllers::FS& /*filesystem*/) { + return true; + } + }; + } +} diff --git a/src/displayapp/screens/WatchFaceTerminal.cpp b/src/displayapp/screens/WatchFaceTerminal.cpp new file mode 100644 index 00000000..96d77741 --- /dev/null +++ b/src/displayapp/screens/WatchFaceTerminal.cpp @@ -0,0 +1,151 @@ +#include +#include "displayapp/screens/WatchFaceTerminal.h" +#include "displayapp/screens/BatteryIcon.h" +#include "displayapp/screens/NotificationIcon.h" +#include "displayapp/screens/Symbols.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; + +WatchFaceTerminal::WatchFaceTerminal(Controllers::DateTime& dateTimeController, + const Controllers::Battery& batteryController, + const Controllers::Ble& bleController, + Controllers::NotificationManager& notificationManager, + Controllers::Settings& settingsController, + Controllers::HeartRateController& heartRateController, + Controllers::MotionController& motionController) + : currentDateTime {{}}, + dateTimeController {dateTimeController}, + batteryController {batteryController}, + bleController {bleController}, + notificationManager {notificationManager}, + settingsController {settingsController}, + heartRateController {heartRateController}, + motionController {motionController} { + batteryValue = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_recolor(batteryValue, true); + lv_obj_align(batteryValue, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 0, -20); + + connectState = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_recolor(connectState, true); + lv_obj_align(connectState, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 0, 40); + + notificationIcon = lv_label_create(lv_scr_act(), nullptr); + lv_obj_align(notificationIcon, nullptr, LV_ALIGN_IN_LEFT_MID, 0, -100); + + label_date = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_recolor(label_date, true); + lv_obj_align(label_date, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 0, -40); + + label_prompt_1 = lv_label_create(lv_scr_act(), nullptr); + lv_obj_align(label_prompt_1, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 0, -80); + lv_label_set_text_static(label_prompt_1, "user@watch:~ $ now"); + + label_prompt_2 = lv_label_create(lv_scr_act(), nullptr); + lv_obj_align(label_prompt_2, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 0, 60); + lv_label_set_text_static(label_prompt_2, "user@watch:~ $"); + + label_time = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_recolor(label_time, true); + lv_obj_align(label_time, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 0, -60); + + heartbeatValue = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_recolor(heartbeatValue, true); + lv_obj_align(heartbeatValue, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 0, 20); + + stepValue = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_recolor(stepValue, true); + lv_obj_align(stepValue, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 0, 0); + + taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); + Refresh(); +} + +WatchFaceTerminal::~WatchFaceTerminal() { + lv_task_del(taskRefresh); + lv_obj_clean(lv_scr_act()); +} + +void WatchFaceTerminal::Refresh() { + powerPresent = batteryController.IsPowerPresent(); + batteryPercentRemaining = batteryController.PercentRemaining(); + if (batteryPercentRemaining.IsUpdated() || powerPresent.IsUpdated()) { + lv_label_set_text_fmt(batteryValue, "[BATT]#387b54 %d%%", batteryPercentRemaining.Get()); + if (batteryController.IsPowerPresent()) { + lv_label_ins_text(batteryValue, LV_LABEL_POS_LAST, " Charging"); + } + } + + bleState = bleController.IsConnected(); + bleRadioEnabled = bleController.IsRadioEnabled(); + if (bleState.IsUpdated() || bleRadioEnabled.IsUpdated()) { + if (!bleRadioEnabled.Get()) { + lv_label_set_text_static(connectState, "[STAT]#0082fc Disabled#"); + } else { + if (bleState.Get()) { + lv_label_set_text_static(connectState, "[STAT]#0082fc Connected#"); + } else { + lv_label_set_text_static(connectState, "[STAT]#0082fc Disconnected#"); + } + } + } + + notificationState = notificationManager.AreNewNotificationsAvailable(); + if (notificationState.IsUpdated()) { + if (notificationState.Get()) { + lv_label_set_text_static(notificationIcon, "You have mail."); + } else { + lv_label_set_text_static(notificationIcon, ""); + } + } + + currentDateTime = std::chrono::time_point_cast(dateTimeController.CurrentDateTime()); + if (currentDateTime.IsUpdated()) { + uint8_t hour = dateTimeController.Hours(); + uint8_t minute = dateTimeController.Minutes(); + uint8_t second = dateTimeController.Seconds(); + + if (settingsController.GetClockType() == Controllers::Settings::ClockType::H12) { + char ampmChar[3] = "AM"; + 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_fmt(label_time, "[TIME]#11cc55 %02d:%02d:%02d %s#", hour, minute, second, ampmChar); + } else { + lv_label_set_text_fmt(label_time, "[TIME]#11cc55 %02d:%02d:%02d", hour, minute, second); + } + + currentDate = std::chrono::time_point_cast(currentDateTime.Get()); + if (currentDate.IsUpdated()) { + uint16_t year = dateTimeController.Year(); + Controllers::DateTime::Months month = dateTimeController.Month(); + uint8_t day = dateTimeController.Day(); + lv_label_set_text_fmt(label_date, "[DATE]#007fff %04d-%02d-%02d#", short(year), char(month), char(day)); + } + } + + heartbeat = heartRateController.HeartRate(); + heartbeatRunning = heartRateController.State() != Controllers::HeartRateController::States::Stopped; + if (heartbeat.IsUpdated() || heartbeatRunning.IsUpdated()) { + if (heartbeatRunning.Get()) { + lv_label_set_text_fmt(heartbeatValue, "[L_HR]#ee3311 %d bpm#", heartbeat.Get()); + } else { + lv_label_set_text_static(heartbeatValue, "[L_HR]#ee3311 ---#"); + } + } + + stepCount = motionController.NbSteps(); + if (stepCount.IsUpdated()) { + lv_label_set_text_fmt(stepValue, "[STEP]#ee3377 %lu steps#", stepCount.Get()); + } +} diff --git a/src/displayapp/screens/WatchFaceTerminal.h b/src/displayapp/screens/WatchFaceTerminal.h new file mode 100644 index 00000000..bf460866 --- /dev/null +++ b/src/displayapp/screens/WatchFaceTerminal.h @@ -0,0 +1,92 @@ +#pragma once + +#include +#include +#include +#include +#include +#include "displayapp/screens/Screen.h" +#include "components/datetime/DateTimeController.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 WatchFaceTerminal : public Screen { + public: + WatchFaceTerminal(Controllers::DateTime& dateTimeController, + const Controllers::Battery& batteryController, + const Controllers::Ble& bleController, + Controllers::NotificationManager& notificationManager, + Controllers::Settings& settingsController, + Controllers::HeartRateController& heartRateController, + Controllers::MotionController& motionController); + ~WatchFaceTerminal() override; + + void Refresh() override; + + private: + Utility::DirtyValue batteryPercentRemaining {}; + Utility::DirtyValue powerPresent {}; + Utility::DirtyValue bleState {}; + Utility::DirtyValue bleRadioEnabled {}; + Utility::DirtyValue> currentDateTime {}; + Utility::DirtyValue stepCount {}; + Utility::DirtyValue heartbeat {}; + Utility::DirtyValue heartbeatRunning {}; + Utility::DirtyValue notificationState {}; + Utility::DirtyValue> currentDate; + + lv_obj_t* label_time; + lv_obj_t* label_date; + lv_obj_t* label_prompt_1; + lv_obj_t* label_prompt_2; + lv_obj_t* batteryValue; + lv_obj_t* heartbeatValue; + lv_obj_t* stepValue; + lv_obj_t* notificationIcon; + lv_obj_t* connectState; + + Controllers::DateTime& dateTimeController; + const Controllers::Battery& batteryController; + const Controllers::Ble& bleController; + Controllers::NotificationManager& notificationManager; + Controllers::Settings& settingsController; + Controllers::HeartRateController& heartRateController; + Controllers::MotionController& motionController; + + lv_task_t* taskRefresh; + }; + } + + template <> + struct WatchFaceTraits { + static constexpr WatchFace watchFace = WatchFace::Terminal; + static constexpr const char* name = "Terminal"; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::WatchFaceTerminal(controllers.dateTimeController, + controllers.batteryController, + controllers.bleController, + controllers.notificationManager, + controllers.settingsController, + controllers.heartRateController, + controllers.motionController); + }; + + static bool IsAvailable(Pinetime::Controllers::FS& /*filesystem*/) { + return true; + } + }; + } +} diff --git a/src/displayapp/screens/Weather.cpp b/src/displayapp/screens/Weather.cpp new file mode 100644 index 00000000..5321b7cc --- /dev/null +++ b/src/displayapp/screens/Weather.cpp @@ -0,0 +1,198 @@ +#include "displayapp/screens/Weather.h" +#include +#include "components/ble/SimpleWeatherService.h" +#include "components/datetime/DateTimeController.h" +#include "components/settings/Settings.h" +#include "displayapp/DisplayApp.h" +#include "displayapp/screens/WeatherSymbols.h" +#include "displayapp/InfiniTimeTheme.h" + +using namespace Pinetime::Applications::Screens; + +namespace { + lv_color_t TemperatureColor(int16_t temperature) { + if (temperature <= 0) { // freezing + return Colors::blue; + } else if (temperature <= 400) { // ice + return LV_COLOR_CYAN; + } else if (temperature >= 2700) { // hot + return Colors::deepOrange; + } + return Colors::orange; // normal + } + + uint8_t TemperatureStyle(int16_t temperature) { + if (temperature <= 0) { // freezing + return LV_TABLE_PART_CELL3; + } else if (temperature <= 400) { // ice + return LV_TABLE_PART_CELL4; + } else if (temperature >= 2700) { // hot + return LV_TABLE_PART_CELL6; + } + return LV_TABLE_PART_CELL5; // normal + } + + int16_t RoundTemperature(int16_t temp) { + return temp = temp / 100 + (temp % 100 >= 50 ? 1 : 0); + } +} + +Weather::Weather(Controllers::Settings& settingsController, Controllers::SimpleWeatherService& weatherService) + : settingsController {settingsController}, weatherService {weatherService} { + + temperature = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(temperature, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE); + lv_obj_set_style_local_text_font(temperature, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_42); + lv_label_set_text(temperature, "---"); + lv_obj_align(temperature, nullptr, LV_ALIGN_CENTER, 0, -30); + lv_obj_set_auto_realign(temperature, true); + + minTemperature = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(minTemperature, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::bg); + lv_label_set_text(minTemperature, ""); + lv_obj_align(minTemperature, temperature, LV_ALIGN_OUT_LEFT_MID, -10, 0); + lv_obj_set_auto_realign(minTemperature, true); + + maxTemperature = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(maxTemperature, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::bg); + lv_label_set_text(maxTemperature, ""); + lv_obj_align(maxTemperature, temperature, LV_ALIGN_OUT_RIGHT_MID, 10, 0); + lv_obj_set_auto_realign(maxTemperature, true); + + condition = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(condition, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::lightGray); + lv_label_set_text(condition, ""); + lv_obj_align(condition, temperature, LV_ALIGN_OUT_TOP_MID, 0, -10); + lv_obj_set_auto_realign(condition, true); + + icon = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(icon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE); + lv_obj_set_style_local_text_font(icon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &fontawesome_weathericons); + lv_label_set_text(icon, ""); + lv_obj_align(icon, condition, LV_ALIGN_OUT_TOP_MID, 0, 0); + lv_obj_set_auto_realign(icon, true); + + forecast = lv_table_create(lv_scr_act(), nullptr); + lv_table_set_col_cnt(forecast, Controllers::SimpleWeatherService::MaxNbForecastDays); + lv_table_set_row_cnt(forecast, 4); + // LV_TABLE_PART_CELL1: Default table style + lv_obj_set_style_local_border_color(forecast, LV_TABLE_PART_CELL1, LV_STATE_DEFAULT, LV_COLOR_BLACK); + lv_obj_set_style_local_text_color(forecast, LV_TABLE_PART_CELL1, LV_STATE_DEFAULT, Colors::lightGray); + // LV_TABLE_PART_CELL2: Condition icon + lv_obj_set_style_local_border_color(forecast, LV_TABLE_PART_CELL2, LV_STATE_DEFAULT, LV_COLOR_BLACK); + lv_obj_set_style_local_text_color(forecast, LV_TABLE_PART_CELL2, LV_STATE_DEFAULT, LV_COLOR_WHITE); + lv_obj_set_style_local_text_font(forecast, LV_TABLE_PART_CELL2, LV_STATE_DEFAULT, &fontawesome_weathericons); + // LV_TABLE_PART_CELL3: Freezing + lv_obj_set_style_local_border_color(forecast, LV_TABLE_PART_CELL3, LV_STATE_DEFAULT, LV_COLOR_BLACK); + lv_obj_set_style_local_text_color(forecast, LV_TABLE_PART_CELL3, LV_STATE_DEFAULT, Colors::blue); + // LV_TABLE_PART_CELL4: Ice + lv_obj_set_style_local_border_color(forecast, LV_TABLE_PART_CELL4, LV_STATE_DEFAULT, LV_COLOR_BLACK); + lv_obj_set_style_local_text_color(forecast, LV_TABLE_PART_CELL4, LV_STATE_DEFAULT, LV_COLOR_CYAN); + // LV_TABLE_PART_CELL5: Normal + lv_obj_set_style_local_border_color(forecast, LV_TABLE_PART_CELL5, LV_STATE_DEFAULT, LV_COLOR_BLACK); + lv_obj_set_style_local_text_color(forecast, LV_TABLE_PART_CELL5, LV_STATE_DEFAULT, Colors::orange); + // LV_TABLE_PART_CELL6: Hot + lv_obj_set_style_local_border_color(forecast, LV_TABLE_PART_CELL6, LV_STATE_DEFAULT, LV_COLOR_BLACK); + lv_obj_set_style_local_text_color(forecast, LV_TABLE_PART_CELL6, LV_STATE_DEFAULT, Colors::deepOrange); + + lv_obj_align(forecast, nullptr, LV_ALIGN_IN_BOTTOM_LEFT, 0, 0); + + for (int i = 0; i < Controllers::SimpleWeatherService::MaxNbForecastDays; i++) { + lv_table_set_col_width(forecast, i, 48); + lv_table_set_cell_type(forecast, 1, i, LV_TABLE_PART_CELL2); + lv_table_set_cell_align(forecast, 0, i, LV_LABEL_ALIGN_CENTER); + lv_table_set_cell_align(forecast, 1, i, LV_LABEL_ALIGN_CENTER); + lv_table_set_cell_align(forecast, 2, i, LV_LABEL_ALIGN_CENTER); + lv_table_set_cell_align(forecast, 3, i, LV_LABEL_ALIGN_CENTER); + } + + taskRefresh = lv_task_create(RefreshTaskCallback, 1000, LV_TASK_PRIO_MID, this); + Refresh(); +} + +Weather::~Weather() { + lv_task_del(taskRefresh); + lv_obj_clean(lv_scr_act()); +} + +void Weather::Refresh() { + currentWeather = weatherService.Current(); + if (currentWeather.IsUpdated()) { + auto optCurrentWeather = currentWeather.Get(); + if (optCurrentWeather) { + int16_t temp = optCurrentWeather->temperature; + int16_t minTemp = optCurrentWeather->minTemperature; + int16_t maxTemp = optCurrentWeather->maxTemperature; + lv_obj_set_style_local_text_color(temperature, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, TemperatureColor(temp)); + char tempUnit = 'C'; + if (settingsController.GetWeatherFormat() == Controllers::Settings::WeatherFormat::Imperial) { + temp = Controllers::SimpleWeatherService::CelsiusToFahrenheit(temp); + minTemp = Controllers::SimpleWeatherService::CelsiusToFahrenheit(minTemp); + maxTemp = Controllers::SimpleWeatherService::CelsiusToFahrenheit(maxTemp); + tempUnit = 'F'; + } + lv_label_set_text(icon, Symbols::GetSymbol(optCurrentWeather->iconId)); + lv_label_set_text(condition, Symbols::GetCondition(optCurrentWeather->iconId)); + lv_label_set_text_fmt(temperature, "%d°%c", RoundTemperature(temp), tempUnit); + lv_label_set_text_fmt(minTemperature, "%d°", RoundTemperature(minTemp)); + lv_label_set_text_fmt(maxTemperature, "%d°", RoundTemperature(maxTemp)); + } else { + lv_label_set_text(icon, ""); + lv_label_set_text(condition, ""); + lv_label_set_text(temperature, "---"); + lv_obj_set_style_local_text_color(temperature, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE); + lv_label_set_text(minTemperature, ""); + lv_label_set_text(maxTemperature, ""); + } + } + + currentForecast = weatherService.GetForecast(); + if (currentForecast.IsUpdated()) { + auto optCurrentForecast = currentForecast.Get(); + if (optCurrentForecast) { + std::tm localTime = *std::localtime(reinterpret_cast(&optCurrentForecast->timestamp)); + + for (int i = 0; i < Controllers::SimpleWeatherService::MaxNbForecastDays; i++) { + int16_t maxTemp = optCurrentForecast->days[i].maxTemperature; + int16_t minTemp = optCurrentForecast->days[i].minTemperature; + lv_table_set_cell_type(forecast, 2, i, TemperatureStyle(maxTemp)); + lv_table_set_cell_type(forecast, 3, i, TemperatureStyle(minTemp)); + if (settingsController.GetWeatherFormat() == Controllers::Settings::WeatherFormat::Imperial) { + maxTemp = Controllers::SimpleWeatherService::CelsiusToFahrenheit(maxTemp); + minTemp = Controllers::SimpleWeatherService::CelsiusToFahrenheit(minTemp); + } + uint8_t wday = localTime.tm_wday + i + 1; + if (wday > 7) { + wday -= 7; + } + maxTemp = RoundTemperature(maxTemp); + minTemp = RoundTemperature(minTemp); + const char* dayOfWeek = Controllers::DateTime::DayOfWeekShortToStringLow(static_cast(wday)); + lv_table_set_cell_value(forecast, 0, i, dayOfWeek); + lv_table_set_cell_value(forecast, 1, i, Symbols::GetSymbol(optCurrentForecast->days[i].iconId)); + // Pad cells based on the largest number of digits on each column + char maxPadding[3] = " "; + char minPadding[3] = " "; + int diff = snprintf(nullptr, 0, "%d", maxTemp) - snprintf(nullptr, 0, "%d", minTemp); + if (diff <= 0) { + maxPadding[-diff] = '\0'; + minPadding[0] = '\0'; + } else { + maxPadding[0] = '\0'; + minPadding[diff] = '\0'; + } + lv_table_set_cell_value_fmt(forecast, 2, i, "%s%d", maxPadding, maxTemp); + lv_table_set_cell_value_fmt(forecast, 3, i, "%s%d", minPadding, minTemp); + } + } else { + for (int i = 0; i < Controllers::SimpleWeatherService::MaxNbForecastDays; i++) { + lv_table_set_cell_value(forecast, 0, i, ""); + lv_table_set_cell_value(forecast, 1, i, ""); + lv_table_set_cell_value(forecast, 2, i, ""); + lv_table_set_cell_value(forecast, 3, i, ""); + lv_table_set_cell_type(forecast, 2, i, LV_TABLE_PART_CELL1); + lv_table_set_cell_type(forecast, 3, i, LV_TABLE_PART_CELL1); + } + } + } +} diff --git a/src/displayapp/screens/Weather.h b/src/displayapp/screens/Weather.h new file mode 100644 index 00000000..6975311e --- /dev/null +++ b/src/displayapp/screens/Weather.h @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include "displayapp/screens/Screen.h" +#include "components/ble/SimpleWeatherService.h" +#include "displayapp/apps/Apps.h" +#include "displayapp/Controllers.h" +#include "Symbols.h" +#include "utility/DirtyValue.h" + +namespace Pinetime { + + namespace Controllers { + class Settings; + } + + namespace Applications { + namespace Screens { + + class Weather : public Screen { + public: + Weather(Controllers::Settings& settingsController, Controllers::SimpleWeatherService& weatherService); + ~Weather() override; + + void Refresh() override; + + private: + Controllers::Settings& settingsController; + Controllers::SimpleWeatherService& weatherService; + + Utility::DirtyValue> currentWeather {}; + Utility::DirtyValue> currentForecast {}; + + lv_obj_t* icon; + lv_obj_t* condition; + lv_obj_t* temperature; + lv_obj_t* minTemperature; + lv_obj_t* maxTemperature; + lv_obj_t* forecast; + + lv_task_t* taskRefresh; + }; + } + + template <> + struct AppTraits { + static constexpr Apps app = Apps::Weather; + static constexpr const char* icon = Screens::Symbols::cloudSunRain; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::Weather(controllers.settingsController, *controllers.weatherController); + }; + }; + } +} diff --git a/src/displayapp/screens/WeatherSymbols.cpp b/src/displayapp/screens/WeatherSymbols.cpp new file mode 100644 index 00000000..de66312f --- /dev/null +++ b/src/displayapp/screens/WeatherSymbols.cpp @@ -0,0 +1,61 @@ +#include "displayapp/screens/WeatherSymbols.h" + +const char* Pinetime::Applications::Screens::Symbols::GetSymbol(const Pinetime::Controllers::SimpleWeatherService::Icons icon) { + switch (icon) { + case Pinetime::Controllers::SimpleWeatherService::Icons::Sun: + return Symbols::sun; + break; + case Pinetime::Controllers::SimpleWeatherService::Icons::CloudsSun: + return Symbols::cloudSun; + break; + case Pinetime::Controllers::SimpleWeatherService::Icons::Clouds: + return Symbols::cloud; + break; + case Pinetime::Controllers::SimpleWeatherService::Icons::BrokenClouds: + return Symbols::cloudMeatball; + break; + case Pinetime::Controllers::SimpleWeatherService::Icons::Thunderstorm: + return Symbols::bolt; + break; + case Pinetime::Controllers::SimpleWeatherService::Icons::Snow: + return Symbols::snowflake; + break; + case Pinetime::Controllers::SimpleWeatherService::Icons::CloudShowerHeavy: + return Symbols::cloudShowersHeavy; + break; + case Pinetime::Controllers::SimpleWeatherService::Icons::CloudSunRain: + return Symbols::cloudSunRain; + break; + case Pinetime::Controllers::SimpleWeatherService::Icons::Smog: + return Symbols::smog; + break; + default: + return Symbols::ban; + break; + } +} + +const char* Pinetime::Applications::Screens::Symbols::GetCondition(const Pinetime::Controllers::SimpleWeatherService::Icons icon) { + switch (icon) { + case Pinetime::Controllers::SimpleWeatherService::Icons::Sun: + return "Clear sky"; + case Pinetime::Controllers::SimpleWeatherService::Icons::CloudsSun: + return "Few clouds"; + case Pinetime::Controllers::SimpleWeatherService::Icons::Clouds: + return "Scattered clouds"; + case Pinetime::Controllers::SimpleWeatherService::Icons::BrokenClouds: + return "Broken clouds"; + case Pinetime::Controllers::SimpleWeatherService::Icons::CloudShowerHeavy: + return "Shower rain"; + case Pinetime::Controllers::SimpleWeatherService::Icons::CloudSunRain: + return "Rain"; + case Pinetime::Controllers::SimpleWeatherService::Icons::Thunderstorm: + return "Thunderstorm"; + case Pinetime::Controllers::SimpleWeatherService::Icons::Snow: + return "Snow"; + case Pinetime::Controllers::SimpleWeatherService::Icons::Smog: + return "Mist"; + default: + return ""; + } +} diff --git a/src/displayapp/screens/WeatherSymbols.h b/src/displayapp/screens/WeatherSymbols.h index 99ce3887..f3eeed55 100644 --- a/src/displayapp/screens/WeatherSymbols.h +++ b/src/displayapp/screens/WeatherSymbols.h @@ -6,40 +6,8 @@ namespace Pinetime { namespace Applications { namespace Screens { namespace Symbols { - const char* GetSymbol(const Pinetime::Controllers::SimpleWeatherService::Icons icon) { - switch (icon) { - case Pinetime::Controllers::SimpleWeatherService::Icons::Sun: - return Symbols::sun; - break; - case Pinetime::Controllers::SimpleWeatherService::Icons::CloudsSun: - return Symbols::cloudSun; - break; - case Pinetime::Controllers::SimpleWeatherService::Icons::Clouds: - return Symbols::cloud; - break; - case Pinetime::Controllers::SimpleWeatherService::Icons::BrokenClouds: - return Symbols::cloudMeatball; - break; - case Pinetime::Controllers::SimpleWeatherService::Icons::Thunderstorm: - return Symbols::bolt; - break; - case Pinetime::Controllers::SimpleWeatherService::Icons::Snow: - return Symbols::snowflake; - break; - case Pinetime::Controllers::SimpleWeatherService::Icons::CloudShowerHeavy: - return Symbols::cloudShowersHeavy; - break; - case Pinetime::Controllers::SimpleWeatherService::Icons::CloudSunRain: - return Symbols::cloudSunRain; - break; - case Pinetime::Controllers::SimpleWeatherService::Icons::Smog: - return Symbols::smog; - break; - default: - return Symbols::ban; - break; - } - } + const char* GetSymbol(const Pinetime::Controllers::SimpleWeatherService::Icons icon); + const char* GetCondition(const Pinetime::Controllers::SimpleWeatherService::Icons icon); } } } diff --git a/src/displayapp/screens/settings/SettingBluetooth.cpp b/src/displayapp/screens/settings/SettingBluetooth.cpp index 82c3dee1..e4dc695c 100644 --- a/src/displayapp/screens/settings/SettingBluetooth.cpp +++ b/src/displayapp/screens/settings/SettingBluetooth.cpp @@ -36,17 +36,19 @@ namespace { SettingBluetooth::SettingBluetooth(Pinetime::Applications::DisplayApp* app, Pinetime::Controllers::Settings& settingsController) : app {app}, + settings {settingsController}, checkboxList( 0, 1, "Bluetooth", Symbols::bluetooth, settingsController.GetBleRadioEnabled() ? 0 : 1, - [&settings = settingsController](uint32_t index) { + [this](uint32_t index) { const bool priorMode = settings.GetBleRadioEnabled(); const bool newMode = options[index].radioEnabled; if (newMode != priorMode) { settings.SetBleRadioEnabled(newMode); + this->app->PushMessage(Pinetime::Applications::Display::Messages::BleRadioEnableToggle); } }, CreateOptionArray()) { @@ -54,6 +56,4 @@ SettingBluetooth::SettingBluetooth(Pinetime::Applications::DisplayApp* app, Pine SettingBluetooth::~SettingBluetooth() { lv_obj_clean(lv_scr_act()); - // Pushing the message in the OnValueChanged function causes a freeze? - app->PushMessage(Pinetime::Applications::Display::Messages::BleRadioEnableToggle); } diff --git a/src/displayapp/screens/settings/SettingBluetooth.h b/src/displayapp/screens/settings/SettingBluetooth.h index 1e3f9b81..0cf014f5 100644 --- a/src/displayapp/screens/settings/SettingBluetooth.h +++ b/src/displayapp/screens/settings/SettingBluetooth.h @@ -20,6 +20,7 @@ namespace Pinetime { private: DisplayApp* app; + Pinetime::Controllers::Settings& settings; CheckboxList checkboxList; }; } diff --git a/src/displayapp/screens/settings/SettingLocation.cpp b/src/displayapp/screens/settings/SettingLocation.cpp index 07b2c468..a50bc791 100644 --- a/src/displayapp/screens/settings/SettingLocation.cpp +++ b/src/displayapp/screens/settings/SettingLocation.cpp @@ -9,6 +9,9 @@ using namespace Pinetime::Applications::Screens; namespace { + constexpr int16_t POS_X_LAT = -80; + constexpr int16_t POS_X_LONG = 0; + constexpr int16_t POS_X_TZ = 80; constexpr int16_t POS_Y_TEXT = 25; void ValueChangedHandler(void* userData) { @@ -27,6 +30,7 @@ SettingLocation::SettingLocation(Pinetime::Controllers::Settings& settingsContro lv_obj_t* icon = lv_label_create(lv_scr_act(), nullptr); lv_obj_set_style_local_text_color(icon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_ORANGE); + lv_label_set_text_static(icon, Symbols::map); lv_label_set_align(icon, LV_LABEL_ALIGN_CENTER); lv_obj_align(icon, title, LV_ALIGN_OUT_LEFT_MID, -10, 0); @@ -34,21 +38,18 @@ SettingLocation::SettingLocation(Pinetime::Controllers::Settings& settingsContro Controllers::Settings::Location loc = settingsController.GetLocation(); latCounter.Create(); - latCounter.SetWidth(80); latCounter.SetValue(loc.latitude); - lv_obj_align(latCounter.GetObject(), nullptr, LV_ALIGN_CENTER, -90, POS_Y_TEXT); + lv_obj_align(latCounter.GetObject(), nullptr, LV_ALIGN_CENTER, POS_X_LAT, POS_Y_TEXT); latCounter.SetValueChangedEventCallback(this, ValueChangedHandler); longCounter.Create(); - longCounter.SetWidth(110); longCounter.SetValue(loc.longitude); - lv_obj_align(longCounter.GetObject(), nullptr, LV_ALIGN_CENTER, -5, POS_Y_TEXT); + lv_obj_align(longCounter.GetObject(), nullptr, LV_ALIGN_CENTER, POS_X_LONG, POS_Y_TEXT); longCounter.SetValueChangedEventCallback(this, ValueChangedHandler); tzCounter.Create(); - tzCounter.SetWidth(60); tzCounter.SetValue(loc.tzOffset); - lv_obj_align(tzCounter.GetObject(), nullptr, LV_ALIGN_CENTER, 75, POS_Y_TEXT); + lv_obj_align(tzCounter.GetObject(), nullptr, LV_ALIGN_CENTER, POS_X_TZ, POS_Y_TEXT); tzCounter.SetValueChangedEventCallback(this, ValueChangedHandler); UpdateScreen(); @@ -66,4 +67,4 @@ void SettingLocation::UpdateScreen() { tzOffset: (int8_t)tzCounter.GetValue(), }; settingsController.SetLocation(loc); -} \ No newline at end of file +} diff --git a/src/displayapp/screens/settings/SettingLocation.h b/src/displayapp/screens/settings/SettingLocation.h index 9596d12f..9d977ac5 100644 --- a/src/displayapp/screens/settings/SettingLocation.h +++ b/src/displayapp/screens/settings/SettingLocation.h @@ -22,9 +22,9 @@ namespace Pinetime { private: Controllers::Settings& settingsController; - Widgets::Counter latCounter = Widgets::Counter(-90, 90, jetbrains_mono_42); - Widgets::Counter longCounter = Widgets::Counter(-180, 180, jetbrains_mono_42); - Widgets::Counter tzCounter = Widgets::Counter(-12, 12, jetbrains_mono_42); + Widgets::Counter latCounter = Widgets::Counter(-90, 90, jetbrains_mono_bold_20); + Widgets::Counter longCounter = Widgets::Counter(-180, 180, jetbrains_mono_bold_20); + Widgets::Counter tzCounter = Widgets::Counter(-12, 12, jetbrains_mono_bold_20); }; } } diff --git a/src/displayapp/screens/settings/SettingWatchFace.cpp b/src/displayapp/screens/settings/SettingWatchFace.cpp index f052573c..e01e9f84 100644 --- a/src/displayapp/screens/settings/SettingWatchFace.cpp +++ b/src/displayapp/screens/settings/SettingWatchFace.cpp @@ -9,6 +9,37 @@ using namespace Pinetime::Applications::Screens; constexpr const char* SettingWatchFace::title; constexpr const char* SettingWatchFace::symbol; +namespace { + uint32_t IndexOf(const std::array& watchfaces, + Pinetime::Applications::WatchFace watchface) { + size_t index = 0; + auto found = std::find_if(watchfaces.begin(), + watchfaces.end(), + [&index, &watchface](const Pinetime::Applications::Screens::SettingWatchFace::Item& item) { + const bool result = item.watchface == watchface; + if (!result) { + index++; + } + return result; + }); + if (found == watchfaces.end()) { + index = 0; + } + + return index; + } + + Pinetime::Applications::WatchFace IndexToWatchFace(const std::array& watchfaces, + size_t index) { + if (index >= watchfaces.size()) { + return watchfaces[0].watchface; + } + return watchfaces[index].watchface; + } +} + auto SettingWatchFace::CreateScreenList() const { std::array()>, nScreens> screens; for (size_t i = 0; i < screens.size(); i++) { @@ -20,7 +51,7 @@ auto SettingWatchFace::CreateScreenList() const { } SettingWatchFace::SettingWatchFace(Pinetime::Applications::DisplayApp* app, - std::array&& watchfaceItems, + std::array&& watchfaceItems, Pinetime::Controllers::Settings& settingsController, Pinetime::Controllers::FS& filesystem) : app {app}, @@ -44,7 +75,8 @@ std::unique_ptr SettingWatchFace::CreateScreen(unsigned int screenNum) c if (i + (screenNum * settingsPerScreen) >= watchfaceItems.size()) { watchfacesOnThisScreen[i] = {"", false}; } else { - watchfacesOnThisScreen[i] = watchfaceItems[i + (screenNum * settingsPerScreen)]; + auto& item = watchfaceItems[i + (screenNum * settingsPerScreen)]; + watchfacesOnThisScreen[i] = Screens::CheckboxList::Item {item.name, item.enabled}; } } @@ -53,9 +85,9 @@ std::unique_ptr SettingWatchFace::CreateScreen(unsigned int screenNum) c nScreens, title, symbol, - static_cast(settingsController.GetWatchFace()), - [&settings = settingsController](uint32_t index) { - settings.SetWatchFace(static_cast(index)); + static_cast(IndexOf(watchfaceItems, settingsController.GetWatchFace())), + [this, &settings = settingsController](uint32_t index) { + settings.SetWatchFace(IndexToWatchFace(watchfaceItems, index)); settings.SaveSettings(); }, watchfacesOnThisScreen); diff --git a/src/displayapp/screens/settings/SettingWatchFace.h b/src/displayapp/screens/settings/SettingWatchFace.h index fa369ecb..2dee4b82 100644 --- a/src/displayapp/screens/settings/SettingWatchFace.h +++ b/src/displayapp/screens/settings/SettingWatchFace.h @@ -19,8 +19,14 @@ namespace Pinetime { class SettingWatchFace : public Screen { public: + struct Item { + const char* name; + WatchFace watchface; + bool enabled; + }; + SettingWatchFace(DisplayApp* app, - std::array&& watchfaceItems, + std::array&& watchfaceItems, Pinetime::Controllers::Settings& settingsController, Pinetime::Controllers::FS& filesystem); ~SettingWatchFace() override; @@ -33,7 +39,7 @@ namespace Pinetime { std::unique_ptr CreateScreen(unsigned int screenNum) const; static constexpr int settingsPerScreen = 4; - std::array watchfaceItems; + std::array watchfaceItems; static constexpr int nScreens = UserWatchFaceTypes::Count > 0 ? (UserWatchFaceTypes ::Count - 1) / settingsPerScreen + 1 : 1; Controllers::Settings& settingsController; diff --git a/src/drivers/Bma421.cpp b/src/drivers/Bma421.cpp index 84d76ab3..74d47d06 100644 --- a/src/drivers/Bma421.cpp +++ b/src/drivers/Bma421.cpp @@ -22,6 +22,16 @@ namespace { void user_delay(uint32_t period_us, void* /*intf_ptr*/) { nrf_delay_us(period_us); } + + // Scale factors to convert accelerometer counts to milli-g + // from datasheet: https://files.pine64.org/doc/datasheet/pinetime/BST-BMA421-FL000.pdf + // The array index to use is stored in accel_conf.range + constexpr int16_t accelScaleFactors[] = { + [BMA4_ACCEL_RANGE_2G] = 1024, // LSB/g +/- 2g range + [BMA4_ACCEL_RANGE_4G] = 512, // LSB/g +/- 4g range + [BMA4_ACCEL_RANGE_8G] = 256, // LSB/g +/- 8g range + [BMA4_ACCEL_RANGE_16G] = 128 // LSB/g +/- 16g range + }; } Bma421::Bma421(TwiMaster& twiMaster, uint8_t twiAddress) : twiMaster {twiMaster}, deviceAddress {twiAddress} { @@ -74,7 +84,6 @@ void Bma421::Init() { if (ret != BMA4_OK) return; - struct bma4_accel_config accel_conf; accel_conf.odr = BMA4_OUTPUT_DATA_RATE_100HZ; accel_conf.range = BMA4_ACCEL_RANGE_2G; accel_conf.bandwidth = BMA4_ACCEL_NORMAL_AVG4; @@ -102,19 +111,21 @@ void Bma421::Write(uint8_t registerAddress, const uint8_t* data, size_t size) { Bma421::Values Bma421::Process() { if (not isOk) return {}; + struct bma4_accel rawData; struct bma4_accel data; - bma4_read_accel_xyz(&data, &bma); + bma4_read_accel_xyz(&rawData, &bma); + + // Scale the measured ADC counts to units of 'binary milli-g' + // where 1g = 1024 'binary milli-g' units. + // See https://github.com/InfiniTimeOrg/InfiniTime/pull/1950 for + // discussion of why we opted for scaling to 1024 rather than 1000. + data.x = 1024 * rawData.x / accelScaleFactors[accel_conf.range]; + data.y = 1024 * rawData.y / accelScaleFactors[accel_conf.range]; + data.z = 1024 * rawData.z / accelScaleFactors[accel_conf.range]; uint32_t steps = 0; bma423_step_counter_output(&steps, &bma); - int32_t temperature; - bma4_get_temperature(&temperature, BMA4_DEG, &bma); - temperature = temperature / 1000; - - uint8_t activity = 0; - bma423_activity_output(&activity, &bma); - // X and Y axis are swapped because of the way the sensor is mounted in the PineTime return {steps, data.y, data.x, data.z}; } diff --git a/src/drivers/Bma421.h b/src/drivers/Bma421.h index fb832514..5269f62b 100644 --- a/src/drivers/Bma421.h +++ b/src/drivers/Bma421.h @@ -41,6 +41,7 @@ namespace Pinetime { TwiMaster& twiMaster; uint8_t deviceAddress = 0x18; struct bma4_dev bma; + struct bma4_accel_config accel_conf; // Store the device configuration for later reference. bool isOk = false; bool isResetOk = false; DeviceTypes deviceType = DeviceTypes::Unknown; diff --git a/src/drivers/Hrs3300.cpp b/src/drivers/Hrs3300.cpp index 7dfd301c..33889b6f 100644 --- a/src/drivers/Hrs3300.cpp +++ b/src/drivers/Hrs3300.cpp @@ -19,7 +19,7 @@ namespace { } /** Driver for the HRS3300 heart rate sensor. - * Original implementation from wasp-os : https://github.com/daniel-thompson/wasp-os/blob/master/wasp/drivers/hrs3300.py + * Original implementation from wasp-os : https://github.com/wasp-os/wasp-os/blob/master/wasp/drivers/hrs3300.py * * Experimentaly derived changes to improve signal/noise (see comments below) - Ceimour */ diff --git a/src/drivers/Spi.cpp b/src/drivers/Spi.cpp index c85b90c1..a95a7eae 100644 --- a/src/drivers/Spi.cpp +++ b/src/drivers/Spi.cpp @@ -9,8 +9,8 @@ Spi::Spi(SpiMaster& spiMaster, uint8_t pinCsn) : spiMaster {spiMaster}, pinCsn { nrf_gpio_pin_set(pinCsn); } -bool Spi::Write(const uint8_t* data, size_t size) { - return spiMaster.Write(pinCsn, data, size); +bool Spi::Write(const uint8_t* data, size_t size, const std::function& preTransactionHook) { + return spiMaster.Write(pinCsn, data, size, preTransactionHook); } bool Spi::Read(uint8_t* cmd, size_t cmdSize, uint8_t* data, size_t dataSize) { diff --git a/src/drivers/Spi.h b/src/drivers/Spi.h index 9b6a30f4..0c5edf08 100644 --- a/src/drivers/Spi.h +++ b/src/drivers/Spi.h @@ -1,6 +1,7 @@ #pragma once #include #include +#include #include "drivers/SpiMaster.h" namespace Pinetime { @@ -14,7 +15,7 @@ namespace Pinetime { Spi& operator=(Spi&&) = delete; bool Init(); - bool Write(const uint8_t* data, size_t size); + bool Write(const uint8_t* data, size_t size, const std::function& preTransactionHook); bool Read(uint8_t* cmd, size_t cmdSize, uint8_t* data, size_t dataSize); bool WriteCmdAndBuffer(const uint8_t* cmd, size_t cmdSize, const uint8_t* data, size_t dataSize); void Sleep(); diff --git a/src/drivers/SpiMaster.cpp b/src/drivers/SpiMaster.cpp index 3446d639..19422ef3 100644 --- a/src/drivers/SpiMaster.cpp +++ b/src/drivers/SpiMaster.cpp @@ -94,32 +94,45 @@ bool SpiMaster::Init() { return true; } -void SpiMaster::SetupWorkaroundForFtpan58(NRF_SPIM_Type* spim, uint32_t ppi_channel, uint32_t gpiote_channel) { - // Create an event when SCK toggles. - NRF_GPIOTE->CONFIG[gpiote_channel] = (GPIOTE_CONFIG_MODE_Event << GPIOTE_CONFIG_MODE_Pos) | (spim->PSEL.SCK << GPIOTE_CONFIG_PSEL_Pos) | - (GPIOTE_CONFIG_POLARITY_Toggle << GPIOTE_CONFIG_POLARITY_Pos); +void SpiMaster::SetupWorkaroundForErratum58() { + nrfx_gpiote_pin_t pin = spiBaseAddress->PSEL.SCK; + nrfx_gpiote_in_config_t gpioteCfg = {.sense = NRF_GPIOTE_POLARITY_TOGGLE, + .pull = NRF_GPIO_PIN_NOPULL, + .is_watcher = false, + .hi_accuracy = true, + .skip_gpio_setup = true}; + if (!workaroundActive) { + // Create an event when SCK toggles. + APP_ERROR_CHECK(nrfx_gpiote_in_init(pin, &gpioteCfg, NULL)); + nrfx_gpiote_in_event_enable(pin, false); + + // Stop the spim instance when SCK toggles. + nrf_ppi_channel_endpoint_setup(workaroundPpi, nrfx_gpiote_in_event_addr_get(pin), spiBaseAddress->TASKS_STOP); + nrf_ppi_channel_enable(workaroundPpi); + } - // Stop the spim instance when SCK toggles. - NRF_PPI->CH[ppi_channel].EEP = (uint32_t) &NRF_GPIOTE->EVENTS_IN[gpiote_channel]; - NRF_PPI->CH[ppi_channel].TEP = (uint32_t) &spim->TASKS_STOP; - NRF_PPI->CHENSET = 1U << ppi_channel; spiBaseAddress->EVENTS_END = 0; // Disable IRQ - spim->INTENCLR = (1 << 6); - spim->INTENCLR = (1 << 1); - spim->INTENCLR = (1 << 19); + spiBaseAddress->INTENCLR = (1 << 6); + spiBaseAddress->INTENCLR = (1 << 1); + spiBaseAddress->INTENCLR = (1 << 19); + workaroundActive = true; } -void SpiMaster::DisableWorkaroundForFtpan58(NRF_SPIM_Type* spim, uint32_t ppi_channel, uint32_t gpiote_channel) { - NRF_GPIOTE->CONFIG[gpiote_channel] = 0; - NRF_PPI->CH[ppi_channel].EEP = 0; - NRF_PPI->CH[ppi_channel].TEP = 0; - NRF_PPI->CHENSET = ppi_channel; +void SpiMaster::DisableWorkaroundForErratum58() { + nrfx_gpiote_pin_t pin = spiBaseAddress->PSEL.SCK; + if (workaroundActive) { + nrfx_gpiote_in_uninit(pin); + nrf_ppi_channel_disable(workaroundPpi); + } spiBaseAddress->EVENTS_END = 0; - spim->INTENSET = (1 << 6); - spim->INTENSET = (1 << 1); - spim->INTENSET = (1 << 19); + + // Enable IRQ + spiBaseAddress->INTENSET = (1 << 6); + spiBaseAddress->INTENSET = (1 << 1); + spiBaseAddress->INTENSET = (1 << 19); + workaroundActive = false; } void SpiMaster::OnEndEvent() { @@ -136,17 +149,11 @@ void SpiMaster::OnEndEvent() { spiBaseAddress->TASKS_START = 1; } else { - BaseType_t xHigherPriorityTaskWoken = pdFALSE; - if (taskToNotify != nullptr) { - vTaskNotifyGiveFromISR(taskToNotify, &xHigherPriorityTaskWoken); - portYIELD_FROM_ISR(xHigherPriorityTaskWoken); - } - nrf_gpio_pin_set(this->pinCsn); currentBufferAddr = 0; - BaseType_t xHigherPriorityTaskWoken2 = pdFALSE; - xSemaphoreGiveFromISR(mutex, &xHigherPriorityTaskWoken2); - portYIELD_FROM_ISR(xHigherPriorityTaskWoken | xHigherPriorityTaskWoken2); + BaseType_t xHigherPriorityTaskWoken = pdFALSE; + xSemaphoreGiveFromISR(mutex, &xHigherPriorityTaskWoken); + portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } } @@ -173,21 +180,23 @@ void SpiMaster::PrepareRx(const uint32_t bufferAddress, const size_t size) { spiBaseAddress->EVENTS_END = 0; } -bool SpiMaster::Write(uint8_t pinCsn, const uint8_t* data, size_t size) { +bool SpiMaster::Write(uint8_t pinCsn, const uint8_t* data, size_t size, const std::function& preTransactionHook) { if (data == nullptr) return false; auto ok = xSemaphoreTake(mutex, portMAX_DELAY); ASSERT(ok == true); - taskToNotify = xTaskGetCurrentTaskHandle(); this->pinCsn = pinCsn; if (size == 1) { - SetupWorkaroundForFtpan58(spiBaseAddress, 0, 0); + SetupWorkaroundForErratum58(); } else { - DisableWorkaroundForFtpan58(spiBaseAddress, 0, 0); + DisableWorkaroundForErratum58(); } + if (preTransactionHook != nullptr) { + preTransactionHook(); + } nrf_gpio_pin_clear(this->pinCsn); currentBufferAddr = (uint32_t) data; @@ -205,7 +214,7 @@ bool SpiMaster::Write(uint8_t pinCsn, const uint8_t* data, size_t size) { nrf_gpio_pin_set(this->pinCsn); currentBufferAddr = 0; - DisableWorkaroundForFtpan58(spiBaseAddress, 0, 0); + DisableWorkaroundForErratum58(); xSemaphoreGive(mutex); } @@ -216,10 +225,8 @@ bool SpiMaster::Write(uint8_t pinCsn, const uint8_t* data, size_t size) { bool SpiMaster::Read(uint8_t pinCsn, uint8_t* cmd, size_t cmdSize, uint8_t* data, size_t dataSize) { xSemaphoreTake(mutex, portMAX_DELAY); - taskToNotify = nullptr; - this->pinCsn = pinCsn; - DisableWorkaroundForFtpan58(spiBaseAddress, 0, 0); + DisableWorkaroundForErratum58(); spiBaseAddress->INTENCLR = (1 << 6); spiBaseAddress->INTENCLR = (1 << 1); spiBaseAddress->INTENCLR = (1 << 19); @@ -265,10 +272,8 @@ void SpiMaster::Wakeup() { bool SpiMaster::WriteCmdAndBuffer(uint8_t pinCsn, const uint8_t* cmd, size_t cmdSize, const uint8_t* data, size_t dataSize) { xSemaphoreTake(mutex, portMAX_DELAY); - taskToNotify = nullptr; - this->pinCsn = pinCsn; - DisableWorkaroundForFtpan58(spiBaseAddress, 0, 0); + DisableWorkaroundForErratum58(); spiBaseAddress->INTENCLR = (1 << 6); spiBaseAddress->INTENCLR = (1 << 1); spiBaseAddress->INTENCLR = (1 << 19); diff --git a/src/drivers/SpiMaster.h b/src/drivers/SpiMaster.h index 8b698c57..be6e5351 100644 --- a/src/drivers/SpiMaster.h +++ b/src/drivers/SpiMaster.h @@ -1,10 +1,13 @@ #pragma once #include #include +#include #include #include #include +#include "nrfx_gpiote.h" +#include "nrf_ppi.h" namespace Pinetime { namespace Drivers { @@ -31,7 +34,7 @@ namespace Pinetime { SpiMaster& operator=(SpiMaster&&) = delete; bool Init(); - bool Write(uint8_t pinCsn, const uint8_t* data, size_t size); + bool Write(uint8_t pinCsn, const uint8_t* data, size_t size, const std::function& preTransactionHook); bool Read(uint8_t pinCsn, uint8_t* cmd, size_t cmdSize, uint8_t* data, size_t dataSize); bool WriteCmdAndBuffer(uint8_t pinCsn, const uint8_t* cmd, size_t cmdSize, const uint8_t* data, size_t dataSize); @@ -43,8 +46,8 @@ namespace Pinetime { void Wakeup(); private: - void SetupWorkaroundForFtpan58(NRF_SPIM_Type* spim, uint32_t ppi_channel, uint32_t gpiote_channel); - void DisableWorkaroundForFtpan58(NRF_SPIM_Type* spim, uint32_t ppi_channel, uint32_t gpiote_channel); + void SetupWorkaroundForErratum58(); + void DisableWorkaroundForErratum58(); void PrepareTx(const volatile uint32_t bufferAddress, const volatile size_t size); void PrepareRx(const volatile uint32_t bufferAddress, const volatile size_t size); @@ -56,8 +59,9 @@ namespace Pinetime { volatile uint32_t currentBufferAddr = 0; volatile size_t currentBufferSize = 0; - volatile TaskHandle_t taskToNotify; SemaphoreHandle_t mutex = nullptr; + static constexpr nrf_ppi_channel_t workaroundPpi = NRF_PPI_CHANNEL0; + bool workaroundActive = false; }; } } diff --git a/src/drivers/SpiNorFlash.cpp b/src/drivers/SpiNorFlash.cpp index 28f82fe6..56a8aabd 100644 --- a/src/drivers/SpiNorFlash.cpp +++ b/src/drivers/SpiNorFlash.cpp @@ -22,7 +22,7 @@ void SpiNorFlash::Uninit() { void SpiNorFlash::Sleep() { auto cmd = static_cast(Commands::DeepPowerDown); - spi.Write(&cmd, sizeof(uint8_t)); + spi.Write(&cmd, sizeof(uint8_t), nullptr); NRF_LOG_INFO("[SpiNorFlash] Sleep") } diff --git a/src/drivers/St7789.cpp b/src/drivers/St7789.cpp index 17d14ce6..12e95a41 100644 --- a/src/drivers/St7789.cpp +++ b/src/drivers/St7789.cpp @@ -1,8 +1,8 @@ #include "drivers/St7789.h" #include -#include #include #include "drivers/Spi.h" +#include "task.h" using namespace Pinetime::Drivers; @@ -29,37 +29,77 @@ void St7789::Init() { DisplayOn(); } -void St7789::WriteCommand(uint8_t cmd) { - nrf_gpio_pin_clear(pinDataCommand); - WriteSpi(&cmd, 1); -} - void St7789::WriteData(uint8_t data) { - nrf_gpio_pin_set(pinDataCommand); - WriteSpi(&data, 1); + WriteData(&data, 1); } -void St7789::WriteSpi(const uint8_t* data, size_t size) { - spi.Write(data, size); +void St7789::WriteData(const uint8_t* data, size_t size) { + WriteSpi(data, size, [pinDataCommand = pinDataCommand]() { + nrf_gpio_pin_set(pinDataCommand); + }); +} + +void St7789::WriteCommand(uint8_t data) { + WriteCommand(&data, 1); +} + +void St7789::WriteCommand(const uint8_t* data, size_t size) { + WriteSpi(data, size, [pinDataCommand = pinDataCommand]() { + nrf_gpio_pin_clear(pinDataCommand); + }); +} + +void St7789::WriteSpi(const uint8_t* data, size_t size, const std::function& preTransactionHook) { + spi.Write(data, size, preTransactionHook); } void St7789::SoftwareReset() { + EnsureSleepOutPostDelay(); WriteCommand(static_cast(Commands::SoftwareReset)); - nrf_delay_ms(150); + // If sleep in: must wait 120ms before sleep out can sent (see driver datasheet) + // Unconditionally wait as software reset doesn't need to be performant + sleepIn = true; + lastSleepExit = xTaskGetTickCount(); + vTaskDelay(pdMS_TO_TICKS(125)); } void St7789::SleepOut() { + if (!sleepIn) { + return; + } WriteCommand(static_cast(Commands::SleepOut)); + // Wait 5ms for clocks to stabilise + // pdMS rounds down => 6 used here + vTaskDelay(pdMS_TO_TICKS(6)); + // Cannot send sleep in or software reset for 120ms + lastSleepExit = xTaskGetTickCount(); + sleepIn = false; +} + +void St7789::EnsureSleepOutPostDelay() { + TickType_t delta = xTaskGetTickCount() - lastSleepExit; + // Due to timer wraparound, there is a chance of delaying when not necessary + // It is very low (pdMS_TO_TICKS(125)/2^32) and waiting an extra 125ms isn't too bad + if (delta < pdMS_TO_TICKS(125)) { + vTaskDelay(pdMS_TO_TICKS(125) - delta); + } } void St7789::SleepIn() { + if (sleepIn) { + return; + } + EnsureSleepOutPostDelay(); WriteCommand(static_cast(Commands::SleepIn)); + // Wait 5ms for clocks to stabilise + // pdMS rounds down => 6 used here + vTaskDelay(pdMS_TO_TICKS(6)); + sleepIn = true; } void St7789::ColMod() { WriteCommand(static_cast(Commands::ColMod)); WriteData(0x55); - nrf_delay_ms(10); } void St7789::MemoryDataAccessControl() { @@ -96,12 +136,10 @@ void St7789::RowAddressSet() { void St7789::DisplayInversionOn() { WriteCommand(static_cast(Commands::DisplayInversionOn)); - nrf_delay_ms(10); } void St7789::NormalModeOn() { WriteCommand(static_cast(Commands::NormalModeOn)); - nrf_delay_ms(10); } void St7789::DisplayOn() { @@ -120,12 +158,11 @@ void St7789::SetAddrWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { WriteData(y0 & 0xff); WriteData(y1 >> 8); WriteData(y1 & 0xff); - - WriteToRam(); } -void St7789::WriteToRam() { +void St7789::WriteToRam(const uint8_t* data, size_t size) { WriteCommand(static_cast(Commands::WriteToRam)); + WriteData(data, size); } void St7789::SetVdv() { @@ -137,17 +174,6 @@ void St7789::SetVdv() { void St7789::DisplayOff() { WriteCommand(static_cast(Commands::DisplayOff)); - nrf_delay_ms(500); -} - -void St7789::VerticalScrollDefinition(uint16_t topFixedLines, uint16_t scrollLines, uint16_t bottomFixedLines) { - WriteCommand(static_cast(Commands::VerticalScrollDefinition)); - WriteData(topFixedLines >> 8u); - WriteData(topFixedLines & 0x00ffu); - WriteData(scrollLines >> 8u); - WriteData(scrollLines & 0x00ffu); - WriteData(bottomFixedLines >> 8u); - WriteData(bottomFixedLines & 0x00ffu); } void St7789::VerticalScrollStartAddress(uint16_t line) { @@ -160,27 +186,20 @@ void St7789::VerticalScrollStartAddress(uint16_t line) { void St7789::Uninit() { } -void St7789::DrawPixel(uint16_t x, uint16_t y, uint32_t color) { - if (x >= Width || y >= Height) { - return; - } - - SetAddrWindow(x, y, x + 1, y + 1); - - nrf_gpio_pin_set(pinDataCommand); - WriteSpi(reinterpret_cast(&color), 2); -} - void St7789::DrawBuffer(uint16_t x, uint16_t y, uint16_t width, uint16_t height, const uint8_t* data, size_t size) { SetAddrWindow(x, y, x + width - 1, y + height - 1); - nrf_gpio_pin_set(pinDataCommand); - WriteSpi(data, size); + WriteToRam(data, size); } void St7789::HardwareReset() { nrf_gpio_pin_clear(pinReset); - nrf_delay_ms(10); + vTaskDelay(pdMS_TO_TICKS(1)); nrf_gpio_pin_set(pinReset); + // If hardware reset started while sleep out, reset time may be up to 120ms + // Unconditionally wait as hardware reset doesn't need to be performant + sleepIn = true; + lastSleepExit = xTaskGetTickCount(); + vTaskDelay(pdMS_TO_TICKS(125)); } void St7789::Sleep() { diff --git a/src/drivers/St7789.h b/src/drivers/St7789.h index 68e1da44..45d4b56d 100644 --- a/src/drivers/St7789.h +++ b/src/drivers/St7789.h @@ -1,6 +1,9 @@ #pragma once #include #include +#include + +#include namespace Pinetime { namespace Drivers { @@ -16,9 +19,7 @@ namespace Pinetime { void Init(); void Uninit(); - void DrawPixel(uint16_t x, uint16_t y, uint32_t color); - void VerticalScrollDefinition(uint16_t topFixedLines, uint16_t scrollLines, uint16_t bottomFixedLines); void VerticalScrollStartAddress(uint16_t line); void DrawBuffer(uint16_t x, uint16_t y, uint16_t width, uint16_t height, const uint8_t* data, size_t size); @@ -31,23 +32,27 @@ namespace Pinetime { uint8_t pinDataCommand; uint8_t pinReset; uint8_t verticalScrollingStartAddress = 0; + bool sleepIn; + TickType_t lastSleepExit; void HardwareReset(); void SoftwareReset(); void SleepOut(); + void EnsureSleepOutPostDelay(); void SleepIn(); void ColMod(); void MemoryDataAccessControl(); void DisplayInversionOn(); void NormalModeOn(); - void WriteToRam(); + void WriteToRam(const uint8_t* data, size_t size); void DisplayOn(); void DisplayOff(); void SetAddrWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1); void SetVdv(); void WriteCommand(uint8_t cmd); - void WriteSpi(const uint8_t* data, size_t size); + void WriteCommand(const uint8_t* data, size_t size); + void WriteSpi(const uint8_t* data, size_t size, const std::function& preTransactionHook); enum class Commands : uint8_t { SoftwareReset = 0x01, @@ -67,6 +72,7 @@ namespace Pinetime { VdvSet = 0xc4, }; void WriteData(uint8_t data); + void WriteData(const uint8_t* data, size_t size); void ColumnAddressSet(); static constexpr uint16_t Width = 240; diff --git a/src/heartratetask/HeartRateTask.cpp b/src/heartratetask/HeartRateTask.cpp index 414cdf2c..9d82d11e 100644 --- a/src/heartratetask/HeartRateTask.cpp +++ b/src/heartratetask/HeartRateTask.cpp @@ -103,10 +103,7 @@ void HeartRateTask::Work() { void HeartRateTask::PushMessage(HeartRateTask::Messages msg) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; xQueueSendFromISR(messageQueue, &msg, &xHigherPriorityTaskWoken); - if (xHigherPriorityTaskWoken) { - /* Actual macro used here is port specific. */ - // TODO : should I do something here? - } + portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } void HeartRateTask::StartMeasurement() { diff --git a/src/libs/lv_conf.h b/src/libs/lv_conf.h index 62074b21..cc6cd88d 100644 --- a/src/libs/lv_conf.h +++ b/src/libs/lv_conf.h @@ -731,7 +731,9 @@ typedef void* lv_obj_user_data_t; #define LV_USE_TABLE 1 #if LV_USE_TABLE #define LV_TABLE_COL_MAX 12 -#define LV_TABLE_CELL_STYLE_CNT 5 +#define LV_TABLE_CELL_STYLE_CNT 6 +#define LV_TABLE_PART_CELL5 5 +#define LV_TABLE_PART_CELL6 6 #endif diff --git a/src/recoveryLoader.cpp b/src/recoveryLoader.cpp index 723977e3..fc9ab76c 100644 --- a/src/recoveryLoader.cpp +++ b/src/recoveryLoader.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include #include @@ -48,7 +47,6 @@ Pinetime::Drivers::SpiNorFlash spiNorFlash {flashSpi}; Pinetime::Drivers::Spi lcdSpi {spi, Pinetime::PinMap::SpiLcdCsn}; Pinetime::Drivers::St7789 lcd {lcdSpi, Pinetime::PinMap::LcdDataCommand, Pinetime::PinMap::LcdReset}; -Pinetime::Components::Gfx gfx {lcd}; Pinetime::Controllers::BrightnessController brightnessController; void DisplayProgressBar(uint8_t percent, uint16_t color); @@ -92,7 +90,6 @@ void Process(void* /*instance*/) { spiNorFlash.Wakeup(); brightnessController.Init(); lcd.Init(); - gfx.Init(); NRF_LOG_INFO("Display logo") DisplayLogo(); @@ -124,7 +121,6 @@ void DisplayLogo() { Pinetime::Tools::RleDecoder rleDecoder(infinitime_nb, sizeof(infinitime_nb)); for (int i = 0; i < displayWidth; i++) { rleDecoder.DecodeNext(displayBuffer, displayWidth * bytesPerPixel); - ulTaskNotifyTake(pdTRUE, 500); lcd.DrawBuffer(0, i, displayWidth, 1, reinterpret_cast(displayBuffer), displayWidth * bytesPerPixel); } } @@ -133,7 +129,6 @@ void DisplayProgressBar(uint8_t percent, uint16_t color) { static constexpr uint8_t barHeight = 20; std::fill(displayBuffer, displayBuffer + (displayWidth * bytesPerPixel), color); for (int i = 0; i < barHeight; i++) { - ulTaskNotifyTake(pdTRUE, 500); uint16_t barWidth = std::min(static_cast(percent) * 2.4f, static_cast(displayWidth)); lcd.DrawBuffer(0, displayWidth - barHeight + i, barWidth, 1, reinterpret_cast(displayBuffer), barWidth * bytesPerPixel); } diff --git a/src/resources/lv_img_conv.py b/src/resources/lv_img_conv.py index 04765462..4c6b84e3 100755 --- a/src/resources/lv_img_conv.py +++ b/src/resources/lv_img_conv.py @@ -101,6 +101,13 @@ def main(): img = Image.open(img_path) img_height = img.height img_width = img.width + if args.color_format == "CF_TRUE_COLOR_ALPHA" and img.mode != "RGBA": + # support pictures stored in other formats like with a color palette 'P' + # see: https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes + img = img.convert(mode="RGBA") + elif args.color_format == "CF_INDEXED_1_BIT" and img.mode != "L": + # for CF_INDEXED_1_BIT we need just a grayscale value per pixel + img = img.convert(mode="L") if args.color_format == "CF_TRUE_COLOR_ALPHA" and args.binary_format == "ARGB8888": buf = bytearray(img_height*img_width*4) # 4 bytes (32 bit) per pixel for y in range(img_height): @@ -140,7 +147,7 @@ def main(): for y in range(img_height): for x in range(img_width): - c, a = img.getpixel((x,y)) + c = img.getpixel((x,y)) p = w * y + (x >> 3) + 8 # +8 for the palette buf[p] |= (c & 0x1) << (7 - (x & 0x7)) # write palette information, for indexed-1-bit we need palette with two values diff --git a/src/systemtask/SystemTask.cpp b/src/systemtask/SystemTask.cpp index b245e392..e3d40d35 100644 --- a/src/systemtask/SystemTask.cpp +++ b/src/systemtask/SystemTask.cpp @@ -342,8 +342,12 @@ void SystemTask::Work() { if (settingsController.GetNotificationStatus() != Controllers::Settings::Notification::Sleep && settingsController.GetChimeOption() == Controllers::Settings::ChimesOption::Hours && alarmController.State() != AlarmController::AlarmState::Alerting) { + // if sleeping, we can't send a chime to displayApp yet (SPI flash switched off) + // request running first and repush the chime message if (state == SystemTaskState::Sleeping) { GoToRunning(); + PushMessage(msg); + } else { displayApp.PushMessage(Pinetime::Applications::Display::Messages::Chime); } } @@ -353,8 +357,12 @@ void SystemTask::Work() { if (settingsController.GetNotificationStatus() != Controllers::Settings::Notification::Sleep && settingsController.GetChimeOption() == Controllers::Settings::ChimesOption::HalfHours && alarmController.State() != AlarmController::AlarmState::Alerting) { + // if sleeping, we can't send a chime to displayApp yet (SPI flash switched off) + // request running first and repush the chime message if (state == SystemTaskState::Sleeping) { GoToRunning(); + PushMessage(msg); + } else { displayApp.PushMessage(Pinetime::Applications::Display::Messages::Chime); } } @@ -418,7 +426,8 @@ void SystemTask::UpdateMotion() { } if (state == SystemTaskState::Sleeping && !(settingsController.isWakeUpModeOn(Pinetime::Controllers::Settings::WakeUpMode::RaiseWrist) || - settingsController.isWakeUpModeOn(Pinetime::Controllers::Settings::WakeUpMode::Shake))) { + settingsController.isWakeUpModeOn(Pinetime::Controllers::Settings::WakeUpMode::Shake) || + motionController.GetService()->IsMotionNotificationSubscribed())) { return; } @@ -503,10 +512,7 @@ void SystemTask::PushMessage(System::Messages msg) { if (in_isr()) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; xQueueSendFromISR(systemTasksMsgQueue, &msg, &xHigherPriorityTaskWoken); - if (xHigherPriorityTaskWoken == pdTRUE) { - /* Actual macro used here is port specific. */ - portYIELD_FROM_ISR(xHigherPriorityTaskWoken); - } + portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } else { xQueueSend(systemTasksMsgQueue, &msg, portMAX_DELAY); }