diff --git a/README.md b/README.md index a293baa34..bf9ce02b9 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,9 @@ When the device is left idle it will enter light sleep. Light sleep turns the sc After two minutes the device will go into deep sleep. The leds will turn completely off. +Settings > System > Shutdown timeout can optionally power off the device after a +configured period in deep sleep. The setting is Never by default. + --- ## What about X feature? diff --git a/makefile b/makefile index f1f6d49c7..7d1576abb 100644 --- a/makefile +++ b/makefile @@ -114,6 +114,7 @@ ifeq ($(PLATFORM), h700) # Limbo fix (AXP2202 needs an explicit software power-off) cp ./workspace/$(PLATFORM)/poweroff_next/build/$(PLATFORM)/poweroff_next.elf ./build/SYSTEM/$(PLATFORM)/bin/poweroff_next cp ./workspace/$(PLATFORM)/poweroff_next/build/$(PLATFORM)/reboot_next.elf ./build/SYSTEM/$(PLATFORM)/bin/reboot_next + cp ./workspace/$(PLATFORM)/poweroff_next/build/$(PLATFORM)/rtc_alarm.elf ./build/SYSTEM/$(PLATFORM)/bin/rtc_alarm endif ifneq (,$(filter $(PLATFORM),tg5040 tg5050 h700)) cp ./workspace/all/bootlogo/build/$(PLATFORM)/bootlogo.elf ./build/EXTRAS/Tools/$(PLATFORM)/Bootlogo.pak/ diff --git a/skeleton/BASE/README.txt b/skeleton/BASE/README.txt index 2477498bb..d3dcfa272 100644 --- a/skeleton/BASE/README.txt +++ b/skeleton/BASE/README.txt @@ -76,7 +76,7 @@ ANBERNIC RG XX ---------------------------------------- Quicksave & auto-resume -NextUI will create a quicksave when powering off in-game. The next time you power on the device it will automatically resume from where you left off. A quicksave is created when powering off manually or automatically after a short sleep. On devices without a POWER button (eg. the Trimui Smart or M17) press the MENU button twice to put the device to sleep before flipping the POWER switch. +NextUI will create a quicksave when powering off in-game. The next time you power on the device it will automatically resume from where you left off. A quicksave is created when powering off manually or when the optional Shutdown timeout is reached in deep sleep. On devices without a POWER button (eg. the Trimui Smart or M17) press the MENU button twice to put the device to sleep before flipping the POWER switch. ---------------------------------------- Roms diff --git a/skeleton/SYSTEM/h700/bin/suspend b/skeleton/SYSTEM/h700/bin/suspend index 4832a2614..b1e89753a 100755 --- a/skeleton/SYSTEM/h700/bin/suspend +++ b/skeleton/SYSTEM/h700/bin/suspend @@ -16,6 +16,102 @@ sleep_retval= asound_state_dir=/tmp/asound-suspend +shutdown_timeout_secs=${NEXTUI_SHUTDOWN_TIMEOUT_SECS:-0} +case "$shutdown_timeout_secs" in + ''|*[!0-9]*) shutdown_timeout_secs=0 ;; +esac +rtc_dir=${NEXTUI_RTC_DIR:-} +rtc_device=${NEXTUI_RTC_DEVICE:-} +if [ -z "$rtc_dir" ] && [ -n "$rtc_device" ]; then + rtc_dir=/sys/class/rtc/${rtc_device##*/} +fi +if [ -z "$rtc_dir" ]; then + for rtc_candidate_dir in /sys/class/rtc/rtc*; do + [ -d "$rtc_candidate_dir" ] || continue + if [ "$(cat "$rtc_candidate_dir/name" 2>/dev/null)" = "sunxi-rtc" ]; then + rtc_dir=$rtc_candidate_dir + break + fi + done +fi +[ -n "$rtc_dir" ] || rtc_dir=/sys/class/rtc/rtc0 +[ -n "$rtc_device" ] || rtc_device=/dev/${rtc_dir##*/} +rtc_since_epoch=$rtc_dir/since_epoch +rtc_wakeup=$rtc_dir/device/power/wakeup +rtc_name=$(cat "$rtc_dir/name" 2>/dev/null) +case "$rtc_name" in + sunxi-rtc) rtc_alarm_resolution=1 ;; + *) rtc_alarm_resolution=60 ;; +esac +if [ -n "${NEXTUI_RTC_RESOLUTION_SECONDS:-}" ]; then + rtc_alarm_resolution=$NEXTUI_RTC_RESOLUTION_SECONDS +fi +case "$rtc_alarm_resolution" in + ''|*[!0-9]*|0) rtc_alarm_resolution=60 ;; +esac +rtc_alarm_tool=${NEXTUI_RTC_ALARM_TOOL:-$SYSTEM_PATH/bin/rtc_alarm} +shutdown_timeout_deadline= +shutdown_timeout_rtc_deadline= + +arm_shutdown_timeout() { + [ "$shutdown_timeout_secs" -gt 0 ] || return 0 + if [ ! -x "$rtc_alarm_tool" ]; then + >&2 echo "Shutdown timeout unavailable: $rtc_alarm_tool is not executable" + return 0 + fi + + rtc_wakeup_status=$(cat "$rtc_wakeup" 2>/dev/null) + >&2 echo "RTC wake source: name=${rtc_name:-unknown} device=$rtc_device wakeup=${rtc_wakeup_status:-unknown} resolution=${rtc_alarm_resolution}s" + + rtc_now=$(cat "$rtc_since_epoch" 2>/dev/null) + case "$rtc_now" in + ''|*[!0-9]*) + >&2 echo "Shutdown timeout unavailable: invalid RTC epoch '${rtc_now:-empty}'" + return 0 + ;; + esac + + if ! shutdown_timeout_rtc_deadline=$(NEXTUI_RTC_DEVICE="$rtc_device" NEXTUI_RTC_WAKEUP="$rtc_wakeup" NEXTUI_RTC_RESOLUTION_SECONDS="$rtc_alarm_resolution" \ + "$rtc_alarm_tool" arm "$shutdown_timeout_secs"); then + shutdown_timeout_rtc_deadline= + >&2 echo "Shutdown timeout unavailable: failed to replace RTC wake alarm" + return 0 + fi + + case "$shutdown_timeout_rtc_deadline" in + ''|*[!0-9]*) + NEXTUI_RTC_DEVICE="$rtc_device" "$rtc_alarm_tool" clear || true + shutdown_timeout_rtc_deadline= + >&2 echo "Shutdown timeout unavailable: RTC helper returned an invalid deadline" + return 0 + ;; + esac + + now=$(date +%s) + shutdown_timeout_deadline=$((now + shutdown_timeout_rtc_deadline - rtc_now)) + >&2 echo "Shutdown timeout armed for $shutdown_timeout_secs seconds (RTC $rtc_now -> $shutdown_timeout_rtc_deadline)" + # Persist the armed deadline before an automatic wake and poweroff. + sync +} + +clear_shutdown_timeout() { + [ -n "$shutdown_timeout_deadline" ] || return 0 + if ! NEXTUI_RTC_DEVICE="$rtc_device" "$rtc_alarm_tool" clear; then + >&2 echo "Shutdown timeout warning: failed to clear RTC wake alarm after resume" + fi +} + +shutdown_timeout_expired() { + [ -n "$shutdown_timeout_deadline" ] || return 1 + rtc_now=$(cat "$rtc_since_epoch" 2>/dev/null) + case "$rtc_now" in + ''|*[!0-9]*) ;; + *) [ "$rtc_now" -ge "$shutdown_timeout_rtc_deadline" ] && return 0 ;; + esac + now=$(date +%s) + [ "$now" -ge "$shutdown_timeout_deadline" ] +} + before() { >&2 echo "Preparing for suspend..." echo 0 > /sys/class/power_supply/axp2202-battery/work_led 2>/dev/null || true @@ -82,6 +178,7 @@ after_async() { } before +arm_shutdown_timeout hallkey=/sys/class/power_supply/axp2202-battery/hallkey os_sleep=/sys/class/power_supply/axp2202-battery/os_sleep @@ -99,7 +196,13 @@ while :; do # it, but any non-zero value has the same effect. # Non-SP kernels do not expose this attribute and already use the # full USB-controller suspend path. - [ -w "$os_sleep" ] && printf '16' > "$os_sleep" + if [ -w "$os_sleep" ]; then + if printf '16' > "$os_sleep"; then + >&2 echo "Deep sleep policy: Super Standby (os_sleep=16)" + else + >&2 echo "Deep sleep policy warning: failed to enable Super Standby" + fi + fi # Now issue suspend-to-ram command. echo mem >/sys/power/state @@ -131,6 +234,13 @@ while :; do sleep 3 done + # An RTC timeout takes priority over the clamshell re-suspend loop so a + # closed device can power off without first lighting the display. + if [ $sleep_retval -eq 0 ] && shutdown_timeout_expired; then + sleep_retval=2 + break + fi + # Clamshells: a pocketed power press wakes the kernel even with the # lid shut. Only hand control back to the caller (which turns the # screen on) once the lid is actually open; otherwise re-suspend. @@ -144,6 +254,13 @@ while :; do break done +clear_shutdown_timeout +if [ $sleep_retval -eq 2 ]; then + >&2 echo "Shutdown timeout reached" + # Exit 2 is interpreted by PWR_deepSleep as a requested safe shutdown. + exit 2 +fi + after_sync after_async & diff --git a/skeleton/SYSTEM/tg5040/bin/suspend b/skeleton/SYSTEM/tg5040/bin/suspend index 593159423..f3dfc5a19 100644 --- a/skeleton/SYSTEM/tg5040/bin/suspend +++ b/skeleton/SYSTEM/tg5040/bin/suspend @@ -13,6 +13,60 @@ sleep_retval= asound_state_dir=/tmp/asound-suspend +shutdown_timeout_secs=${NEXTUI_SHUTDOWN_TIMEOUT_SECS:-0} +case "$shutdown_timeout_secs" in + ''|*[!0-9]*) shutdown_timeout_secs=0 ;; +esac +rtc_wakealarm=/sys/class/rtc/rtc0/wakealarm +shutdown_timeout_deadline= + +rtc_alarm_available() { + if ! existing_alarm=$(cat "$rtc_wakealarm" 2>/dev/null); then + >&2 echo "Shutdown timeout unavailable: failed to read RTC wake alarm" + return 1 + fi + + case "$existing_alarm" in + '') return 0 ;; + *[!0-9]*) + >&2 echo "Shutdown timeout unavailable: invalid RTC wake alarm '$existing_alarm'" + return 1 + ;; + *) + >&2 echo "Shutdown timeout skipped: preserving existing RTC alarm $existing_alarm" + return 1 + ;; + esac +} + +arm_shutdown_timeout() { + [ "$shutdown_timeout_secs" -gt 0 ] || return 0 + if [ ! -w "$rtc_wakealarm" ]; then + >&2 echo "Shutdown timeout unavailable: RTC wake alarm is not writable" + return 0 + fi + rtc_alarm_available || return 0 + + now=$(date +%s) + if printf '+%s\n' "$shutdown_timeout_secs" > "$rtc_wakealarm" 2>/dev/null; then + shutdown_timeout_deadline=$((now + shutdown_timeout_secs)) + >&2 echo "Shutdown timeout armed for $shutdown_timeout_secs seconds" + else + >&2 echo "Shutdown timeout unavailable: failed to arm RTC wake alarm" + fi +} + +clear_shutdown_timeout() { + [ -n "$shutdown_timeout_deadline" ] || return 0 + printf '0\n' > "$rtc_wakealarm" 2>/dev/null || true +} + +shutdown_timeout_expired() { + [ -n "$shutdown_timeout_deadline" ] || return 1 + now=$(date +%s) + [ "$now" -ge "$shutdown_timeout_deadline" ] +} + before() { >&2 echo "Preparing for suspend..." @@ -59,6 +113,7 @@ after() { } before +arm_shutdown_timeout >&2 echo "Suspending..." sleep_max_tries=5 @@ -95,6 +150,14 @@ for i in $(seq 1 $sleep_max_tries); do sleep 3 done +if shutdown_timeout_expired; then + clear_shutdown_timeout + >&2 echo "Shutdown timeout reached" + # Exit 2 is interpreted by PWR_deepSleep as a requested safe shutdown. + exit 2 +fi +clear_shutdown_timeout + # Resume services in background to reduce UI latency after & diff --git a/skeleton/SYSTEM/tg5050/bin/suspend b/skeleton/SYSTEM/tg5050/bin/suspend index 090cfbad5..a247ee56a 100644 --- a/skeleton/SYSTEM/tg5050/bin/suspend +++ b/skeleton/SYSTEM/tg5050/bin/suspend @@ -13,6 +13,60 @@ sleep_retval= asound_state_dir=/tmp/asound-suspend +shutdown_timeout_secs=${NEXTUI_SHUTDOWN_TIMEOUT_SECS:-0} +case "$shutdown_timeout_secs" in + ''|*[!0-9]*) shutdown_timeout_secs=0 ;; +esac +rtc_wakealarm=/sys/class/rtc/rtc0/wakealarm +shutdown_timeout_deadline= + +rtc_alarm_available() { + if ! existing_alarm=$(cat "$rtc_wakealarm" 2>/dev/null); then + >&2 echo "Shutdown timeout unavailable: failed to read RTC wake alarm" + return 1 + fi + + case "$existing_alarm" in + '') return 0 ;; + *[!0-9]*) + >&2 echo "Shutdown timeout unavailable: invalid RTC wake alarm '$existing_alarm'" + return 1 + ;; + *) + >&2 echo "Shutdown timeout skipped: preserving existing RTC alarm $existing_alarm" + return 1 + ;; + esac +} + +arm_shutdown_timeout() { + [ "$shutdown_timeout_secs" -gt 0 ] || return 0 + if [ ! -w "$rtc_wakealarm" ]; then + >&2 echo "Shutdown timeout unavailable: RTC wake alarm is not writable" + return 0 + fi + rtc_alarm_available || return 0 + + now=$(date +%s) + if printf '+%s\n' "$shutdown_timeout_secs" > "$rtc_wakealarm" 2>/dev/null; then + shutdown_timeout_deadline=$((now + shutdown_timeout_secs)) + >&2 echo "Shutdown timeout armed for $shutdown_timeout_secs seconds" + else + >&2 echo "Shutdown timeout unavailable: failed to arm RTC wake alarm" + fi +} + +clear_shutdown_timeout() { + [ -n "$shutdown_timeout_deadline" ] || return 0 + printf '0\n' > "$rtc_wakealarm" 2>/dev/null || true +} + +shutdown_timeout_expired() { + [ -n "$shutdown_timeout_deadline" ] || return 1 + now=$(date +%s) + [ "$now" -ge "$shutdown_timeout_deadline" ] +} + before() { >&2 echo "Preparing for suspend..." @@ -59,6 +113,7 @@ after() { } before +arm_shutdown_timeout >&2 echo "Suspending..." sleep_max_tries=5 @@ -77,6 +132,14 @@ for i in $(seq 1 $sleep_max_tries); do sleep 3 done +if shutdown_timeout_expired; then + clear_shutdown_timeout + >&2 echo "Shutdown timeout reached" + # Exit 2 is interpreted by PWR_deepSleep as a requested safe shutdown. + exit 2 +fi +clear_shutdown_timeout + # Resume services in background to reduce UI latency after & diff --git a/workspace/all/common/api.c b/workspace/all/common/api.c index e107225da..58262117c 100644 --- a/workspace/all/common/api.c +++ b/workspace/all/common/api.c @@ -13,9 +13,10 @@ #include #include #include -#include #include +#include #include +#include #include "utils.h" #include "config.h" @@ -4404,7 +4405,7 @@ static void PWR_exitSleep(void) sync(); } -static void PWR_waitForWake(void) +static int PWR_waitForWake(void) { uint32_t sleep_ticks = SDL_GetTicks(); int deep_sleep_attempts = 0; @@ -4430,9 +4431,13 @@ static void PWR_waitForWake(void) if (PLAT_supportsDeepSleep()) { int ret = PWR_deepSleep(); - if (ret == 0) + if (ret == PWR_DEEPSLEEP_RESUMED) + { + return PWR_DEEPSLEEP_RESUMED; + } + else if (ret == PWR_DEEPSLEEP_SHUTDOWN_TIMEOUT) { - return; + return PWR_DEEPSLEEP_SHUTDOWN_TIMEOUT; } else { @@ -4447,7 +4452,7 @@ static void PWR_waitForWake(void) } } - return; + return PWR_DEEPSLEEP_RESUMED; } void PWR_sleep(void) { @@ -4458,7 +4463,13 @@ void PWR_sleep(void) GFX_clear(gfx.screen); PAD_reset(); PWR_enterSleep(); - PWR_waitForWake(); + int wake_result = PWR_waitForWake(); + if (wake_result == PWR_DEEPSLEEP_SHUTDOWN_TIMEOUT) + { + LOG_info("deep-sleep shutdown timeout reached - powering off\n"); + PWR_powerOff(0); + return; + } PWR_exitSleep(); PAD_reset(); @@ -4479,15 +4490,36 @@ int PWR_deepSleep(void) { LOG_info("suspending using platform suspend executable\n"); + uint32_t shutdown_timeout_secs = pwr.can_poweroff ? CFG_getShutdownTimeoutSecs() : 0; + unsetenv("NEXTUI_SHUTDOWN_TIMEOUT_SECS"); + if (shutdown_timeout_secs > 0) + { + char timeout[16]; + snprintf(timeout, sizeof(timeout), "%u", shutdown_timeout_secs); + if (setenv("NEXTUI_SHUTDOWN_TIMEOUT_SECS", timeout, 1) != 0) + { + LOG_error("failed to configure shutdown timeout: %d\n", errno); + } + } + int ret = system(suspend_path); + unsetenv("NEXTUI_SHUTDOWN_TIMEOUT_SECS"); if (ret < 0) { LOG_error("failed to launch suspend executable: %d\n", errno); - return -1; + return PWR_DEEPSLEEP_FAILED; } LOG_info("suspend executable exited with %d\n", ret); - return ret == 0 ? 0 : -1; + if (WIFEXITED(ret)) + { + int exit_status = WEXITSTATUS(ret); + if (exit_status == 0) + return PWR_DEEPSLEEP_RESUMED; + if (exit_status == 2) + return PWR_DEEPSLEEP_SHUTDOWN_TIMEOUT; + } + return PWR_DEEPSLEEP_FAILED; } return PLAT_deepSleep(); diff --git a/workspace/all/common/api.h b/workspace/all/common/api.h index e7b5d6808..ebbcaa5ed 100644 --- a/workspace/all/common/api.h +++ b/workspace/all/common/api.h @@ -573,6 +573,11 @@ void PWR_powerOff(int reboot); int PWR_isPoweringOff(void); void PWR_sleep(void); +enum { + PWR_DEEPSLEEP_FAILED = -1, + PWR_DEEPSLEEP_RESUMED = 0, + PWR_DEEPSLEEP_SHUTDOWN_TIMEOUT = 1, +}; int PWR_deepSleep(void); void PWR_requestSleep(void); diff --git a/workspace/all/common/config.c b/workspace/all/common/config.c index aea4cb496..ba1aba16b 100644 --- a/workspace/all/common/config.c +++ b/workspace/all/common/config.c @@ -69,6 +69,7 @@ void CFG_defaults(NextUISettings *cfg) .screenTimeoutSecs = CFG_DEFAULT_SCREENTIMEOUTSECS, .suspendTimeoutSecs = CFG_DEFAULT_SUSPENDTIMEOUTSECS, + .shutdownTimeoutSecs = CFG_DEFAULT_SHUTDOWNTIMEOUTSECS, .powerOffProtection = CFG_DEFAULT_POWEROFFPROTECTION, .keepAwakeWhenUSB = CFG_DEFAULT_KEEPAWAKEWHENUSB, @@ -301,6 +302,12 @@ void CFG_init(FontLoad_callback_t cb, ColorSet_callback_t ccb) CFG_setSuspendTimeoutSecs(temp_value); continue; } + if (sscanf(line, "shutdownTimeout=%i", &temp_value) == 1) + { + if (temp_value >= 0) + CFG_setShutdownTimeoutSecs((uint32_t)temp_value); + continue; + } if (sscanf(line, "powerOffProtection=%i", &temp_value) == 1) { CFG_setPowerOffProtection((bool)temp_value); @@ -727,6 +734,17 @@ void CFG_setSuspendTimeoutSecs(uint32_t secs) CFG_sync(); } +uint32_t CFG_getShutdownTimeoutSecs(void) +{ + return settings.shutdownTimeoutSecs; +} + +void CFG_setShutdownTimeoutSecs(uint32_t secs) +{ + settings.shutdownTimeoutSecs = secs; + CFG_sync(); +} + bool CFG_getPowerOffProtection(void) { return settings.powerOffProtection; @@ -1426,6 +1444,10 @@ void CFG_get(const char *key, char *value) { sprintf(value, "%i", CFG_getSuspendTimeoutSecs()); } + else if (strcmp(key, "shutdownTimeout") == 0) + { + sprintf(value, "%u", CFG_getShutdownTimeoutSecs()); + } else if (strcmp(key, "powerOffProtection") == 0) { sprintf(value, "%i", CFG_getPowerOffProtection()); @@ -1646,6 +1668,7 @@ void CFG_sync(void) fprintf(file, "showfoldernamesatroot=%i\n", settings.showFolderNamesAtRoot); fprintf(file, "screentimeout=%i\n", settings.screenTimeoutSecs); fprintf(file, "suspendTimeout=%i\n", settings.suspendTimeoutSecs); + fprintf(file, "shutdownTimeout=%u\n", settings.shutdownTimeoutSecs); fprintf(file, "powerOffProtection=%i\n", settings.powerOffProtection); fprintf(file, "keepAwakeWhenUSB=%i\n", settings.keepAwakeWhenUSB); fprintf(file, "switcherscale=%i\n", settings.gameSwitcherScaling); @@ -1721,6 +1744,7 @@ void CFG_print(void) printf("\t\"showfoldernamesatroot\": %i,\n", settings.showFolderNamesAtRoot); printf("\t\"screentimeout\": %i,\n", settings.screenTimeoutSecs); printf("\t\"suspendTimeout\": %i,\n", settings.suspendTimeoutSecs); + printf("\t\"shutdownTimeout\": %u,\n", settings.shutdownTimeoutSecs); printf("\t\"powerOffProtection\": %i,\n", settings.powerOffProtection); printf("\t\"keepAwakeWhenUSB\": %i,\n", settings.keepAwakeWhenUSB); printf("\t\"switcherscale\": %i,\n", settings.gameSwitcherScaling); diff --git a/workspace/all/common/config.h b/workspace/all/common/config.h index bad3f5fc0..7b9cec16d 100644 --- a/workspace/all/common/config.h +++ b/workspace/all/common/config.h @@ -151,6 +151,7 @@ typedef struct // Power uint32_t screenTimeoutSecs; uint32_t suspendTimeoutSecs; + uint32_t shutdownTimeoutSecs; bool powerOffProtection; bool keepAwakeWhenUSB; @@ -229,6 +230,7 @@ typedef struct #define CFG_DEFAULT_GAMESWITCHERSCALING GFX_SCALE_FULLSCREEN #define CFG_DEFAULT_SCREENTIMEOUTSECS 60 #define CFG_DEFAULT_SUSPENDTIMEOUTSECS 30 +#define CFG_DEFAULT_SHUTDOWNTIMEOUTSECS 0 #define CFG_DEFAULT_POWEROFFPROTECTION true #define CFG_DEFAULT_KEEPAWAKEWHENUSB false #define CFG_DEFAULT_HAPTICS false @@ -325,6 +327,10 @@ void CFG_setScreenTimeoutSecs(uint32_t secs); // Time in secs before the device enters suspend mode (aka deep sleep). uint32_t CFG_getSuspendTimeoutSecs(void); void CFG_setSuspendTimeoutSecs(uint32_t secs); +// Time in secs spent in deep sleep before the device wakes and powers off. +// Zero disables automatic poweroff. +uint32_t CFG_getShutdownTimeoutSecs(void); +void CFG_setShutdownTimeoutSecs(uint32_t secs); // Enable/disable PMIC power-off protection mode. bool CFG_getPowerOffProtection(void); void CFG_setPowerOffProtection(bool enable); diff --git a/workspace/all/minarch/ma_menu.c b/workspace/all/minarch/ma_menu.c index 85ca4341a..027d820fc 100644 --- a/workspace/all/minarch/ma_menu.c +++ b/workspace/all/minarch/ma_menu.c @@ -224,8 +224,10 @@ void Menu_quit(void) { void Menu_beforeSleep() { SRAM_write(); RTC_write(); - State_autosave(); - putFile(AUTO_RESUME_PATH, game.path + strlen(SDCARD_PATH)); + if (State_autosave()) + putFile(AUTO_RESUME_PATH, game.path + strlen(SDCARD_PATH)); + else + unlink(AUTO_RESUME_PATH); } void Menu_afterSleep() { unlink(AUTO_RESUME_PATH); diff --git a/workspace/all/minarch/ma_saves.c b/workspace/all/minarch/ma_saves.c index 8fe053e01..b00426114 100644 --- a/workspace/all/minarch/ma_saves.c +++ b/workspace/all/minarch/ma_saves.c @@ -368,11 +368,12 @@ int State_write(void) { // from picoarch return success; } -void State_autosave(void) { +int State_autosave(void) { int last_state_slot = state_slot; state_slot = AUTO_RESUME_SLOT; - State_write(); + int success = State_write(); state_slot = last_state_slot; + return success; } void State_resume(void) { if (!exists(RESUME_SLOT_PATH)) return; diff --git a/workspace/all/minarch/ma_saves.h b/workspace/all/minarch/ma_saves.h index 0d8882dc7..4405b486b 100644 --- a/workspace/all/minarch/ma_saves.h +++ b/workspace/all/minarch/ma_saves.h @@ -9,5 +9,5 @@ void RTC_write(void); void State_getPath(char* filename); int State_read(void); int State_write(void); -void State_autosave(void); +int State_autosave(void); void State_resume(void); diff --git a/workspace/all/settings/settings.cpp b/workspace/all/settings/settings.cpp index f9f740470..064d008a9 100644 --- a/workspace/all/settings/settings.cpp +++ b/workspace/all/settings/settings.cpp @@ -135,6 +135,9 @@ static const std::vector screen_timeout_labels = {"Never", "5s", "1 static const std::vector sleep_timeout_secs = {5U, 10U, 15U, 30U, 45U, 60U, 90U, 120U, 240U, 360U, 600U}; static const std::vector sleep_timeout_labels = {"5s", "10s", "15s", "30s", "45s", "60s", "90s", "2m", "4m", "6m", "10m"}; +static const std::vector shutdown_timeout_secs = {0U, 60U, 300U, 600U, 900U, 1800U, 3600U, 7200U, 14400U, 28800U, 43200U, 86400U}; +static const std::vector shutdown_timeout_labels = {"Never", "1m", "5m", "10m", "15m", "30m", "1h", "2h", "4h", "8h", "12h", "24h"}; + static const std::vector on_off = {"Off", "On"}; static const std::vector scaling_strings = {"Fullscreen", "Fit", "Fill"}; @@ -653,6 +656,10 @@ int main(int argc, char *argv[]) { return CFG_getSuspendTimeoutSecs(); }, [](const std::any &value) { CFG_setSuspendTimeoutSecs(std::any_cast(value)); }, []() { CFG_setSuspendTimeoutSecs(CFG_DEFAULT_SUSPENDTIMEOUTSECS);}}, + new MenuItem{ListItemType::Generic, "Shutdown timeout", "Time in sleep mode before the device powers off", shutdown_timeout_secs, shutdown_timeout_labels, []() -> std::any + { return CFG_getShutdownTimeoutSecs(); }, [](const std::any &value) + { CFG_setShutdownTimeoutSecs(std::any_cast(value)); }, + []() { CFG_setShutdownTimeoutSecs(CFG_DEFAULT_SHUTDOWNTIMEOUTSECS);}}, new MenuItem{ListItemType::Generic, "Haptic feedback", "Enable or disable haptic feedback on certain actions in the OS", {false, true}, on_off, []() -> std::any { return CFG_getHaptics(); }, [](const std::any &value) { CFG_setHaptics(std::any_cast(value)); }, diff --git a/workspace/h700/poweroff_next/makefile b/workspace/h700/poweroff_next/makefile index f44ffc30d..9a6330b89 100644 --- a/workspace/h700/poweroff_next/makefile +++ b/workspace/h700/poweroff_next/makefile @@ -34,6 +34,7 @@ all: mkdir -p $(BUILDDIR) $(CC) $(SOURCE) -o $(BUILDDIR)/poweroff_next.elf $(CFLAGS) $(LDFLAGS) $(CC) $(SOURCE) -o $(BUILDDIR)/reboot_next.elf $(CFLAGS) -DREBOOT_NEXT $(LDFLAGS) + $(CC) rtc_alarm.c -o $(BUILDDIR)/rtc_alarm.elf $(CFLAGS) clean: rm -rf $(BUILDDIR) diff --git a/workspace/h700/poweroff_next/rtc_alarm.c b/workspace/h700/poweroff_next/rtc_alarm.c new file mode 100644 index 000000000..f939dfe25 --- /dev/null +++ b/workspace/h700/poweroff_next/rtc_alarm.c @@ -0,0 +1,358 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DEFAULT_RTC_DEVICE "/dev/rtc0" +#define DEFAULT_RTC_WAKEUP "/sys/class/rtc/rtc0/device/power/wakeup" +#define DEFAULT_ALARM_RESOLUTION_SECONDS 60 + +static void usage(const char *program) +{ + fprintf(stderr, "Usage: %s enable|arm |clear\n", program); +} + +static int read_wakeup_status(const char *path, char *status, size_t status_size) +{ + int fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + fprintf(stderr, "rtc_alarm: failed to open wake source %s: %s\n", + path, strerror(errno)); + return -1; + } + + ssize_t length = read(fd, status, status_size - 1); + int read_errno = errno; + if (close(fd) < 0 && length >= 0) { + fprintf(stderr, "rtc_alarm: failed to close wake source %s: %s\n", + path, strerror(errno)); + return -1; + } + if (length < 0) { + fprintf(stderr, "rtc_alarm: failed to read wake source %s: %s\n", + path, strerror(read_errno)); + return -1; + } + if (length == 0) { + fprintf(stderr, "rtc_alarm: wake source %s returned an empty status\n", path); + return -1; + } + + status[length] = '\0'; + status[strcspn(status, "\r\n")] = '\0'; + return 0; +} + +static int enable_wakeup(const char *path) +{ + char status[32]; + if (read_wakeup_status(path, status, sizeof(status)) < 0) + return -1; + if (strcmp(status, "enabled") == 0) { + fprintf(stderr, "rtc_alarm: wake source already enabled (%s)\n", path); + return 0; + } + if (strcmp(status, "disabled") != 0) { + fprintf(stderr, "rtc_alarm: invalid wake source status '%s' from %s\n", + status, path); + return -1; + } + + int fd = open(path, O_WRONLY | O_CLOEXEC); + if (fd < 0) { + fprintf(stderr, "rtc_alarm: failed to open wake source %s for writing: %s\n", + path, strerror(errno)); + return -1; + } + + static const char enabled[] = "enabled\n"; + ssize_t written = write(fd, enabled, sizeof(enabled) - 1); + int write_errno = errno; + if (close(fd) < 0 && written >= 0) { + fprintf(stderr, "rtc_alarm: failed to close wake source %s: %s\n", + path, strerror(errno)); + return -1; + } + if (written < 0) { + fprintf(stderr, "rtc_alarm: failed to enable wake source %s: %s\n", + path, strerror(write_errno)); + return -1; + } + if (written != (ssize_t)(sizeof(enabled) - 1)) { + fprintf(stderr, "rtc_alarm: short write enabling wake source %s (%zd of %zu)\n", + path, written, sizeof(enabled) - 1); + return -1; + } + + if (read_wakeup_status(path, status, sizeof(status)) < 0) + return -1; + if (strcmp(status, "enabled") != 0) { + fprintf(stderr, "rtc_alarm: wake source %s remained '%s' after enable\n", + path, status); + return -1; + } + + fprintf(stderr, "rtc_alarm: wake source enabled (%s)\n", path); + return 0; +} + +static int rtc_time_to_epoch(const struct rtc_time *rtc_time, int64_t *epoch) +{ + struct tm tm = { + .tm_sec = rtc_time->tm_sec, + .tm_min = rtc_time->tm_min, + .tm_hour = rtc_time->tm_hour, + .tm_mday = rtc_time->tm_mday, + .tm_mon = rtc_time->tm_mon, + .tm_year = rtc_time->tm_year, + .tm_isdst = 0, + }; + + errno = 0; + time_t value = timegm(&tm); + if (value == (time_t)-1 && errno != 0) { + fprintf(stderr, "rtc_alarm: failed to convert RTC time: %s\n", strerror(errno)); + return -1; + } + + *epoch = (int64_t)value; + return 0; +} + +static int epoch_to_rtc_time(int64_t epoch, struct rtc_time *rtc_time) +{ + time_t value = (time_t)epoch; + struct tm tm; + + if ((int64_t)value != epoch || !gmtime_r(&value, &tm)) { + fprintf(stderr, "rtc_alarm: alarm time is outside the supported range\n"); + return -1; + } + + rtc_time->tm_sec = tm.tm_sec; + rtc_time->tm_min = tm.tm_min; + rtc_time->tm_hour = tm.tm_hour; + rtc_time->tm_mday = tm.tm_mday; + rtc_time->tm_mon = tm.tm_mon; + rtc_time->tm_year = tm.tm_year; + rtc_time->tm_wday = tm.tm_wday; + rtc_time->tm_yday = tm.tm_yday; + rtc_time->tm_isdst = 0; + return 0; +} + +static int read_alarm(int fd, struct rtc_wkalrm *alarm, int64_t *epoch) +{ + memset(alarm, 0, sizeof(*alarm)); + if (ioctl(fd, RTC_WKALM_RD, alarm) < 0) { + fprintf(stderr, "rtc_alarm: RTC_WKALM_RD failed: %s\n", strerror(errno)); + return -1; + } + + if (rtc_time_to_epoch(&alarm->time, epoch) < 0) + return -1; + + return 0; +} + +static int clear_alarm(int fd) +{ + struct rtc_wkalrm alarm; + int64_t alarm_epoch; + + if (read_alarm(fd, &alarm, &alarm_epoch) < 0) + return -1; + + unsigned int was_enabled = alarm.enabled; + unsigned int was_pending = alarm.pending; + if (ioctl(fd, RTC_AIE_OFF, 0) < 0) { + fprintf(stderr, + "rtc_alarm: RTC_AIE_OFF failed for alarm %" PRId64 + " (enabled=%u pending=%u): %s\n", + alarm_epoch, was_enabled, was_pending, strerror(errno)); + return -1; + } + + if (read_alarm(fd, &alarm, &alarm_epoch) < 0) + return -1; + if (alarm.enabled || alarm.pending) { + fprintf(stderr, + "rtc_alarm: alarm interrupt remained active after RTC_AIE_OFF" + " (%" PRId64 ", enabled=%u pending=%u)\n", + alarm_epoch, alarm.enabled, alarm.pending); + return -1; + } + + fprintf(stderr, + "rtc_alarm: alarm interrupt cleared (was enabled=%u pending=%u)\n", + was_enabled, was_pending); + return 0; +} + +static int parse_delay(const char *seconds_arg, uint64_t *delay) +{ + char *end = NULL; + errno = 0; + uint64_t value = strtoull(seconds_arg, &end, 10); + if (errno != 0 || !end || *end != '\0' || value == 0 || value > UINT32_MAX) { + fprintf(stderr, "rtc_alarm: invalid delay '%s'\n", seconds_arg); + return -1; + } + + *delay = value; + return 0; +} + +static int alarm_resolution(uint64_t *resolution) +{ + const char *resolution_arg = getenv("NEXTUI_RTC_RESOLUTION_SECONDS"); + if (!resolution_arg || !*resolution_arg) { + *resolution = DEFAULT_ALARM_RESOLUTION_SECONDS; + return 0; + } + + char *end = NULL; + errno = 0; + uint64_t value = strtoull(resolution_arg, &end, 10); + if (errno != 0 || !end || *end != '\0' || value == 0 || value > UINT32_MAX) { + fprintf(stderr, "rtc_alarm: invalid alarm resolution '%s'\n", resolution_arg); + return -1; + } + + *resolution = value; + return 0; +} + +static int arm_alarm(int fd, uint64_t delay, uint64_t resolution) +{ + /* Explicitly disable the previous alarm interrupt before every arm. This + * follows the reliable clear-then-set sequence while avoiding wakealarm + * sysfs EBUSY handling. */ + if (clear_alarm(fd) < 0) { + fprintf(stderr, "rtc_alarm: refusing to arm without clearing the prior alarm interrupt\n"); + return -1; + } + + struct rtc_time now_time; + if (ioctl(fd, RTC_RD_TIME, &now_time) < 0) { + fprintf(stderr, "rtc_alarm: RTC_RD_TIME failed: %s\n", strerror(errno)); + return -1; + } + + int64_t now; + if (rtc_time_to_epoch(&now_time, &now) < 0) + return -1; + if (now < 0 || resolution > (uint64_t)(INT64_MAX - now) || + delay > (uint64_t)(INT64_MAX - now - (int64_t)(resolution - 1))) { + fprintf(stderr, "rtc_alarm: requested deadline overflows supported time\n"); + return -1; + } + + int64_t minimum_deadline = now + (int64_t)delay; + int64_t deadline = ((minimum_deadline + (int64_t)resolution - 1) / + (int64_t)resolution) * (int64_t)resolution; + + struct rtc_wkalrm alarm; + memset(&alarm, 0, sizeof(alarm)); + alarm.enabled = 1; + if (epoch_to_rtc_time(deadline, &alarm.time) < 0) + return -1; + + /* RTC_WKALM_SET arms the replacement after RTC_AIE_OFF cleared any stale + * alarm flag or interrupt state. */ + if (ioctl(fd, RTC_WKALM_SET, &alarm) < 0) { + fprintf(stderr, "rtc_alarm: RTC_WKALM_SET failed for %" PRId64 ": %s\n", + deadline, strerror(errno)); + return -1; + } + + int64_t armed_epoch; + if (read_alarm(fd, &alarm, &armed_epoch) < 0) + return -1; + if (!alarm.enabled) { + fprintf(stderr, "rtc_alarm: alarm readback is disabled\n"); + return -1; + } + if (armed_epoch < minimum_deadline || armed_epoch > deadline + (int64_t)resolution) { + fprintf(stderr, + "rtc_alarm: alarm readback mismatch (minimum=%" PRId64 + " requested=%" PRId64 " actual=%" PRId64 ")\n", + minimum_deadline, deadline, armed_epoch); + return -1; + } + + fprintf(stderr, + "rtc_alarm: armed after %" PRIu64 " seconds (RTC %" PRId64 + " -> %" PRId64 ", resolution=%" PRIu64 "s)\n", + delay, now, armed_epoch, resolution); + printf("%" PRId64 "\n", armed_epoch); + return 0; +} + +int main(int argc, char **argv) +{ + if (argc < 2 || (strcmp(argv[1], "arm") == 0 && argc != 3) || + (strcmp(argv[1], "clear") == 0 && argc != 2) || + (strcmp(argv[1], "enable") == 0 && argc != 2)) { + usage(argv[0]); + return EXIT_FAILURE; + } + + const char *rtc_wakeup = getenv("NEXTUI_RTC_WAKEUP"); + if (!rtc_wakeup || !*rtc_wakeup) + rtc_wakeup = DEFAULT_RTC_WAKEUP; + + if (strcmp(argv[1], "enable") == 0) + return enable_wakeup(rtc_wakeup) == 0 ? EXIT_SUCCESS : EXIT_FAILURE; + + if (strcmp(argv[1], "arm") != 0 && strcmp(argv[1], "clear") != 0) { + usage(argv[0]); + return EXIT_FAILURE; + } + + if (strcmp(argv[1], "arm") == 0 && enable_wakeup(rtc_wakeup) < 0) { + fprintf(stderr, "rtc_alarm: refusing to arm without a verified wake source\n"); + return EXIT_FAILURE; + } + + const char *rtc_device = getenv("NEXTUI_RTC_DEVICE"); + if (!rtc_device || !*rtc_device) + rtc_device = DEFAULT_RTC_DEVICE; + + int fd = open(rtc_device, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + fprintf(stderr, "rtc_alarm: failed to open %s: %s\n", rtc_device, strerror(errno)); + return EXIT_FAILURE; + } + + int result; + if (strcmp(argv[1], "arm") == 0) { + uint64_t delay; + uint64_t resolution; + result = parse_delay(argv[2], &delay); + if (result == 0) + result = alarm_resolution(&resolution); + if (result == 0) + result = arm_alarm(fd, delay, resolution); + } else if (strcmp(argv[1], "clear") == 0) { + result = clear_alarm(fd); + } else { + result = clear_alarm(fd); + } + + if (close(fd) < 0 && result == 0) { + fprintf(stderr, "rtc_alarm: failed to close %s: %s\n", rtc_device, strerror(errno)); + result = -1; + } + return result == 0 ? EXIT_SUCCESS : EXIT_FAILURE; +}