~aleteoryx/muditaos

49bbaf51a012cce32d173394038eb981d9f0e278 — Adam Wulkiewicz 3 years ago 97bd117
[MOS-550] Improve refresh of the display

1. Implement partial refresh.
2. Implement refresh canceling mechanism.
3. Refactor some parts of the gui and display code.

ad 1.
- Detect parts of the screen changed since last update and merge them
  into bigger regions. These regions defines parts of the context sent
  to the display.
- Refresh the region covering all of the parts since this is the most
  time consuming part and the size of the refreshed region doesn't
  change the time much.
- Refresh the whole screen if deep refresh is requested and previously
  fast refresh was used. This is needed to prevent unwanted artifacts
  in some cases.

ad 2.
- Separate display update and refresh logic.
- Divide image display message handling into two handlers, one updating
  and other one refreshing the screen.
- Add cancel refresh message and use it to cancel refresh during update.
- Store sum of refresh regions gathered during updates to refresh them
  all at once at the end.
36 files changed, 837 insertions(+), 310 deletions(-)

M .vscode/launch.json
M .vscode/tasks.json
M harmony_changelog.md
M module-apps/application-call/windows/CallWindow.cpp
M module-bsp/board/linux/eink/ED028TC1.c
M module-bsp/board/linux/eink/ED028TC1.h
M module-bsp/board/linux/eink/LinuxEinkDisplay.cpp
M module-bsp/board/linux/eink/LinuxEinkDisplay.hpp
M module-bsp/board/rt1051/bsp/eink/ED028TC1.cpp
M module-bsp/board/rt1051/bsp/eink/ED028TC1.h
M module-bsp/board/rt1051/bsp/eink/EinkDisplay.cpp
M module-bsp/board/rt1051/bsp/eink/EinkDisplay.hpp
M module-bsp/board/rt1051/bsp/eink/eink_dimensions.cpp
M module-bsp/hal/include/hal/eink/AbstractEinkDisplay.hpp
M module-gui/gui/core/BoundingBox.cpp
M module-gui/gui/core/BoundingBox.hpp
M module-gui/gui/core/Context.cpp
M module-gui/gui/core/Context.hpp
M module-gui/gui/core/DrawCommand.cpp
M module-gui/gui/core/ImageManager.cpp
M module-gui/gui/core/RawFont.cpp
M module-gui/gui/core/Renderer.cpp
M module-gui/gui/core/Renderer.hpp
M module-gui/gui/core/renderers/RectangleRenderer.cpp
M module-gui/gui/widgets/Item.cpp
M module-gui/gui/widgets/Rect.cpp
M module-gui/test/test-catch/test-context.cpp
M module-services/service-eink/ServiceEink.cpp
M module-services/service-eink/ServiceEink.hpp
M module-services/service-eink/messages/ImageMessage.cpp
M module-services/service-eink/messages/ImageMessage.hpp
M module-services/service-gui/ServiceGUI.cpp
M module-services/service-gui/WorkerGUI.cpp
M module-services/service-gui/service-gui/ServiceGUI.hpp
M module-utils/log/api/log/debug.hpp
M pure_changelog.md
M .vscode/launch.json => .vscode/launch.json +2 -1
@@ 64,7 64,7 @@
            "cwd": "${workspaceFolder}",
            "gdbPath": "arm-none-eabi-gdb",
            // "debuggerArgs": ["-x", "${workspaceFolder}/.gdbinit-1051"],
            // "serverArgs": ["-strict", "-ir", "-singlerun", "-speed", "25000"],
            "serverArgs": ["-strict", "-ir", "-singlerun", "-speed", "25000"],
            // "runToEntryPoint": "main",
            // "runToMain": true,
            "preLaunchCommands": [],


@@ 109,6 109,7 @@
            "serverpath": "JLinkGDBServerCLExe",
            "cwd": "${workspaceFolder}",
            "gdbPath": "arm-none-eabi-gdb",
            "serverArgs": ["-strict", "-ir", "-singlerun", "-speed", "25000"],
            "interface": "swd",
            "device": "MCIMXRT1051",
            "jlinkscript": "${workspaceFolder}/evkbimxrt1050_sdram_init_T6.jlinkscript",

M .vscode/tasks.json => .vscode/tasks.json +38 -0
@@ 169,6 169,44 @@
            "detail": "Configure cmake project and build with Ninja",
        },

        {
            "type": "shell",
            "label": "Configure PurePhone (RT1051, DEV)",
            "command": "./configure.sh",
            "args": [
                "pure",
                "rt1051",
                "RelWithDebInfo",
                "-DWITH_DEVELOPMENT_FEATURES=ON",
                "-G",
                "Ninja"
            ],
            "options": {
                "cwd": "${workspaceFolder}"
            },
            "group": "build",
            "detail": "Run cmake configure script",
        },

        {
            "type": "shell",
            "label": "Configure BellHybrid (RT1051, DEV)",
            "command": "./configure.sh",
            "args": [
                "bell",
                "rt1051",
                "RelWithDebInfo",
                "-DWITH_DEVELOPMENT_FEATURES=ON",
                "-G",
                "Ninja"
            ],
            "options": {
                "cwd": "${workspaceFolder}"
            },
            "group": "build",
            "detail": "Run cmake configure script",
        },

        // {
        //     "type": "shell",
        //     "label": "Run JLink Server for PurePhone (RT1051)",

M harmony_changelog.md => harmony_changelog.md +1 -0
@@ 23,6 23,7 @@

#### UI/UX:
* UI update (Home Screen settings).
* Improve refreshing of the display.

#### Translations:
* Completed missing translations.

M module-apps/application-call/windows/CallWindow.cpp => module-apps/application-call/windows/CallWindow.cpp +4 -0
@@ 123,6 123,10 @@ namespace gui
                                                        app::manager::OnSwitchBehaviour::RunInBackground);
        };

        sendSmsIcon->setVisible(false);
        microphoneIcon->setVisible(false);
        speakerIcon->setVisible(false);

        // define navigation between icons
        microphoneIcon->setNavigationItem(NavigationDirection::LEFT, speakerIcon);
        microphoneIcon->setNavigationItem(NavigationDirection::RIGHT, speakerIcon);

M module-bsp/board/linux/eink/ED028TC1.c => module-bsp/board/linux/eink/ED028TC1.c +2 -2
@@ 1,4 1,4 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

/**


@@ 119,7 119,7 @@ EinkStatus_e EinkResetAndInitialize()
    return EinkOK;
}

EinkStatus_e EinkUpdateFrame(EinkFrame_t frame, uint8_t *buffer)
EinkStatus_e EinkUpdateFrame(EinkFrame_t frame, const uint8_t *buffer)
{
    uint32_t offset_eink   = frame.pos_y * BOARD_EINK_DISPLAY_RES_X + frame.pos_x;
    uint32_t offset_buffer = 0;

M module-bsp/board/linux/eink/ED028TC1.h => module-bsp/board/linux/eink/ED028TC1.h +2 -2
@@ 1,4 1,4 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

/**


@@ 90,7 90,7 @@ extern "C"
     * @return  EinkNoMem - Could not allocate the temporary buffer
     *          EinkOK - Part of image send successfully
     */
    EinkStatus_e EinkUpdateFrame(EinkFrame_t frame, uint8_t *buffer);
    EinkStatus_e EinkUpdateFrame(EinkFrame_t frame, const uint8_t *buffer);

    /**
     * @brief This function sets the waveform to the \ref EinkWaveformINIT to make the display clearing more deep and

M module-bsp/board/linux/eink/LinuxEinkDisplay.cpp => module-bsp/board/linux/eink/LinuxEinkDisplay.cpp +23 -2
@@ 47,9 47,30 @@ namespace hal::eink
        displayColorMode = mode;
    }

    EinkStatus LinuxEinkDisplay::showImage(std::uint8_t *frameBuffer, const EinkRefreshMode refreshMode)
    EinkStatus LinuxEinkDisplay::showImageUpdate(const std::vector<EinkFrame> &updateFrames,
                                                 const std::uint8_t *frameBuffer)
    {
        return translateStatus(EinkUpdateFrame(EinkFrame_t{0, 0, size.width, size.height}, frameBuffer));
        for (const EinkFrame &frame : updateFrames) {
            const std::uint8_t *buffer = frameBuffer + frame.pos_y * frame.size.width;
            const auto status          = translateStatus(
                EinkUpdateFrame({frame.pos_x, frame.pos_y, frame.size.width, frame.size.height}, buffer));
            if (status != EinkStatus::EinkOK)
                return status;
        }
        return EinkStatus::EinkOK;
    }

    EinkStatus LinuxEinkDisplay::showImageRefresh(const EinkFrame &refreshFrame, const EinkRefreshMode refreshMode)
    {
        return EinkStatus::EinkOK;
    }

    EinkStatus LinuxEinkDisplay::showImage(const std::vector<EinkFrame> &updateFrames,
                                           const EinkFrame &refreshFrame,
                                           const std::uint8_t *frameBuffer,
                                           const EinkRefreshMode refreshMode)
    {
        return showImageUpdate(updateFrames, frameBuffer);
    }

    void LinuxEinkDisplay::prepareEarlyRequest([[maybe_unused]] const EinkRefreshMode refreshMode,

M module-bsp/board/linux/eink/LinuxEinkDisplay.hpp => module-bsp/board/linux/eink/LinuxEinkDisplay.hpp +7 -1
@@ 15,7 15,13 @@ namespace hal::eink

      private:
        void setMode(const EinkDisplayColorMode mode) noexcept override;
        EinkStatus showImage(std::uint8_t *frameBuffer, const EinkRefreshMode refreshMode) override;
        EinkStatus showImageUpdate(const std::vector<EinkFrame> &updateFrames,
                                   const std::uint8_t *frameBuffer) override;
        EinkStatus showImageRefresh(const EinkFrame &refreshFrame, const EinkRefreshMode refreshMode) override;
        EinkStatus showImage(const std::vector<EinkFrame> &updateFrames,
                             const EinkFrame &refreshFrame,
                             const std::uint8_t *frameBuffer,
                             const EinkRefreshMode refreshMode) override;
        void prepareEarlyRequest(const EinkRefreshMode refreshMode, const WaveformTemperature behaviour) override;

        void dither() override;

M module-bsp/board/rt1051/bsp/eink/ED028TC1.cpp => module-bsp/board/rt1051/bsp/eink/ED028TC1.cpp +22 -19
@@ 138,7 138,7 @@ static uint8_t s_einkMaskLut_2Bpp[16] = {0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 
 *
 * @return
 */
