From 86c35ad066c72a6e52a5a935ab2fc4a133d829d6 Mon Sep 17 00:00:00 2001 From: NiLuJe Date: Sun, 1 May 2022 23:41:08 +0200 Subject: [PATCH] A host of low power states related tweaks (#9036) * Disable all non power management related input during suspend. (This prevents wonky touch events from being tripped when closing a sleep cover on an already-in-suspend device, among other things). * Kobo: Use our WakeupMgr instance, not the class. * WakupMgr: split `removeTask` in two: * `removeTask`, which *only* takes a queue index as input, and only removes a single task. Greatly simplifies the function (i.e., it's just a `table.remove`). * `removeTasks`, which takes an epoch or a cb ref, and removes *every* task that matches. * Both of these will also *always* re-schedule the next task (if any) on exit, since we can have multiple WakeupMgr tasks queued, but we can only have a single RTC wake alarm set ;). * `wakeupAction` now takes a `proximity` argument, which it passes on to its `validateWakeupAlarmByProximity` call, allowing call sites to avoir having to duplicate that call themselves when they want to use a custom proximity window. * `wakeupAction` now re-schedules the next task (if any) on exit. * Simplify `Kobo:checkUnexpectedWakeup`, by removing the duplicate `WakerupMgr:validateWakeupAlarmByProximity` call, now that we can pass a proximity window to `WakeuoMgr:wakeupAction`. * The various network activity timeouts are now halved when autostandby is enabled. * Autostandby: get rid of the dummy deadline_guard task, as it's no longer necessary since #9009. * UIManager: The previous change allows us to simplify `getNextTaskTimes` into a simpler `getNextTaskTime` variant, getting rid of a table & a loop. * ReaderFooter & ReaderHeader: Make sure we only perform a single refresh when exiting standby. * Kobo: Rewrite sysfs writes to use ANSI C via FFI instead of stdio via Lua, as it obscured some common error cases (e.g., EBUSY on /sys/power/state). * Kobo: Simplify `suspend`, now that we have sane error handling in sysfs writes. * Kobo.powerd: Change `isCharging` & `isAuxCharging` behavior to match the behavior of the NTX ioctl (i.e., Charging == Plugged-in). This has the added benefit of making the AutoSuspend checks behave sensibly in the "fully-charged but still plugged in" scenario (because being plugged in is enough to break PM on `!canPowerSaveWhileCharging` devices). * AutoSuspend: Disable our `AllowStandby` handler when auto standby is disabled, so as to not interfere with other modules using `UIManager:allowStandby` (fix #9038). * PowerD: Allow platforms to implement `isCharged`, indicating that the battery is full while still plugged in to a power source (battery icon becomes a power plug icon). * Kobo.powerd: Implement `isCharged`, and kill charging LEDs once battery is full. * Kindle.powerd: Implement `isCharged` on post-Wario devices. (`isCharging` is still true in that state, as it ought to). --- .../reader/modules/readercoptlistener.lua | 8 +- frontend/apps/reader/modules/readerfooter.lua | 8 +- frontend/device/generic/powerd.lua | 18 +- frontend/device/input.lua | 120 ++++++++++- frontend/device/kindle/device.lua | 14 +- frontend/device/kindle/powerd.lua | 9 + frontend/device/kobo/device.lua | 187 ++++++++++-------- frontend/device/kobo/powerd.lua | 27 ++- frontend/device/wakeupmgr.lua | 84 +++++--- frontend/ui/network/networklistener.lua | 6 + frontend/ui/uimanager.lua | 26 ++- frontend/ui/widget/touchmenu.lua | 4 +- plugins/autosuspend.koplugin/main.lua | 67 ++++--- spec/unit/wakeupmgr_spec.lua | 4 +- 14 files changed, 408 insertions(+), 174 deletions(-) diff --git a/frontend/apps/reader/modules/readercoptlistener.lua b/frontend/apps/reader/modules/readercoptlistener.lua index 451681fcf..2d2103572 100644 --- a/frontend/apps/reader/modules/readercoptlistener.lua +++ b/frontend/apps/reader/modules/readercoptlistener.lua @@ -130,7 +130,7 @@ function ReaderCoptListener:rescheduleHeaderRefreshIfNeeded() end end --- Schedule or stop scheluding on these events, as they may change what is shown: +-- Schedule or stop scheduling on these events, as they may change what is shown: ReaderCoptListener.onSetStatusLine = ReaderCoptListener.rescheduleHeaderRefreshIfNeeded -- configurable.status_line is set before this event is triggered ReaderCoptListener.onSetViewMode = ReaderCoptListener.rescheduleHeaderRefreshIfNeeded @@ -145,11 +145,12 @@ function ReaderCoptListener:onResume() end self:headerRefresh() + self:rescheduleHeaderRefreshIfNeeded() end function ReaderCoptListener:onLeaveStandby() - self:onResume() - self:onOutOfScreenSaver() + self:headerRefresh() + self:rescheduleHeaderRefreshIfNeeded() end function ReaderCoptListener:onOutOfScreenSaver() @@ -159,6 +160,7 @@ function ReaderCoptListener:onOutOfScreenSaver() self._delayed_screensaver = nil self:headerRefresh() + self:rescheduleHeaderRefreshIfNeeded() end -- Unschedule on these events diff --git a/frontend/apps/reader/modules/readerfooter.lua b/frontend/apps/reader/modules/readerfooter.lua index 25a365ffb..bdce03524 100644 --- a/frontend/apps/reader/modules/readerfooter.lua +++ b/frontend/apps/reader/modules/readerfooter.lua @@ -191,13 +191,13 @@ local footerTextGeneratorMap = { batt_lvl = main_batt_lvl + aux_batt_lvl -- But average 'em to compute the icon... if symbol_type == "icons" or symbol_type == "compact_items" then - prefix = powerd:getBatterySymbol(is_charging, batt_lvl / 2) + prefix = powerd:getBatterySymbol(powerd:isAuxCharged(), is_charging, batt_lvl / 2) end else is_charging = powerd:isCharging() batt_lvl = main_batt_lvl if symbol_type == "icons" or symbol_type == "compact_items" then - prefix = powerd:getBatterySymbol(is_charging, main_batt_lvl) + prefix = powerd:getBatterySymbol(powerd:isCharged(), is_charging, main_batt_lvl) end end end @@ -2448,8 +2448,8 @@ function ReaderFooter:onOutOfScreenSaver() end function ReaderFooter:onLeaveStandby() - self:onResume() - self:onOutOfScreenSaver() + self:maybeUpdateFooter() + self:rescheduleFooterAutoRefreshIfNeeded() end function ReaderFooter:onSuspend() diff --git a/frontend/device/generic/powerd.lua b/frontend/device/generic/powerd.lua index e765b9533..4f0635347 100644 --- a/frontend/device/generic/powerd.lua +++ b/frontend/device/generic/powerd.lua @@ -54,8 +54,12 @@ function BasePowerD:getAuxCapacityHW() return 0 end function BasePowerD:isAuxBatteryConnectedHW() return false end function BasePowerD:getDismissBatteryStatus() return self.battery_warning end function BasePowerD:setDismissBatteryStatus(status) self.battery_warning = status end +--- @note: Should ideally return true as long as the device is plugged in, even once the battery is full... function BasePowerD:isChargingHW() return false end +--- @note: ...at which point this should start returning true (i.e., plugged in & fully charged). +function BasePowerD:isChargedHW() return false end function BasePowerD:isAuxChargingHW() return false end +function BasePowerD:isAuxChargedHW() return false end function BasePowerD:frontlightIntensityHW() return 0 end function BasePowerD:isFrontlightOnHW() return self.fl_intensity > self.fl_min end function BasePowerD:turnOffFrontlightHW() self:setIntensityHW(self.fl_min) end @@ -231,6 +235,10 @@ function BasePowerD:isCharging() return self:isChargingHW() end +function BasePowerD:isCharged() + return self:isChargedHW() +end + function BasePowerD:getAuxCapacity() local now_btv @@ -261,6 +269,10 @@ function BasePowerD:isAuxCharging() return self:isAuxChargingHW() end +function BasePowerD:isAuxCharged() + return self:isAuxChargedHW() +end + function BasePowerD:isAuxBatteryConnected() return self:isAuxBatteryConnectedHW() end @@ -274,8 +286,10 @@ function BasePowerD:stateChanged() end -- Silly helper to avoid code duplication ;). -function BasePowerD:getBatterySymbol(is_charging, capacity) - if is_charging then +function BasePowerD:getBatterySymbol(is_charged, is_charging, capacity) + if is_charged then + return "" + elseif is_charging then return "" else if capacity >= 100 then diff --git a/frontend/device/input.lua b/frontend/device/input.lua index 6a3c34881..8b2f0479b 100644 --- a/frontend/device/input.lua +++ b/frontend/device/input.lua @@ -578,6 +578,57 @@ function Input:handleKeyBoardEv(ev) end end +-- Mangled variant of handleKeyBoardEv that will only handle power management related keys. +-- (Used when blocking input during suspend via sleep cover). +function Input:handlePowerManagementOnlyEv(ev) + local keycode = self.event_map[ev.code] + if not keycode then + -- Do not handle keypress for keys we don't know + return + end + + -- We'll need to parse the synthetic event map, because SleepCover* events are synthetic. + if self.event_map_adapter[keycode] then + keycode = self.event_map_adapter[keycode](ev) + end + + -- Power management synthetic events + if keycode == "SleepCoverClosed" or keycode == "SleepCoverOpened" + or keycode == "Suspend" or keycode == "Resume" then + return keycode + end + + -- Fake events + if keycode == "IntoSS" or keycode == "OutOfSS" + or keycode == "UsbPlugIn" or keycode == "UsbPlugOut" + or keycode == "Charging" or keycode == "NotCharging" then + return keycode + end + + if keycode == "Power" then + -- Kobo generates Power keycode only, we need to decide whether it's + -- power-on or power-off ourselves. + if ev.value == EVENT_VALUE_KEY_PRESS then + return "PowerPress" + elseif ev.value == EVENT_VALUE_KEY_RELEASE then + return "PowerRelease" + end + end + + -- Nothing to see, move along! + return +end + +-- Empty event handler used to send input to the void +function Input:voidEv(ev) + return +end + +-- Generic event handler for unhandled input events +function Input:handleGenericEv(ev) + return Event:new("GenericInput", ev) +end + function Input:handleMiscEv(ev) -- should be handled by a misc event protocol plugin end @@ -892,13 +943,13 @@ function Input:toggleMiscEvNTX(toggle) elseif toggle == false then -- Ignore Gyro events if self.isNTXAccelHooked then - self.handleMiscEv = function() end + self.handleMiscEv = self.voidEv self.isNTXAccelHooked = false end else -- Toggle it if self.isNTXAccelHooked then - self.handleMiscEv = function() end + self.handleMiscEv = self.voidEv else self.handleMiscEv = self.handleMiscEvNTX end @@ -1204,7 +1255,10 @@ function Input:waitEvent(now, deadline) end else -- Received some other kind of event that we do not know how to specifically handle yet - table.insert(handled, Event:new("GenericInput", event)) + local handled_ev = self:handleGenericEv(event) + if handled_ev then + table.insert(handled, handled_ev) + end end end return handled @@ -1220,4 +1274,64 @@ function Input:waitEvent(now, deadline) end end +-- Allow toggling the handling of most every kind of input, except for power management related events. +function Input:inhibitInput(toggle) + if toggle then + -- Only handle power management events + if not self._key_ev_handler then + logger.info("Inhibiting user input") + self._key_ev_handler = self.handleKeyBoardEv + self.handleKeyBoardEv = self.handlePowerManagementOnlyEv + end + -- And send everything else to the void + if not self._oasis_ev_handler then + self._oasis_ev_handler = self.handleOasisOrientationEv + self.handleOasisOrientationEv = self.voidEv + end + if not self._abs_ev_handler then + self._abs_ev_handler = self.handleTouchEv + self.handleTouchEv = self.voidEv + end + if not self._msc_ev_handler then + self._msc_ev_handler = self.handleMiscEv + self.handleMiscEv = self.voidEv + end + if not self._sdl_ev_handler then + self._sdl_ev_handler = self.handleSdlEv + self.handleSdlEv = self.voidEv + end + if not self._generic_ev_handler then + self._generic_ev_handler = self.handleGenericEv + self.handleGenericEv = self.voidEv + end + else + -- Restore event handlers, if any + if self._key_ev_handler then + logger.info("Restoring user input handling") + self.handleKeyBoardEv = self._key_ev_handler + self._key_ev_handler = nil + end + if self._oasis_ev_handler then + self.handleOasisOrientationEv = self._oasis_ev_handler + self._oasis_ev_handler = nil + end + if self._abs_ev_handler then + self.handleTouchEv = self._abs_ev_handler + self._abs_ev_handler = nil + end + if self._msc_ev_handler then + self.handleMiscEv = self._msc_ev_handler + self._msc_ev_handler = nil + end + if self._sdl_ev_handler then + self.handleSdlEv = self._sdl_ev_handler + self._sdl_ev_handler = nil + end + if self._generic_ev_handler then + self.handleGenericEv = self._generic_ev_handler + self._generic_ev_handler = nil + end + end +end + return Input diff --git a/frontend/device/kindle/device.lua b/frontend/device/kindle/device.lua index 2477ece08..defe540bd 100644 --- a/frontend/device/kindle/device.lua +++ b/frontend/device/kindle/device.lua @@ -757,6 +757,7 @@ function KindleOasis2:init() fl_intensity_file = "/sys/class/backlight/max77796-bl/brightness", batt_capacity_file = "/sys/class/power_supply/max77796-battery/capacity", is_charging_file = "/sys/class/power_supply/max77796-charger/charging", + batt_status_file = "/sys/class/power_supply/max77796-charger/status", } self.input = require("device/input"):new{ @@ -832,6 +833,7 @@ function KindleOasis3:init() warmth_intensity_file = "/sys/class/backlight/lm3697-bl0/brightness", batt_capacity_file = "/sys/class/power_supply/max77796-battery/capacity", is_charging_file = "/sys/class/power_supply/max77796-charger/charging", + batt_status_file = "/sys/class/power_supply/max77796-charger/status", } self.input = require("device/input"):new{ @@ -845,13 +847,7 @@ function KindleOasis3:init() } - --- @fixme When starting KOReader with the device upside down ("D"), touch input is registered wrong - -- (i.e., probably upside down). - -- If it's started upright ("U"), everything's okay, and turning it upside down after that works just fine. - -- See #2206 & #2209 for the original KOA implementation, which obviously doesn't quite cut it here... - -- See also - -- NOTE: It'd take some effort to actually start KOReader while in a LANDSCAPE orientation, - -- since they're only exposed inside the stock reader, and not the Home/KUAL Booklets. + --- @fixme The same quirks as on the Oasis 2 apply ;). local haslipc, lipc = pcall(require, "liblipclua") if haslipc and lipc then local lipc_handle = lipc.init("com.github.koreader.screen") @@ -906,6 +902,7 @@ function KindleBasic2:init() device = self, batt_capacity_file = "/sys/class/power_supply/bd7181x_bat/capacity", is_charging_file = "/sys/class/power_supply/bd7181x_bat/charging", + batt_status_file = "/sys/class/power_supply/bd7181x_bat/status", } Kindle.init(self) @@ -921,6 +918,7 @@ function KindlePaperWhite4:init() fl_intensity_file = "/sys/class/backlight/bl/brightness", batt_capacity_file = "/sys/class/power_supply/bd71827_bat/capacity", is_charging_file = "/sys/class/power_supply/bd71827_bat/charging", + batt_status_file = "/sys/class/power_supply/bd71827_bat/status", } Kindle.init(self) @@ -946,6 +944,7 @@ function KindleBasic3:init() fl_intensity_file = "/sys/class/backlight/bl/brightness", batt_capacity_file = "/sys/class/power_supply/bd71827_bat/capacity", is_charging_file = "/sys/class/power_supply/bd71827_bat/charging", + batt_status_file = "/sys/class/power_supply/bd71827_bat/status", } Kindle.init(self) @@ -963,6 +962,7 @@ function KindlePaperWhite5:init() warmth_intensity_file = "/sys/class/backlight/fp9966-bl0/brightness", batt_capacity_file = "/sys/class/power_supply/bd71827_bat/capacity", is_charging_file = "/sys/class/power_supply/bd71827_bat/charging", + batt_status_file = "/sys/class/power_supply/bd71827_bat/status", } -- Enable the so-called "fast" mode, so as to prevent the driver from silently promoting refreshes to REAGL. diff --git a/frontend/device/kindle/powerd.lua b/frontend/device/kindle/powerd.lua index 37c223465..6ed329f01 100644 --- a/frontend/device/kindle/powerd.lua +++ b/frontend/device/kindle/powerd.lua @@ -148,6 +148,15 @@ function KindlePowerD:isChargingHW() return is_charging == 1 end +function KindlePowerD:isChargedHW() + -- Older kernels don't necessarily have this... + if self.batt_status_file then + return self:read_str_file(self.batt_status_file) == "Full" + end + + return false +end + function KindlePowerD:_readFLIntensity() return self:read_int_file(self.fl_intensity_file) end diff --git a/frontend/device/kobo/device.lua b/frontend/device/kobo/device.lua index df05831b2..d9ccc4e64 100644 --- a/frontend/device/kobo/device.lua +++ b/frontend/device/kobo/device.lua @@ -43,17 +43,22 @@ local function checkStandby() end local function writeToSys(val, file) - local f = io.open(file, "we") - if not f then - logger.err("Cannot open:", file) + -- NOTE: We do things by hand via ffi, because io.write uses fwrite, + -- which isn't a great fit for procfs/sysfs (e.g., we lose failure cases like EBUSY, + -- as it only reports failures to write to the *stream*, not to the disk/file!). + local fd = C.open(file, bit.bor(C.O_WRONLY, C.O_CLOEXEC)) -- procfs/sysfs, we shouldn't need O_TRUNC + if fd == -1 then + logger.err("Cannot open file `" .. file .. "`:", ffi.string(C.strerror(ffi.errno()))) 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) + local bytes = #val + 1 -- + LF + local nw = C.write(fd, val .. "\n", bytes) + if nw == -1 then + logger.err("Cannot write `" .. val .. "` to file `" .. file .. "`:", ffi.string(C.strerror(ffi.errno()))) end - f:close() - return re + C.close(fd) + -- NOTE: Allows the caller to possibly handle short writes (not that these should ever happen here). + return nw == bytes end local Kobo = Generic:new{ @@ -424,9 +429,9 @@ local KoboIo = Kobo:new{ function Kobo:setupChargingLED() if G_reader_settings:nilOrTrue("enable_charging_led") then if self:hasAuxBattery() and self.powerd:isAuxBatteryConnected() then - self:toggleChargingLED(self.powerd:isAuxCharging()) + self:toggleChargingLED(self.powerd:isAuxCharging() and not self.powerd:isAuxCharged()) else - self:toggleChargingLED(self.powerd:isCharging()) + self:toggleChargingLED(self.powerd:isCharging() and not self.powerd:isCharged()) end end end @@ -618,7 +623,7 @@ function Kobo:setDateTime(year, month, day, hour, min, sec) command = string.format("date -s '%d:%d'",hour, min) end if os.execute(command) == 0 then - os.execute('hwclock -u -w') + os.execute("hwclock -u -w") return true else return false @@ -727,7 +732,7 @@ function Kobo:initEventAdjustHooks() end end -function Kobo:getCodeName() +local function getCodeName() -- Try to get it from the env first local codename = os.getenv("PRODUCT") -- If that fails, run the script ourselves @@ -776,23 +781,21 @@ end function Kobo:checkUnexpectedWakeup() local UIManager = require("ui/uimanager") - -- just in case other events like SleepCoverClosed also scheduled a suspend - UIManager:unschedule(Kobo.suspend) - - -- Do an initial validation to discriminate unscheduled wakeups happening *outside* of the alarm proximity window. - if WakeupMgr:isWakeupAlarmScheduled() and WakeupMgr:validateWakeupAlarmByProximity() then - logger.info("Kobo suspend: scheduled wakeup.") - local res = WakeupMgr:wakeupAction() - if not res then - logger.err("Kobo suspend: wakeup action failed.") - end - 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, self) + -- Just in case another event like SleepCoverClosed also scheduled a suspend + UIManager:unschedule(self.suspend) + + -- The proximity window is rather large, because we're scheduled to run 15 seconds after resuming, + -- so we're already guaranteed to be at least 15s away from the alarm ;). + if self.wakeup_mgr:isWakeupAlarmScheduled() and self.wakeup_mgr:wakeupAction(30) then + -- Assume we want to go back to sleep after running the scheduled action + -- (Kobo:resume will unschedule this on an user-triggered resume). + logger.info("Kobo suspend: scheduled wakeup; the device will go back to sleep in 30s.") + -- We need significant leeway for the poweroff action to send out close events to all requisite widgets, + -- since we don't actually want to suspend behind its back ;). + UIManager:scheduleIn(30, self.suspend, self) else - logger.dbg("Kobo suspend: checking unexpected wakeup:", - self.unexpected_wakeup_count) + -- We've hit an early resume, assume this is unexpected (as we only run if Kobo:resume hasn't already). + logger.dbg("Kobo suspend: checking unexpected wakeup number", 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 @@ -803,9 +806,8 @@ function Kobo:checkUnexpectedWakeup() return end - logger.err("Kobo suspend: putting device back to sleep. Unexpected wakeups:", - self.unexpected_wakeup_count) - Kobo:suspend() + logger.err("Kobo suspend: putting device back to sleep after", self.unexpected_wakeup_count, "unexpected wakeups.") + self:suspend() end end @@ -814,26 +816,43 @@ 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 + -- We don't really have anything to schedule, we just need an alarm out of WakeupMgr ;). + local function standby_alarm() + end if max_duration then - self.wakeup_mgr:addTask(max_duration, dummy) + self.wakeup_mgr:addTask(max_duration, standby_alarm) end local TimeVal = require("ui/timeval") + logger.info("Kobo standby: asking to enter standby . . .") local standby_time_tv = TimeVal:boottime_or_realtime_coarse() - logger.info("Kobo suspend: asking to enter standby . . .") 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: zZz zZz zZz zZz? Write syscall returned: ", ret) + if ret then + logger.info("Kobo standby: zZz zZz zZz zZz... And woke up!") + else + logger.warn("Kobo standby: the kernel refused to enter standby!") + end if max_duration then - self.wakeup_mgr:removeTask(nil, nil, dummy) + -- NOTE: We don't actually care about discriminating exactly *why* we woke up, + -- and our scheduled wakeup action is a NOP anyway, + -- so we can just drop the task instead of doing things the right way like suspend ;). + -- This saves us some pointless RTC shenanigans, so, everybody wins. + --[[ + -- There's no scheduling shenanigans like in suspend, so the proximity window can be much tighter... + if self.wakeup_mgr:isWakeupAlarmScheduled() and self.wakeup_mgr:wakeupAction(5) then + -- We tripped the standby alarm, UIManager will be able to run whatever was actually scheduled, + -- and AutoSuspend will handle going back to standby if necessary. + logger.dbg("Kobo standby: tripped rtc wake alarm") + end + --]] + self.wakeup_mgr:removeTasks(nil, standby_alarm) end end @@ -841,7 +860,6 @@ function Kobo:suspend() logger.info("Kobo suspend: going to sleep . . .") local UIManager = require("ui/uimanager") 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... @@ -854,6 +872,7 @@ function Kobo:suspend() -- So, unless that changes, unconditionally disable it. --[[ + local f, re, err_msg, err_code local has_wakeup_count = false f = io.open("/sys/power/wakeup_count", "re") if f ~= nil then @@ -873,10 +892,14 @@ function Kobo:suspend() -]] -- NOTE: Sets gSleep_Mode_Suspend to 1. Used as a flag throughout the - -- kernel to suspend/resume various subsystems - -- c.f., state_extended_store @ kernel/power/main.c + -- kernel to suspend/resume various subsystems + -- c.f., state_extended_store @ kernel/power/main.c local ret = writeToSys("1", "/sys/power/state-extended") - logger.info("Kobo suspend: asked the kernel to put subsystems to sleep, ret:", ret) + if ret then + logger.info("Kobo suspend: successfully asked the kernel to put subsystems to sleep") + else + logger.warn("Kobo suspend: the kernel refused to flag subsystems for suspend!") + end util.sleep(2) logger.info("Kobo suspend: waited for 2s because of reasons...") @@ -902,90 +925,82 @@ function Kobo:suspend() end --]] - logger.info("Kobo suspend: asking for a suspend to RAM . . .") - f = io.open("/sys/power/state", "we") - if not f then - -- Reset state-extended back to 0 since we are giving up. - local ext_fd = io.open("/sys/power/state-extended", "we") - if not ext_fd then - logger.err("cannot open /sys/power/state-extended for writing!") - else - ext_fd:write("0\n") - ext_fd:close() - end - return false - end - local TimeVal = require("ui/timeval") + logger.info("Kobo suspend: asking for a suspend to RAM . . .") 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() + ret = writeToSys("mem", "/sys/power/state") -- NOTE: At this point, we *should* be in suspend to RAM, as such, - -- execution should only resume on wakeup... - + -- 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) - -- 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 - -- looking at the detailed stderr message - -- (most of the failures we'll see are -EBUSY anyway) - -- For reference, when that happens to nickel, it appears to keep retrying - -- to wakeup & sleep ad nauseam, - -- which is where the non-sensical 1 -> mem -> 0 loop idea comes from... - -- cf. nickel_suspend_strace.txt for more details. + if ret then + logger.info("Kobo suspend: ZzZ ZzZ ZzZ... And woke up!") + else + logger.warn("Kobo suspend: the kernel refused to enter suspend!") + -- Reset state-extended back to 0 since we are giving up. + writeToSys("0", "/sys/power/state-extended") + end - logger.info("Kobo suspend: woke up!") + -- 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 + -- looking at the detailed stderr message + -- (most of the failures we'll see are -EBUSY anyway) + -- For reference, when that happens to nickel, it appears to keep retrying + -- to wakeup & sleep ad nauseam, + -- which is where the non-sensical 1 -> mem -> 0 loop idea comes from... + -- cf. nickel_suspend_strace.txt for more details. --[[ - if has_wakeup_count then logger.info("wakeup count: $(cat /sys/power/wakeup_count)") end -- Print tke kernel log since our attempt to sleep... --dmesg -c - --]] -- NOTE: We unflag /sys/power/state-extended in Kobo:resume() to keep - -- things tidy and easier to follow + -- things tidy and easier to follow -- Kobo:resume() will reset unexpected_wakeup_count = 0 to signal an -- 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 + -- We're assuming Kobo:resume() will be called in the next 15 seconds in ordrer to cancel that check. logger.dbg("Kobo suspend: scheduling unexpected wakeup guard") UIManager:scheduleIn(15, self.checkUnexpectedWakeup, self) end function Kobo:resume() logger.info("Kobo resume: clean up after wakeup") - -- reset unexpected_wakeup_count ASAP + -- Reset unexpected_wakeup_count ASAP self.unexpected_wakeup_count = 0 - require("ui/uimanager"):unschedule(self.checkUnexpectedWakeup) + -- Unschedule the checkUnexpectedWakeup shenanigans. + local UIManager = require("ui/uimanager") + UIManager:unschedule(self.checkUnexpectedWakeup) + UIManager:unschedule(self.suspend) -- 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 - + -- kernel to suspend/resume various subsystems + -- cf. kernel/power/main.c @ L#207 + -- Among other things, this sets up the wakeup pins (e.g., resume on input). local ret = writeToSys("0", "/sys/power/state-extended") - logger.info("Kobo resume: unflagged kernel subsystems for resume, ret:", ret) + if ret then + logger.info("Kobo resume: successfully asked the kernel to resume subsystems") + else + logger.warn("Kobo resume: the kernel refused to flag subsystems for resume!") + end -- HACK: wait a bit (0.1 sec) for the kernel to catch up util.usleep(100000) if self.hasIRGrid then -- cf. #1862, I can reliably break IR touch input on resume... - -- cf. also #1943 for the rationale behind applying this workaorund in every case... + -- cf. also #1943 for the rationale behind applying this workaround in every case... writeToSys("a", "/sys/devices/virtual/input/input1/neocmd") end @@ -1000,7 +1015,7 @@ end function Kobo:powerOff() -- Much like Nickel itself, disable the RTC alarm before powering down. - WakeupMgr:unsetWakeupAlarm() + self.wakeup_mgr:unsetWakeupAlarm() -- Then shut down without init's help os.execute("poweroff -f") @@ -1141,7 +1156,7 @@ end -------------- device probe ------------ -local codename = Kobo:getCodeName() +local codename = getCodeName() local product_id = getProductId() if codename == "dahlia" then diff --git a/frontend/device/kobo/powerd.lua b/frontend/device/kobo/powerd.lua index 0db663339..aa012a5e5 100644 --- a/frontend/device/kobo/powerd.lua +++ b/frontend/device/kobo/powerd.lua @@ -114,11 +114,14 @@ function KoboPowerD:init() end self.isAuxChargingHW = function(this) - -- 0 when not charging + -- 0 when discharging -- 3 when full -- 2 when charging via DCP - local charge_status = this:read_int_file(this.aux_batt_charging_file) - return charge_status ~= 0 and charge_status ~= 3 + return this:read_int_file(this.aux_batt_charging_file) ~= 0 + end + + self.isAuxChargedHW = function(this) + return this:read_int_file(this.aux_batt_charging_file) == 3 end end @@ -274,8 +277,24 @@ function KoboPowerD:getCapacityHW() return self:read_int_file(self.batt_capacity_file) end +-- NOTE: Match the behavior of the NXP ntx_io _Is_USB_plugged ioctl! +-- (Otherwise, a device that is fully charged, but still plugged in will no longer be flagged as charging). function KoboPowerD:isChargingHW() - return self:read_str_file(self.is_charging_file) == "Charging" + return self:read_str_file(self.is_charging_file) ~= "Discharging" +end + +function KoboPowerD:isChargedHW() + -- On sunxi, the proper "Full" status is reported, while older kernels (even Mk. 9) report "Not charging" + -- c.f., POWER_SUPPLY_PROP_STATUS in ricoh61x_batt_get_prop @ drivers/power/ricoh619-battery.c + -- (or drivers/power/supply/ricoh619-battery.c on newer kernels). + local status = self:read_str_file(self.is_charging_file) + if status == "Full" then + return true + elseif status == "Not charging" and self:getCapacity() == 100 then + return true + end + + return false end function KoboPowerD:turnOffFrontlightHW() diff --git a/frontend/device/wakeupmgr.lua b/frontend/device/wakeupmgr.lua index fcf15e381..46dc43c6a 100644 --- a/frontend/device/wakeupmgr.lua +++ b/frontend/device/wakeupmgr.lua @@ -52,7 +52,9 @@ I'm not sure if the distinction between maintenance and sync makes sense but it's wifi on vs. off. --]] function WakeupMgr:addTask(seconds_from_now, callback) - if not type(seconds_from_now) == "number" and not type(callback) == "function" then return end + -- Make sure we passed valid input, so that stuff doesn't break in fun and interesting ways (especially in removeTasks). + assert(type(seconds_from_now) == "number", "delay is not a number") + assert(type(callback) == "function", "callback is not a function") local epoch = RTC:secondsFromNowToEpoch(seconds_from_now) logger.info("WakeupMgr: scheduling wakeup in", seconds_from_now) @@ -68,36 +70,67 @@ function WakeupMgr:addTask(seconds_from_now, callback) table.sort(self._task_queue, function(a, b) return a.epoch < b.epoch end) local new_upcoming_task = self._task_queue[1].epoch - if not old_upcoming_task or (new_upcoming_task < old_upcoming_task) then - self:setWakeupAlarm(self._task_queue[1].epoch) + self:setWakeupAlarm(new_upcoming_task) end end --[[-- -Remove task from queue. +Remove task(s) from queue. -This method removes a task by either index, scheduled time or callback. +This method removes one or more tasks by either scheduled time or callback. +If any tasks are left on exit, the upcoming one will automatically be scheduled (if necessary). -@int idx Task queue index. Mainly useful within this module. @int epoch The epoch for when this task is scheduled to wake up. Normally the preferred method for outside callers. @int callback A scheduled callback function. Store a reference for use with anonymous functions. +@treturn bool (true if one or more tasks were removed; false otherwise; nil if the task queue is empty). --]] -function WakeupMgr:removeTask(idx, epoch, callback) - if not type(idx) == "number" - and not type(epoch) == "number" - and not type(callback) == "function" then return end - +function WakeupMgr:removeTasks(epoch, callback) if #self._task_queue < 1 then return end - for k, v in ipairs(self._task_queue) do - if k == idx or epoch == v.epoch or callback == v.callback then + local removed = false + local reschedule = false + for k = #self._task_queue, 1, -1 do + local v = self._task_queue[k] + if epoch == v.epoch or callback == v.callback then table.remove(self._task_queue, k) - return true + removed = true + -- If we've successfuly pop'ed the upcoming task, we need to schedule the next one (if any) on exit. + if k == 1 then + reschedule = true + end end end + + -- Schedule the next wakeup action, if any (and if necessary). + if reschedule and self._task_queue[1] then + self:setWakeupAlarm(self._task_queue[1].epoch) + end + + return removed +end + +--[[-- +Variant of @{removeTasks} that will only remove a single task, identified by its task queue index. + +@int idx Task queue index. Mainly useful within this module. +@treturn bool (true if a task was removed; false otherwise). +--]] +function WakeupMgr:removeTask(idx) + local removed = false + -- We don't want to keep the pop'ed entry around, we just want to know if we pop'ed something. + if table.remove(self._task_queue, idx) then + removed = true + end + + -- Schedule the next wakeup action, if any (and if necessary). + if removed and idx == 1 and self._task_queue[1] then + self:setWakeupAlarm(self._task_queue[1].epoch) + end + + return removed end --[[-- @@ -106,25 +139,28 @@ Execute wakeup action. This method should be called by the device resume logic in case of a scheduled wakeup. It checks if the wakeup was scheduled by us using @{validateWakeupAlarmByProximity}, -executes the task, and schedules the next wakeup if any. +in which case the task is executed. + +If necessary, the next upcoming task (if any) is scheduled on exit. -@treturn bool +@int proximity Proximity window to the scheduled wakeup (passed to @{validateWakeupAlarmByProximity}). +@treturn bool (true if we were truly woken up by the scheduled wakeup; false otherwise; nil if there weren't any tasks scheduled). --]] -function WakeupMgr:wakeupAction() +function WakeupMgr:wakeupAction(proximity) if #self._task_queue > 0 then local task = self._task_queue[1] - if self:validateWakeupAlarmByProximity(task.epoch) then + if self:validateWakeupAlarmByProximity(task.epoch, proximity) then task.callback() + -- NOTE: removeTask will take care of scheduling the next upcoming task, if necessary. self:removeTask(1) - if self._task_queue[1] then - -- Set next scheduled wakeup, if any. - self:setWakeupAlarm(self._task_queue[1].epoch) - end + return true - else - return false end + + return false end + + return nil end --[[-- diff --git a/frontend/ui/network/networklistener.lua b/frontend/ui/network/networklistener.lua index 35665afa1..cdd9d73fc 100644 --- a/frontend/ui/network/networklistener.lua +++ b/frontend/ui/network/networklistener.lua @@ -84,6 +84,12 @@ end -- Everything below is to handle auto_disable_wifi ;). local default_network_timeout_seconds = 5*60 local max_network_timeout_seconds = 30*60 +-- If autostandby is enabled, shorten the timeouts +local auto_standby = G_reader_settings:readSetting("auto_standby_timeout_seconds", -1) +if auto_standby > 0 then + default_network_timeout_seconds = default_network_timeout_seconds / 2 + max_network_timeout_seconds = max_network_timeout_seconds / 2 +end -- This should be more than enough to catch actual activity vs. noise spread over 5 minutes. local network_activity_noise_margin = 12 -- unscaled_size_check: ignore diff --git a/frontend/ui/uimanager.lua b/frontend/ui/uimanager.lua index 513c7cf14..1b20e14af 100644 --- a/frontend/ui/uimanager.lua +++ b/frontend/ui/uimanager.lua @@ -123,20 +123,12 @@ function UIManager:init() -- suspend. So let's unschedule it when suspending, and restart it after -- resume. Done via the plugin's onSuspend/onResume handlers. self.event_handlers["Suspend"] = function() - -- Ignore the accelerometer (if that's not already the case) while we're alseep - if G_reader_settings:nilOrFalse("input_ignore_gsensor") then - Device:toggleGSensor(false) - end self:_beforeSuspend() Device:onPowerEvent("Suspend") end self.event_handlers["Resume"] = function() Device:onPowerEvent("Resume") self:_afterResume() - -- Stop ignoring the accelerometer (unless requested) when we wakeup - if G_reader_settings:nilOrFalse("input_ignore_gsensor") then - Device:toggleGSensor(true) - end end self.event_handlers["PowerPress"] = function() -- Always schedule power off. @@ -1179,14 +1171,24 @@ 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() + times[i] = self._task_queue[i].time - TimeVal:now() end return times end +--]] + +function UIManager:getNextTaskTime() + if #self._task_queue > 0 then + return self._task_queue[1].time - TimeVal:now() + else + return nil + end +end function UIManager:_checkTasks() self._now = TimeVal:now() @@ -1771,6 +1773,9 @@ function UIManager:_beforeSuspend() self:flushSettings() self:broadcastEvent(Event:new("Suspend")) + -- Block input events unrelated to power management + Input:inhibitInput(true) + -- Disable key repeat to avoid useless chatter (especially where Sleep Covers are concerned...) Device:disableKeyRepeat() @@ -1783,6 +1788,9 @@ function UIManager:_afterResume() -- Restore key repeat Device:restoreKeyRepeat() + -- Restore full input handling + Input:inhibitInput(false) + self:broadcastEvent(Event:new("Resume")) end diff --git a/frontend/ui/widget/touchmenu.lua b/frontend/ui/widget/touchmenu.lua index ca2ae99a6..dbf1fd730 100644 --- a/frontend/ui/widget/touchmenu.lua +++ b/frontend/ui/widget/touchmenu.lua @@ -697,12 +697,12 @@ function TouchMenu:updateItems() local powerd = Device:getPowerDevice() if Device:hasBattery() then local batt_lvl = powerd:getCapacity() - local batt_symbol = powerd:getBatterySymbol(powerd:isCharging(), batt_lvl) + local batt_symbol = powerd:getBatterySymbol(powerd:isCharged(), powerd:isCharging(), batt_lvl) time_info_txt = BD.wrap(time_info_txt) .. " " .. BD.wrap("⌁") .. BD.wrap(batt_symbol) .. BD.wrap(batt_lvl .. "%") if Device:hasAuxBattery() and powerd:isAuxBatteryConnected() then local aux_batt_lvl = powerd:getAuxCapacity() - local aux_batt_symbol = powerd:getBatterySymbol(powerd:isAuxCharging(), aux_batt_lvl) + local aux_batt_symbol = powerd:getBatterySymbol(powerd:isAuxCharged(), powerd:isAuxCharging(), aux_batt_lvl) time_info_txt = time_info_txt .. " " .. BD.wrap("+") .. BD.wrap(aux_batt_symbol) .. BD.wrap(aux_batt_lvl .. "%") end end diff --git a/plugins/autosuspend.koplugin/main.lua b/plugins/autosuspend.koplugin/main.lua index 7a91a28a9..55cbccfe8 100644 --- a/plugins/autosuspend.koplugin/main.lua +++ b/plugins/autosuspend.koplugin/main.lua @@ -53,8 +53,8 @@ function AutoSuspend:_enabledShutdown() end function AutoSuspend:_schedule(shutdown_only) - if not self:_enabled() and Device:canPowerOff() and not self:_enabledShutdown() then - logger.dbg("AutoSuspend:_schedule is disabled") + if not self:_enabled() and not self:_enabledShutdown() then + logger.dbg("AutoSuspend: suspend/shutdown timer is disabled") return end @@ -126,16 +126,15 @@ end function AutoSuspend:init() logger.dbg("AutoSuspend: init") - if Device:isPocketBook() and not Device:canSuspend() then return end - self.autoshutdown_timeout_seconds = G_reader_settings:readSetting("autoshutdown_timeout_seconds", default_autoshutdown_timeout_seconds) 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) + if Device:isPocketBook() and not Device:canSuspend() then return end + 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. @@ -159,6 +158,9 @@ function AutoSuspend:init() UIManager:broadcastEvent(Event:new("LeaveStandby")) end + -- Make sure we only have an AllowStandby handler when we actually want one... + self:toggleStandbyHandler(self:_enabledStandby()) + self.last_action_tv = UIManager:getElapsedTimeSinceBoot() self:_start() self:_start_standby() @@ -168,7 +170,7 @@ function AutoSuspend:init() self.ui.menu:registerToMainMenu(self) end --- For event_hook automagic deregistration purposes +-- NOTE: event_hook takes care of overloading this to unregister the hook, too. function AutoSuspend:onCloseWidget() logger.dbg("AutoSuspend: onCloseWidget") if Device:isPocketBook() and not Device:canSuspend() then return end @@ -205,13 +207,13 @@ end function AutoSuspend:_schedule_standby() -- Start the long list of conditions in which we do *NOT* want to go into standby ;). if not Device:canStandby() then - -- NOTE: This partly duplicates what `_enabledStandby` does, - -- but it's here to avoid logging noise on devices that can't even standby ;). return end -- Don't even schedule standby if we haven't set a proper timeout yet. - if not self:_enabledStandby() then + -- NOTE: We've essentially split the _enabledStandby check in two branches, + -- simply to avoid logging noise on devices that can't even standby ;). + if self.auto_standby_timeout_seconds <= 0 then logger.dbg("AutoSuspend: No timeout set, no standby") return end @@ -274,13 +276,6 @@ function AutoSuspend:allowStandby() -- Tell UIManager that we now allow standby. 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 should do. - -- Let's call it deadline_guard. - UIManager:scheduleIn(0.100, function() end) - -- We've just run our course. self.is_standby_scheduled = false end @@ -319,7 +314,7 @@ function AutoSuspend:onResume() self.going_to_suspend = false if self:_enabledShutdown() and Device.wakeup_mgr then - Device.wakeup_mgr:removeTask(nil, nil, UIManager.poweroff_action) + Device.wakeup_mgr:removeTasks(nil, UIManager.poweroff_action) end -- Unschedule in case we tripped onUnexpectedWakeupLimit first... self:_unschedule() @@ -408,6 +403,7 @@ function AutoSuspend:pickTimeoutValue(touchmenu_instance, title, info, setting, G_reader_settings:saveSetting(setting, self[setting]) if is_standby then self:_unschedule_standby() + self:toggleStandbyHandler(self:_enabledStandby()) self:_start_standby() else self:_unschedule() @@ -449,6 +445,7 @@ function AutoSuspend:pickTimeoutValue(touchmenu_instance, title, info, setting, G_reader_settings:saveSetting(setting, -1) if is_standby then self:_unschedule_standby() + self:toggleStandbyHandler(false) else self:_unschedule() end @@ -519,11 +516,14 @@ function AutoSuspend:addToMainMenu(menu_items) } end if Device:canStandby() then + --- @fixme: Reword the final warning when we have more data on the hangs on some Kobo kernels (e.g., #9038). 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. Generally, the newer the device, the less noticeable this delay will be, but it can be fairly aggravating on slower devices.]]) +Upon user input, the device needs a certain amount of time to wake up. Generally, the newer the device, the less noticeable this delay will be, but it can be fairly aggravating on slower devices. + +This is experimental on most devices, except those running on a sunxi SoC (Kobo Elipsa & Sage), so much so that it might in fact hang some of the more broken kernels out there (Kobo Libra 2).]]) menu_items.autostandby = { sorting_hint = "device", @@ -557,19 +557,23 @@ 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() +-- NOTE: To make sure this will not even run when autostandby is disabled, +-- this is only aliased as `onAllowStandby` when necessary. +-- (Because the Event is generated regardless of us, as many things can call UIManager:allowStandby). +function AutoSuspend:AllowStandbyHandler() logger.dbg("AutoSuspend: onAllowStandby") -- This piggy-backs minimally on the UI framework implemented for the PocketBook autostandby plugin, -- see its own AllowStandby handler for more details. - local wake_in = math.huge - -- The next scheduled function should be our deadline_guard (c.f., `AutoSuspend:allowStandby`). - -- Wake up before the second next scheduled function executes (e.g. footer update, suspend ...) - local scheduler_times = UIManager:getNextTaskTimes(2) - if #scheduler_times == 2 then + local wake_in + -- Wake up before the next scheduled function executes (e.g. footer update, suspend ...) + local next_task_time = UIManager:getNextTaskTime() + if next_task_time 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 + wake_in = math.floor(next_task_time:tonumber()) + 1 + else + wake_in = math.huge end if wake_in >= 3 then -- don't go into standby, if scheduled wakeup is in less than 3 secs @@ -585,9 +589,16 @@ function AutoSuspend:onAllowStandby() -- to make sure UIManager will consume the input events that woke us up first -- (in case we were woken up by user input, as opposed to an rtc wake alarm)! -- (This ensures we'll use an up to date last_action_tv, and that it only ever gets updated from *user* input). - -- NOTE: UIManager consumes scheduled tasks before input events, so make sure we delay by a significant amount, - -- especially given that this delay will likely be used as the next input polling loop timeout... - UIManager:scheduleIn(1, self.leave_standby_task) + -- NOTE: UIManager consumes scheduled tasks before input events, which is why we can't use nextTick. + UIManager:tickAfterNext(self.leave_standby_task) + end +end + +function AutoSuspend:toggleStandbyHandler(toggle) + if toggle then + self.onAllowStandby = self.AllowStandbyHandler + else + self.onAllowStandby = nil end end diff --git a/spec/unit/wakeupmgr_spec.lua b/spec/unit/wakeupmgr_spec.lua index 4cd3191ca..9402f558e 100644 --- a/spec/unit/wakeupmgr_spec.lua +++ b/spec/unit/wakeupmgr_spec.lua @@ -39,7 +39,7 @@ describe("WakeupMgr", function() assert.is_equal(epoch3, WakeupMgr._task_queue[2].epoch) end) it("should have scheduled next task after execution", function() - assert.stub(WakeupMgr.setWakeupAlarm).was.called(3) + assert.stub(WakeupMgr.setWakeupAlarm).was.called(3) -- 2 from addTask (the second addTask doesn't replace the upcoming task), 1 from wakeupAction (via removeTask). end) it("should remove arbitrary task from stack", function() WakeupMgr:removeTask(2) @@ -50,6 +50,6 @@ describe("WakeupMgr", function() assert.is_true(WakeupMgr:wakeupAction()) end) it("should not have scheduled a wakeup without a task", function() - assert.stub(WakeupMgr.setWakeupAlarm).was.called(3) + assert.stub(WakeupMgr.setWakeupAlarm).was.called(3) -- 2 from addTask, 1 from wakeupAction, 0 from removeTask (because it wasn't the upcoming task that was removed) end) end)