diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c24e2374..2eb8a959 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -56,6 +56,11 @@ jobs: with: name: InfiniTime MCUBoot image ${{ github.head_ref }} path: ./build/output/pinetime-mcuboot-app-image-*.bin + - name: Upload standalone ELF artifacts + uses: actions/upload-artifact@v3 + with: + name: InfiniTime image ${{ github.head_ref }} + path: ./build/output/src/pinetime-app-*.out - name: Upload resources artifacts uses: actions/upload-artifact@v3 with: diff --git a/README.md b/README.md index 4ee9cfb6..cc667f98 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,8 @@ Branches : - [Getting started with InfiniTime](doc/gettingStarted/gettingStarted-1.0.md) - [Updating the software](doc/gettingStarted/updating-software.md) - [About the firmware and bootloader](doc/gettingStarted/about-software.md) +- [Available apps](doc/gettingStarted/Applications.md) +- [Available watch faces](/doc/gettingStarted/Watchfaces.md) - [PineTimeStyle Watch face](https://pine64.org/documentation/PineTime/Watchfaces/PineTimeStyle) - [Weather integration](https://pine64.org/documentation/PineTime/Software/InfiniTime_weather/) @@ -67,7 +69,7 @@ Branches : ### Contributing -- [How to contribute?](CONTRIBUTING.md) +- [How to contribute](CONTRIBUTING.md) - [Coding conventions](doc/coding-convention.md) ### Build, flash and debug diff --git a/doc/gettingStarted/Applications.md b/doc/gettingStarted/Applications.md new file mode 100644 index 00000000..8ca2b252 --- /dev/null +++ b/doc/gettingStarted/Applications.md @@ -0,0 +1,99 @@ +# Applications + +InfiniTime has 13 apps on the `main` branch at the time of writing. + +## List of apps +- Stopwatch +- Alarm +- Timer +- Steps +- Heartrate +- Music +- InfiniPaint +- Paddle +- 2 +- InfiniDice +- Metronome +- Maps +- Weather + +### Stopwatch +![Stopwatch UI](/doc/gettingStarted/AppsScreenshots/stopwatch.png) +- Press the Start button (bottom right) to start or stop the timer. + - You can also press the side button while the timer is running to pause the timer. +- Press the Flag button (bottom left) to add a lap. +- The stopwatch will not yet continue counting time while the app is closed. + +### Alarm +![Alarm UI](/doc/gettingStarted/AppsScreenshots/alarm.png) +- Ajust the time with the time picker. +- Press the Info button in the top middle to see time remaning. +- Use the toggle in the bottom left to turn the alarm on/off. +- Use the button in the bottom right to change the alarm frequency. + - You can choose between once, daily, or Monday - Friday. + +### Timer +![Timer UI](/doc/gettingStarted/AppsScreenshots/timer.png) +- Ajust how long the timer should go for with the time picker. +- Press the Start button at the bottom to start/stop the timer. + +### Steps +![Steps UI](/doc/gettingStarted/AppsScreenshots/steps.png) +- The total count of steps for the current display will show in the middle of the screen. +- The Reset button in the bottom middle resets the Trip counter. (Total of all steps taken.) +- The progress circle shows the percentage of your daily goal completed. + +### Heartrate +![Heartrate UI](/doc/gettingStarted/AppsScreenshots/Heartrate.png) +- Press Start to start measuring your heartrate. + - It may take a bit to get the first measurement. + +### Music +![Music UI](/doc/gettingStarted/AppsScreenshots/Music.png) +- This app shows currently playing music. + - Please note that this app is not very useful without a device connected. +- Press the button in the center to play/pause, and the buttons on the left and right to go to the previous and next tracks, respectively. +- Swipe up to get to volume controls. + +### InfiniPaint +![InfiniPaint UI](/doc/gettingStarted/AppsScreenshots/Paint.png) +- This app does not allow you to swipe from the top to exit, use the side button instead. +- Draw on the screen to add lines. +- Hold down in one spot to change paint colors. + +### Paddle +![Paddle UI](/doc/gettingStarted/AppsScreenshots/Pong.png) +- This app does not allow you to swipe from the top to exit, use the side button instead. +- Drag your finger to move the paddle. +- Goal: Don't let the ball go off the left side of the screen. + +### 2 +![2 UI](/doc/gettingStarted/AppsScreenshots/2048.png) +- This app does not allow you to swipe from the top to exit, use the side button instead. +- Play a game of 2048. +- Swipe up, down, left, or right tomove the tiles. +- When two tiles with the same number run into each other, they will add together. +- Goal: Don't let the screen fill up with tiles, and get to the 2048 tile to win. + +### InfiniDice +![InfiniDice UI](/doc/gettingStarted/AppsScreenshots/Dice.png) +- Ajust the count to change the number of dice. +- Ajust the sides to change the number of sides. +- Press the button at the bottom to roll. +- The result will be on the right side of the screen. + +### Metronome +![Metronome UI](/doc/gettingStarted/AppsScreenshots/Metronome.png) +- Ajust the BPM with the circular slider. + - A bug currently makes it always snap to 98 BPM. +- Use the button in the bottom left to start the metronome. + +### Maps +![Maps UI](/doc/gettingStarted/AppsScreenshots/Maps.png) +- This app shows info from a navigation app. + - Please note that this app is not very useful without a device connected. + +### Weather +![Weather UI](/doc/gettingStarted/AppsScreenshots/Weather.png) +- This app shows weather info. + - Please note that this app is not very useful without a device connected. diff --git a/doc/gettingStarted/AppsScreenshots/2048.png b/doc/gettingStarted/AppsScreenshots/2048.png new file mode 100644 index 00000000..472e4fce Binary files /dev/null and b/doc/gettingStarted/AppsScreenshots/2048.png differ diff --git a/doc/gettingStarted/AppsScreenshots/Dice.png b/doc/gettingStarted/AppsScreenshots/Dice.png new file mode 100644 index 00000000..f5421c83 Binary files /dev/null and b/doc/gettingStarted/AppsScreenshots/Dice.png differ diff --git a/doc/gettingStarted/AppsScreenshots/Heartrate.png b/doc/gettingStarted/AppsScreenshots/Heartrate.png new file mode 100644 index 00000000..ed0470dc Binary files /dev/null and b/doc/gettingStarted/AppsScreenshots/Heartrate.png differ diff --git a/doc/gettingStarted/AppsScreenshots/Maps.png b/doc/gettingStarted/AppsScreenshots/Maps.png new file mode 100644 index 00000000..84e3ebef Binary files /dev/null and b/doc/gettingStarted/AppsScreenshots/Maps.png differ diff --git a/doc/gettingStarted/AppsScreenshots/Metronome.png b/doc/gettingStarted/AppsScreenshots/Metronome.png new file mode 100644 index 00000000..989fbfda Binary files /dev/null and b/doc/gettingStarted/AppsScreenshots/Metronome.png differ diff --git a/doc/gettingStarted/AppsScreenshots/Music.png b/doc/gettingStarted/AppsScreenshots/Music.png new file mode 100644 index 00000000..063603e7 Binary files /dev/null and b/doc/gettingStarted/AppsScreenshots/Music.png differ diff --git a/doc/gettingStarted/AppsScreenshots/Paint.png b/doc/gettingStarted/AppsScreenshots/Paint.png new file mode 100644 index 00000000..147487f9 Binary files /dev/null and b/doc/gettingStarted/AppsScreenshots/Paint.png differ diff --git a/doc/gettingStarted/AppsScreenshots/Pong.png b/doc/gettingStarted/AppsScreenshots/Pong.png new file mode 100644 index 00000000..08f739ef Binary files /dev/null and b/doc/gettingStarted/AppsScreenshots/Pong.png differ diff --git a/doc/gettingStarted/AppsScreenshots/Weather.png b/doc/gettingStarted/AppsScreenshots/Weather.png new file mode 100644 index 00000000..89714aa4 Binary files /dev/null and b/doc/gettingStarted/AppsScreenshots/Weather.png differ diff --git a/doc/gettingStarted/AppsScreenshots/alarm.png b/doc/gettingStarted/AppsScreenshots/alarm.png new file mode 100644 index 00000000..5d840e80 Binary files /dev/null and b/doc/gettingStarted/AppsScreenshots/alarm.png differ diff --git a/doc/gettingStarted/AppsScreenshots/steps.png b/doc/gettingStarted/AppsScreenshots/steps.png new file mode 100644 index 00000000..1d7f5608 Binary files /dev/null and b/doc/gettingStarted/AppsScreenshots/steps.png differ diff --git a/doc/gettingStarted/AppsScreenshots/stopwatch.png b/doc/gettingStarted/AppsScreenshots/stopwatch.png new file mode 100644 index 00000000..19b34a5a Binary files /dev/null and b/doc/gettingStarted/AppsScreenshots/stopwatch.png differ diff --git a/doc/gettingStarted/AppsScreenshots/timer.png b/doc/gettingStarted/AppsScreenshots/timer.png new file mode 100644 index 00000000..a0d3babf Binary files /dev/null and b/doc/gettingStarted/AppsScreenshots/timer.png differ diff --git a/doc/gettingStarted/Watchfaces.md b/doc/gettingStarted/Watchfaces.md new file mode 100644 index 00000000..9edff0bb --- /dev/null +++ b/doc/gettingStarted/Watchfaces.md @@ -0,0 +1,31 @@ +# Watchfaces + +InfiniTime has 6 apps on the `main` branch at the time of writing. + +## List of apps +- Digital +- Analog +- PineTimeStyle +- Terminal +- Infinineat +- Casio G7710 + +### Digital +![Digital face](/doc/gettingStarted/Watchfaces/Digital.png) + +### Analog +![Analog face](/doc/gettingStarted/Watchfaces/Analog.png) + +### PineTimeStyle +![PineTimeStyle face](/doc/gettingStarted/Watchfaces/PineTimeStyle.png) + - You can long-press on the display to change colors, step style, and weather. + +### Terminal +![Terminal face](/doc/gettingStarted/Watchfaces/Terminal.png) + +### Infinineat +![Infinineat face](/doc/gettingStarted/Watchfaces/Infinineat.png) + - You can long-press on the display to change colors. + +### Casio G7710 +![Casio G7710 face](/doc/gettingStarted/Watchfaces/CasioG7710.png) diff --git a/doc/gettingStarted/Watchfaces/Analog.png b/doc/gettingStarted/Watchfaces/Analog.png new file mode 100644 index 00000000..9e64e5aa Binary files /dev/null and b/doc/gettingStarted/Watchfaces/Analog.png differ diff --git a/doc/gettingStarted/Watchfaces/CasioG7710.png b/doc/gettingStarted/Watchfaces/CasioG7710.png new file mode 100644 index 00000000..b8a55cd5 Binary files /dev/null and b/doc/gettingStarted/Watchfaces/CasioG7710.png differ diff --git a/doc/gettingStarted/Watchfaces/Digital.png b/doc/gettingStarted/Watchfaces/Digital.png new file mode 100644 index 00000000..242c9d5b Binary files /dev/null and b/doc/gettingStarted/Watchfaces/Digital.png differ diff --git a/doc/gettingStarted/Watchfaces/Infinineat.png b/doc/gettingStarted/Watchfaces/Infinineat.png new file mode 100644 index 00000000..6b8cd614 Binary files /dev/null and b/doc/gettingStarted/Watchfaces/Infinineat.png differ diff --git a/doc/gettingStarted/Watchfaces/PineTimeStyle.png b/doc/gettingStarted/Watchfaces/PineTimeStyle.png new file mode 100644 index 00000000..46f0b42b Binary files /dev/null and b/doc/gettingStarted/Watchfaces/PineTimeStyle.png differ diff --git a/doc/gettingStarted/Watchfaces/Terminal.png b/doc/gettingStarted/Watchfaces/Terminal.png new file mode 100644 index 00000000..3f148c5f Binary files /dev/null and b/doc/gettingStarted/Watchfaces/Terminal.png differ diff --git a/doc/gettingStarted/gadgetbridge0.jpg b/doc/gettingStarted/gadgetbridge0.jpg index 7ea2e3cd..2a0a295e 100644 Binary files a/doc/gettingStarted/gadgetbridge0.jpg and b/doc/gettingStarted/gadgetbridge0.jpg differ diff --git a/doc/gettingStarted/gadgetbridge1.jpg b/doc/gettingStarted/gadgetbridge1.jpg index 470b0b3c..20fa7d5a 100644 Binary files a/doc/gettingStarted/gadgetbridge1.jpg and b/doc/gettingStarted/gadgetbridge1.jpg differ diff --git a/doc/gettingStarted/gadgetbridge2.jpg b/doc/gettingStarted/gadgetbridge2.jpg index 8f75b232..a03b071a 100644 Binary files a/doc/gettingStarted/gadgetbridge2.jpg and b/doc/gettingStarted/gadgetbridge2.jpg differ diff --git a/doc/gettingStarted/gadgetbridge3.jpg b/doc/gettingStarted/gadgetbridge3.jpg index cfb83f52..2e9bea34 100644 Binary files a/doc/gettingStarted/gadgetbridge3.jpg and b/doc/gettingStarted/gadgetbridge3.jpg differ diff --git a/doc/gettingStarted/gadgetbridge4.jpg b/doc/gettingStarted/gadgetbridge4.jpg index e41aff04..1b4210ca 100644 Binary files a/doc/gettingStarted/gadgetbridge4.jpg and b/doc/gettingStarted/gadgetbridge4.jpg differ diff --git a/doc/gettingStarted/gadgetbridge5.jpg b/doc/gettingStarted/gadgetbridge5.jpg index 1ef9f779..15efb4d4 100644 Binary files a/doc/gettingStarted/gadgetbridge5.jpg and b/doc/gettingStarted/gadgetbridge5.jpg differ diff --git a/doc/gettingStarted/gettingStarted-1.0.md b/doc/gettingStarted/gettingStarted-1.0.md index 5c607444..2c35f1c1 100644 --- a/doc/gettingStarted/gettingStarted-1.0.md +++ b/doc/gettingStarted/gettingStarted-1.0.md @@ -14,7 +14,7 @@ You can sync the time using companion apps. - Gadgetbridge automatically synchronizes the time when you connect it to your watch. More information on Gadgetbridge [here](/doc/gettingStarted/ota-gadgetbridge.md) - [Sync the time with NRFConnect](/doc/gettingStarted/time-nrfconnect.md) -- Sync the time with your browser https://hubmartin.github.io/WebBLEWatch/ +- [Sync the time with your browser](https://hubmartin.github.io/WebBLEWatch/) You can also set the time in the settings without a companion app. (version >1.7.0) @@ -46,7 +46,7 @@ On the bottom right, you can see how many steps you have taken today. ![Settings](ui/settings.jpg) - Swipe **up** to display the application menus. Apps (stopwatch, music, step, games,...) can be started from this menu. -- Swipe **down** to display the notification panel. Notification sent by your companion app will be displayed here. +- Swipe **down** to display the notification panel. Notifications sent by your companion app will be displayed here. - Swipe **right** to display the Quick Actions menu. This menu allows you to - Set the brightness of the display - Start the **flashlight** app diff --git a/doc/gettingStarted/ota-gadgetbridge.md b/doc/gettingStarted/ota-gadgetbridge.md index fe26c03b..76f99145 100644 --- a/doc/gettingStarted/ota-gadgetbridge.md +++ b/doc/gettingStarted/ota-gadgetbridge.md @@ -1,29 +1,35 @@ # Connecting to Gadgetbridge -Launch Gadgetbridge and tap on the **"+"** button on the bottom right to add a new device: +Launch Gadgetbridge and tap on the menu button in the top left: ![Gadgetbridge 0](gadgetbridge0.jpg) -Wait for the scan to complete, your PineTime should be detected: +Press the "Connect new device" button: ![Gadgetbridge 1](gadgetbridge1.jpg) +Your PineTime should appear on the list. Tap on it. + Tap on it. Gadgdetbridge will pair and connect to your device: ![Gadgetbridge 2](gadgetbridge2.jpg) # Updating with Gadgetbridge -Now that Gadgetbridge is connected to your PineTime, use a file browser application and find the DFU file (`pinetime-mcuboot-app-dfu-x.x.x.zip`) you downloaded previously. Tap on it and open it using the Gadgetbridge application/firmware installer: +Now that Gadgetbridge is connected to your PineTime, press the three dots on the device card: ![Gadgetbridge 3](gadgetbridge3.jpg) -Read the warning carefully and tap **Install**: +Now press the "File Installer" button: ![Gadgetbridge 4](gadgetbridge4.jpg) -Wait for the transfer to finish. Your PineTime should reset and reboot with the new version of InfiniTime! - -Don't forget to **validate** your firmware. In the InfiniTime go to the settings (swipe right, select gear icon) and Firmware option and click **validate**. Otherwise after reboot the previous firmware will be used. +Select the firmware you downloaded (`pinetime-mcuboot-app-dfu-x.x.x.zip`) from the [Releases tab](https://github.com/InfiniTimeOrg/InfiniTime/releases/latest): ![Gadgetbridge 5](gadgetbridge5.jpg) + +Wait for the transfer to finish. There will be a progress bar on both the watch and the phone. Your PineTime should reboot with the new version of InfiniTime! + +Don't forget to **validate** your firmware. In the InfiniTime go to the settings (swipe right, select gear icon) and scroll to the Firmware option and click **validate**. Otherwise, after reboot the previous firmware will be used. + +![Validate](validate.png) diff --git a/doc/gettingStarted/updating-software.md b/doc/gettingStarted/updating-software.md index 2b5d5d92..95149975 100644 --- a/doc/gettingStarted/updating-software.md +++ b/doc/gettingStarted/updating-software.md @@ -6,7 +6,7 @@ If you just want to flash or upgrade InfiniTime on your PineTime, this page is f You can check the InfiniTime version by first swiping right on the watch face to open quick settings, tapping the cogwheel to open settings, swipe up until you find an entry named "About" and tap on it. -![InfiniTime 1.0 version](version-1.0.jpg) +![InfiniTime 1.14 version](version.png) PineTimes shipped after June 2021 will ship with the latest version of [the bootloader](https://github.com/JF002/pinetime-mcuboot-bootloader/releases/tag/1.0.0) and [recovery firmware](https://github.com/InfiniTimeOrg/InfiniTime/releases/tag/0.14.1) @@ -49,7 +49,7 @@ Since those resources are not part of the firmware, they need to be flashed and Resources are packaged into a single .zip file named `infinitime-resources-x.y.z.zip` (where `x`, `y` and `z` are the version numbers of InfiniTime). You can use the companion app of your choice to flash the resources. -**Note : at the time of writing this page, [Amazfish](https://github.com/piggz/harbour-amazfish) and [ITD](https://gitea.arsenm.dev/Arsen6331/itd) have already integrated this functionality. Other companion apps will hopefully implement it soon!* +**Note: at the time of writing this page, [Amazfish](https://github.com/piggz/harbour-amazfish) and [ITD](https://gitea.arsenm.dev/Arsen6331/itd) have already integrated this functionality. Other companion apps will hopefully implement it soon!* ## Amazfish Use the `Download file` functionality of Amazfish. diff --git a/doc/gettingStarted/validate.png b/doc/gettingStarted/validate.png new file mode 100644 index 00000000..a6f3286b Binary files /dev/null and b/doc/gettingStarted/validate.png differ diff --git a/doc/gettingStarted/version.png b/doc/gettingStarted/version.png new file mode 100644 index 00000000..89f505a3 Binary files /dev/null and b/doc/gettingStarted/version.png differ diff --git a/src/components/ble/DfuService.h b/src/components/ble/DfuService.h index b56911b9..6652cdc1 100644 --- a/src/components/ble/DfuService.h +++ b/src/components/ble/DfuService.h @@ -77,6 +77,10 @@ namespace Pinetime { uint16_t ComputeCrc(uint8_t const* p_data, uint32_t size, uint16_t const* p_crc); }; + static constexpr ble_uuid128_t serviceUuid { + .u {.type = BLE_UUID_TYPE_128}, + .value = {0x23, 0xD1, 0xBC, 0xEA, 0x5F, 0x78, 0x23, 0x15, 0xDE, 0xEF, 0x12, 0x12, 0x30, 0x15, 0x00, 0x00}}; + private: Pinetime::System::SystemTask& systemTask; Pinetime::Controllers::Ble& bleController; @@ -90,10 +94,6 @@ namespace Pinetime { uint16_t revision {0x0008}; - static constexpr ble_uuid128_t serviceUuid { - .u {.type = BLE_UUID_TYPE_128}, - .value = {0x23, 0xD1, 0xBC, 0xEA, 0x5F, 0x78, 0x23, 0x15, 0xDE, 0xEF, 0x12, 0x12, 0x30, 0x15, 0x00, 0x00}}; - static constexpr ble_uuid128_t packetCharacteristicUuid { .u {.type = BLE_UUID_TYPE_128}, .value = {0x23, 0xD1, 0xBC, 0xEA, 0x5F, 0x78, 0x23, 0x15, 0xDE, 0xEF, 0x12, 0x12, 0x32, 0x15, 0x00, 0x00}}; diff --git a/src/components/ble/HeartRateService.h b/src/components/ble/HeartRateService.h index 3f32fd09..ca8f10fb 100644 --- a/src/components/ble/HeartRateService.h +++ b/src/components/ble/HeartRateService.h @@ -21,14 +21,14 @@ namespace Pinetime { void SubscribeNotification(uint16_t attributeHandle); void UnsubscribeNotification(uint16_t attributeHandle); + static constexpr uint16_t heartRateServiceId {0x180D}; + static constexpr ble_uuid16_t heartRateServiceUuid {.u {.type = BLE_UUID_TYPE_16}, .value = heartRateServiceId}; + private: NimbleController& nimble; Controllers::HeartRateController& heartRateController; - static constexpr uint16_t heartRateServiceId {0x180D}; static constexpr uint16_t heartRateMeasurementId {0x2A37}; - static constexpr ble_uuid16_t heartRateServiceUuid {.u {.type = BLE_UUID_TYPE_16}, .value = heartRateServiceId}; - static constexpr ble_uuid16_t heartRateMeasurementUuid {.u {.type = BLE_UUID_TYPE_16}, .value = heartRateMeasurementId}; struct ble_gatt_chr_def characteristicDefinition[2]; diff --git a/src/components/ble/NimbleController.cpp b/src/components/ble/NimbleController.cpp index 2e7f8003..f1411a3e 100644 --- a/src/components/ble/NimbleController.cpp +++ b/src/components/ble/NimbleController.cpp @@ -158,7 +158,10 @@ void NimbleController::StartAdvertising() { } fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP; - fields.uuids128 = &dfuServiceUuid; + fields.uuids16 = &HeartRateService::heartRateServiceUuid; + fields.num_uuids16 = 1; + fields.uuids16_is_complete = 1; + fields.uuids128 = &DfuService::serviceUuid; fields.num_uuids128 = 1; fields.uuids128_is_complete = 1; fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO; diff --git a/src/components/ble/NimbleController.h b/src/components/ble/NimbleController.h index 29a395ea..597ef0cc 100644 --- a/src/components/ble/NimbleController.h +++ b/src/components/ble/NimbleController.h @@ -112,10 +112,6 @@ namespace Pinetime { uint16_t connectionHandle = BLE_HS_CONN_HANDLE_NONE; uint8_t fastAdvCount = 0; uint8_t bondId[16] = {0}; - - ble_uuid128_t dfuServiceUuid { - .u {.type = BLE_UUID_TYPE_128}, - .value = {0x23, 0xD1, 0xBC, 0xEA, 0x5F, 0x78, 0x23, 0x15, 0xDE, 0xEF, 0x12, 0x12, 0x30, 0x15, 0x00, 0x00}}; }; static NimbleController* nptr; diff --git a/src/components/brightness/BrightnessController.cpp b/src/components/brightness/BrightnessController.cpp index 0392158c..4d1eba6a 100644 --- a/src/components/brightness/BrightnessController.cpp +++ b/src/components/brightness/BrightnessController.cpp @@ -2,38 +2,138 @@ #include #include "displayapp/screens/Symbols.h" #include "drivers/PinMap.h" +#include using namespace Pinetime::Controllers; +namespace { + // reinterpret_cast is not constexpr so this is the best we can do + static NRF_RTC_Type* const RTC = reinterpret_cast(NRF_RTC2_BASE); +} + void BrightnessController::Init() { nrf_gpio_cfg_output(PinMap::LcdBacklightLow); nrf_gpio_cfg_output(PinMap::LcdBacklightMedium); nrf_gpio_cfg_output(PinMap::LcdBacklightHigh); + + nrf_gpio_pin_clear(PinMap::LcdBacklightLow); + nrf_gpio_pin_clear(PinMap::LcdBacklightMedium); + nrf_gpio_pin_clear(PinMap::LcdBacklightHigh); + + static_assert(timerFrequency == 32768, "Change the prescaler below"); + RTC->PRESCALER = 0; + // CC1 switches the backlight on (pin transitions from high to low) and resets the counter to 0 + RTC->CC[1] = timerPeriod; + // Enable compare events for CC0,CC1 + RTC->EVTEN = 0b0000'0000'0000'0011'0000'0000'0000'0000; + // Disable all interrupts + RTC->INTENCLR = 0b0000'0000'0000'1111'0000'0000'0000'0011; Set(level); } +void BrightnessController::ApplyBrightness(uint16_t rawBrightness) { + // The classic off, low, medium, high brightnesses are at {0, timerPeriod, timerPeriod*2, timerPeriod*3} + // These brightness levels do not use PWM: they only set/clear the corresponding pins + // Any brightness level between the above levels is achieved with efficient RTC based PWM on the next pin up + // E.g 2.5*timerPeriod corresponds to medium brightness with 50% PWM on the high pin + // Note: Raw brightness does not necessarily correspond to a linear perceived brightness + + uint8_t pin; + if (rawBrightness > 2 * timerPeriod) { + rawBrightness -= 2 * timerPeriod; + pin = PinMap::LcdBacklightHigh; + } else if (rawBrightness > timerPeriod) { + rawBrightness -= timerPeriod; + pin = PinMap::LcdBacklightMedium; + } else { + pin = PinMap::LcdBacklightLow; + } + if (rawBrightness == timerPeriod || rawBrightness == 0) { + if (lastPin != UNSET) { + RTC->TASKS_STOP = 1; + nrf_delay_us(rtcStopTime); + nrf_ppi_channel_disable(ppiBacklightOff); + nrf_ppi_channel_disable(ppiBacklightOn); + nrfx_gpiote_out_uninit(lastPin); + nrf_gpio_cfg_output(lastPin); + } + lastPin = UNSET; + if (rawBrightness == 0) { + nrf_gpio_pin_set(pin); + } else { + nrf_gpio_pin_clear(pin); + } + } else { + // If the pin on which we are doing PWM is changing + // Disable old PWM channel (if exists) and set up new one + if (lastPin != pin) { + if (lastPin != UNSET) { + RTC->TASKS_STOP = 1; + nrf_delay_us(rtcStopTime); + nrf_ppi_channel_disable(ppiBacklightOff); + nrf_ppi_channel_disable(ppiBacklightOn); + nrfx_gpiote_out_uninit(lastPin); + nrf_gpio_cfg_output(lastPin); + } + nrfx_gpiote_out_config_t gpioteCfg = {.action = NRF_GPIOTE_POLARITY_TOGGLE, + .init_state = NRF_GPIOTE_INITIAL_VALUE_LOW, + .task_pin = true}; + APP_ERROR_CHECK(nrfx_gpiote_out_init(pin, &gpioteCfg)); + nrfx_gpiote_out_task_enable(pin); + nrf_ppi_channel_endpoint_setup(ppiBacklightOff, + reinterpret_cast(&RTC->EVENTS_COMPARE[0]), + nrfx_gpiote_out_task_addr_get(pin)); + nrf_ppi_channel_endpoint_setup(ppiBacklightOn, + reinterpret_cast(&RTC->EVENTS_COMPARE[1]), + nrfx_gpiote_out_task_addr_get(pin)); + nrf_ppi_fork_endpoint_setup(ppiBacklightOn, reinterpret_cast(&RTC->TASKS_CLEAR)); + nrf_ppi_channel_enable(ppiBacklightOff); + nrf_ppi_channel_enable(ppiBacklightOn); + } else { + // If the pin used for PWM isn't changing, we only need to set the pin state to the initial value (low) + RTC->TASKS_STOP = 1; + nrf_delay_us(rtcStopTime); + // Due to errata 20,179 and the intricacies of RTC timing, keep it simple: override the pin state + nrfx_gpiote_out_task_force(pin, false); + } + // CC0 switches the backlight off (pin transitions from low to high) + RTC->CC[0] = rawBrightness; + RTC->TASKS_CLEAR = 1; + RTC->TASKS_START = 1; + lastPin = pin; + } + switch (pin) { + case PinMap::LcdBacklightHigh: + nrf_gpio_pin_clear(PinMap::LcdBacklightLow); + nrf_gpio_pin_clear(PinMap::LcdBacklightMedium); + break; + case PinMap::LcdBacklightMedium: + nrf_gpio_pin_clear(PinMap::LcdBacklightLow); + nrf_gpio_pin_set(PinMap::LcdBacklightHigh); + break; + case PinMap::LcdBacklightLow: + nrf_gpio_pin_set(PinMap::LcdBacklightMedium); + nrf_gpio_pin_set(PinMap::LcdBacklightHigh); + } +} + void BrightnessController::Set(BrightnessController::Levels level) { this->level = level; switch (level) { default: case Levels::High: - nrf_gpio_pin_clear(PinMap::LcdBacklightLow); - nrf_gpio_pin_clear(PinMap::LcdBacklightMedium); - nrf_gpio_pin_clear(PinMap::LcdBacklightHigh); + ApplyBrightness(3 * timerPeriod); break; case Levels::Medium: - nrf_gpio_pin_clear(PinMap::LcdBacklightLow); - nrf_gpio_pin_clear(PinMap::LcdBacklightMedium); - nrf_gpio_pin_set(PinMap::LcdBacklightHigh); + ApplyBrightness(2 * timerPeriod); break; case Levels::Low: - nrf_gpio_pin_clear(PinMap::LcdBacklightLow); - nrf_gpio_pin_set(PinMap::LcdBacklightMedium); - nrf_gpio_pin_set(PinMap::LcdBacklightHigh); + ApplyBrightness(timerPeriod); + break; + case Levels::AlwaysOn: + ApplyBrightness(timerPeriod / 10); break; case Levels::Off: - nrf_gpio_pin_set(PinMap::LcdBacklightLow); - nrf_gpio_pin_set(PinMap::LcdBacklightMedium); - nrf_gpio_pin_set(PinMap::LcdBacklightHigh); + ApplyBrightness(0); break; } } diff --git a/src/components/brightness/BrightnessController.h b/src/components/brightness/BrightnessController.h index 7f86759a..650749a8 100644 --- a/src/components/brightness/BrightnessController.h +++ b/src/components/brightness/BrightnessController.h @@ -2,11 +2,14 @@ #include +#include "nrf_ppi.h" +#include "nrfx_gpiote.h" + namespace Pinetime { namespace Controllers { class BrightnessController { public: - enum class Levels { Off, Low, Medium, High }; + enum class Levels { Off, AlwaysOn, Low, Medium, High }; void Init(); void Set(Levels level); @@ -20,6 +23,25 @@ namespace Pinetime { private: Levels level = Levels::High; + static constexpr uint8_t UNSET = UINT8_MAX; + uint8_t lastPin = UNSET; + // Maximum time (μs) it takes for the RTC to fully stop + static constexpr uint8_t rtcStopTime = 46; + // Frequency of timer used for PWM (Hz) + static constexpr uint16_t timerFrequency = 32768; + // Backlight PWM frequency (Hz) + static constexpr uint16_t pwmFreq = 1000; + // Wraparound point in timer ticks + // Defines the number of brightness levels between each pin + static constexpr uint16_t timerPeriod = timerFrequency / pwmFreq; + // Warning: nimble reserves some PPIs + // https://github.com/InfiniTimeOrg/InfiniTime/blob/034d83fe6baf1ab3875a34f8cee387e24410a824/src/libs/mynewt-nimble/nimble/drivers/nrf52/src/ble_phy.c#L53 + // SpiMaster uses PPI 0 for an erratum workaround + // Channel 1, 2 should be free to use + static constexpr nrf_ppi_channel_t ppiBacklightOn = NRF_PPI_CHANNEL1; + static constexpr nrf_ppi_channel_t ppiBacklightOff = NRF_PPI_CHANNEL2; + + void ApplyBrightness(uint16_t val); }; } } diff --git a/src/components/settings/Settings.h b/src/components/settings/Settings.h index e175ae50..ef43ff2f 100644 --- a/src/components/settings/Settings.h +++ b/src/components/settings/Settings.h @@ -227,6 +227,21 @@ namespace Pinetime { return settings.screenTimeOut; }; + bool GetAlwaysOnDisplay() const { + return settings.alwaysOnDisplay && GetNotificationStatus() != Notification::Sleep; + }; + + void SetAlwaysOnDisplaySetting(bool state) { + if (state != settings.alwaysOnDisplay) { + settingsChanged = true; + } + settings.alwaysOnDisplay = state; + } + + bool GetAlwaysOnDisplaySetting() const { + return settings.alwaysOnDisplay; + } + void SetShakeThreshold(uint16_t thresh) { if (settings.shakeWakeThreshold != thresh) { settings.shakeWakeThreshold = thresh; @@ -299,13 +314,15 @@ namespace Pinetime { private: Pinetime::Controllers::FS& fs; - static constexpr uint32_t settingsVersion = 0x0007; + static constexpr uint32_t settingsVersion = 0x0008; struct SettingsData { uint32_t version = settingsVersion; uint32_t stepsGoal = 10000; uint32_t screenTimeOut = 15000; + bool alwaysOnDisplay = false; + ClockType clockType = ClockType::H24; WeatherFormat weatherFormat = WeatherFormat::Metric; Notification notificationStatus = Notification::On; diff --git a/src/displayapp/DisplayApp.cpp b/src/displayapp/DisplayApp.cpp index 3fd34b3a..3be7656d 100644 --- a/src/displayapp/DisplayApp.cpp +++ b/src/displayapp/DisplayApp.cpp @@ -81,7 +81,8 @@ DisplayApp::DisplayApp(Drivers::St7789& lcd, Pinetime::Controllers::AlarmController& alarmController, Pinetime::Controllers::BrightnessController& brightnessController, Pinetime::Controllers::TouchHandler& touchHandler, - Pinetime::Controllers::FS& filesystem) + Pinetime::Controllers::FS& filesystem, + Pinetime::Drivers::SpiNorFlash& spiNorFlash) : lcd {lcd}, touchPanel {touchPanel}, batteryController {batteryController}, @@ -97,6 +98,7 @@ DisplayApp::DisplayApp(Drivers::St7789& lcd, brightnessController {brightnessController}, touchHandler {touchHandler}, filesystem {filesystem}, + spiNorFlash {spiNorFlash}, lvgl {lcd, filesystem}, timer(this, TimerCallback), controllers {batteryController, @@ -154,6 +156,36 @@ void DisplayApp::InitHw() { lcd.Init(); } +TickType_t DisplayApp::CalculateSleepTime() { + TickType_t ticksElapsed = xTaskGetTickCount() - alwaysOnStartTime; + // Divide both the numerator and denominator by 8 to increase the number of ticks (frames) before the overflow tick is reached + TickType_t elapsedTarget = ROUNDED_DIV((configTICK_RATE_HZ / 8) * alwaysOnTickCount * alwaysOnRefreshPeriod, 1000 / 8); + // ROUNDED_DIV overflows when numerator + (denominator floordiv 2) > uint32 max + // in this case around 9 hours + constexpr TickType_t overflowTick = (UINT32_MAX - (1000 / 16)) / ((configTICK_RATE_HZ / 8) * alwaysOnRefreshPeriod); + + // Assumptions + + // Tick rate is multiple of 8 + // Needed for division trick above + static_assert(configTICK_RATE_HZ % 8 == 0); + + // Local tick count must always wraparound before the system tick count does + // As a static assert we can use 64 bit ints and therefore dodge overflows + // Always on overflow time (ms) < system tick overflow time (ms) + static_assert((uint64_t) overflowTick * (uint64_t) alwaysOnRefreshPeriod < (uint64_t) UINT32_MAX * 1000ULL / configTICK_RATE_HZ); + + if (alwaysOnTickCount == overflowTick) { + alwaysOnTickCount = 0; + alwaysOnStartTime = xTaskGetTickCount(); + } + if (elapsedTarget > ticksElapsed) { + return elapsedTarget - ticksElapsed; + } else { + return 0; + } +} + void DisplayApp::Refresh() { auto LoadPreviousScreen = [this]() { FullRefreshDirections returnDirection; @@ -203,7 +235,29 @@ void DisplayApp::Refresh() { TickType_t queueTimeout; switch (state) { case States::Idle: - queueTimeout = portMAX_DELAY; + if (settingsController.GetAlwaysOnDisplay()) { + if (!currentScreen->IsRunning()) { + LoadPreviousScreen(); + } + // Check we've slept long enough + // Might not be true if the loop received an event + // If not true, then wait that amount of time + queueTimeout = CalculateSleepTime(); + if (queueTimeout == 0) { + // Only advance the tick count when LVGL is done + // Otherwise keep running the task handler while it still has things to draw + // Note: under high graphics load, LVGL will always have more work to do + if (lv_task_handler() > 0) { + // Drop frames that we've missed if drawing/event handling took way longer than expected + while (queueTimeout == 0) { + alwaysOnTickCount += 1; + queueTimeout = CalculateSleepTime(); + } + }; + } + } else { + queueTimeout = portMAX_DELAY; + } break; case States::Running: if (!currentScreen->IsRunning()) { @@ -234,20 +288,46 @@ void DisplayApp::Refresh() { case Messages::DimScreen: DimScreen(); break; - case Messages::RestoreBrightness: - RestoreBrightness(); - break; case Messages::GoToSleep: - while (brightnessController.Level() != Controllers::BrightnessController::Levels::Off) { + while (brightnessController.Level() != Controllers::BrightnessController::Levels::Low) { brightnessController.Lower(); vTaskDelay(100); } - lcd.Sleep(); + // Turn brightness down (or set to AlwaysOn mode) + if (settingsController.GetAlwaysOnDisplay()) { + brightnessController.Set(Controllers::BrightnessController::Levels::AlwaysOn); + } else { + brightnessController.Set(Controllers::BrightnessController::Levels::Off); + } + // Since the active screen is not really an app, go back to Clock. + if (currentApp == Apps::Launcher || currentApp == Apps::Notifications || currentApp == Apps::QuickSettings || + currentApp == Apps::Settings) { + LoadScreen(Apps::Clock, DisplayApp::FullRefreshDirections::None); + // Wait for the clock app to load before moving on. + while (!lv_task_handler()) { + }; + } + // Turn LCD display off (or set to low power for AlwaysOn mode) + if (settingsController.GetAlwaysOnDisplay()) { + lcd.LowPowerOn(); + // Record idle entry time + alwaysOnTickCount = 0; + alwaysOnStartTime = xTaskGetTickCount(); + } else { + lcd.Sleep(); + } PushMessageToSystemTask(Pinetime::System::Messages::OnDisplayTaskSleeping); state = States::Idle; break; + case Messages::NotifyDeviceActivity: + lv_disp_trig_activity(nullptr); + break; case Messages::GoToRunning: - lcd.Wakeup(); + if (settingsController.GetAlwaysOnDisplay()) { + lcd.LowPowerOff(); + } else { + lcd.Wakeup(); + } lv_disp_trig_activity(nullptr); ApplyBrightness(); state = States::Running; @@ -535,7 +615,8 @@ void DisplayApp::LoadScreen(Apps app, DisplayApp::FullRefreshDirections directio bleController, watchdog, motionController, - touchPanel); + touchPanel, + spiNorFlash); break; case Apps::FlashLight: currentScreen = std::make_unique(*systemTask, brightnessController); diff --git a/src/displayapp/DisplayApp.h b/src/displayapp/DisplayApp.h index 96bce4dd..d443b8b2 100644 --- a/src/displayapp/DisplayApp.h +++ b/src/displayapp/DisplayApp.h @@ -66,7 +66,8 @@ namespace Pinetime { Pinetime::Controllers::AlarmController& alarmController, Pinetime::Controllers::BrightnessController& brightnessController, Pinetime::Controllers::TouchHandler& touchHandler, - Pinetime::Controllers::FS& filesystem); + Pinetime::Controllers::FS& filesystem, + Pinetime::Drivers::SpiNorFlash& spiNorFlash); void Start(System::BootErrors error); void PushMessage(Display::Messages msg); @@ -96,6 +97,7 @@ namespace Pinetime { Pinetime::Controllers::BrightnessController& brightnessController; Pinetime::Controllers::TouchHandler& touchHandler; Pinetime::Controllers::FS& filesystem; + Pinetime::Drivers::SpiNorFlash& spiNorFlash; Pinetime::Controllers::FirmwareValidator validator; Pinetime::Components::LittleVgl lvgl; @@ -135,6 +137,13 @@ namespace Pinetime { Utility::StaticStack appStackDirections; bool isDimmed = false; + + TickType_t CalculateSleepTime(); + TickType_t alwaysOnTickCount; + TickType_t alwaysOnStartTime; + // If this is to be changed, make sure the actual always on refresh rate is changed + // by configuring the LCD refresh timings + static constexpr uint32_t alwaysOnRefreshPeriod = 500; }; } } diff --git a/src/displayapp/DisplayAppRecovery.cpp b/src/displayapp/DisplayAppRecovery.cpp index 28892723..bcb8db0e 100644 --- a/src/displayapp/DisplayAppRecovery.cpp +++ b/src/displayapp/DisplayAppRecovery.cpp @@ -24,7 +24,8 @@ DisplayApp::DisplayApp(Drivers::St7789& lcd, Pinetime::Controllers::AlarmController& /*alarmController*/, Pinetime::Controllers::BrightnessController& /*brightnessController*/, Pinetime::Controllers::TouchHandler& /*touchHandler*/, - Pinetime::Controllers::FS& /*filesystem*/) + Pinetime::Controllers::FS& /*filesystem*/, + Pinetime::Drivers::SpiNorFlash& /*spiNorFlash*/) : lcd {lcd}, bleController {bleController} { } diff --git a/src/displayapp/DisplayAppRecovery.h b/src/displayapp/DisplayAppRecovery.h index c1bf6243..162ff257 100644 --- a/src/displayapp/DisplayAppRecovery.h +++ b/src/displayapp/DisplayAppRecovery.h @@ -18,6 +18,7 @@ namespace Pinetime { class St7789; class Cst816S; class Watchdog; + class SpiNorFlash; } namespace Controllers { @@ -59,7 +60,8 @@ namespace Pinetime { Pinetime::Controllers::AlarmController& alarmController, Pinetime::Controllers::BrightnessController& brightnessController, Pinetime::Controllers::TouchHandler& touchHandler, - Pinetime::Controllers::FS& filesystem); + Pinetime::Controllers::FS& filesystem, + Pinetime::Drivers::SpiNorFlash& spiNorFlash); void Start(); void Start(Pinetime::System::BootErrors) { diff --git a/src/displayapp/Messages.h b/src/displayapp/Messages.h index dada3088..1418f6be 100644 --- a/src/displayapp/Messages.h +++ b/src/displayapp/Messages.h @@ -18,7 +18,7 @@ namespace Pinetime { TimerDone, BleFirmwareUpdateStarted, DimScreen, - RestoreBrightness, + NotifyDeviceActivity, ShowPairingKey, AlarmTriggered, Chime, diff --git a/src/displayapp/screens/FirmwareValidation.cpp b/src/displayapp/screens/FirmwareValidation.cpp index 4a1b3d9f..2a9919d5 100644 --- a/src/displayapp/screens/FirmwareValidation.cpp +++ b/src/displayapp/screens/FirmwareValidation.cpp @@ -17,8 +17,8 @@ namespace { FirmwareValidation::FirmwareValidation(Pinetime::Controllers::FirmwareValidator& validator) : validator {validator} { labelVersion = lv_label_create(lv_scr_act(), nullptr); lv_label_set_text_fmt(labelVersion, - "Version : %lu.%lu.%lu\n" - "ShortRef : %s", + "Version: %lu.%lu.%lu\n" + "ShortRef: %s", Version::Major(), Version::Minor(), Version::Patch(), diff --git a/src/displayapp/screens/FlashLight.cpp b/src/displayapp/screens/FlashLight.cpp index 1b7cf39c..f169fac1 100644 --- a/src/displayapp/screens/FlashLight.cpp +++ b/src/displayapp/screens/FlashLight.cpp @@ -17,6 +17,7 @@ namespace { FlashLight::FlashLight(System::SystemTask& systemTask, Controllers::BrightnessController& brightnessController) : systemTask {systemTask}, brightnessController {brightnessController} { + previousBrightnessLevel = brightnessController.Level(); brightnessController.Set(Controllers::BrightnessController::Levels::Low); flashLight = lv_label_create(lv_scr_act(), nullptr); @@ -52,6 +53,7 @@ FlashLight::FlashLight(System::SystemTask& systemTask, Controllers::BrightnessCo FlashLight::~FlashLight() { lv_obj_clean(lv_scr_act()); lv_obj_set_style_local_bg_color(lv_scr_act(), LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_BLACK); + brightnessController.Set(previousBrightnessLevel); systemTask.PushMessage(Pinetime::System::Messages::EnableSleeping); } diff --git a/src/displayapp/screens/FlashLight.h b/src/displayapp/screens/FlashLight.h index 2b710ed5..c5404e93 100644 --- a/src/displayapp/screens/FlashLight.h +++ b/src/displayapp/screens/FlashLight.h @@ -27,6 +27,7 @@ namespace Pinetime { Controllers::BrightnessController& brightnessController; Controllers::BrightnessController::Levels brightnessLevel = Controllers::BrightnessController::Levels::High; + Controllers::BrightnessController::Levels previousBrightnessLevel; lv_obj_t* flashLight; lv_obj_t* backgroundAction; diff --git a/src/displayapp/screens/SystemInfo.cpp b/src/displayapp/screens/SystemInfo.cpp index 18fb7ad2..dd39f88a 100644 --- a/src/displayapp/screens/SystemInfo.cpp +++ b/src/displayapp/screens/SystemInfo.cpp @@ -38,7 +38,8 @@ SystemInfo::SystemInfo(Pinetime::Applications::DisplayApp* app, const Pinetime::Controllers::Ble& bleController, const Pinetime::Drivers::Watchdog& watchdog, Pinetime::Controllers::MotionController& motionController, - const Pinetime::Drivers::Cst816S& touchPanel) + const Pinetime::Drivers::Cst816S& touchPanel, + const Pinetime::Drivers::SpiNorFlash& spiNorFlash) : app {app}, dateTimeController {dateTimeController}, batteryController {batteryController}, @@ -47,6 +48,7 @@ SystemInfo::SystemInfo(Pinetime::Applications::DisplayApp* app, watchdog {watchdog}, motionController {motionController}, touchPanel {touchPanel}, + spiNorFlash {spiNorFlash}, screens {app, 0, {[this]() -> std::unique_ptr { @@ -186,10 +188,12 @@ std::unique_ptr SystemInfo::CreateScreen3() { lv_obj_t* label = lv_label_create(lv_scr_act(), nullptr); lv_label_set_recolor(label, true); const auto& bleAddr = bleController.Address(); + auto spiFlashId = spiNorFlash.GetIdentification(); lv_label_set_text_fmt(label, "#808080 BLE MAC#\n" - " %02x:%02x:%02x:%02x:%02x:%02x" + " %02x:%02x:%02x:%02x:%02x:%02x\n" "\n" + "#808080 SPI Flash# %02x-%02x-%02x\n" "\n" "#808080 Memory heap#\n" " #808080 Free# %d\n" @@ -202,6 +206,9 @@ std::unique_ptr SystemInfo::CreateScreen3() { bleAddr[2], bleAddr[1], bleAddr[0], + spiFlashId.manufacturer, + spiFlashId.type, + spiFlashId.density, xPortGetFreeHeapSize(), xPortGetMinimumEverFreeHeapSize(), mallocFailedCount, diff --git a/src/displayapp/screens/SystemInfo.h b/src/displayapp/screens/SystemInfo.h index 199af51e..3129c463 100644 --- a/src/displayapp/screens/SystemInfo.h +++ b/src/displayapp/screens/SystemInfo.h @@ -29,7 +29,8 @@ namespace Pinetime { const Pinetime::Controllers::Ble& bleController, const Pinetime::Drivers::Watchdog& watchdog, Pinetime::Controllers::MotionController& motionController, - const Pinetime::Drivers::Cst816S& touchPanel); + const Pinetime::Drivers::Cst816S& touchPanel, + const Pinetime::Drivers::SpiNorFlash& spiNorFlash); ~SystemInfo() override; bool OnTouchEvent(TouchEvents event) override; @@ -42,6 +43,7 @@ namespace Pinetime { const Pinetime::Drivers::Watchdog& watchdog; Pinetime::Controllers::MotionController& motionController; const Pinetime::Drivers::Cst816S& touchPanel; + const Pinetime::Drivers::SpiNorFlash& spiNorFlash; ScreenList<5> screens; diff --git a/src/displayapp/screens/settings/SettingDisplay.cpp b/src/displayapp/screens/settings/SettingDisplay.cpp index bd533e67..57a64d7f 100644 --- a/src/displayapp/screens/settings/SettingDisplay.cpp +++ b/src/displayapp/screens/settings/SettingDisplay.cpp @@ -9,10 +9,17 @@ using namespace Pinetime::Applications::Screens; namespace { - void event_handler(lv_obj_t* obj, lv_event_t event) { + void TimeoutEventHandler(lv_obj_t* obj, lv_event_t event) { auto* screen = static_cast(obj->user_data); screen->UpdateSelected(obj, event); } + + void AlwaysOnEventHandler(lv_obj_t* obj, lv_event_t event) { + if (event == LV_EVENT_VALUE_CHANGED) { + auto* screen = static_cast(obj->user_data); + screen->ToggleAlwaysOn(); + } + } } constexpr std::array SettingDisplay::options; @@ -49,13 +56,20 @@ SettingDisplay::SettingDisplay(Pinetime::Applications::DisplayApp* app, Pinetime snprintf(buffer, sizeof(buffer), "%2" PRIu16 "s", options[i] / 1000); lv_checkbox_set_text(cbOption[i], buffer); cbOption[i]->user_data = this; - lv_obj_set_event_cb(cbOption[i], event_handler); + lv_obj_set_event_cb(cbOption[i], TimeoutEventHandler); SetRadioButtonStyle(cbOption[i]); if (settingsController.GetScreenTimeOut() == options[i]) { lv_checkbox_set_checked(cbOption[i], true); } } + + alwaysOnCheckbox = lv_checkbox_create(container1, nullptr); + lv_checkbox_set_text(alwaysOnCheckbox, "Always On"); + lv_checkbox_set_checked(alwaysOnCheckbox, settingsController.GetAlwaysOnDisplaySetting()); + lv_obj_add_state(alwaysOnCheckbox, LV_STATE_DEFAULT); + alwaysOnCheckbox->user_data = this; + lv_obj_set_event_cb(alwaysOnCheckbox, AlwaysOnEventHandler); } SettingDisplay::~SettingDisplay() { @@ -63,6 +77,11 @@ SettingDisplay::~SettingDisplay() { settingsController.SaveSettings(); } +void SettingDisplay::ToggleAlwaysOn() { + settingsController.SetAlwaysOnDisplaySetting(!settingsController.GetAlwaysOnDisplaySetting()); + lv_checkbox_set_checked(alwaysOnCheckbox, settingsController.GetAlwaysOnDisplaySetting()); +} + void SettingDisplay::UpdateSelected(lv_obj_t* object, lv_event_t event) { if (event == LV_EVENT_CLICKED) { for (unsigned int i = 0; i < options.size(); i++) { diff --git a/src/displayapp/screens/settings/SettingDisplay.h b/src/displayapp/screens/settings/SettingDisplay.h index 64212c02..b6d147c8 100644 --- a/src/displayapp/screens/settings/SettingDisplay.h +++ b/src/displayapp/screens/settings/SettingDisplay.h @@ -18,6 +18,7 @@ namespace Pinetime { ~SettingDisplay() override; void UpdateSelected(lv_obj_t* object, lv_event_t event); + void ToggleAlwaysOn(); private: DisplayApp* app; @@ -25,6 +26,7 @@ namespace Pinetime { Controllers::Settings& settingsController; lv_obj_t* cbOption[options.size()]; + lv_obj_t* alwaysOnCheckbox; }; } } diff --git a/src/drivers/SpiNorFlash.cpp b/src/drivers/SpiNorFlash.cpp index 56a8aabd..3bc45cca 100644 --- a/src/drivers/SpiNorFlash.cpp +++ b/src/drivers/SpiNorFlash.cpp @@ -10,7 +10,7 @@ SpiNorFlash::SpiNorFlash(Spi& spi) : spi {spi} { } void SpiNorFlash::Init() { - device_id = ReadIdentificaion(); + device_id = ReadIdentification(); NRF_LOG_INFO("[SpiNorFlash] Manufacturer : %d, Memory type : %d, memory density : %d", device_id.manufacturer, device_id.type, @@ -32,7 +32,7 @@ void SpiNorFlash::Wakeup() { uint8_t cmd[cmdSize] = {static_cast(Commands::ReleaseFromDeepPowerDown), 0x01, 0x02, 0x03}; uint8_t id = 0; spi.Read(reinterpret_cast(&cmd), cmdSize, &id, 1); - auto devId = device_id = ReadIdentificaion(); + auto devId = device_id = ReadIdentification(); if (devId.type != device_id.type) { NRF_LOG_INFO("[SpiNorFlash] ID on Wakeup: Failed"); } else { @@ -41,7 +41,7 @@ void SpiNorFlash::Wakeup() { NRF_LOG_INFO("[SpiNorFlash] Wakeup") } -SpiNorFlash::Identification SpiNorFlash::ReadIdentificaion() { +SpiNorFlash::Identification SpiNorFlash::ReadIdentification() { auto cmd = static_cast(Commands::ReadIdentification); Identification identification; spi.Read(&cmd, 1, reinterpret_cast(&identification), sizeof(Identification)); @@ -145,3 +145,7 @@ void SpiNorFlash::Write(uint32_t address, const uint8_t* buffer, size_t size) { len -= toWrite; } } + +SpiNorFlash::Identification SpiNorFlash::GetIdentification() const { + return device_id; +} diff --git a/src/drivers/SpiNorFlash.h b/src/drivers/SpiNorFlash.h index 8a063fea..028f92b4 100644 --- a/src/drivers/SpiNorFlash.h +++ b/src/drivers/SpiNorFlash.h @@ -20,7 +20,6 @@ namespace Pinetime { uint8_t density = 0; }; - Identification ReadIdentificaion(); uint8_t ReadStatusRegister(); bool WriteInProgress(); bool WriteEnabled(); @@ -33,6 +32,8 @@ namespace Pinetime { bool ProgramFailed(); bool EraseFailed(); + Identification GetIdentification() const; + void Init(); void Uninit(); @@ -40,6 +41,8 @@ namespace Pinetime { void Wakeup(); private: + Identification ReadIdentification(); + enum class Commands : uint8_t { PageProgram = 0x02, Read = 0x03, diff --git a/src/drivers/St7789.cpp b/src/drivers/St7789.cpp index c22f2199..0df19b45 100644 --- a/src/drivers/St7789.cpp +++ b/src/drivers/St7789.cpp @@ -16,6 +16,7 @@ void St7789::Init() { nrf_gpio_pin_set(pinReset); HardwareReset(); SoftwareReset(); + Command2Enable(); SleepOut(); PixelFormat(); MemoryDataAccessControl(); @@ -24,8 +25,13 @@ void St7789::Init() { #ifndef DRIVER_DISPLAY_MIRROR DisplayInversionOn(); #endif + PorchSet(); + FrameRateNormalSet(); + IdleFrameRateOff(); NormalModeOn(); SetVdv(); + PowerControl(); + GateControl(); DisplayOn(); } @@ -63,6 +69,17 @@ void St7789::SoftwareReset() { vTaskDelay(pdMS_TO_TICKS(125)); } +void St7789::Command2Enable() { + WriteCommand(static_cast(Commands::Command2Enable)); + constexpr uint8_t args[] = { + 0x5a, // Constant + 0x69, // Constant + 0x02, // Constant + 0x01, // Enable + }; + WriteData(args, sizeof(args)); +} + void St7789::SleepOut() { if (!sleepIn) { return; @@ -127,10 +144,79 @@ void St7789::NormalModeOn() { WriteCommand(static_cast(Commands::NormalModeOn)); } +void St7789::IdleModeOn() { + WriteCommand(static_cast(Commands::IdleModeOn)); +} + +void St7789::IdleModeOff() { + WriteCommand(static_cast(Commands::IdleModeOff)); +} + +void St7789::PorchSet() { + WriteCommand(static_cast(Commands::Porch)); + constexpr uint8_t args[] = { + 0x02, // Normal mode front porch + 0x03, // Normal mode back porch + 0x01, // Porch control enable + 0xed, // Idle mode front:back porch + 0xed, // Partial mode front:back porch (partial mode unused but set anyway) + }; + WriteData(args, sizeof(args)); +} + +void St7789::FrameRateNormalSet() { + WriteCommand(static_cast(Commands::FrameRateNormal)); + // Note that the datasheet table is imprecise - see formula below table + WriteData(0x0a); +} + +void St7789::IdleFrameRateOn() { + WriteCommand(static_cast(Commands::FrameRateIdle)); + // According to the datasheet, these controls should apply only to partial/idle mode + // However they appear to apply to normal mode, so we have to enable/disable + // every time we enter/exit always on + // In testing this divider appears to actually be 16x? + constexpr uint8_t args[] = { + 0x13, // Enable frame rate control for partial/idle mode, 8x frame divider + 0x1e, // Idle mode frame rate + 0x1e, // Partial mode frame rate (unused) + }; + WriteData(args, sizeof(args)); +} + +void St7789::IdleFrameRateOff() { + WriteCommand(static_cast(Commands::FrameRateIdle)); + constexpr uint8_t args[] = { + 0x00, // Disable frame rate control and divider + 0x0a, // Idle mode frame rate (normal) + 0x0a, // Partial mode frame rate (normal, unused) + }; + WriteData(args, sizeof(args)); +} + void St7789::DisplayOn() { WriteCommand(static_cast(Commands::DisplayOn)); } +void St7789::PowerControl() { + WriteCommand(static_cast(Commands::PowerControl1)); + constexpr uint8_t args[] = { + 0xa4, // Constant + 0x00, // Lowest possible voltages + }; + WriteData(args, sizeof(args)); + + WriteCommand(static_cast(Commands::PowerControl2)); + // Lowest possible boost circuit clocks + WriteData(0xb3); +} + +void St7789::GateControl() { + WriteCommand(static_cast(Commands::GateControl)); + // Lowest possible VGL/VGH + WriteData(0x00); +} + void St7789::SetAddrWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { WriteCommand(static_cast(Commands::ColumnAddressSet)); uint8_t colArgs[] = { @@ -198,6 +284,18 @@ void St7789::HardwareReset() { vTaskDelay(pdMS_TO_TICKS(125)); } +void St7789::LowPowerOn() { + IdleModeOn(); + IdleFrameRateOn(); + NRF_LOG_INFO("[LCD] Low power mode"); +} + +void St7789::LowPowerOff() { + IdleModeOff(); + IdleFrameRateOff(); + NRF_LOG_INFO("[LCD] Normal power mode"); +} + void St7789::Sleep() { SleepIn(); nrf_gpio_cfg_default(pinDataCommand); diff --git a/src/drivers/St7789.h b/src/drivers/St7789.h index 844e0180..9c778905 100644 --- a/src/drivers/St7789.h +++ b/src/drivers/St7789.h @@ -24,6 +24,8 @@ namespace Pinetime { void DrawBuffer(uint16_t x, uint16_t y, uint16_t width, uint16_t height, const uint8_t* data, size_t size); + void LowPowerOn(); + void LowPowerOff(); void Sleep(); void Wakeup(); @@ -37,6 +39,7 @@ namespace Pinetime { void HardwareReset(); void SoftwareReset(); + void Command2Enable(); void SleepOut(); void EnsureSleepOutPostDelay(); void SleepIn(); @@ -45,8 +48,16 @@ namespace Pinetime { void DisplayInversionOn(); void NormalModeOn(); void WriteToRam(const uint8_t* data, size_t size); + void IdleModeOn(); + void IdleModeOff(); + void FrameRateNormalSet(); + void IdleFrameRateOff(); + void IdleFrameRateOn(); void DisplayOn(); void DisplayOff(); + void PowerControl(); + void GateControl(); + void PorchSet(); void SetAddrWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1); void SetVdv(); @@ -68,8 +79,17 @@ namespace Pinetime { MemoryDataAccessControl = 0x36, VerticalScrollDefinition = 0x33, VerticalScrollStartAddress = 0x37, + IdleModeOff = 0x38, + IdleModeOn = 0x39, PixelFormat = 0x3a, + FrameRateIdle = 0xb3, + FrameRateNormal = 0xc6, VdvSet = 0xc4, + Command2Enable = 0xdf, + PowerControl1 = 0xd0, + PowerControl2 = 0xe8, + GateControl = 0xb7, + Porch = 0xb2, }; void WriteData(uint8_t data); void WriteData(const uint8_t* data, size_t size); diff --git a/src/main.cpp b/src/main.cpp index ee6a6d3d..ab50fa74 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -123,7 +123,8 @@ Pinetime::Applications::DisplayApp displayApp(lcd, alarmController, brightnessController, touchHandler, - fs); + fs, + spiNorFlash); Pinetime::System::SystemTask systemTask(spi, spiNorFlash, diff --git a/src/systemtask/SystemTask.cpp b/src/systemtask/SystemTask.cpp index a56c2591..211e19ec 100644 --- a/src/systemtask/SystemTask.cpp +++ b/src/systemtask/SystemTask.cpp @@ -102,7 +102,9 @@ void SystemTask::Work() { watchdog.Setup(7, Drivers::Watchdog::SleepBehaviour::Run, Drivers::Watchdog::HaltBehaviour::Pause); watchdog.Start(); NRF_LOG_INFO("Last reset reason : %s", Pinetime::Drivers::ResetReasonToString(watchdog.GetResetReason())); - APP_GPIOTE_INIT(2); + if (!nrfx_gpiote_is_init()) { + nrfx_gpiote_init(); + } spi.Init(); spiNorFlash.Init(); @@ -192,13 +194,16 @@ void SystemTask::Work() { if (!bleController.IsFirmwareUpdating()) { doNotGoToSleep = false; } - displayApp.PushMessage(Pinetime::Applications::Display::Messages::RestoreBrightness); + displayApp.PushMessage(Pinetime::Applications::Display::Messages::NotifyDeviceActivity); break; case Messages::DisableSleeping: doNotGoToSleep = true; break; case Messages::GoToRunning: - spi.Wakeup(); + // SPI doesn't go to sleep for always on mode + if (!settingsController.GetAlwaysOnDisplay()) { + spi.Wakeup(); + } // Double Tap needs the touch screen to be in normal mode if (!settingsController.isWakeUpModeOn(Pinetime::Controllers::Settings::WakeUpMode::DoubleTap)) { @@ -240,7 +245,7 @@ void SystemTask::Work() { heartRateApp.PushMessage(Pinetime::Applications::HeartRateTask::Messages::GoToSleep); break; case Messages::OnNewTime: - displayApp.PushMessage(Pinetime::Applications::Display::Messages::RestoreBrightness); + displayApp.PushMessage(Pinetime::Applications::Display::Messages::NotifyDeviceActivity); displayApp.PushMessage(Pinetime::Applications::Display::Messages::UpdateDateTime); if (alarmController.State() == Controllers::AlarmController::AlarmState::Set) { alarmController.ScheduleAlarm(); @@ -251,7 +256,7 @@ void SystemTask::Work() { if (state == SystemTaskState::Sleeping) { GoToRunning(); } else { - displayApp.PushMessage(Pinetime::Applications::Display::Messages::RestoreBrightness); + displayApp.PushMessage(Pinetime::Applications::Display::Messages::NotifyDeviceActivity); } displayApp.PushMessage(Pinetime::Applications::Display::Messages::NewNotification); } @@ -263,7 +268,7 @@ void SystemTask::Work() { displayApp.PushMessage(Pinetime::Applications::Display::Messages::AlarmTriggered); break; case Messages::BleConnected: - displayApp.PushMessage(Pinetime::Applications::Display::Messages::RestoreBrightness); + displayApp.PushMessage(Pinetime::Applications::Display::Messages::NotifyDeviceActivity); isBleDiscoveryTimerRunning = true; bleDiscoveryTimer = 5; break; @@ -323,7 +328,11 @@ void SystemTask::Work() { // if it's in sleep mode. Avoid bricked device by disabling sleep mode on these versions. spiNorFlash.Sleep(); } - spi.Sleep(); + + // Must keep SPI awake when still updating the display for always on + if (!settingsController.GetAlwaysOnDisplay()) { + spi.Sleep(); + } // Double Tap needs the touch screen to be in normal mode if (!settingsController.isWakeUpModeOn(Pinetime::Controllers::Settings::WakeUpMode::DoubleTap)) { @@ -457,7 +466,7 @@ void SystemTask::HandleButtonAction(Controllers::ButtonActions action) { return; } - displayApp.PushMessage(Pinetime::Applications::Display::Messages::RestoreBrightness); + displayApp.PushMessage(Pinetime::Applications::Display::Messages::NotifyDeviceActivity); using Actions = Controllers::ButtonActions;