static uint8_t *s_EinkTransformFrameCoordinateSystem_1Bpp(uint8_t *dataIn,
static uint8_t *s_EinkTransformFrameCoordinateSystem_1Bpp(const uint8_t *dataIn,
                                                          uint16_t windowWidthPx,
                                                          uint16_t windowHeightPx,
                                                          uint8_t *dataOut,


@@ 187,7 187,7 @@ static uint8_t *s_EinkTransformFrameCoordinateSystem_1Bpp(uint8_t *dataIn,
 *
 * @return
 */
static uint8_t *s_EinkTransformFrameCoordinateSystem_2Bpp(uint8_t *dataIn,
static uint8_t *s_EinkTransformFrameCoordinateSystem_2Bpp(const uint8_t *dataIn,
                                                          uint16_t windowWidthPx,
                                                          uint16_t windowHeightPx,
                                                          uint8_t *dataOut,


@@ 236,7 236,7 @@ static uint8_t *s_EinkTransformFrameCoordinateSystem_2Bpp(uint8_t *dataIn,
 *
 * @return
 */
static uint8_t *s_EinkTransformFrameCoordinateSystem_3Bpp(uint8_t *dataIn,
static uint8_t *s_EinkTransformFrameCoordinateSystem_3Bpp(const uint8_t *dataIn,
                                                          uint16_t windowWidthPx,
                                                          uint16_t windowHeightPx,
                                                          uint8_t *dataOut,


@@ 285,7 285,7 @@ static uint8_t *s_EinkTransformFrameCoordinateSystem_3Bpp(uint8_t *dataIn,
 *
 * @return
 */
static uint8_t *s_EinkTransformFrameCoordinateSystem_4Bpp(uint8_t *dataIn,
static uint8_t *s_EinkTransformFrameCoordinateSystem_4Bpp(const uint8_t *dataIn,
                                                          uint16_t windowWidthPx,
                                                          uint16_t windowHeightPx,
                                                          uint8_t *dataOut,


@@ 334,7 334,7 @@ static uint8_t *s_EinkTransformFrameCoordinateSystem_4Bpp(uint8_t *dataIn,
 *
 * @return
 */
static uint8_t *s_EinkTransformAnimationFrameCoordinateSystem_1Bpp(uint8_t *dataIn,
static uint8_t *s_EinkTransformAnimationFrameCoordinateSystem_1Bpp(const uint8_t *dataIn,
                                                                   uint16_t windowWidthPx,
                                                                   uint16_t windowHeightPx,
                                                                   uint8_t *dataOut,


@@ 383,7 383,7 @@ static uint8_t *s_EinkTransformAnimationFrameCoordinateSystem_1Bpp(uint8_t *data
 *
 * @return
 */
static uint8_t *s_EinkTransformAnimationFrameCoordinateSystem_2Bpp(uint8_t *dataIn,
static uint8_t *s_EinkTransformAnimationFrameCoordinateSystem_2Bpp(const uint8_t *dataIn,
                                                                   uint16_t windowWidthPx,
                                                                   uint16_t windowHeightPx,
                                                                   uint8_t *dataOut,


@@ 432,7 432,7 @@ static uint8_t *s_EinkTransformAnimationFrameCoordinateSystem_2Bpp(uint8_t *data
 *
 * @return
 */
static uint8_t *s_EinkTransformAnimationFrameCoordinateSystem_3Bpp(uint8_t *dataIn,
static uint8_t *s_EinkTransformAnimationFrameCoordinateSystem_3Bpp(const uint8_t *dataIn,
                                                                   uint16_t windowWidthPx,
                                                                   uint16_t windowHeightPx,
                                                                   uint8_t *dataOut,


@@ 487,7 487,7 @@ static uint8_t *s_EinkTransformAnimationFrameCoordinateSystem_3Bpp(uint8_t *data
 * It is used when EINK_ROTATE_90_CLOCKWISE is not defined.
 */

static uint8_t *s_EinkTransformFrameCoordinateSystemNoRotation_4Bpp(uint8_t *dataIn,
static uint8_t *s_EinkTransformFrameCoordinateSystemNoRotation_4Bpp(const uint8_t *dataIn,
                                                                    uint16_t windowWidthPx,
                                                                    uint16_t windowHeightPx,
                                                                    uint8_t *dataOut,


@@ 799,7 799,10 @@ EinkStatus_e EinkDitherDisplay()
    return EinkOK;
}

EinkStatus_e EinkUpdateFrame(EinkFrame_t frame, uint8_t *buffer, EinkBpp_e bpp, EinkDisplayColorMode_e invertColors)
EinkStatus_e EinkUpdateFrame(EinkFrame_t frame,
                             const uint8_t *buffer,
                             EinkBpp_e bpp,
                             EinkDisplayColorMode_e invertColors)
{
    uint8_t buf[10];
    uint8_t pixelsInByte = 8 / bpp;


@@ 1004,7 1007,7 @@ EinkStatus_e EinkRefreshImage(EinkFrame_t frame, EinkDisplayTimingsMode_e refres
    return EinkOK;
}

__attribute__((optimize("O3"))) void EinkARGBToLuminance(uint8_t *dataIn,
__attribute__((optimize("O3"))) void EinkARGBToLuminance(const uint8_t *dataIn,
                                                         uint8_t *dataOut,
                                                         uint32_t displayWidth,
                                                         uint32_t displayHeight)


@@ 1037,7 1040,7 @@ __attribute__((optimize("O3"))) void EinkARGBToLuminance(uint8_t *dataIn,
}

__attribute__((optimize("O1"))) static uint8_t *s_EinkTransformFrameCoordinateSystem_1Bpp(
    uint8_t *dataIn,
    const uint8_t *dataIn,
    uint16_t windowWidthPx,
    uint16_t windowHeightPx,
    uint8_t *dataOut,


@@ 1084,7 1087,7 @@ __attribute__((optimize("O1"))) static uint8_t *s_EinkTransformFrameCoordinateSy
}

__attribute__((optimize("O1"))) static uint8_t *s_EinkTransformFrameCoordinateSystem_2Bpp(
    uint8_t *dataIn,
    const uint8_t *dataIn,
    uint16_t windowWidthPx,
    uint16_t windowHeightPx,
    uint8_t *dataOut,


@@ 1135,7 1138,7 @@ __attribute__((optimize("O1"))) static uint8_t *s_EinkTransformFrameCoordinateSy
}

__attribute__((optimize("O1"))) static uint8_t *s_EinkTransformFrameCoordinateSystem_3Bpp(
    uint8_t *dataIn,
    const uint8_t *dataIn,
    uint16_t windowWidthPx,
    uint16_t windowHeightPx,
    uint8_t *dataOut,


@@ 1146,7 1149,7 @@ __attribute__((optimize("O1"))) static uint8_t *s_EinkTransformFrameCoordinateSy
}

__attribute__((optimize("O1"))) static uint8_t *s_EinkTransformAnimationFrameCoordinateSystem_1Bpp(
    uint8_t *dataIn,
    const uint8_t *dataIn,
    uint16_t windowWidthPx,
    uint16_t windowHeightPx,
    uint8_t *dataOut,


@@ 1193,7 1196,7 @@ __attribute__((optimize("O1"))) static uint8_t *s_EinkTransformAnimationFrameCoo
}

__attribute__((optimize("O1"))) static uint8_t *s_EinkTransformAnimationFrameCoordinateSystem_2Bpp(
    uint8_t *dataIn,
    const uint8_t *dataIn,
    uint16_t windowWidthPx,
    uint16_t windowHeightPx,
    uint8_t *dataOut,


@@ 1242,7 1245,7 @@ __attribute__((optimize("O1"))) static uint8_t *s_EinkTransformAnimationFrameCoo
}

__attribute__((optimize("O3"))) static uint8_t *s_EinkTransformAnimationFrameCoordinateSystem_3Bpp(
    uint8_t *dataIn,
    const uint8_t *dataIn,
    uint16_t windowWidthPx,
    uint16_t windowHeightPx,
    uint8_t *dataOut,


@@ 1253,7 1256,7 @@ __attribute__((optimize("O3"))) static uint8_t *s_EinkTransformAnimationFrameCoo
}

__attribute__((optimize("O1"))) static uint8_t *s_EinkTransformFrameCoordinateSystem_4Bpp(
    uint8_t *dataIn,
    const uint8_t *dataIn,
    uint16_t windowWidthPx,
    uint16_t windowHeightPx,
    uint8_t *dataOut,


@@ 1300,7 1303,7 @@ __attribute__((optimize("O1"))) static uint8_t *s_EinkTransformFrameCoordinateSy
}

__attribute__((optimize("O1"))) static uint8_t *s_EinkTransformFrameCoordinateSystemNoRotation_4Bpp(
    uint8_t *dataIn,
    const uint8_t *dataIn,
    uint16_t windowWidthPx,
    uint16_t windowHeightPx,
    uint8_t *dataOut,


@@ 1315,7 1318,7 @@ __attribute__((optimize("O1"))) static uint8_t *s_EinkTransformFrameCoordinateSy
    int32_t inputRow   = 0;
    int32_t inputCol   = 0;

    for (inputRow = 0; inputRow < windowHeightPx - 1; ++inputRow) {
    for (inputRow = 0; inputRow < windowHeightPx; ++inputRow) {
        for (inputCol = windowWidthPx - 7; inputCol >= 0; inputCol -= pixelsInByte) {
            // HACK: Did not create the loop for accessing pixels and merging them in the single byte for better
            // performance.

M module-bsp/board/rt1051/bsp/eink/ED028TC1.h => module-bsp/board/rt1051/bsp/eink/ED028TC1.h +2 -2
@@ 382,7 382,7 @@ extern "C"
     *          EinkOK - Part of image send successfully
     */
    EinkStatus_e EinkUpdateFrame(EinkFrame_t frame,
                                 uint8_t *buffer,
                                 const uint8_t *buffer,
                                 EinkBpp_e bpp,
                                 EinkDisplayColorMode_e invertColors);



@@ 431,7 431,7 @@ extern "C"
     * @param displayWidth    [in] - display width in pixels
     * @param displayHeight   [in] - display height in pixels
     */
    void EinkARGBToLuminance(uint8_t *dataIn, uint8_t *dataOut, uint32_t displayWidth, uint32_t displayHeight);
    void EinkARGBToLuminance(const uint8_t *dataIn, uint8_t *dataOut, uint32_t displayWidth, uint32_t displayHeight);

#if defined(__cplusplus)
}

M module-bsp/board/rt1051/bsp/eink/EinkDisplay.cpp => module-bsp/board/rt1051/bsp/eink/EinkDisplay.cpp +59 -29
@@ 4,10 4,10 @@
#include "EinkDisplay.hpp"

#include <board/BoardDefinitions.hpp>

#include <log/log.hpp>

#include <gsl/util>

#include <stdexcept>
#include <cstdio>
#include <cstring>


@@ 132,17 132,6 @@ namespace hal::eink
        delete[] currentWaveform.LUTDData;
    }

    EinkStatus EinkDisplay::updateDisplay(std::uint8_t *frameBuffer, [[maybe_unused]] const EinkRefreshMode refreshMode)
    {
        return update(frameBuffer);
    }

    EinkStatus EinkDisplay::refreshDisplay(const EinkRefreshMode refreshMode)
    {
        const auto isDeepRefresh = refreshMode == EinkRefreshMode::REFRESH_DEEP;
        return refresh(isDeepRefresh ? EinkDisplayTimingsDeepCleanMode : EinkDisplayTimingsFastRefreshMode);
    }

    EinkStatus EinkDisplay::prepareDisplay(const EinkRefreshMode refreshMode, const WaveformTemperature behaviour)
    {
        powerOn();


@@ 160,20 149,75 @@ namespace hal::eink
        return setWaveform(EinkWaveforms_e::EinkWaveformDU2, temperature);
    }

    EinkStatus EinkDisplay::showImage(std::uint8_t *frameBuffer, const EinkRefreshMode refreshMode)
    EinkStatus EinkDisplay::updateDisplay(EinkFrame frame, const std::uint8_t *frameBuffer)
    {
        return translateStatus(
            EinkUpdateFrame(EinkFrame_t{frame.pos_x, frame.pos_y, frame.size.width, frame.size.height},
                            frameBuffer,
                            getCurrentBitsPerPixelFormat(),
                            translateDisplayColorMode(displayMode)));
    }

    EinkStatus EinkDisplay::refreshDisplay(EinkFrame frame, const EinkRefreshMode refreshMode)
    {
        const auto refreshTimingsMode = refreshMode == EinkRefreshMode::REFRESH_DEEP
                                            ? EinkDisplayTimingsDeepCleanMode
                                            : EinkDisplayTimingsFastRefreshMode;

        currentWaveform.useCounter += 1;
        return translateStatus(EinkRefreshImage(
            EinkFrame_t{frame.pos_x, frame.pos_y, frame.size.width, frame.size.height}, refreshTimingsMode));
    }

    EinkStatus EinkDisplay::showImageUpdate(const std::vector<EinkFrame> &updateFrames, const std::uint8_t *frameBuffer)
    {
        powerOn();

        for (const EinkFrame &frame : updateFrames) {
            const std::uint8_t *buffer = frameBuffer + frame.pos_y * frame.size.width;
            if (const auto status = updateDisplay(frame, buffer); status != EinkStatus::EinkOK) {
                return status;
            }
        }

        return EinkStatus::EinkOK;
    }

    EinkStatus EinkDisplay::showImageRefresh(const EinkFrame &refreshFrame, const EinkRefreshMode refreshMode)
    {
        if (const auto status = prepareDisplay(refreshMode, WaveformTemperature::KEEP_CURRENT);
            status != EinkStatus::EinkOK) {
            return status;
        }

        if (const auto status = updateDisplay(frameBuffer, refreshMode); status != EinkStatus::EinkOK) {
        if (const auto status = refreshDisplay(refreshFrame, refreshMode); status != EinkStatus::EinkOK) {
            return status;
        }

        return EinkStatus::EinkOK;
    }

    EinkStatus EinkDisplay::showImage(const std::vector<EinkFrame> &updateFrames,
                                      const EinkFrame &refreshFrame,
                                      const std::uint8_t *frameBuffer,
                                      const EinkRefreshMode refreshMode)
    {
        if (const auto status = prepareDisplay(refreshMode, WaveformTemperature::KEEP_CURRENT);
            status != EinkStatus::EinkOK) {
            return status;
        }

        if (const auto status = refreshDisplay(refreshMode); status != EinkStatus::EinkOK) {
        for (const EinkFrame &frame : updateFrames) {
            const std::uint8_t *buffer = frameBuffer + frame.pos_y * frame.size.width;
            if (const auto status = updateDisplay(frame, buffer); status != EinkStatus::EinkOK) {
                return status;
            }
        }

        if (const auto status = refreshDisplay(refreshFrame, refreshMode); status != EinkStatus::EinkOK) {
            return status;
        }

        return EinkStatus::EinkOK;
    }



@@ 218,14 262,6 @@ namespace hal::eink
        EinkFillScreenWithColor(EinkDisplayColorFilling_e::EinkDisplayColorWhite);
    }

    EinkStatus EinkDisplay::update(std::uint8_t *displayBuffer)
    {
        return translateStatus(EinkUpdateFrame(EinkFrame_t{0, 0, size.width, size.height},
                                               displayBuffer,
                                               getCurrentBitsPerPixelFormat(),
                                               translateDisplayColorMode(displayMode)));
    }

    EinkBpp_e EinkDisplay::getCurrentBitsPerPixelFormat() const noexcept
    {
        if ((currentWaveform.mode == EinkWaveformA2) || (currentWaveform.mode == EinkWaveformDU2)) {


@@ 234,12 270,6 @@ namespace hal::eink
        return Eink4Bpp;
    }

    EinkStatus EinkDisplay::refresh(const EinkDisplayTimingsMode_e refreshMode)
    {
        currentWaveform.useCounter += 1;
        return translateStatus(EinkRefreshImage(EinkFrame_t{0, 0, size.width, size.height}, refreshMode));
    }

    bool EinkDisplay::isNewWaveformNeeded(const EinkWaveforms_e newMode, const std::int32_t newTemperature) const
    {
        constexpr auto lenientTemperatureUseCounter = 50; // arbitrary. not documented

M module-bsp/board/rt1051/bsp/eink/EinkDisplay.hpp => module-bsp/board/rt1051/bsp/eink/EinkDisplay.hpp +9 -6
@@ 26,7 26,13 @@ namespace hal::eink
        ~EinkDisplay() noexcept;

        void setMode(EinkDisplayColorMode mode) noexcept override;
        EinkStatus showImage(std::uint8_t *frameBuffer, const EinkRefreshMode refreshMode) override;
        EinkStatus showImageUpdate(const std::vector<EinkFrame> &updateFrames,
                                   const std::uint8_t *frameBuffer) override;
        EinkStatus showImageRefresh(const EinkFrame &refreshFrame, const EinkRefreshMode refreshMode) override;
        EinkStatus showImage(const std::vector<EinkFrame> &updateFrames,
                             const EinkFrame &refreshFrame,
                             const std::uint8_t *frameBuffer,
                             const EinkRefreshMode refreshMode) override;
        void prepareEarlyRequest(EinkRefreshMode refreshMode, const WaveformTemperature behaviour) override;

        EinkStatus resetAndInit() override;


@@ 48,11 54,8 @@ namespace hal::eink

        std::int32_t getLastTemperature() const noexcept;

        EinkStatus update(std::uint8_t *displayBuffer);
        EinkStatus refresh(const EinkDisplayTimingsMode_e refreshMode);

        EinkStatus updateDisplay(std::uint8_t *frameBuffer, const EinkRefreshMode refreshMode);
        EinkStatus refreshDisplay(const EinkRefreshMode refreshMode);
        EinkStatus updateDisplay(EinkFrame frame, const std::uint8_t *frameBuffer);
        EinkStatus refreshDisplay(EinkFrame frame, const EinkRefreshMode refreshMode);
        EinkStatus prepareDisplay(const EinkRefreshMode refreshMode, const WaveformTemperature behaviour);

        FrameSize size;

M module-bsp/board/rt1051/bsp/eink/eink_dimensions.cpp => module-bsp/board/rt1051/bsp/eink/eink_dimensions.cpp +1 -1
@@ 22,7 22,7 @@ namespace hal::eink
#if defined(EINK_ROTATE_90_CLOCKWISE)
        return BOARD_EINK_DISPLAY_RES_X - frame.pos_x - frame.width;
#else
        return BOARD_EINK_DISPLAY_RES_Y - frame.pos_y - frame.height;
        return frame.pos_y;
#endif
    }


M module-bsp/hal/include/hal/eink/AbstractEinkDisplay.hpp => module-bsp/hal/include/hal/eink/AbstractEinkDisplay.hpp +11 -2
@@ 1,7 1,10 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include <memory>
#include <vector>

#include <devices/Device.hpp>



@@ 65,8 68,14 @@ namespace hal::eink

        virtual ~AbstractEinkDisplay() = default;

        virtual void setMode(const EinkDisplayColorMode mode) noexcept                                     = 0;
        virtual EinkStatus showImage(std::uint8_t *frameBuffer, const EinkRefreshMode refreshMode)         = 0;
        virtual void setMode(const EinkDisplayColorMode mode) noexcept                                        = 0;
        virtual EinkStatus showImageUpdate(const std::vector<EinkFrame> &updateFrames,
                                           const std::uint8_t *frameBuffer)                                   = 0;
        virtual EinkStatus showImageRefresh(const EinkFrame &refreshFrame, const EinkRefreshMode refreshMode) = 0;
        virtual EinkStatus showImage(const std::vector<EinkFrame> &updateFrames,
                                     const EinkFrame &refreshFrame,
                                     const std::uint8_t *frameBuffer,
                                     const EinkRefreshMode refreshMode)                                       = 0;
        virtual void prepareEarlyRequest(EinkRefreshMode refreshMode, const WaveformTemperature behaviour) = 0;

        virtual void dither()                                               = 0;

M module-gui/gui/core/BoundingBox.cpp => module-gui/gui/core/BoundingBox.cpp +2 -2
@@ 1,4 1,4 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "BoundingBox.hpp"


@@ 105,7 105,7 @@ namespace gui
        return ss.str();
    }

    void BoundingBox::sum(const BoundingBox &box)
    void BoundingBox::expandSize(const BoundingBox &box)
    {
        w = box.w > w ? box.w : w;
        h = box.h > h ? box.h : h;

M module-gui/gui/core/BoundingBox.hpp => module-gui/gui/core/BoundingBox.hpp +6 -5
@@ 1,4 1,4 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once


@@ 31,8 31,8 @@ namespace gui
            Position x = zero_position, y = zero_position;
            Length w = zero_size, h = zero_size;
        };
        BoundingBox(Position x = zero_position, Position y = zero_position, Length w = 0, Length h = 0);
        virtual ~BoundingBox() = default;
        BoundingBox() = default;
        BoundingBox(Position x, Position y, Length w, Length h);

        static bool intersect(const BoundingBox &box1, const BoundingBox &box2, BoundingBox &result);



@@ 43,8 43,9 @@ namespace gui
        /// get position in axis - in x get x, in y get y
        Position pos(gui::Axis axis) const;
        std::string str() const;
        /// logical sum of bounding box by another bounding box values
        void sum(const BoundingBox &box);
        /// assign width and/or height of bigger bounding box
        void expandSize(const BoundingBox &box);

        bool operator==(const BoundingBox &box) const;
        bool operator!=(const BoundingBox &box) const;
    };

M module-gui/gui/core/Context.cpp => module-gui/gui/core/Context.cpp +140 -43
@@ 1,4 1,4 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

/*


@@ 8,35 8,29 @@
 *      Author: robert
 */

#include <ios>
#include <cstring>

#include "BoundingBox.hpp"
#include "Context.hpp"

#include <cassert>
#include <cstring>
#include <ios>
#include <vector>

namespace gui
{

    Context::Context() : x{0}, y{0}, w{0}, h{0}, data{nullptr}
    {}

    Context::Context(uint16_t width, uint16_t height) : x{0}, y{0}, w{width}, h{height}, data(new uint8_t[w * h])
    Context::Context(std::uint16_t width, std::uint16_t height) : w{width}, h{height}, data(new std::uint8_t[w * h])
    {
        memset(data, 15, w * h);
        memset(data.get(), clearColor, w * h);
    }

    Context::~Context()
    Context Context::get(std::int16_t gx, std::int16_t gy, std::uint16_t width, std::uint16_t height) const
    {
        if (data != nullptr)
            delete[] data;
    }
        Context retContext = Context(width, height);

    Context *Context::get(int16_t gx, int16_t gy, uint16_t width, uint16_t height)
    {
        Context *retContext = new Context(width, height);

        retContext->x = gx;
        retContext->y = gy;
        // Copy the whole block if context is fully inside and covering the whole width
        if (gx == 0 && width == w && gy >= 0 && std::uint16_t(gy + height) <= h) {
            memcpy(retContext.data.get(), data.get() + gy * w, w * height);
            return retContext;
        }

        // create bounding boxes for the current context and return context
        BoundingBox currentBox = BoundingBox(0, 0, w, h);


@@ 48,7 42,7 @@ namespace gui
            Length sourceOffset = resultBox.y * w + resultBox.x;
            Length destOffset   = (resultBox.y - gy) * width + (resultBox.x - gx);
            for (Length h = 0; h < resultBox.h; h++) {
                memcpy(retContext->data + destOffset, data + sourceOffset, resultBox.w);
                memcpy(retContext.data.get() + destOffset, data.get() + sourceOffset, resultBox.w);
                sourceOffset += w;
                destOffset += width;
            }


@@ 58,27 52,38 @@ namespace gui
        return retContext;
    }

    void Context::insert(int16_t ix, int16_t iy, Context *context)
    void Context::insert(std::int16_t ix, std::int16_t iy, const Context &context)
    {
        // Copy the whole block if context is fully inside and covering the whole width
        if (ix == 0 && context.w == w && iy >= 0 && std::uint16_t(iy + context.h) <= h) {
            memcpy(data.get() + iy * w, context.data.get(), w * context.h);
            return;
        }

        // create bounding boxes for the current context and return context
        BoundingBox currentBox = BoundingBox(0, 0, w, h);
        BoundingBox insertBox  = BoundingBox(ix, iy, context->w, context->h);
        BoundingBox insertBox  = BoundingBox(ix, iy, context.w, context.h);
        BoundingBox resultBox;

        // if boxes overlap copy part defined by result from current context to the new context.
        if (BoundingBox::intersect(currentBox, insertBox, resultBox)) {
            Length sourceOffset = (resultBox.y - iy) * context->w + (resultBox.x - ix);
            Length sourceOffset = (resultBox.y - iy) * context.w + (resultBox.x - ix);
            Length destOffset   = (resultBox.y) * w + (resultBox.x);
            for (Length h = 0; h < resultBox.h; h++) {
                memcpy(data + destOffset, context->data + sourceOffset, resultBox.w);
                sourceOffset += context->w;
                memcpy(data.get() + destOffset, context.data.get() + sourceOffset, resultBox.w);
                sourceOffset += context.w;
                destOffset += w;
            }
        }
    }

    void Context::insertArea(
        int16_t ix, int16_t iy, int16_t iareaX, int16_t iareaY, int16_t iareaW, int16_t iareaH, Context *context)
    void Context::insertArea(std::int16_t ix,
                             std::int16_t iy,
                             std::int16_t iareaX,
                             std::int16_t iareaY,
                             std::int16_t iareaW,
                             std::int16_t iareaH,
                             const Context &context)
    {
        // create bounding boxes for the current context and return context
        BoundingBox currentBox = BoundingBox(0, 0, w, h);


@@ 87,39 92,98 @@ namespace gui

        // if boxes overlap copy part defined by result from current context to the new context.
        if (BoundingBox::intersect(currentBox, insertBox, resultBox)) {
            int16_t xBoxOffset = 0;
            int16_t yBoxOffset = 0;
            std::int16_t xBoxOffset = 0;
            std::int16_t yBoxOffset = 0;
            if (iareaX < 0)
                xBoxOffset = iareaX;
            if (iareaY < 0)
                yBoxOffset = iareaY;
            Length sourceOffset = (resultBox.y - iy - yBoxOffset) * context->w + (resultBox.x - ix - xBoxOffset);
            Length sourceOffset = (resultBox.y - iy - yBoxOffset) * context.w + (resultBox.x - ix - xBoxOffset);
            Length destOffset   = (resultBox.y) * w + (resultBox.x);
            for (Length h = 0; h < resultBox.h; h++) {
                memcpy(data + destOffset, context->data + sourceOffset, resultBox.w);
                sourceOffset += context->w;
                memcpy(data.get() + destOffset, context.data.get() + sourceOffset, resultBox.w);
                sourceOffset += context.w;
                destOffset += w;
            }
        }
    }

    void Context::fill(uint8_t colour)
    struct LRange
    {
        LRange(std::uint16_t begin, std::uint16_t end) : begin(begin), end(end)
        {}
        void expand(LRange const &other)
        {
            begin = std::min(begin, other.begin);
            end   = std::max(end, other.end);
        }
        static LRange inversed(std::uint16_t end)
        {
            return {end, 0};
        }
        std::uint16_t begin, end;
    };

    inline BoundingBox makeBoundingBox(const LRange &rangeX, const LRange &rangeY)
    {
        return {rangeX.begin,
                rangeY.begin,
                std::uint16_t(rangeX.end - rangeX.begin),
                std::uint16_t(rangeY.end - rangeY.begin)};
    }

    // Currently the algorithm only works properly for width divisible by 8 due to the use of 64b integers.
    std::deque<BoundingBox> gui::Context::linesDiffs(const gui::Context &ctx1, const gui::Context &ctx2)
    {
        using casted_t        = std::uint64_t;
        const std::uint16_t w = ctx1.getW();
        const std::uint16_t h = ctx1.getH();
        assert(w == ctx2.getW() && h == ctx2.getH() && w % 8 == 0);
        const std::uint16_t cw = w / sizeof(casted_t);
        const auto data1       = reinterpret_cast<const casted_t *>(ctx1.getData());
        const auto data2       = reinterpret_cast<const casted_t *>(ctx2.getData());

        std::deque<BoundingBox> result;
        LRange rangeY = LRange::inversed(h);
        for (std::uint16_t y = 0; y < h; ++y) {
            const auto begin1 = data1 + y * cw;
            const auto end1   = begin1 + cw;
            const auto begin2 = data2 + y * cw;
            if (std::mismatch(begin1, end1, begin2).first != end1) {
                if (rangeY.begin == h) { // diff pixels found first time
                    rangeY.begin = y;
                }
            }
            else {
                if (rangeY.begin != h) { // diff pixels found before
                    rangeY.end = y;
                    result.push_back(makeBoundingBox({0, w}, rangeY));
                    rangeY = LRange::inversed(h);
                }
            }
        }
        if (rangeY.begin != h) { // diff pixels found before
            rangeY.end = h;
            result.push_back(makeBoundingBox({0, w}, rangeY));
        }
        return result;
    }

    void Context::fill(std::uint8_t colour)
    {
        if (data) {
            memset(data, colour, w * h);
            memset(data.get(), colour, w * h);
        }
        //	uint32_t size = 480*600;
        //	memset( data, colour, size );
    }

    std::ostream &operator<<(std::ostream &out, const Context &c)
    {
        out << "x:" << c.x << "y:" << c.y << "w:" << c.w << "h:" << c.h << std::endl;
        out << "w:" << c.w << "h:" << c.h << std::endl;

        uint32_t offset = 0;
        for (uint32_t y = 0; y < c.h; y++) {
            for (uint32_t x = 0; x < c.w; x++) {
                uint32_t value = *(c.data + offset);
        std::uint32_t offset = 0;
        for (std::uint32_t y = 0; y < c.h; y++) {
            for (std::uint32_t x = 0; x < c.w; x++) {
                std::uint32_t value = *(c.data.get() + offset);
                std::cout << std::hex << value;
                offset++;
            }


@@ 128,4 192,37 @@ namespace gui
        return out;
    }

    std::string Context::toAsciiScaled(std::uint16_t scale) const
    {
        scale = std::max(std::uint16_t(1), std::min(scale, std::min(w, h)));

        const std::uint16_t pixelsPerChar = scale * scale;
        const std::uint16_t sw            = w / scale;
        const std::uint16_t sh            = h / scale;
        const std::uint8_t white          = 15;
        const char *chars                 = " .,-\"*^:;+=!?%#@";

        std::vector<std::uint16_t> accum(sw * sh, 0);
        for (std::uint16_t j = 0; j < h; ++j) {
            for (std::uint16_t i = 0; i < w; ++i) {
                const std::uint8_t c = std::min(data[j * w + i], white);
                const auto off       = (j / scale) * sw + (i / scale);
                accum[off] += c;
            }
        }

        const std::uint16_t sw_nl = sw + 1;
        std::string result(sw_nl * sh - 1, '\n'); // last new line is not needed

        for (std::uint16_t j = 0; j < sh; ++j) {
            for (std::uint16_t i = 0; i < sw; ++i) {
                const auto off_nl = j * sw_nl + i;
                const auto off    = j * sw + i;
                result[off_nl]    = chars[accum[off] / pixelsPerChar];
            }
        }

        return result;
    }

} /* namespace gui */

M module-gui/gui/core/Context.hpp => module-gui/gui/core/Context.hpp +53 -36
@@ 1,4 1,4 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

/*


@@ 11,83 11,100 @@
#ifndef GUI_CORE_CONTEXT_HPP_
#define GUI_CORE_CONTEXT_HPP_

#include "BoundingBox.hpp"

#include "module-gui/gui/Common.hpp"

#include <cstdint>
#include <deque>
#include <iostream>
#include "module-gui/gui/Common.hpp"
#include <memory>
#include <string>

namespace gui
{

    class Context
    {
      protected:
        int16_t x, y;
        uint32_t w, h;
        uint8_t *data;
        static constexpr std::uint8_t clearColor = 15;

      public:
        Context();
        Context(uint16_t width, uint16_t height);
        virtual ~Context();
        Context() = default;
        Context(std::uint16_t width, std::uint16_t height);

        /**
         * @brief Creates new context using provided coordinates. If there is no common part Context with 0 width and 0
         * height is returned.
         * @brief Creates new context using provided coordinates. If there is no common part Context filled with
         * clearColor is returned. The size of the returned context is defined by parameters, so it may represent
         * area outside the original context.
         */
        Context *get(int16_t gx, int16_t gy, uint16_t width, uint16_t height);
        Context get(std::int16_t gx, std::int16_t gy, std::uint16_t width, std::uint16_t height) const;
        /**
         * @brief Pastes provided context into current one. Overlapping content will be inserted into current context.
         */
        void insert(int16_t ix, int16_t iy, Context *context);
        void insert(std::int16_t ix, std::int16_t iy, const Context &context);
        /**
         * @brief Pastes provided context into current one. Overlapping content will be inserted into current context.
         */
        void insertArea(
            int16_t ix, int16_t iy, int16_t iareaX, int16_t iareaY, int16_t iareaW, int16_t iareaH, Context *context);
        void insertArea(std::int16_t ix,
                        std::int16_t iy,
                        std::int16_t iareaX,
                        std::int16_t iareaY,
                        std::int16_t iareaW,
                        std::int16_t iareaH,
                        const Context &context);

        /**
         * @brief Calculate regions of difference between contexts. Each bounding box covers the whole width of the
         * context. They are disjoint and sorted by y coordinate. The contexts has to have the same sizes.
         */
        static std::deque<BoundingBox> linesDiffs(const gui::Context &ctx1, const gui::Context &ctx2);

        /**
         * @brief Fills whole context with specified colour;
         */
        void fill(uint8_t colour);
        void fill(std::uint8_t colour);
        /**
         * @brief returns pointer to context's data;
         */
        inline const uint8_t *getData() const
        inline const std::uint8_t *getData() const
        {
            return data;
            return data.get();
        }
        inline uint8_t *getData()
        inline std::uint8_t *getData()
        {
            return data;
            return data.get();
        }
        inline int16_t getX() const
        {
            return x;
        }
        inline int16_t getY() const
        {
            return y;
        }
        inline uint16_t getW() const
        inline std::uint16_t getW() const
        {
            return w;
        }
        inline uint16_t getH() const
        inline std::uint16_t getH() const
        {
            return h;
        }
        inline BoundingBox getBoundingBox() const
        {
            return {0, 0, w, h};
        }

        inline bool addressInData(const uint8_t *ptr) const
        inline std::uint8_t getPixel(const Point point, uint8_t defaultColor = clearColor) const
        {
            return (ptr >= data) && (ptr < data + w * h);
            return hasPixel(point) ? data[point.y * w + point.x] : defaultColor;
        }

        inline bool addressInData(const Point point) const noexcept
        inline bool hasPixel(const Point point) const noexcept
        {
            return (point.x >= 0 && static_cast<uint32_t>(point.x) <= w) &&
                   (point.y >= 0 && static_cast<uint32_t>(point.y) <= h);
            return (point.x >= 0 && static_cast<std::uint32_t>(point.x) < w) &&
                   (point.y >= 0 && static_cast<std::uint32_t>(point.y) < h);
        }

        friend std::ostream &operator<<(std::ostream &out, const Context &c);

        std::string toAsciiScaled(std::uint16_t scale = 15) const;

      private:
        std::uint16_t w = 0;
        std::uint16_t h = 0;
        std::unique_ptr<uint8_t[]> data;
    };

} /* namespace gui */

M module-gui/gui/core/DrawCommand.cpp => module-gui/gui/core/DrawCommand.cpp +15 -18
@@ 1,4 1,4 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "DrawCommand.hpp"


@@ 56,8 56,9 @@ namespace gui
            return;
        }

        Context tempContext;
        Context *drawingContext = ctx;
        Point position;
        Point position(0, 0);

        if (yaps & (RectangleYap::BottomLeft | RectangleYap::TopLeft)) {
            position.x += yapSize;


@@ 76,7 77,8 @@ namespace gui
        else {
            const auto xCtx = areaX < 0 ? origin.x + areaX : origin.x;
            const auto yCtx = areaY < 0 ? origin.y + areaY : origin.y;
            drawingContext  = ctx->get(xCtx, yCtx, areaW, areaH);
            tempContext     = ctx->get(xCtx, yCtx, areaW, areaH);
            drawingContext  = &tempContext;
        }

        if (radius == 0) {


@@ 89,8 91,7 @@ namespace gui
        }

        if (drawingContext != ctx) {
            ctx->insertArea(origin.x, origin.y, areaX, areaY, width, height, drawingContext);
            delete drawingContext;
            ctx->insertArea(origin.x, origin.y, areaX, areaY, width, height, tempContext);
        }
    }



@@ 116,7 117,7 @@ namespace gui

        for (position.y = glyphOrigin.y - glyph->yoffset; position.y < glyphMaxY; ++position.y) {
            for (position.x = glyphOrigin.x; position.x < glyphMaxX; ++position.x) {
                if (!ctx->addressInData(position)) {
                if (!ctx->hasPixel(position)) {
                    log_warn_glyph(
                        "drawing out of: {x=%d,y=%d} vs {w=%d,h=%d}", position.x, position.y, ctx->getW(), ctx->getH());
                    return;


@@ 138,18 139,17 @@ namespace gui
        }

        // get copy of original context using x,y of draw coordinates and original size of the widget
        Context *drawCtx;
        Context ctxCopy;
        bool copyContext   = false;
        Point widgetOrigin = {0, 0};

        // check if there is a need of making copy of context to use it as background
        if ((areaW == width) && (areaH == height)) {
            drawCtx      = ctx;
            widgetOrigin = origin;
        }
        else {
            copyContext = true;
            drawCtx     = ctx->get(origin.x, origin.y, areaW, areaH);
            ctxCopy     = ctx->get(origin.x, origin.y, areaW, areaH);
        }

        // retrieve font used to draw text


@@ 164,6 164,7 @@ namespace gui
            FontGlyph *glyph = font->getGlyph(idCurrent);

            // do not start drawing outside of draw context.
            Context *drawCtx = copyContext ? &ctxCopy : ctx;
            if ((widgetOrigin.x + position.x + glyph->xoffset >= drawCtx->getW()) ||
                (widgetOrigin.x + position.x + glyph->xoffset < 0)) {
                LOG_FATAL("Drawing outside context's X boundary for glyph: %" PRIu32, glyph->id);


@@ 188,10 189,9 @@ namespace gui
        }

        // if drawing was performed in temporary context
        // reinsert drawCtx into bast context
        // reinsert drawCtx into base context
        if (copyContext) {
            ctx->insert(origin.x, origin.y, drawCtx);
            delete drawCtx;
            ctx->insert(origin.x, origin.y, ctxCopy);
        }
    }



@@ 269,23 269,20 @@ namespace gui
        }

        // get copy of original context using x,y of draw coordinates and original size of the widget
        Context *drawCtx = ctx->get(origin.x, origin.y, areaW, areaH);
        Context drawCtx = ctx->get(origin.x, origin.y, areaW, areaH);

        if (imageMap->getType() == gui::ImageMap::Type::PIXMAP) {
            auto pixMap = dynamic_cast<PixMap *>(imageMap);
            assert(pixMap);
            drawPixMap(drawCtx, pixMap);
            drawPixMap(&drawCtx, pixMap);
        }
        else if (imageMap->getType() == gui::ImageMap::Type::VECMAP) {
            auto vecMap = dynamic_cast<VecMap *>(imageMap);
            assert(vecMap);
            drawVecMap(drawCtx, vecMap);
            drawVecMap(&drawCtx, vecMap);
        }

        // reinsert drawCtx into bast context
        ctx->insert(origin.x, origin.y, drawCtx);

        // remove draw context
        delete drawCtx;
    }
} /* namespace gui */

M module-gui/gui/core/ImageManager.cpp => module-gui/gui/core/ImageManager.cpp +22 -26
@@ 1,4 1,4 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "ImageManager.hpp"


@@ 176,31 176,27 @@ namespace gui
        // Creation of square with crossed lines as fallback image
        constexpr auto squareWidth = 15;

        std::list<std::unique_ptr<gui::DrawCommand>> commands;
        auto rectangle    = std::make_unique<DrawRectangle>();
        rectangle->origin = {0, 0};
        rectangle->width  = squareWidth;
        rectangle->height = squareWidth;
        rectangle->areaX  = 0;
        rectangle->areaY  = 0;
        rectangle->areaW  = squareWidth;
        rectangle->areaH  = squareWidth;
        commands.emplace_back(std::move(rectangle));

        auto line1   = std::make_unique<DrawLine>();
        line1->start = {0, 0};
        line1->end   = {squareWidth, squareWidth};
        commands.emplace_back(std::move(line1));

        auto line2   = std::make_unique<DrawLine>();
        line2->start = {squareWidth - 1, 0};
        line2->end   = {0, squareWidth - 1};
        commands.emplace_back(std::move(line2));

        auto renderContext = std::make_unique<Context>(squareWidth, squareWidth);
        Renderer().render(renderContext.get(), commands);

        return new PixMap(squareWidth, squareWidth, renderContext->getData());
        DrawRectangle rectangle;
        rectangle.origin = {0, 0};
        rectangle.width  = squareWidth;
        rectangle.height = squareWidth;
        rectangle.areaX  = 0;
        rectangle.areaY  = 0;
        rectangle.areaW  = squareWidth;
        rectangle.areaH  = squareWidth;

        DrawLine line1;
        line1.start = {0, 0};
        line1.end   = {squareWidth, squareWidth};

        DrawLine line2;
        line2.start = {squareWidth - 1, 0};
        line2.end   = {0, squareWidth - 1};

        Context renderContext(squareWidth, squareWidth);
        Renderer().render(renderContext, rectangle, line1, line2);

        return new PixMap(squareWidth, squareWidth, renderContext.getData());
    }

    auto ImageManager::getImageMapList(std::string ext1, std::string ext2)

M module-gui/gui/core/RawFont.cpp => module-gui/gui/core/RawFont.cpp +13 -15
@@ 239,24 239,22 @@ namespace gui
        unsupported->xadvance =
            unsupported->width + (2 * unsupported->xoffset); // use xoffset as margins on the left/right of the glyph
        // populate with a bitmap (glyph)
        auto commandRect      = std::make_unique<DrawRectangle>();
        commandRect->origin   = {0, 0};
        commandRect->width    = unsupported->width;
        commandRect->height   = unsupported->height;
        commandRect->areaX    = 0;
        commandRect->areaY    = 0;
        commandRect->areaW    = unsupported->width;
        commandRect->areaH    = unsupported->height;
        commandRect->penWidth = unsupported->xoffset;

        auto renderCtx = std::make_unique<Context>(unsupported->width, unsupported->height);
        std::list<std::unique_ptr<gui::DrawCommand>> commands;
        commands.emplace_back(std::move(commandRect));
        Renderer().render(renderCtx.get(), commands);
        DrawRectangle commandRect;
        commandRect.origin   = {0, 0};
        commandRect.width    = unsupported->width;
        commandRect.height   = unsupported->height;
        commandRect.areaX    = 0;
        commandRect.areaY    = 0;
        commandRect.areaW    = unsupported->width;
        commandRect.areaH    = unsupported->height;
        commandRect.penWidth = unsupported->xoffset;

        Context renderCtx(unsupported->width, unsupported->height);
        Renderer().render(renderCtx, commandRect);

        auto size         = unsupported->width * unsupported->height;
        unsupported->data = new uint8_t[size];
        std::memcpy(unsupported->data, renderCtx->getData(), size);
        std::memcpy(unsupported->data, renderCtx.getData(), size);
    }

    void RawFont::setFallbackFont(RawFont *fallback)

M module-gui/gui/core/Renderer.cpp => module-gui/gui/core/Renderer.cpp +3 -3
@@ 1,4 1,4 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

// for memset


@@ 10,12 10,12 @@

namespace gui
{
    void Renderer::changeColorScheme(const std::unique_ptr<ColorScheme> &scheme)
    void Renderer::changeColorScheme(const std::unique_ptr<ColorScheme> &scheme) const
    {
        renderer::PixelRenderer::updateColorScheme(scheme);
    }

    void Renderer::render(Context *ctx, std::list<std::unique_ptr<DrawCommand>> &commands)
    void Renderer::render(Context *ctx, const std::list<std::unique_ptr<DrawCommand>> &commands) const
    {
        if (ctx == nullptr) {
            return;

M module-gui/gui/core/Renderer.hpp => module-gui/gui/core/Renderer.hpp +9 -5
@@ 1,4 1,4 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once


@@ 34,10 34,14 @@ namespace gui
         */

      public:
        virtual ~Renderer() = default;

        void render(Context *ctx, std::list<std::unique_ptr<DrawCommand>> &commands);
        void changeColorScheme(const std::unique_ptr<ColorScheme> &scheme);
        void changeColorScheme(const std::unique_ptr<ColorScheme> &scheme) const;
        void render(Context *ctx, const std::list<std::unique_ptr<DrawCommand>> &commands) const;

        template <typename... Commands>
        void render(Context &ctx, const Commands &...commands) const
        {
            (..., commands.draw(&ctx));
        }
    };

} /* namespace gui */

M module-gui/gui/core/renderers/RectangleRenderer.cpp => module-gui/gui/core/renderers/RectangleRenderer.cpp +2 -15
@@ 1,4 1,4 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "RectangleRenderer.hpp"


@@ 13,19 13,6 @@

namespace gui::renderer
{
    namespace
    {
        std::uint8_t getPixelColor(Context *ctx, const Point &point, std::uint8_t defaultColor)
        {
            const auto contextWidth = ctx->getW();
            const auto position     = point.y * contextWidth + point.x;
            if (!ctx->addressInData(ctx->getData() + position)) {
                return defaultColor;
            }
            return *(ctx->getData() + position);
        }
    } // namespace

    auto RectangleRenderer::DrawableStyle::from(const DrawRectangle &command) -> DrawableStyle
    {
        return DrawableStyle{command.penWidth,


@@ 183,7 170,7 @@ namespace gui::renderer
        while (!q.empty()) {
            const auto currPoint = q.front();
            q.pop();
            if (const auto color = getPixelColor(ctx, currPoint, PixelRenderer::getColor(borderColor.intensity));
            if (const auto color = ctx->getPixel(currPoint, PixelRenderer::getColor(borderColor.intensity));
                color == PixelRenderer::getColor(borderColor.intensity) ||
                color == PixelRenderer::getColor(fillColor.intensity)) {
                continue;

M module-gui/gui/widgets/Item.cpp => module-gui/gui/widgets/Item.cpp +1 -1
@@ 136,7 136,7 @@ namespace gui
    {
        BoundingBox oldArea = widgetArea;
        widgetArea          = area;
        widgetMaximumArea.sum(widgetArea);
        widgetMaximumArea.expandSize(widgetArea);
        contentChanged = false;
        updateDrawArea();
        onDimensionChanged(oldArea, widgetArea);

M module-gui/gui/widgets/Rect.cpp => module-gui/gui/widgets/Rect.cpp +1 -1
@@ 1,4 1,4 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

/*

M module-gui/test/test-catch/test-context.cpp => module-gui/test/test-catch/test-context.cpp +37 -51
@@ 14,77 14,63 @@

TEST_CASE("test context size and position")
{
    auto ctx = new gui::Context(30, 30);
    ctx->fill(0);
    auto ctx = gui::Context(30, 30);
    ctx.fill(0);

    gui::Context *test = ctx->get(17, 17, 30, 30);
    gui::Context test = ctx.get(17, 17, 30, 30);

    delete test;
    test = ctx->get(-17, -17, 30, 30);
    delete test;

    delete ctx;
    test = ctx.get(-17, -17, 30, 30);
}

TEST_CASE("insertContextTest")
{

    gui::Context *dstCtx;
    gui::Context *insCtx;
    gui::Context dstCtx;
    gui::Context insCtx;

    SECTION("RECTANGLE INSIDE")
    {
        dstCtx = new gui::Context(30, 30);
        dstCtx->fill(0);
        insCtx = new gui::Context(static_cast<unsigned short>(28), static_cast<unsigned short>(28));
        insCtx->fill(15);
        dstCtx->insert(1, 1, insCtx);
        delete dstCtx;
        delete insCtx;
        dstCtx = gui::Context(30, 30);
        dstCtx.fill(0);
        insCtx = gui::Context(28, 28);
        insCtx.fill(15);
        dstCtx.insert(1, 1, insCtx);
    }

    SECTION("2 COLUMNS ON RIGHT SIDE, TOP AND BOTTOM ROW UNTOUCHED")
    {
        dstCtx = new gui::Context(static_cast<unsigned short>(30), static_cast<unsigned short>(30));
        dstCtx->fill(0);
        insCtx = new gui::Context(static_cast<unsigned short>(28), static_cast<unsigned short>(28));
        insCtx->fill(15);
        dstCtx->insert(28, 1, insCtx);
        delete dstCtx;
        delete insCtx;
        dstCtx = gui::Context(30, 30);
        dstCtx.fill(0);
        insCtx = gui::Context(28, 28);
        insCtx.fill(15);
        dstCtx.insert(28, 1, insCtx);
    }

    SECTION("2 COLUMNS ON LEFT SIDE, TOP AND BOTTOM ROW UNTOUCHED")
    {
        dstCtx = new gui::Context(static_cast<unsigned short>(30), static_cast<unsigned short>(30));
        dstCtx->fill(0);
        insCtx = new gui::Context(static_cast<unsigned short>(28), static_cast<unsigned short>(28));
        insCtx->fill(15);
        dstCtx->insert(-26, 1, insCtx);
        delete dstCtx;
        delete insCtx;
        dstCtx = gui::Context(30, 30);
        dstCtx.fill(0);
        insCtx = gui::Context(28, 28);
        insCtx.fill(15);
        dstCtx.insert(-26, 1, insCtx);
    }

    SECTION("2 COLUMNS ON RIGHT SIDE")
    {
        dstCtx = new gui::Context(static_cast<unsigned short>(30), static_cast<unsigned short>(30));
        dstCtx->fill(0);
        insCtx = new gui::Context(static_cast<unsigned short>(32), static_cast<unsigned short>(32));
        insCtx->fill(15);
        dstCtx->insert(28, -1, insCtx);
        delete dstCtx;
        delete insCtx;
        dstCtx = gui::Context(30, 30);
        dstCtx.fill(0);
        insCtx = gui::Context(32, 32);
        insCtx.fill(15);
        dstCtx.insert(28, -1, insCtx);
    }

    SECTION("2 COLUMNS ON LEFT SIDE")
    {
        dstCtx = new gui::Context(static_cast<unsigned short>(30), static_cast<unsigned short>(30));
        dstCtx->fill(0);
        insCtx = new gui::Context(static_cast<unsigned short>(32), static_cast<unsigned short>(32));
        insCtx->fill(15);
        dstCtx->insert(-30, -1, insCtx);
        delete dstCtx;
        delete insCtx;
        dstCtx = gui::Context(30, 30);
        dstCtx.fill(0);
        insCtx = gui::Context(32, 32);
        insCtx.fill(15);
        dstCtx.insert(-30, -1, insCtx);
    }
}



@@ 124,7 110,7 @@ TEST_CASE("Draw vector image test")
                                                   "ffffffffffffffffffffffffffffffff\n"
                                                   "ffffffffffffffffffffffffffffffff\n";

    auto context = std::make_unique<gui::Context>(static_cast<unsigned short>(32), static_cast<unsigned short>(32));
    auto context = gui::Context(32, 32);

    gui::ImageManager::getInstance().init(".");
    auto id                 = gui::ImageManager::getInstance().getImageMapID("plus_32px_W_M");


@@ 138,13 124,13 @@ TEST_CASE("Draw vector image test")
    drawCommand.imageID = id;
    drawCommand.areaH   = 32;
    drawCommand.areaW   = 32;
    drawCommand.draw(context.get());
    drawCommand.draw(&context);
    std::string dump;

    uint32_t offset = 0;
    for (uint32_t y = 0; y < context->getH(); y++) {
        for (uint32_t x = 0; x < context->getW(); x++) {
            uint32_t value = *(context->getData() + offset);
    std::uint32_t offset = 0;
    for (std::uint32_t y = 0; y < context.getH(); y++) {
        for (std::uint32_t x = 0; x < context.getW(); x++) {
            std::uint32_t value = *(context.getData() + offset);
            std::stringstream stream;
            stream << std::hex << value;
            dump.append(stream.str());

M module-services/service-eink/ServiceEink.cpp => module-services/service-eink/ServiceEink.cpp +244 -6
@@ 22,6 22,8 @@
#include <gsl/util>
#include "Utils.hpp"

#include <service-gui/Common.hpp>

namespace service::eink
{
    namespace


@@ 67,6 69,12 @@ namespace service::eink
        connect(typeid(ImageMessage),
                [this](sys::Message *request) -> sys::MessagePointer { return handleImageMessage(request); });

        connect(typeid(RefreshMessage),
                [this](sys::Message *request) -> sys::MessagePointer { return handleRefreshMessage(request); });

        connect(typeid(CancelRefreshMessage),
                [this](sys::Message *request) -> sys::MessagePointer { return handleCancelRefreshMessage(request); });

        connect(typeid(PrepareDisplayEarlyRequest),
                [this](sys::Message *request) -> sys::MessagePointer { return handlePrepareEarlyRequest(request); });



@@ 175,27 183,257 @@ namespace service::eink
            display->setMode(hal::eink::EinkDisplayColorMode::EinkDisplayColorModeStandard);
        }
        internal::StaticData::get().setInvertedMode(invertedModeRequested);

        previousContext.reset();
        previousRefreshMode = hal::eink::EinkRefreshMode::REFRESH_NONE;
    }

#if DEBUG_EINK_REFRESH == 1
    inline std::string debug_toString(const gui::BoundingBox &bb)
    {
        return bb.str();
    }

    inline std::string debug_toString(const hal::eink::EinkFrame &f)
    {
        std::stringstream ss;
        ss << '{' << f.pos_x << ' ' << f.pos_y << ' ' << f.size.width << ' ' << f.size.height << "}";
        return ss.str();
    }

    template <typename Container>
    inline void debug_handleImageMessage(const char *name, const Container &container)
    {
        std::stringstream ss;
        for (auto const &el : container)
            ss << debug_toString(el) << ' ';
        LOG_INFO("%s: %s", name, ss.str().c_str());
    }
#endif

    // Merge boxes if the gap between them is smaller than the threshold
    template <typename BoxesContainer>
    inline auto mergeBoundingBoxes(const BoxesContainer &boxes, std::uint16_t gapThreshold)
    {
        std::vector<gui::BoundingBox> mergedBoxes;
        if (boxes.empty())
            return mergedBoxes;
        mergedBoxes.reserve(boxes.size());
        gui::BoundingBox merged = boxes.front();
        for (std::size_t i = 1; i < boxes.size(); ++i) {
            const auto &bb = boxes[i];
            const auto gap = bb.y - (merged.y + merged.h);
            if (gap < gapThreshold) {
                merged.h = (bb.y + bb.h) - merged.y;
            }
            else {
                mergedBoxes.push_back(merged);
                merged = bb;
            }
        }
        mergedBoxes.push_back(merged);
        return mergedBoxes;
    }

    // Enlarge each box to match alignment-wide grid in y coordinate
    template <typename BoxesContainer>
    inline auto makeAlignedFrames(const BoxesContainer &boxes, std::uint16_t alignment)
    {
        std::vector<hal::eink::EinkFrame> frames;
        if (boxes.empty())
            return frames;
        frames.reserve(boxes.size());
        auto a = alignment;
        for (const auto &bb : boxes) {
            auto f = hal::eink::EinkFrame{
                std::uint16_t(bb.x), std::uint16_t(bb.y), {std::uint16_t(bb.w), std::uint16_t(bb.h)}};
            auto y        = f.pos_y;
            auto h        = f.size.height;
            f.pos_y       = y / a * a;
            f.size.height = (y - f.pos_y + h + (a - 1)) / a * a;
            frames.push_back(f);
        }
        return frames;
    }

    // Return frames representing difference of contexts
    inline auto calculateUpdateFrames(const gui::Context &context, const gui::Context &previousContext)
    {
        std::vector<hal::eink::EinkFrame> updateFrames;
        // Each bounding box cover the whole width of the context. They are disjoint and sorted by y coordinate.
        const auto diffBoundingBoxes = gui::Context::linesDiffs(context, previousContext);
        if (!diffBoundingBoxes.empty()) {
            const std::uint16_t gapThreshold = context.getH() / 4;
            const std::uint16_t alignment    = 8;

            const auto mergedBoxes = mergeBoundingBoxes(diffBoundingBoxes, gapThreshold);
            updateFrames           = makeAlignedFrames(mergedBoxes, alignment);

#if DEBUG_EINK_REFRESH == 1
            debug_handleImageMessage("Diff boxes", diffBoundingBoxes);
            debug_handleImageMessage("Merged boxes", mergedBoxes);
#endif
        }
        return updateFrames;
    }

    inline auto expandFrame(hal::eink::EinkFrame &frame, const hal::eink::EinkFrame &other)
    {
        const auto x      = std::min(frame.pos_x, other.pos_x);
        const auto y      = std::min(frame.pos_y, other.pos_y);
        const auto xmax1  = frame.pos_x + frame.size.width;
        const auto xmax2  = other.pos_x + other.size.width;
        const auto w      = std::max(xmax1, xmax2) - x;
        const auto ymax1  = frame.pos_y + frame.size.height;
        const auto ymax2  = other.pos_y + other.size.height;
        const auto h      = std::max(ymax1, ymax2) - y;
        frame.pos_x       = x;
        frame.pos_y       = y;
        frame.size.width  = w;
        frame.size.height = h;
    }

#if DEBUG_EINK_REFRESH == 1
    TickType_t tick1, tick2, tick3;
#endif

    sys::MessagePointer ServiceEink::handleImageMessage(sys::Message *request)
    {
#if DEBUG_EINK_REFRESH == 1
        tick1 = xTaskGetTickCount();
#endif

        const auto message = static_cast<service::eink::ImageMessage *>(request);
        if (isInState(State::Suspended)) {
            LOG_WARN("Received image while suspended, ignoring");
            return sys::MessageNone{};
        }
        utils::time::Scoped measurement("ImageMessage");

        displayPowerOffTimer.stop();
        auto displayPowerOffTimerReload = gsl::finally([this]() { displayPowerOffTimer.start(); });

        eInkSentinel->HoldMinimumFrequency();
        auto status = display->showImage(message->getData(), translateToEinkRefreshMode(message->getRefreshMode()));
        if (status != hal::eink::EinkStatus::EinkOK) {
            LOG_ERROR("Error during drawing image on eink: %s", magic_enum::enum_name(status).data());
        const gui::Context &ctx = *message->getContext();
        auto refreshMode        = translateToEinkRefreshMode(message->getRefreshMode());

        // Calculate update and refresh frames based on areas changed since last update
        std::vector<hal::eink::EinkFrame> updateFrames;
        hal::eink::EinkFrame refreshFrame{0, 0, {ctx.getW(), ctx.getH()}};
        bool isRefreshRequired = true;
        if (!previousContext) {
            updateFrames = {refreshFrame};
            previousContext.reset(new gui::Context(ctx.get(0, 0, ctx.getW(), ctx.getH())));
        }
        else {
            updateFrames = calculateUpdateFrames(ctx, *previousContext);
            if (refreshMode > previousRefreshMode) {
                previousContext->insert(0, 0, ctx);
            }
            else if (updateFrames.empty()) {
                isRefreshRequired = false;
            }
            else {
                refreshFrame = updateFrames.front();
                refreshFrame.size.height =
                    updateFrames.back().pos_y + updateFrames.back().size.height - updateFrames.front().pos_y;
                previousContext->insert(0, 0, ctx);
            }
        }

        // If parts of the screen were changed, update eink
        bool isImageUpdated = false;
        if (!updateFrames.empty()) {

#if DEBUG_EINK_REFRESH == 1
            debug_handleImageMessage("Update frames", updateFrames);
            debug_handleImageMessage("Refresh frame", std::vector<hal::eink::EinkFrame>({refreshFrame}));
#endif

            eInkSentinel->HoldMinimumFrequency();

            auto status = display->showImageUpdate(updateFrames, ctx.getData());
            if (status == hal::eink::EinkStatus::EinkOK) {
                isImageUpdated = true;
            }
            else {
                previousContext.reset();
                refreshMode = hal::eink::EinkRefreshMode::REFRESH_NONE;
                LOG_ERROR("Error during drawing image on eink: %s", magic_enum::enum_name(status).data());
            }
        }

        previousRefreshMode = refreshMode;

#if DEBUG_EINK_REFRESH == 1
        LOG_INFO("Update contextId: %d, mode: %d", (int)message->getContextId(), (int)refreshMode);
        tick2 = xTaskGetTickCount();
        LOG_INFO("Time to update: %d", (int)(tick2 - tick1));
#endif

        // Refresh is required if:
        // - there are parts of the screen that were changed
        // - deep refresh is requested after fast refresh even if nothing has changed
        // - previous refresh was canceled
        if (isImageUpdated || isRefreshRequired || isRefreshFramesSumValid) {
            if (refreshMode > refreshModeSum) {
                refreshModeSum = refreshMode;
            }
            if (!isRefreshFramesSumValid) {
                refreshFramesSum        = refreshFrame;
                isRefreshFramesSumValid = true;
            }
            else {
                expandFrame(refreshFramesSum, refreshFrame);
            }
            einkDisplayState = EinkDisplayState::NeedRefresh;
            auto msg         = std::make_shared<service::eink::RefreshMessage>(
                message->getContextId(), refreshFrame, refreshMode, message->sender);
            bus.sendUnicast(msg, this->GetName());
            return sys::MessageNone{};
        }
        else {
            einkDisplayState = EinkDisplayState::Idle;
            return std::make_shared<service::eink::ImageDisplayedNotification>(message->getContextId());
        }
    }

        return std::make_shared<service::eink::ImageDisplayedNotification>(message->getContextId());
    sys::MessagePointer ServiceEink::handleRefreshMessage(sys::Message *request)
    {
        const auto message = static_cast<service::eink::RefreshMessage *>(request);

        if (einkDisplayState == EinkDisplayState::NeedRefresh) {
            const auto status = display->showImageRefresh(refreshFramesSum, refreshModeSum);
            if (status != hal::eink::EinkStatus::EinkOK) {
                previousContext.reset();
                previousRefreshMode = hal::eink::EinkRefreshMode::REFRESH_NONE;
                LOG_ERROR("Error during drawing image on eink: %s", magic_enum::enum_name(status).data());
            }

            einkDisplayState        = EinkDisplayState::Idle;
            isRefreshFramesSumValid = false;
            refreshModeSum          = hal::eink::EinkRefreshMode::REFRESH_NONE;

#if DEBUG_EINK_REFRESH == 1
            LOG_INFO("Refresh contextId: %d, mode: %d", message->getContextId(), (int)message->getRefreshMode());
#endif
        }

#if DEBUG_EINK_REFRESH == 1
        tick3 = xTaskGetTickCount();
        LOG_INFO("Time to refresh: %d", (int)(tick3 - tick1));
#endif

        auto msg = std::make_shared<service::eink::ImageDisplayedNotification>(message->getContextId());
        bus.sendUnicast(msg, message->getOriginalSender());

        return sys::MessageNone{};
    }

    sys::MessagePointer ServiceEink::handleCancelRefreshMessage(sys::Message *request)
    {
        LOG_INFO("Refresh cancel");
        if (einkDisplayState == EinkDisplayState::NeedRefresh)
            einkDisplayState = EinkDisplayState::Canceled;
        return sys::MessageNone{};
    }

    sys::MessagePointer ServiceEink::handlePrepareEarlyRequest(sys::Message *message)

M module-services/service-eink/ServiceEink.hpp => module-services/service-eink/ServiceEink.hpp +17 -0
@@ 3,6 3,7 @@

#pragma once

#include <module-gui/gui/core/Context.hpp>
#include <system/Common.hpp>
#include <Service/Message.hpp>
#include <Service/Service.hpp>


@@ 48,6 49,13 @@ namespace service::eink
            Suspended
        };

        enum class EinkDisplayState
        {
            Idle,
            NeedRefresh,
            Canceled
        };

        void setState(State state) noexcept;
        bool isInState(State state) const noexcept;



@@ 58,6 66,8 @@ namespace service::eink

        sys::MessagePointer handleEinkModeChangedMessage(sys::Message *message);
        sys::MessagePointer handleImageMessage(sys::Message *message);
        sys::MessagePointer handleRefreshMessage(sys::Message *message);
        sys::MessagePointer handleCancelRefreshMessage(sys::Message *message);
        sys::MessagePointer handlePrepareEarlyRequest(sys::Message *message);

        void initStaticData();


@@ 68,6 78,13 @@ namespace service::eink
        sys::TimerHandle displayPowerOffTimer;
        std::shared_ptr<EinkSentinel> eInkSentinel;
        std::unique_ptr<settings::Settings> settings;

        std::unique_ptr<::gui::Context> previousContext;
        hal::eink::EinkRefreshMode previousRefreshMode = hal::eink::EinkRefreshMode::REFRESH_NONE;
        EinkDisplayState einkDisplayState              = EinkDisplayState::Idle;
        hal::eink::EinkFrame refreshFramesSum;
        hal::eink::EinkRefreshMode refreshModeSum = hal::eink::EinkRefreshMode::REFRESH_NONE;
        bool isRefreshFramesSumValid              = false;
    };
} // namespace service::eink


M module-services/service-eink/messages/ImageMessage.cpp => module-services/service-eink/messages/ImageMessage.cpp +27 -2
@@ 10,9 10,9 @@ namespace service::eink
        : contextId{contextId}, context{context}, refreshMode{refreshMode}
    {}

    auto ImageMessage::getData() noexcept -> std::uint8_t *
    auto ImageMessage::getContext() noexcept -> ::gui::Context *
    {
        return context->getData();
        return context;
    }

    auto ImageMessage::getRefreshMode() const noexcept -> ::gui::RefreshModes


@@ 32,4 32,29 @@ namespace service::eink
    {
        return contextId;
    }

    RefreshMessage::RefreshMessage(int contextId,
                                   hal::eink::EinkFrame refreshFrame,
                                   hal::eink::EinkRefreshMode refreshMode,
                                   const std::string &originalSender)
        : contextId(contextId), refreshFrame(refreshFrame), refreshMode(refreshMode), originalSender(originalSender)
    {}

    auto RefreshMessage::getContextId() const noexcept -> int
    {
        return contextId;
    }
    auto RefreshMessage::getRefreshFrame() noexcept -> hal::eink::EinkFrame
    {
        return refreshFrame;
    }
    auto RefreshMessage::getRefreshMode() const noexcept -> hal::eink::EinkRefreshMode
    {
        return refreshMode;
    }
    auto RefreshMessage::getOriginalSender() const noexcept -> const std::string &
    {
        return originalSender;
    }

} // namespace service::eink

M module-services/service-eink/messages/ImageMessage.hpp => module-services/service-eink/messages/ImageMessage.hpp +29 -3
@@ 1,14 1,16 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include "EinkMessage.hpp"

#include <cstdint>
#include <hal/eink/AbstractEinkDisplay.hpp>
#include <module-gui/gui/core/Context.hpp>
#include <module-gui/gui/Common.hpp>

#include <cstdint>

namespace service::eink
{
    class ImageMessage : public EinkMessage


@@ 17,7 19,7 @@ namespace service::eink
        ImageMessage(int contextId, ::gui::Context *context, ::gui::RefreshModes refreshMode);

        [[nodiscard]] auto getContextId() const noexcept -> int;
        [[nodiscard]] auto getData() noexcept -> std::uint8_t *;
        [[nodiscard]] auto getContext() noexcept -> ::gui::Context *;
        [[nodiscard]] auto getRefreshMode() const noexcept -> ::gui::RefreshModes;

      private:


@@ 42,4 44,28 @@ namespace service::eink
      private:
        int contextId;
    };

    class RefreshMessage : public EinkMessage
    {
      public:
        RefreshMessage(int contextId,
                       hal::eink::EinkFrame refreshBox,
                       hal::eink::EinkRefreshMode refreshMode,
                       const std::string &originalSender);

        [[nodiscard]] auto getContextId() const noexcept -> int;
        [[nodiscard]] auto getRefreshFrame() noexcept -> hal::eink::EinkFrame;
        [[nodiscard]] auto getRefreshMode() const noexcept -> hal::eink::EinkRefreshMode;
        [[nodiscard]] auto getOriginalSender() const noexcept -> const std::string &;

      private:
        int contextId;
        hal::eink::EinkFrame refreshFrame;
        hal::eink::EinkRefreshMode refreshMode;
        std::string originalSender;
    };

    class CancelRefreshMessage : public EinkMessage
    {};

} // namespace service::eink

M module-services/service-gui/ServiceGUI.cpp => module-services/service-gui/ServiceGUI.cpp +16 -4
@@ 1,4 1,4 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "ServiceGUI.hpp"


@@ 184,11 184,18 @@ namespace service::gui
                cache.invalidate();
            }
            const auto context = contextPool->peekContext(contextId);
#if DEBUG_EINK_REFRESH == 1
            LOG_INFO("Rendering finished, send, contextId: %d, mode: %d", contextId, (int)refreshMode);
#endif
            sendOnDisplay(context, contextId, refreshMode);
        }
        else {
            cache.cache({contextId, refreshMode});
            contextPool->returnContext(contextId);
#if DEBUG_EINK_REFRESH == 1
            LOG_INFO("Rendering finished, cancel, contextId: %d, mode: %d", contextId, (int)refreshMode);
#endif
            sendCancelRefresh();
        }
        return sys::MessageNone{};
    }


@@ 196,12 203,17 @@ namespace service::gui
    void ServiceGUI::sendOnDisplay(::gui::Context *context, int contextId, ::gui::RefreshModes refreshMode)
    {
        isDisplaying = true;
        std::shared_ptr<service::eink::ImageMessage> imageMsg;
        imageMsg = std::make_shared<service::eink::ImageMessage>(contextId, context, refreshMode);
        bus.sendUnicast(imageMsg, service::name::eink);
        auto msg     = std::make_shared<service::eink::ImageMessage>(contextId, context, refreshMode);
        bus.sendUnicast(std::move(msg), service::name::eink);
        scheduleContextRelease(contextId);
    }

    void ServiceGUI::sendCancelRefresh()
    {
        auto msg = std::make_shared<service::eink::CancelRefreshMessage>();
        bus.sendUnicast(std::move(msg), service::name::eink);
    }

    void ServiceGUI::scheduleContextRelease(int contextId)
    {
        // Whenever the response from ServiceEink doesn't come, the context has to be released automatically after a

M module-services/service-gui/WorkerGUI.cpp => module-services/service-gui/WorkerGUI.cpp +8 -1
@@ 1,4 1,4 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "WorkerGUI.hpp"


@@ 9,6 9,7 @@
#include <service-gui/ServiceGUI.hpp>

#include <memory>
#include <sstream>
#include "messages/RenderingFinished.hpp"

namespace service::gui


@@ 53,7 54,13 @@ namespace service::gui
    void WorkerGUI::render(DrawCommandsQueue::CommandList &commands, ::gui::RefreshModes refreshMode)
    {
        const auto [contextId, context] = guiService->contextPool->borrowContext(); // Waits for the context.

        renderer.render(context, commands);

#if DEBUG_EINK_REFRESH == 1
        LOG_INFO("Render ContextId: %d\n%s", contextId, context->toAsciiScaled().c_str());
#endif

        onRenderingFinished(contextId, refreshMode);
    }


M module-services/service-gui/service-gui/ServiceGUI.hpp => module-services/service-gui/service-gui/ServiceGUI.hpp +1 -0
@@ 59,6 59,7 @@ namespace service::gui
        void notifyRenderColorSchemeChange(::gui::ColorScheme &&scheme);
        void enqueueDrawCommands(DrawCommandsQueue::QueueItem &&item);
        void sendOnDisplay(::gui::Context *context, int contextId, ::gui::RefreshModes refreshMode);
        void sendCancelRefresh();
        void scheduleContextRelease(int contextId);
        bool isNextFrameReady() const noexcept;
        bool isAnyFrameBeingRenderedOrDisplayed() const noexcept;

M module-utils/log/api/log/debug.hpp => module-utils/log/api/log/debug.hpp +7 -6
@@ 4,19 4,20 @@
#pragma once

#define DEBUG_APPLICATION_MANAGEMENT 0 /// show verbose logs in ApplicationManager
#define DEBUG_SCOPED_TIMINGS         0 /// show timings in measured functions
#define DEBUG_CELLULAR_UART          0 /// show full modem uart communication
#define DEBUG_BLUETOOTH_HCI_COMS     0 /// show communication with BT module - transactions
#define DEBUG_BLUETOOTH_HCI_BYTES    0 /// show communication with BT module - all the HCI bytes
#define DEBUG_SERVICE_MESSAGES       0 /// show messages prior to handling in service
#define DEBUG_CELLULAR_UART          0 /// show full modem uart communication
#define DEBUG_DB_MODEL_DATA          0 /// show messages prior to handling in service
#define DEBUG_SIM_IMPORT_DATA        0 /// show messages connected to sim data imports
#define DEBUG_EINK_REFRESH           0 /// show refresh information
#define DEBUG_FONT                   0 /// show Font debug messages
#define DEBUG_GUI_TEXT               0 /// show basic debug messages for gui::Text - warning this can be hard on cpu
#define DEBUG_GUI_TEXT_LINES         0 /// show extended debug messages for gui::Text - lines building
#define DEBUG_GUI_TEXT_CURSOR        0 /// show extended debug messages for gui::Text - cursor handling
#define DEBUG_INPUT_EVENTS           0 /// show input events prints in system
#define DEBUG_TIMER                  0 /// debug timers system utility
#define DEBUG_MISSING_ASSETS         0 /// show debug concerning missing assets
#define DEBUG_SCOPED_TIMINGS         0 /// show timings in measured functions
#define DEBUG_SETTINGS_DB            0 /// show extensive settings logs for all applications
#define DEBUG_SERVICE_CELLULAR       0 /// show various logs in cellular service
#define DEBUG_MISSING_ASSETS         0 /// show debug concerning missing assets
#define DEBUG_SERVICE_MESSAGES       0 /// show messages prior to handling in service
#define DEBUG_SIM_IMPORT_DATA        0 /// show messages connected to sim data imports
#define DEBUG_TIMER                  0 /// debug timers system utility

M pure_changelog.md => pure_changelog.md +1 -0
@@ 7,6 7,7 @@
* Changed USB logging
* Separated system volume from Bluetooth device volume for A2DP
* Made windows flow in SIM cards settings more robust
* Improve refreshing of the display.

### Fixed
* Fixed wrong time displayed on password locked screen with 'Quotes' or 'Logo' wallpaper