mirror of
https://github.com/mudita/MuditaOS.git
synced 2026-01-13 08:18:13 -05:00
* Added mechanism enabling CPU to enter WFI mode when the OS is in idle, what results in large power consumption reduction. * Added mechanism to switch SDRAM to self-refresh mode before entering WFI, what resulted in further power consumption reduction.
486 lines
18 KiB
C++
486 lines
18 KiB
C++
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
|
|
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md
|
|
|
|
#include <board.h>
|
|
#include "ServiceEink.hpp"
|
|
#include "internal/StaticData.hpp"
|
|
#include "messages/EinkModeMessage.hpp"
|
|
#include "messages/PrepareDisplayEarlyRequest.hpp"
|
|
#include <Timers/TimerFactory.hpp>
|
|
|
|
#include <log/log.hpp>
|
|
#include <messages/EinkMessage.hpp>
|
|
#include <messages/ImageMessage.hpp>
|
|
#include <system/messages/DeviceRegistrationMessage.hpp>
|
|
#include <system/messages/SentinelRegistrationMessage.hpp>
|
|
#include <system/Constants.hpp>
|
|
#include <service-db/agents/settings/SystemSettings.hpp>
|
|
|
|
#include <cstring>
|
|
#include <memory>
|
|
#include "Utils.hpp"
|
|
|
|
namespace service::eink
|
|
{
|
|
namespace
|
|
{
|
|
constexpr auto ServiceEinkStackDepth = 1024 * 4;
|
|
constexpr std::chrono::milliseconds displayPowerOffTimeout{200};
|
|
|
|
std::string toSettingString(const EinkModeMessage::Mode mode)
|
|
{
|
|
if (mode == EinkModeMessage::Mode::Normal) {
|
|
return "0";
|
|
}
|
|
return "1";
|
|
}
|
|
|
|
hal::eink::EinkRefreshMode translateToEinkRefreshMode(const gui::RefreshModes guiRefreshMode)
|
|
{
|
|
switch (guiRefreshMode) {
|
|
case gui::RefreshModes::GUI_REFRESH_DEEP:
|
|
return hal::eink::EinkRefreshMode::REFRESH_DEEP;
|
|
case gui::RefreshModes::GUI_REFRESH_FAST:
|
|
return hal::eink::EinkRefreshMode::REFRESH_FAST;
|
|
default:
|
|
return hal::eink::EinkRefreshMode::REFRESH_NONE;
|
|
}
|
|
}
|
|
} // namespace
|
|
|
|
ServiceEink::ServiceEink(ExitAction exitAction, const std::string &name, std::string parent)
|
|
: sys::Service(name, std::move(parent), ServiceEinkStackDepth), exitAction{exitAction},
|
|
currentState{State::Running}, display{hal::eink::AbstractEinkDisplay::Factory::create(
|
|
hal::eink::FrameSize{BOARD_EINK_DISPLAY_RES_X, BOARD_EINK_DISPLAY_RES_Y})},
|
|
settings{std::make_unique<settings::Settings>()}
|
|
{
|
|
displayPowerOffTimer = sys::TimerFactory::createSingleShotTimer(
|
|
this, "einkDisplayPowerOff", displayPowerOffTimeout, [this](sys::Timer &) {
|
|
if (const auto status = display->shutdown(); status != hal::eink::EinkStatus::EinkOK) {
|
|
LOG_ERROR("Error during display powerOff. (%s)", magic_enum::enum_name(status).data());
|
|
}
|
|
eInkSentinel->ReleaseMinimumFrequency();
|
|
});
|
|
connect(typeid(EinkModeMessage),
|
|
[this](sys::Message *message) -> sys::MessagePointer { return handleEinkModeChangedMessage(message); });
|
|
|
|
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); });
|
|
|
|
eInkSentinel = std::make_shared<EinkSentinel>(name::eink, this);
|
|
}
|
|
|
|
sys::MessagePointer ServiceEink::DataReceivedHandler([[maybe_unused]] sys::DataMessage *msgl,
|
|
[[maybe_unused]] sys::ResponseMessage *response)
|
|
{
|
|
return std::make_shared<sys::ResponseMessage>();
|
|
}
|
|
|
|
sys::ReturnCodes ServiceEink::InitHandler()
|
|
{
|
|
if (const auto status = display->reinitAndPowerOn(); status != hal::eink::EinkStatus::EinkOK) {
|
|
LOG_FATAL("Error: Could not initialize Eink display!");
|
|
return sys::ReturnCodes::Failure;
|
|
}
|
|
|
|
settings->init(service::ServiceProxy(shared_from_this()));
|
|
initStaticData();
|
|
|
|
auto deviceRegistrationMsg = std::make_shared<sys::DeviceRegistrationMessage>(display->getDevice());
|
|
bus.sendUnicast(std::move(deviceRegistrationMsg), service::name::system_manager);
|
|
|
|
auto sentinelRegistrationMsg = std::make_shared<sys::SentinelRegistrationMessage>(eInkSentinel);
|
|
bus.sendUnicast(std::move(sentinelRegistrationMsg), service::name::system_manager);
|
|
|
|
eInkSentinel->HoldMinimumFrequency();
|
|
|
|
LOG_INFO("Initialized");
|
|
return sys::ReturnCodes::Success;
|
|
}
|
|
|
|
void ServiceEink::initStaticData()
|
|
{
|
|
const auto invertedModeSetting = settings->getValue(settings::Display::invertedMode);
|
|
const auto isInvertedModeEnabled = utils::toNumeric(invertedModeSetting);
|
|
const auto mode = (isInvertedModeEnabled == 0) ? EinkModeMessage::Mode::Normal : EinkModeMessage::Mode::Invert;
|
|
setDisplayMode(mode);
|
|
}
|
|
|
|
sys::ReturnCodes ServiceEink::DeinitHandler()
|
|
{
|
|
if ((exitAction == ExitAction::WipeOut) ||
|
|
((display->getMode() == hal::eink::EinkDisplayColorMode::EinkDisplayColorModeInverted) &&
|
|
(systemCloseReason != sys::CloseReason::FactoryReset))) {
|
|
LOG_INFO("Performing low-level display wipeout");
|
|
if (const auto status = display->wipeOut(); status != hal::eink::EinkStatus::EinkOK) {
|
|
LOG_ERROR("Low-level display wipeout failed");
|
|
}
|
|
}
|
|
|
|
display->shutdown();
|
|
settings->deinit();
|
|
|
|
LOG_INFO("Deinitialized");
|
|
return sys::ReturnCodes::Success;
|
|
}
|
|
|
|
void ServiceEink::ProcessCloseReason(sys::CloseReason closeReason)
|
|
{
|
|
systemCloseReason = closeReason;
|
|
sendCloseReadyMessage(this);
|
|
}
|
|
|
|
void ServiceEink::ProcessCloseReasonHandler(sys::CloseReason closeReason)
|
|
{
|
|
ProcessCloseReason(closeReason);
|
|
}
|
|
|
|
sys::ReturnCodes ServiceEink::SwitchPowerModeHandler(const sys::ServicePowerMode mode)
|
|
{
|
|
switch (mode) {
|
|
case sys::ServicePowerMode::Active:
|
|
enterActiveMode();
|
|
break;
|
|
case sys::ServicePowerMode::SuspendToRAM:
|
|
case sys::ServicePowerMode::SuspendToNVM:
|
|
suspend();
|
|
break;
|
|
}
|
|
return sys::ReturnCodes::Success;
|
|
}
|
|
|
|
void ServiceEink::enterActiveMode()
|
|
{
|
|
setState(State::Running);
|
|
}
|
|
|
|
void ServiceEink::suspend()
|
|
{
|
|
setState(State::Suspended);
|
|
}
|
|
|
|
sys::MessagePointer ServiceEink::handleEinkModeChangedMessage(sys::Message *message)
|
|
{
|
|
const auto msg = static_cast<EinkModeMessage *>(message);
|
|
const auto mode = msg->getMode();
|
|
setDisplayMode(mode);
|
|
settings->setValue(settings::Display::invertedMode, toSettingString(mode));
|
|
return std::make_shared<EinkModeResponse>();
|
|
}
|
|
|
|
void ServiceEink::setDisplayMode(EinkModeMessage::Mode mode)
|
|
{
|
|
const auto invertedModeRequested = (mode == EinkModeMessage::Mode::Invert);
|
|
if (invertedModeRequested) {
|
|
display->setMode(hal::eink::EinkDisplayColorMode::EinkDisplayColorModeInverted);
|
|
}
|
|
else {
|
|
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{};
|
|
}
|
|
|
|
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 {
|
|
if (refreshMode != hal::eink::EinkRefreshMode::REFRESH_DEEP) {
|
|
refreshFrame = updateFrames.front();
|
|
refreshFrame.size.height =
|
|
updateFrames.back().pos_y + updateFrames.back().size.height - updateFrames.front().pos_y;
|
|
}
|
|
previousContext->insert(0, 0, ctx);
|
|
}
|
|
}
|
|
if ((previousRefreshStatus == RefreshStatus::Failed) && !updateFrames.empty()) {
|
|
updateFrames.front() = {0, 0, BOARD_EINK_DISPLAY_RES_X, BOARD_EINK_DISPLAY_RES_Y};
|
|
}
|
|
|
|
// 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();
|
|
const 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;
|
|
const 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;
|
|
restartDisplayPowerOffTimer();
|
|
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) {
|
|
if (previousRefreshStatus == RefreshStatus::Failed) {
|
|
refreshModeSum = hal::eink::EinkRefreshMode::REFRESH_DEEP;
|
|
previousRefreshStatus = RefreshStatus::Success;
|
|
}
|
|
|
|
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());
|
|
previousRefreshStatus = RefreshStatus::Failed;
|
|
}
|
|
|
|
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
|
|
|
|
restartDisplayPowerOffTimer();
|
|
|
|
const 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)
|
|
{
|
|
#if DEBUG_EINK_REFRESH == 1
|
|
LOG_INFO("Refresh cancel");
|
|
#endif
|
|
|
|
if (einkDisplayState == EinkDisplayState::NeedRefresh) {
|
|
einkDisplayState = EinkDisplayState::Canceled;
|
|
}
|
|
return sys::MessageNone{};
|
|
}
|
|
|
|
sys::MessagePointer ServiceEink::handlePrepareEarlyRequest(sys::Message *message)
|
|
{
|
|
const auto waveformUpdateMsg = static_cast<service::eink::PrepareDisplayEarlyRequest *>(message);
|
|
display->prepareEarlyRequest(translateToEinkRefreshMode(waveformUpdateMsg->getRefreshMode()),
|
|
hal::eink::WaveformTemperature::MEASURE_NEW);
|
|
return sys::MessageNone{};
|
|
}
|
|
|
|
void ServiceEink::setState(State state) noexcept
|
|
{
|
|
currentState = state;
|
|
}
|
|
|
|
bool ServiceEink::isInState(State state) const noexcept
|
|
{
|
|
return currentState == state;
|
|
}
|
|
|
|
void ServiceEink::restartDisplayPowerOffTimer()
|
|
{
|
|
if (displayPowerOffTimer.isActive()) {
|
|
displayPowerOffTimer.stop();
|
|
}
|
|
displayPowerOffTimer.start();
|
|
}
|
|
} // namespace service::eink
|