Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Tactility/Include/Tactility/hal/display/DisplayDevice.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ class DisplayDevice : public Device {
/** For e-paper screens */
virtual void requestFullRefresh() {}

/** Blocks until any frame already handed to this display has physically
* finished drawing. Displays that draw synchronously within their flush
* callback can rely on the default no-op; displays with an asynchronous
* refresh pipeline (e.g. e-paper, where a full refresh can take seconds)
* should override this so callers can safely do something irreversible
* (like cutting power) right after a screen update. */
virtual void waitForFlushComplete() {}

/** Could return nullptr if not started */
virtual std::shared_ptr<touch::TouchDevice> getTouchDevice() = 0;

Expand Down
2 changes: 2 additions & 0 deletions Tactility/Source/Tactility.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ namespace app {
namespace localesettings { extern const AppManifest manifest; }
namespace notes { extern const AppManifest manifest; }
namespace power { extern const AppManifest manifest; }
namespace poweroff { extern const AppManifest manifest; }
namespace selectiondialog { extern const AppManifest manifest; }
namespace settings { extern const AppManifest manifest; }
namespace setup { extern const AppManifest manifest; }
Expand Down Expand Up @@ -157,6 +158,7 @@ static void registerInternalApps() {
addAppManifest(app::launcher::manifest);
addAppManifest(app::localesettings::manifest);
addAppManifest(app::notes::manifest);
addAppManifest(app::poweroff::manifest);
addAppManifest(app::settings::manifest);
addAppManifest(app::selectiondialog::manifest);
addAppManifest(app::setup::manifest);
Expand Down
34 changes: 0 additions & 34 deletions Tactility/Source/app/launcher/Launcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
#include <Tactility/app/AppPaths.h>
#include <Tactility/app/AppRegistration.h>
#include <Tactility/app/setup/Setup.h>
#include <Tactility/hal/power/PowerDevice.h>
#include <Tactility/service/loader/Loader.h>
#include <Tactility/settings/BootSettings.h>

Expand Down Expand Up @@ -66,31 +65,11 @@ class LauncherApp final : public App {
return apps_button;
}

static bool shouldShowPowerButton() {
bool show_power_button = false;
hal::findDevices<hal::power::PowerDevice>(hal::Device::Type::Power, [&show_power_button](const auto& device) {
if (device->supportsPowerOff()) {
show_power_button = true;
return false; // stop iterating
} else {
return true; // continue iterating
}
});
return show_power_button;
}

static void onAppPressed(lv_event_t* e) {
auto* appId = static_cast<const char*>(lv_event_get_user_data(e));
start(appId);
}

static void onPowerOffPressed(lv_event_t* e) {
auto power = hal::findFirstDevice<hal::power::PowerDevice>(hal::Device::Type::Power);
if (power != nullptr && power->supportsPowerOff()) {
power->powerOff();
}
}

// The screen object outlives the launcher's views (it's recreated by GuiService::redraw()
// via lv_obj_clean() on every app switch), so the LV_EVENT_SIZE_CHANGED callback registered
// on it must be removed once buttons_wrapper is destroyed, to avoid a dangling user-data
Expand Down Expand Up @@ -201,19 +180,6 @@ class LauncherApp final : public App {
// handler is attached there, with buttons_wrapper passed through as user data.
lv_obj_add_event_cb(lv_obj_get_screen(parent), onButtonsWrapperResized, LV_EVENT_SIZE_CHANGED, buttons_wrapper);
lv_obj_add_event_cb(buttons_wrapper, onButtonsWrapperDeleted, LV_EVENT_DELETE, nullptr);

if (shouldShowPowerButton()) {
auto* power_button = lv_button_create(parent);
lv_obj_set_style_pad_all(power_button, 8, 0);
lv_obj_align(power_button, LV_ALIGN_BOTTOM_MID, 0, -10);
lv_obj_add_event_cb(power_button, onPowerOffPressed, LV_EVENT_SHORT_CLICKED, nullptr);
lv_obj_set_style_shadow_width(power_button, 0, LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(power_button, 0, LV_PART_MAIN);

auto* power_label = lv_label_create(power_button);
lv_label_set_text(power_label, LV_SYMBOL_POWER);
lv_obj_set_style_text_color(power_label, lv_theme_get_color_primary(parent), LV_STATE_DEFAULT);
}
}
};

Expand Down
104 changes: 104 additions & 0 deletions Tactility/Source/app/poweroff/PowerOff.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#include <Tactility/app/AppContext.h>
#include <Tactility/app/AppRegistration.h>
#include <Tactility/hal/display/DisplayDevice.h>
#include <Tactility/hal/power/PowerDevice.h>
#include <Tactility/service/loader/Loader.h>

#include <lvgl.h>
#include <tactility/hal/Device.h>
#include <tactility/lvgl_fonts.h>
#include <tactility/lvgl_icon_shared.h>

namespace tt::app::poweroff {

extern const AppManifest manifest;

class PowerOffApp final : public App {

/** Replaces the screen with a plain "powered off" message, forces it to draw
* synchronously, and waits for the display to confirm the draw physically
* finished. On e-paper this is what's left showing once power cuts, so it
* doubles as visual confirmation that the device shut down cleanly rather
* than crashed or hung. Called from an LVGL button click callback, so it's
* already running on the LVGL thread. */
static void showPoweredOffScreenAndWait(hal::display::DisplayDevice* display) {
auto* screen = lv_obj_create(nullptr);
lv_obj_set_style_bg_color(screen, lv_color_white(), 0);
lv_obj_set_flex_flow(screen, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(screen, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);

auto* title = lv_label_create(screen);
lv_label_set_text(title, "Tactility OS");
lv_obj_set_style_text_font(title, lvgl_get_text_font(FONT_SIZE_LARGE), 0);
lv_obj_set_style_text_color(title, lv_color_black(), 0);

auto* subtitle = lv_label_create(screen);
lv_label_set_text(subtitle, "Powered off");
lv_obj_set_style_text_color(subtitle, lv_color_black(), 0);

lv_screen_load(screen);

if (display != nullptr) {
auto* lvgl_display = display->getLvglDisplay();
if (lvgl_display != nullptr) {
lv_refr_now(lvgl_display);
}
display->waitForFlushComplete();
}
}

static void onYesPressed(lv_event_t* /*event*/) {
auto power = hal::findFirstDevice<hal::power::PowerDevice>(hal::Device::Type::Power);
if (power == nullptr || !power->supportsPowerOff()) {
return;
}

auto display = hal::findFirstDevice<hal::display::DisplayDevice>(hal::Device::Type::Display);
showPoweredOffScreenAndWait(display.get());

power->powerOff();
}

static void onNoPressed(lv_event_t* /*event*/) {
stop(manifest.appId);
}

public:

void onShow(AppContext& /*app*/, lv_obj_t* parent) override {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(parent, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);

auto* label = lv_label_create(parent);
lv_label_set_text(label, "Power off?");
lv_obj_set_style_text_font(label, lvgl_get_text_font(FONT_SIZE_LARGE), 0);

auto* button_wrapper = lv_obj_create(parent);
lv_obj_set_flex_flow(button_wrapper, LV_FLEX_FLOW_ROW);
lv_obj_set_size(button_wrapper, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(button_wrapper, 0, 0);
lv_obj_set_style_border_width(button_wrapper, 0, 0);
lv_obj_set_flex_align(button_wrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);

auto* yes_button = lv_button_create(button_wrapper);
auto* yes_label = lv_label_create(yes_button);
lv_label_set_text(yes_label, "Yes");
lv_obj_add_event_cb(yes_button, onYesPressed, LV_EVENT_SHORT_CLICKED, nullptr);

auto* no_button = lv_button_create(button_wrapper);
auto* no_label = lv_label_create(no_button);
lv_label_set_text(no_label, "No");
lv_obj_add_event_cb(no_button, onNoPressed, LV_EVENT_SHORT_CLICKED, nullptr);
}
};

extern const AppManifest manifest = {
.appId = "PowerOff",
.appName = "Power Off",
.appIcon = LVGL_ICON_SHARED_POWER_SETTINGS_NEW,
.appCategory = Category::Settings,
.appFlags = AppManifest::Flags::HideStatusBar,
.createApp = create<PowerOffApp>
};

} // namespace
Loading