diff --git a/frontend/apps/reader/modules/readercoptlistener.lua b/frontend/apps/reader/modules/readercoptlistener.lua index c5fdbf69c..451681fcf 100644 --- a/frontend/apps/reader/modules/readercoptlistener.lua +++ b/frontend/apps/reader/modules/readercoptlistener.lua @@ -147,6 +147,11 @@ function ReaderCoptListener:onResume() self:headerRefresh() end +function ReaderCoptListener:onLeaveStandby() + self:onResume() + self:onOutOfScreenSaver() +end + function ReaderCoptListener:onOutOfScreenSaver() if not self._delayed_screensaver then return @@ -159,6 +164,7 @@ end -- Unschedule on these events ReaderCoptListener.onCloseDocument = ReaderCoptListener.unscheduleHeaderRefresh ReaderCoptListener.onSuspend = ReaderCoptListener.unscheduleHeaderRefresh +ReaderCoptListener.onEnterStandby = ReaderCoptListener.unscheduleHeaderRefresh function ReaderCoptListener:setAndSave(setting, property, value) self.ui.document._document:setIntProperty(property, value) diff --git a/frontend/apps/reader/modules/readerfooter.lua b/frontend/apps/reader/modules/readerfooter.lua index bb26a7e75..e5c75439f 100644 --- a/frontend/apps/reader/modules/readerfooter.lua +++ b/frontend/apps/reader/modules/readerfooter.lua @@ -2432,10 +2432,17 @@ function ReaderFooter:onOutOfScreenSaver() self:rescheduleFooterAutoRefreshIfNeeded() end +function ReaderFooter:onLeaveStandby() + self:onResume() + self:onOutOfScreenSaver() +end + function ReaderFooter:onSuspend() self:unscheduleFooterAutoRefresh() end +ReaderFooter.onEnterStandby = ReaderFooter.onSuspend + function ReaderFooter:onCloseDocument() self:unscheduleFooterAutoRefresh() end diff --git a/frontend/device/generic/device.lua b/frontend/device/generic/device.lua index 5cf8ae660..6386adee0 100644 --- a/frontend/device/generic/device.lua +++ b/frontend/device/generic/device.lua @@ -6,6 +6,7 @@ This module defines stubs for common methods. local DataStorage = require("datastorage") local Geom = require("ui/geometry") +local TimeVal = require("ui/timeval") local logger = require("logger") local util = require("util") local _ = require("gettext") @@ -67,6 +68,12 @@ local Device = { canUseWAL = yes, -- requires mmap'ed I/O on the target FS canRestart = yes, canSuspend = yes, + canStandby = no, + canPowerSaveWhileCharging = no, + total_standby_tv = TimeVal.zero, -- total time spent in standby + last_standby_tv = TimeVal.zero, + total_suspend_tv = TimeVal.zero, -- total time spent in suspend + last_suspend_tv = TimeVal.zero, canReboot = no, canPowerOff = no, canAssociateFileExtensions = no, @@ -238,7 +245,7 @@ end function Device:rescheduleSuspend() local UIManager = require("ui/uimanager") UIManager:unschedule(self.suspend) - UIManager:scheduleIn(self.suspend_wait_timeout, self.suspend) + UIManager:scheduleIn(self.suspend_wait_timeout, self.suspend, self) end -- Only used on platforms where we handle suspend ourselves. @@ -430,6 +437,9 @@ function Device:saveSettings() end function Device:simulateSuspend() end function Device:simulateResume() end +-- Put device into standby, input devices (buttons, touchscreen ...) stay enabled +function Device:standby(max_duration) end + --[[-- Device specific method for performing haptic feedback. diff --git a/frontend/device/kobo/device.lua b/frontend/device/kobo/device.lua index a4bd23761..5eeced397 100644 --- a/frontend/device/kobo/device.lua +++ b/frontend/device/kobo/device.lua @@ -24,6 +24,36 @@ local function koboEnableWifi(toggle) end end +-- checks if standby is available on the device +local function checkStandby() + logger.dbg("Kobo: checking if standby is possible ...") + local f = io.open("/sys/power/state") + if not f then + return no + end + local mode = f:read() + logger.dbg("Kobo: available power states", mode) + if mode:find("standby") then + logger.dbg("Kobo: standby state allowed") + return yes + end + logger.dbg("Kobo: standby state not allowed") + return no +end + +local function writeToSys(val, file) + local f = io.open(file, "we") + if not f then + logger.err("Cannot open:", file) + return + end + local re, err_msg, err_code = f:write(val, "\n") + if not re then + logger.err("Error writing value to file:", val, file, err_msg, err_code) + end + f:close() + return re +end local Kobo = Generic:new{ model = "Kobo", @@ -32,9 +62,9 @@ local Kobo = Generic:new{ hasOTAUpdates = yes, hasFastWifiStatusQuery = yes, hasWifiManager = yes, + canStandby = no, -- will get updated by checkStandby() canReboot = yes, canPowerOff = yes, - -- most Kobos have X/Y switched for the touch screen touch_switch_xy = true, -- most Kobos have also mirrored X coordinates @@ -75,6 +105,8 @@ local Kobo = Generic:new{ isSMP = no, -- Device supports "eclipse" waveform modes (i.e., optimized for nightmode). hasEclipseWfm = no, + + unexpected_wakeup_count = 0 } -- Kobo Touch: @@ -517,6 +549,11 @@ function Kobo:init() -- Only enable a single core on startup self:enableCPUCores(1) + + self.canStandby = checkStandby() + if self.canStandby() and (self:isMk7() or self:isSunxi()) then + self.canPowerSaveWhileCharging = yes + end end function Kobo:setDateTime(year, month, day, hour, min, sec) @@ -684,8 +721,7 @@ local function getProductId() return product_id end -local unexpected_wakeup_count = 0 -local function check_unexpected_wakeup() +function Kobo:checkUnexpectedWakeup() local UIManager = require("ui/uimanager") -- just in case other events like SleepCoverClosed also scheduled a suspend UIManager:unschedule(Kobo.suspend) @@ -700,11 +736,11 @@ local function check_unexpected_wakeup() logger.info("Kobo suspend: putting device back to sleep.") -- Most wakeup actions are linear, but we need some leeway for the -- poweroff action to send out close events to all requisite widgets. - UIManager:scheduleIn(30, Kobo.suspend) + UIManager:scheduleIn(30, Kobo.suspend, self) else logger.dbg("Kobo suspend: checking unexpected wakeup:", - unexpected_wakeup_count) - if unexpected_wakeup_count == 0 or unexpected_wakeup_count > 20 then + self.unexpected_wakeup_count) + if self.unexpected_wakeup_count == 0 or self.unexpected_wakeup_count > 20 then -- Don't put device back to sleep under the following two cases: -- 1. a resume event triggered Kobo:resume() function -- 2. trying to put device back to sleep more than 20 times after unexpected wakeup @@ -715,17 +751,42 @@ local function check_unexpected_wakeup() end logger.err("Kobo suspend: putting device back to sleep. Unexpected wakeups:", - unexpected_wakeup_count) - Kobo.suspend() + self.unexpected_wakeup_count) + Kobo:suspend() end end -function Kobo:getUnexpectedWakeup() return unexpected_wakeup_count end +function Kobo:getUnexpectedWakeup() return self.unexpected_wakeup_count end + +--- The function to put the device into standby, with enabled touchscreen. +-- max_duration ... maximum time for the next standby, can wake earlier (e.g. Tap, Button ...) +function Kobo:standby(max_duration) + -- just for wake up, dummy function + local function dummy() end + + if max_duration then + self.wakeup_mgr:addTask(max_duration, dummy) + end + + local TimeVal = require("ui/timeval") + local standby_time_tv = TimeVal:boottime_or_realtime_coarse() + + local ret = writeToSys("standby", "/sys/power/state") + + self.last_standby_tv = TimeVal:boottime_or_realtime_coarse() - standby_time_tv + self.total_standby_tv = self.total_standby_tv + self.last_standby_tv + + logger.info("Kobo suspend: asked the kernel to put subsystems to standby, ret:", ret) + + if max_duration then + self.wakeup_mgr:removeTask(nil, nil, dummy) + end +end function Kobo:suspend() logger.info("Kobo suspend: going to sleep . . .") local UIManager = require("ui/uimanager") - UIManager:unschedule(check_unexpected_wakeup) + UIManager:unschedule(self.checkUnexpectedWakeup) local f, re, err_msg, err_code -- NOTE: Sleep as little as possible here, sleeping has a tendency to make -- everything mysteriously hang... @@ -762,17 +823,8 @@ function Kobo:suspend() -- NOTE: Sets gSleep_Mode_Suspend to 1. Used as a flag throughout the -- kernel to suspend/resume various subsystems -- cf. kernel/power/main.c @ L#207 - f = io.open("/sys/power/state-extended", "we") - if not f then - logger.err("Cannot open /sys/power/state-extended for writing!") - return false - end - re, err_msg, err_code = f:write("1\n") - f:close() - logger.info("Kobo suspend: asked the kernel to put subsystems to sleep, ret:", re) - if not re then - logger.err('write error: ', err_msg, err_code) - end + local ret = writeToSys("1", "/sys/power/state-extended") + logger.info("Kobo suspend: asked the kernel to put subsystems to sleep, ret:", ret) util.sleep(2) logger.info("Kobo suspend: waited for 2s because of reasons...") @@ -813,15 +865,23 @@ function Kobo:suspend() end return false end + + local TimeVal = require("ui/timeval") + local suspend_time_tv = TimeVal:boottime_or_realtime_coarse() + re, err_msg, err_code = f:write("mem\n") + if not re then + logger.err("write error: ", err_msg, err_code) + end + f:close() + -- NOTE: At this point, we *should* be in suspend to RAM, as such, -- execution should only resume on wakeup... + self.last_suspend_tv = TimeVal:boottime_or_realtime_coarse() - suspend_time_tv + self.total_suspend_tv = self.total_suspend_tv + self.last_suspend_tv + logger.info("Kobo suspend: ZzZ ZzZ ZzZ? Write syscall returned: ", re) - if not re then - logger.err('write error: ', err_msg, err_code) - end - f:close() -- NOTE: Ideally, we'd need a way to warn the user that suspending -- gloriously failed at this point... -- We can safely assume that just from a non-zero return code, without @@ -849,44 +909,32 @@ function Kobo:suspend() -- things tidy and easier to follow -- Kobo:resume() will reset unexpected_wakeup_count = 0 to signal an - -- expected wakeup, which gets checked in check_unexpected_wakeup(). - unexpected_wakeup_count = unexpected_wakeup_count + 1 + -- expected wakeup, which gets checked in checkUnexpectedWakeup(). + self.unexpected_wakeup_count = self.unexpected_wakeup_count + 1 -- assuming Kobo:resume() will be called in 15 seconds logger.dbg("Kobo suspend: scheduling unexpected wakeup guard") - UIManager:scheduleIn(15, check_unexpected_wakeup) + UIManager:scheduleIn(15, self.checkUnexpectedWakeup, self) end function Kobo:resume() logger.info("Kobo resume: clean up after wakeup") -- reset unexpected_wakeup_count ASAP - unexpected_wakeup_count = 0 - require("ui/uimanager"):unschedule(check_unexpected_wakeup) + self.unexpected_wakeup_count = 0 + require("ui/uimanager"):unschedule(self.checkUnexpectedWakeup) -- Now that we're up, unflag subsystems for suspend... -- NOTE: Sets gSleep_Mode_Suspend to 0. Used as a flag throughout the -- kernel to suspend/resume various subsystems -- cf. kernel/power/main.c @ L#207 - local f = io.open("/sys/power/state-extended", "we") - if not f then - logger.err("cannot open /sys/power/state-extended for writing!") - return false - end - local re, err_msg, err_code = f:write("0\n") - f:close() - logger.info("Kobo resume: unflagged kernel subsystems for resume, ret:", re) - if not re then - logger.err('write error: ', err_msg, err_code) - end + + local ret = writeToSys("0", "/sys/power/state-extended") + logger.info("Kobo resume: unflagged kernel subsystems for resume, ret:", ret) -- HACK: wait a bit (0.1 sec) for the kernel to catch up util.usleep(100000) -- cf. #1862, I can reliably break IR touch input on resume... -- cf. also #1943 for the rationale behind applying this workaorund in every case... - f = io.open("/sys/devices/virtual/input/input1/neocmd", "we") - if f ~= nil then - f:write("a\n") - f:close() - end + writeToSys("a", "/sys/devices/virtual/input/input1/neocmd") -- A full suspend may have toggled the LED off. self:setupChargingLED() diff --git a/frontend/device/sdl/device.lua b/frontend/device/sdl/device.lua index 981b59bae..94dce441a 100644 --- a/frontend/device/sdl/device.lua +++ b/frontend/device/sdl/device.lua @@ -66,6 +66,7 @@ local Device = Generic:new{ hasEinkScreen = no, hasSystemFonts = yes, canSuspend = no, + canStandby = yes, startTextInput = SDL.startTextInput, stopTextInput = SDL.stopTextInput, canOpenLink = getLinkOpener, diff --git a/frontend/ui/uimanager.lua b/frontend/ui/uimanager.lua index dc0d9069f..3a0cb8860 100644 --- a/frontend/ui/uimanager.lua +++ b/frontend/ui/uimanager.lua @@ -1179,6 +1179,15 @@ function UIManager:broadcastEvent(event) end end +function UIManager:getNextTaskTimes(count) + count = count or 1 + local times = {} + for i = 1, math.min(count, #self._task_queue) do + times[i] = UIManager._task_queue[i].time - TimeVal:now() + end + return times +end + function UIManager:_checkTasks() self._now = TimeVal:now() local wait_until = nil diff --git a/plugins/autosuspend.koplugin/_meta.lua b/plugins/autosuspend.koplugin/_meta.lua index 573b9fde8..3d7bdadb2 100644 --- a/plugins/autosuspend.koplugin/_meta.lua +++ b/plugins/autosuspend.koplugin/_meta.lua @@ -1,6 +1,6 @@ local _ = require("gettext") return { name = "autosuspend", - fullname = _("Auto suspend"), - description = _([[Suspends the device after a period of inactivity.]]), + fullname = _("Auto power save"), + description = _([["Puts the device into standby, suspend or power off after specified periods of inactivity."]]), } diff --git a/plugins/autosuspend.koplugin/main.lua b/plugins/autosuspend.koplugin/main.lua index 283060ffb..5a00af4dc 100644 --- a/plugins/autosuspend.koplugin/main.lua +++ b/plugins/autosuspend.koplugin/main.lua @@ -10,6 +10,8 @@ if not Device:isCervantes() and return { disabled = true, } end +local Event = require("ui/event") +local NetworkMgr = require("ui/network/manager") local PluginShare = require("pluginshare") local TimeVal = require("ui/timeval") local UIManager = require("ui/uimanager") @@ -22,17 +24,23 @@ local T = require("ffi/util").template local default_autoshutdown_timeout_seconds = 3*24*60*60 -- three days local default_auto_suspend_timeout_seconds = 15*60 -- 15 minutes +local default_auto_standby_timeout_seconds = 4 -- 4 seconds; should be safe on Kobo/Sage local AutoSuspend = WidgetContainer:new{ name = "autosuspend", is_doc_only = false, autoshutdown_timeout_seconds = default_autoshutdown_timeout_seconds, auto_suspend_timeout_seconds = default_auto_suspend_timeout_seconds, + auto_standby_timeout_seconds = default_auto_standby_timeout_seconds, last_action_tv = TimeVal.zero, - standby_prevented = false, + is_standby_scheduled = nil, task = nil, } +function AutoSuspend:_enabledStandby() + return Device:canStandby() and self.auto_standby_timeout_seconds > 0 +end + function AutoSuspend:_enabled() return self.auto_suspend_timeout_seconds > 0 end @@ -49,15 +57,13 @@ function AutoSuspend:_schedule(shutdown_only) local delay_suspend, delay_shutdown - if PluginShare.pause_auto_suspend or Device.standby_prevented or Device.powerd:isCharging() then + if PluginShare.pause_auto_suspend or Device.powerd:isCharging() then delay_suspend = self.auto_suspend_timeout_seconds delay_shutdown = self.autoshutdown_timeout_seconds else - local now_tv = UIManager:getTime() - delay_suspend = self.last_action_tv + TimeVal:new{ sec = self.auto_suspend_timeout_seconds, usec = 0 } - now_tv - delay_suspend = delay_suspend:tonumber() - delay_shutdown = self.last_action_tv + TimeVal:new{ sec = self.autoshutdown_timeout_seconds, usec = 0 } - now_tv - delay_shutdown = delay_shutdown:tonumber() + local now_tv = UIManager:getTime() + Device.total_standby_tv + delay_suspend = (self.last_action_tv - now_tv):tonumber() + self.auto_suspend_timeout_seconds + delay_shutdown = (self.last_action_tv - now_tv):tonumber() + self.autoshutdown_timeout_seconds end -- Try to shutdown first, as we may have been woken up from suspend just for the sole purpose of doing that. @@ -88,9 +94,8 @@ end function AutoSuspend:_start() if self:_enabled() or self:_enabledShutdown() then - local now_tv = UIManager:getTime() - logger.dbg("AutoSuspend: start at", now_tv:tonumber()) - self.last_action_tv = now_tv + self.last_action_tv = UIManager:getTime() + Device.total_standby_tv + logger.dbg("AutoSuspend: start at", self.last_action_tv:tonumber()) self:_schedule() end end @@ -98,9 +103,8 @@ end -- Variant that only re-engages the shutdown timer for onUnexpectedWakeupLimit function AutoSuspend:_restart() if self:_enabledShutdown() then - local now_tv = UIManager:getTime() - logger.dbg("AutoSuspend: restart at", now_tv:tonumber()) - self.last_action_tv = now_tv + self.last_action_tv = UIManager:getTime() + Device.total_standby_tv + logger.dbg("AutoSuspend: restart at", self.last_action_tv:tonumber()) self:_schedule(true) end end @@ -113,6 +117,9 @@ function AutoSuspend:init() self.auto_suspend_timeout_seconds = G_reader_settings:readSetting("auto_suspend_timeout_seconds", default_auto_suspend_timeout_seconds) + -- Disabled, until the user opts in. + self.auto_standby_timeout_seconds = G_reader_settings:readSetting("auto_standby_timeout_seconds", -1) + UIManager.event_hook:registerWidget("InputEvent", self) -- We need an instance-specific function reference to schedule, because in some rare cases, -- we may instantiate a new plugin instance *before* tearing down the old one. @@ -120,6 +127,8 @@ function AutoSuspend:init() self:_schedule(shutdown_only) end self:_start() + self:_reschedule_standby() + -- self.ui is nil in the testsuite if not self.ui or not self.ui.menu then return end self.ui.menu:registerToMainMenu(self) @@ -131,11 +140,55 @@ function AutoSuspend:onCloseWidget() if Device:isPocketBook() and not Device:canSuspend() then return end self:_unschedule() self.task = nil + + self:_unschedule_standby() + -- allowStandby is necessary, as we do a preventStandby on plugin start + UIManager:allowStandby() end function AutoSuspend:onInputEvent() logger.dbg("AutoSuspend: onInputEvent") - self.last_action_tv = UIManager:getTime() + self.last_action_tv = UIManager:getTime() + Device.total_standby_tv + + self:_reschedule_standby() +end + +function AutoSuspend:_unschedule_standby() + UIManager:unschedule(AutoSuspend.allowStandby) +end + +function AutoSuspend:_reschedule_standby(standby_timeout) + if not Device:canStandby() then return end + standby_timeout = standby_timeout or self.auto_standby_timeout_seconds + self:_unschedule_standby() + if standby_timeout < 1 then + return + end + + self:preventStandby() + logger.dbg("AutoSuspend: schedule autoStandby in", standby_timeout) -- xxx may be deleted later + UIManager:scheduleIn(standby_timeout, self.allowStandby, self) +end + +function AutoSuspend:preventStandby() + if self.is_standby_scheduled ~= false then + self.is_standby_scheduled = false + UIManager:preventStandby() + end +end + +function AutoSuspend:allowStandby() + if not self.is_standby_scheduled then + self.is_standby_scheduled = true + UIManager:allowStandby() + + -- This is necessary for wakeup from standby, as the deadline for receiving input events + -- is calculated from the time to the next scheduled function. + -- Make sure this function comes soon, as the time for going to standby after a scheduled wakeup + -- is prolonged by the given time. Any time between 0.500 and 0.001 seconds would go. + -- Let's call it deadline_guard. + UIManager:scheduleIn(0.100, function() end) + end end function AutoSuspend:onSuspend() @@ -143,6 +196,7 @@ function AutoSuspend:onSuspend() -- We do not want auto suspend procedure to waste battery during suspend. So let's unschedule it -- when suspending and restart it after resume. self:_unschedule() + self:_unschedule_standby() if self:_enabledShutdown() and Device.wakeup_mgr then Device.wakeup_mgr:addTask(self.autoshutdown_timeout_seconds, UIManager.poweroff_action) end @@ -156,6 +210,11 @@ function AutoSuspend:onResume() -- Unschedule in case we tripped onUnexpectedWakeupLimit first... self:_unschedule() self:_start() + self:_reschedule_standby() +end + +function AutoSuspend:onLeaveStandby() + self:_reschedule_standby() end function AutoSuspend:onUnexpectedWakeupLimit() @@ -164,16 +223,12 @@ function AutoSuspend:onUnexpectedWakeupLimit() self:_restart() end -function AutoSuspend:onAllowStandby() - self.standby_prevented = false -end - -function AutoSuspend:onPreventStandby() - self.standby_prevented = true -end - +-- time_scale: +-- 2 ... display day:hour +-- 1 ... display hour:min +-- else ... display min:sec function AutoSuspend:setSuspendShutdownTimes(touchmenu_instance, title, info, setting, - default_value, range, is_day_hour) + default_value, range, time_scale) -- Attention if is_day_hour then time.hour stands for days and time.min for hours local InfoMessage = require("ui/widget/infomessage") @@ -181,10 +236,23 @@ function AutoSuspend:setSuspendShutdownTimes(touchmenu_instance, title, info, se local setting_val = self[setting] > 0 and self[setting] or default_value - local left_val = is_day_hour and math.floor(setting_val / (24*3600)) - or math.floor(setting_val / 3600) - local right_val = is_day_hour and math.floor(setting_val / 3600) % 24 - or math.floor((setting_val / 60) % 60) + local left_val + if time_scale == 2 then + left_val = math.floor(setting_val / (24*3600)) + elseif time_scale == 1 then + left_val = math.floor(setting_val / 3600) + else + left_val = math.floor(setting_val / 60) + end + + local right_val + if time_scale == 2 then + right_val = math.floor(setting_val / 3600) % 24 + elseif time_scale == 1 then + right_val = math.floor(setting_val / 60) % 60 + else + right_val = math.floor(setting_val) % 60 + end local time_spinner time_spinner = DateTimeWidget:new { is_date = false, @@ -192,36 +260,57 @@ function AutoSuspend:setSuspendShutdownTimes(touchmenu_instance, title, info, se min = right_val, hour_hold_step = 5, min_hold_step = 10, - hour_max = is_day_hour and math.floor(range[2] / (24*3600)) or math.floor(range[2] / 3600), - min_max = is_day_hour and 23 or 59, + hour_max = (time_scale == 2 and math.floor(range[2] / (24*3600))) + or (time_scale == 1 and math.floor(range[2] / 3600)) + or math.floor(range[2] / 60), + min_max = (time_scale == 2 and 23) or 59, ok_text = _("Set timeout"), title_text = title, info_text = info, callback = function(time) - self[setting] = is_day_hour and (time.hour * 24 * 3600 + time.min * 3600) - or (time.hour * 3600 + time.min * 60) + if time_scale == 2 then + self[setting] = (time.hour * 24 + time.min) * 3600 + elseif time_scale == 1 then + self[setting] = time.hour * 3600 + time.min * 60 + else + self[setting] = time.hour * 60 + time.min + end self[setting] = Math.clamp(self[setting], range[1], range[2]) G_reader_settings:saveSetting(setting, self[setting]) self:_unschedule() self:_start() if touchmenu_instance then touchmenu_instance:updateItems() end - local time_string = util.secondsToClockDuration("modern", self[setting], true, true, true) - time_string = time_string:gsub("00m","") + local time_string = util.secondsToClockDuration("modern", self[setting], + time_scale == 2 or time_scale == 1, true, true) + time_string = time_string:gsub("00m$", ""):gsub("^0+m", ""):gsub("^0", "") UIManager:show(InfoMessage:new{ text = T(_("%1: %2"), title, time_string), timeout = 3, }) end, - default_value = util.secondsToClockDuration("modern", default_value, true, true, true):gsub("00m$",""), + default_value = util.secondsToClockDuration("modern", default_value, + time_scale == 2 or time_scale == 1, true, true):gsub("00m$", ""):gsub("^00m:", ""), default_callback = function() - local hour = is_day_hour and math.floor(default_value / (24*3600)) - or math.floor(default_value / 3600) - local min = is_day_hour and math.floor(default_value / 3600) % 24 - or math.floor(default_value / 60) % 60 + local hour + if time_scale == 2 then + hour = math.floor(default_value / (24*3600)) + elseif time_scale == 1 then + hour = math.floor(default_value / 3600) + else + hour = math.floor(default_value / 60) + end + local min + if time_scale == 2 then + min = math.floor(default_value / 3600) % 24 + elseif time_scale == 1 then + min = math.floor(default_value / 60) % 60 + else + min = math.floor(default_value % 60) + end time_spinner:update(nil, hour, min) end, extra_text = _("Disable"), - extra_callback = function(_self) + extra_callback = function(this) self[setting] = -1 -- disable with a negative time/number G_reader_settings:saveSetting(setting, -1) self:_unschedule() @@ -230,7 +319,7 @@ function AutoSuspend:setSuspendShutdownTimes(touchmenu_instance, title, info, se text = T(_("%1: disabled"), title), timeout = 3, }) - _self:onClose() + this:onClose() end, keep_shown_on_apply = true, } @@ -246,7 +335,7 @@ function AutoSuspend:addToMainMenu(menu_items) text_func = function() if self.auto_suspend_timeout_seconds and self.auto_suspend_timeout_seconds > 0 then local time_string = util.secondsToClockDuration("modern", - self.auto_suspend_timeout_seconds, true, true, true):gsub("00m$","") + self.auto_suspend_timeout_seconds, true, true, true):gsub("00m$", ""):gsub("^00m:", "") return T(_("Autosuspend timeout: %1"), time_string) else return _("Autosuspend timeout") @@ -260,36 +349,122 @@ function AutoSuspend:addToMainMenu(menu_items) self:setSuspendShutdownTimes(touchmenu_instance, _("Timeout for autosuspend"), _("Enter time in hours and minutes."), "auto_suspend_timeout_seconds", default_auto_suspend_timeout_seconds, - {60, 24*3600}, false) - end, - } - if not (Device:canPowerOff() or Device:isEmulator()) then return end - menu_items.autoshutdown = { - sorting_hint = "device", - checked_func = function() - return self:_enabledShutdown() - end, - text_func = function() - if self.autoshutdown_timeout_seconds and self.autoshutdown_timeout_seconds > 0 then - local time_string = util.secondsToClockDuration("modern", - self.autoshutdown_timeout_seconds, true, true, true):gsub("00m$","") - return T(_("Autoshutdown timeout: %1"), time_string) - else - return _("Autoshutdown timeout") - end - end, - keep_menu_open = true, - callback = function(touchmenu_instance) - -- 5*60 sec (5') is the minimum and 28*24*3600 (28days) is the maximum shutdown time. - -- Minimum time has to be big enough, to avoid start-stop death scenarious. - -- Maximum more than four weeks seems a bit excessive if you want to enable authoshutdown, - -- even if the battery can last up to three months. - self:setSuspendShutdownTimes(touchmenu_instance, - _("Timeout for autoshutdown"), _("Enter time in days and hours."), - "autoshutdown_timeout_seconds", default_autoshutdown_timeout_seconds, - {5*60, 28*24*3600}, true) + {60, 24*3600}, 1) end, } + if Device:canPowerOff() or Device:isEmulator() then + menu_items.autoshutdown = { + sorting_hint = "device", + checked_func = function() + return self:_enabledShutdown() + end, + text_func = function() + if self.autoshutdown_timeout_seconds and self.autoshutdown_timeout_seconds > 0 then + local time_string = util.secondsToClockDuration("modern", self.autoshutdown_timeout_seconds, + true, true, true):gsub("00m$", ""):gsub("^00m:", "") + return T(_("Autoshutdown timeout: %1"), time_string) + else + return _("Autoshutdown timeout") + end + end, + keep_menu_open = true, + callback = function(touchmenu_instance) + -- 5*60 sec (5') is the minimum and 28*24*3600 (28days) is the maximum shutdown time. + -- Minimum time has to be big enough, to avoid start-stop death scenarious. + -- Maximum more than four weeks seems a bit excessive if you want to enable authoshutdown, + -- even if the battery can last up to three months. + self:setSuspendShutdownTimes(touchmenu_instance, + _("Timeout for autoshutdown"), _("Enter time in days and hours."), + "autoshutdown_timeout_seconds", default_autoshutdown_timeout_seconds, + {5*60, 28*24*3600}, 2) + end, + } + end + if Device:canStandby() then + local standby_help = _([[Standby puts the device into a power-saving state in which the screen is on and user input can be performed. + +Standby can not be entered if Wi-Fi is on. + +Upon user input, the device needs a certain amount of time to wake up. With some devices this period of time is not noticeable, with other devices it can be annoying.]]) + + menu_items.autostandby = { + sorting_hint = "device", + checked_func = function() + return self:_enabledStandby() + end, + text_func = function() + if self.auto_standby_timeout_seconds and self.auto_standby_timeout_seconds > 0 then + local time_string = util.secondsToClockDuration("modern", self.auto_standby_timeout_seconds, + false, true, true):gsub("00m$", ""):gsub("^0+m", ""):gsub("^0", "") + return T(_("Autostandby timeout: %1"), time_string) + else + return _("Autostandby timeout") + end + end, + help_text = standby_help, + keep_menu_open = true, + callback = function(touchmenu_instance) + -- 5 sec is the minimum and 60*60 sec (15min) is the maximum standby time. + -- We need a minimum time, so that scheduled function have a chance to execute. + -- A standby time of 15 min seem excessive. + -- But or battery testing it might give some sense. + self:setSuspendShutdownTimes(touchmenu_instance, + _("Timeout for autostandby"), _("Enter time in minutes and seconds."), + "auto_standby_timeout_seconds", default_auto_standby_timeout_seconds, + {3, 15*60}, 0) + end, + } + end +end + +-- KOReader is merely waiting for user input right now. +-- UI signals us that standby is allowed at this very moment because nothing else goes on in the background. +function AutoSuspend:onAllowStandby() + logger.dbg("AutoSuspend: onAllowStandby") + -- In case the OS frontend itself doesn't manage power state, we can do it on our own here. + -- One should also configure wake-up pins and perhaps wake alarm, + -- if we want to enter deeper sleep states later on from within standby. + + -- Don't enter standby if wifi is on, as this my break reconnecting (at least on Kobo-Sage) + if NetworkMgr:isWifiOn() then + logger.dbg("AutoSuspend: WiFi is on, no standby") + return + end + + -- Don't enter standby if device is charging and it is a non sunxi kobo + if Device.powerd:isCharging() and not Device:canPowerSaveWhileCharging() then + logger.dbg("AutoSuspend: charging, no standby") + return + end + + if Device:canStandby() then + local wake_in = math.huge + -- The next scheduled function should be the deadline_guard + -- Wake before the second next scheduled function executes (e.g. footer update, suspend ...) + local scheduler_times = UIManager:getNextTaskTimes(2) + if #scheduler_times == 2 then + -- Wake up slightly after the formerly scheduled event, to avoid resheduling the same function + -- after a fraction of a second again (e.g. don't draw footer twice) + wake_in = math.floor(scheduler_times[2]:tonumber()) + 1 + end + + if wake_in > 3 then -- don't go into standby, if scheduled wake is in less than 3 secs + UIManager:broadcastEvent(Event:new("EnterStandby")) + logger.dbg("AutoSuspend: going to standby and wake in " .. wake_in .. "s zZzzZzZzzzzZZZzZZZz") + + -- This is for the Kobo Sage/Elipsa for now, as these are the only with useStandby. + -- Other devices may be added + Device:standby(wake_in) + + logger.dbg("AutoSuspend: leaving standby after " .. Device.last_standby_tv:tonumber() .. " s") + + UIManager:broadcastEvent(Event:new("LeaveStandby")) + self:_unschedule() -- unschedule suspend and shutdown as the realtime clock has ticked + self:_schedule() -- reschedule suspend and shutdown with the new time + end + -- Don't do a `self:_reschedule_standby()` here, as this will interfere with suspend. + -- Better to to it in onLeaveStandby. + end end return AutoSuspend diff --git a/plugins/autoturn.koplugin/main.lua b/plugins/autoturn.koplugin/main.lua index d770e4ada..ba3bfeced 100644 --- a/plugins/autoturn.koplugin/main.lua +++ b/plugins/autoturn.koplugin/main.lua @@ -1,3 +1,4 @@ +local Device = require("device") local Event = require("ui/event") local PluginShare = require("pluginshare") local TimeVal = require("ui/timeval") @@ -34,20 +35,24 @@ function AutoTurn:_schedule() if UIManager:getTopWidget() == "ReaderUI" then logger.dbg("AutoTurn: go to next page") self.ui:handleEvent(Event:new("GotoViewRel", self.autoturn_distance)) + self.last_action_tv = UIManager:getTime() end logger.dbg("AutoTurn: schedule in", self.autoturn_sec) UIManager:scheduleIn(self.autoturn_sec, self.task) + self.scheduled = true else logger.dbg("AutoTurn: schedule in", delay) UIManager:scheduleIn(delay, self.task) + self.scheduled = true end end function AutoTurn:_unschedule() PluginShare.pause_auto_suspend = false - if self.task then + if self.scheduled then logger.dbg("AutoTurn: unschedule") UIManager:unschedule(self.task) + self.scheduled = false end end @@ -79,8 +84,8 @@ end function AutoTurn:init() UIManager.event_hook:registerWidget("InputEvent", self) - self.autoturn_sec = G_reader_settings:readSetting("autoturn_timeout_seconds") or 0 - self.autoturn_distance = G_reader_settings:readSetting("autoturn_distance") or 1 + self.autoturn_sec = G_reader_settings:readSetting("autoturn_timeout_seconds", 0) + self.autoturn_distance = G_reader_settings:readSetting("autoturn_distance", 1) self.enabled = G_reader_settings:isTrue("autoturn_enabled") self.ui.menu:registerToMainMenu(self) self.task = function() @@ -105,6 +110,10 @@ function AutoTurn:onInputEvent() self.last_action_tv = UIManager:getTime() end +function AutoTurn:onEnterStandby() + self:_unschedule() +end + -- We do not want autoturn to turn pages during the suspend process. -- Unschedule it and restart after resume. function AutoTurn:onSuspend() @@ -112,7 +121,17 @@ function AutoTurn:onSuspend() self:_unschedule() end -function AutoTurn:onResume() +function AutoTurn:_onLeaveStandby() + self.last_action_tv = self.last_action_tv - Device.last_standby_tv + + -- We messed with last_action_tv, so a complete reschedule has to be done. + if self:_enabled() then + self:_unschedule() + self:_schedule() + end +end + +function AutoTurn:_onResume() logger.dbg("AutoTurn: onResume") self:_start() end @@ -139,7 +158,10 @@ function AutoTurn:addToMainMenu(menu_items) G_reader_settings:makeFalse("autoturn_enabled") self:_unschedule() menu:updateItems() + self.onResume = nil + self.onLeaveStandby = nil end, + ok_always_enabled = true, callback = function(autoturn_spin) self.autoturn_sec = autoturn_spin.value G_reader_settings:saveSetting("autoturn_timeout_seconds", autoturn_spin.value) @@ -148,6 +170,8 @@ function AutoTurn:addToMainMenu(menu_items) self:_unschedule() self:_start() menu:updateItems() + self.onResume = self._onResume + self.onLeaveStandby = self._onLeaveStandby end, } UIManager:show(autoturn_spin) diff --git a/plugins/autowarmth.koplugin/main.lua b/plugins/autowarmth.koplugin/main.lua index e896d7e5e..2eab82af1 100644 --- a/plugins/autowarmth.koplugin/main.lua +++ b/plugins/autowarmth.koplugin/main.lua @@ -5,7 +5,6 @@ Plugin for setting screen warmth based on the sun position and/or a time schedul --]]-- local Device = require("device") - local ConfirmBox = require("ui/widget/confirmbox") local DateTimeWidget = require("ui/widget/datetimewidget") local DoubleSpinWidget = require("/ui/widget/doublespinwidget") @@ -21,6 +20,7 @@ local SunTime = require("suntime") local TextWidget = require("ui/widget/textwidget") local UIManager = require("ui/uimanager") local WidgetContainer = require("ui/widget/container/widgetcontainer") +local logger = require("logger") local _ = require("gettext") local T = FFIUtil.template local Screen = require("device").screen @@ -119,6 +119,7 @@ end function AutoWarmth:onResume() if self.activate == 0 then return end + logger.dbg("AutoWarmth: onResume/onLeaveStandby") local resume_date = os.date("*t") -- check if resume and suspend are done on the same day @@ -131,6 +132,8 @@ function AutoWarmth:onResume() end end +AutoWarmth.onLeaveStandby = AutoWarmth.onResume + -- wrapper for unscheduling, so that only our setWarmth gets unscheduled function AutoWarmth.setWarmth(val) if val then @@ -147,6 +150,7 @@ function AutoWarmth.setWarmth(val) end function AutoWarmth:scheduleMidnightUpdate() + logger.dbg("AutoWarmth: scheduleMidnightUpdate") -- first unschedule all old functions UIManager:unschedule(self.scheduleMidnightUpdate) -- when called from menu or resume @@ -249,7 +253,11 @@ function AutoWarmth:scheduleMidnightUpdate() self:scheduleWarmthChanges(now) end +--- @todo: As we have standby now, don't do the scheduling of the whole schedule, +-- but only the next warmth value plus an additional scheduleWarmthChanges +-- This would safe a bit of energy, but not really much. function AutoWarmth:scheduleWarmthChanges(time) + logger.dbg("AutoWarmth: scheduleWarmthChanges") for i = 1, #self.sched_funcs do -- loop not essential, as unschedule unschedules all functions at once if not UIManager:unschedule(self.sched_funcs[i][1]) then break diff --git a/plugins/systemstat.koplugin/main.lua b/plugins/systemstat.koplugin/main.lua index 306bbfeb2..7808f46f2 100644 --- a/plugins/systemstat.koplugin/main.lua +++ b/plugins/systemstat.koplugin/main.lua @@ -44,8 +44,16 @@ function SystemStat:appendCounters() if self.resume_sec then self:put({_(" Last resume time"), os.date("%c", self.resume_sec)}) end - self:put({_(" Up hours"), - string.format("%.2f", os.difftime(os.time(), self.start_sec) / 60 / 60)}) + self:put({_(" Up time"), + util.secondsToClockDuration("", os.difftime(os.time(), self.start_sec), false, true, true)}) + if Device:canSuspend() then + self:put({_(" Time in suspend"), + util.secondsToClockDuration("", Device.total_suspend_tv:tonumber(), false, true, true)}) + end + if Device:canStandby() then + self:put({_(" Time in standby"), + util.secondsToClockDuration("", Device.total_standby_tv:tonumber(), false, true, true)}) + end self:put({_("Counters"), ""}) self:put({_(" wake-ups"), self.wakeup_count}) -- @translators The number of "sleeps", that is the number of times the device has entered standby. This could also be translated as a rendition of a phrase like "entered sleep". diff --git a/spec/unit/mock_time.lua b/spec/unit/mock_time.lua index 666b63f34..2c335f7ea 100644 --- a/spec/unit/mock_time.lua +++ b/spec/unit/mock_time.lua @@ -15,10 +15,12 @@ local MockTime = { original_tv_monotonic = nil, original_tv_monotonic_coarse = nil, original_tv_boottime = nil, + original_tv_boottime_or_realtime_coarse = nil, original_tv_now = nil, monotonic = 0, realtime = 0, boottime = 0, + boottime_or_realtime_coarse = 0, } function MockTime:install() @@ -47,6 +49,10 @@ function MockTime:install() self.original_tv_boottime = TimeVal.boottime assert(self.original_tv_boottime ~= nil) end + if self.original_tv_boottime_or_realtime_coarse == nil then + self.original_tv_boottime_or_realtime_coarse = TimeVal.boottime_or_realtime_coarse + assert(self.original_tv_boottime_or_realtime_coarse ~= nil) + end if self.original_tv_now == nil then self.original_tv_now = TimeVal.now assert(self.original_tv_now ~= nil) @@ -86,6 +92,10 @@ function MockTime:install() logger.dbg("MockTime:TimeVal.boottime: ", self.boottime) return TimeVal:new{ sec = self.boottime } end + TimeVal.boottime_or_realtime_coarse = function() + logger.dbg("MockTime:TimeVal.boottime: ", self.boottime_or_realtime_coarse) + return TimeVal:new{ sec = self.boottime_or_realtime_coarse } + end TimeVal.now = function() logger.dbg("MockTime:TimeVal.now: ", self.monotonic) return TimeVal:new{ sec = self.monotonic } @@ -113,6 +123,9 @@ function MockTime:uninstall() if self.original_tv_boottime ~= nil then TimeVal.boottime = self.original_tv_boottime end + if self.original_tv_boottime_or_realtime_coarse ~= nil then + TimeVal.boottime_or_realtime_coarse = self.original_tv_boottime_or_realtime_coarse + end if self.original_tv_now ~= nil then TimeVal.now = self.original_tv_now end @@ -178,6 +191,26 @@ function MockTime:increase_boottime(value) return true end +function MockTime:set_boottime_or_realtime_coarse(value) + assert(self ~= nil) + if type(value) ~= "number" then + return false + end + self.boottime_or_realtime_coarse = math.floor(value) + logger.dbg("MockTime:set_boottime ", self.boottime_or_realtime_coarse) + return true +end + +function MockTime:increase_boottime_or_realtime_coarse(value) + assert(self ~= nil) + if type(value) ~= "number" then + return false + end + self.boottime_or_realtime_coarse = math.floor(self.boottime_or_realtime_coarse + value) + logger.dbg("MockTime:increase_boottime ", self.boottime_or_realtime_coarse) + return true +end + function MockTime:set(value) assert(self ~= nil) if type(value) ~= "number" then @@ -189,6 +222,8 @@ function MockTime:set(value) logger.dbg("MockTime:set (monotonic) ", self.monotonic) self.boottime = math.floor(value) logger.dbg("MockTime:set (boottime) ", self.boottime) + self.boottime_or_realtime_coarse = math.floor(value) + logger.dbg("MockTime:set (boottime) ", self.boottime_or_realtime_coarse) return true end @@ -203,6 +238,8 @@ function MockTime:increase(value) logger.dbg("MockTime:increase (monotonic) ", self.monotonic) self.boottime = math.floor(self.boottime + value) logger.dbg("MockTime:increase (boottime) ", self.boottime) + self.boottime_or_realtime_coarse = math.floor(self.boottime_or_realtime_coarse + value) + logger.dbg("MockTime:increase (boottime) ", self.boottime_or_realtime_coarse) return true end