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).
reviewable/pr9058/r1
NiLuJe 2 years ago committed by GitHub
parent 7cac083db4
commit 86c35ad066
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -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()

@ -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

@ -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

@ -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 <https://www.mobileread.com/forums/showthread.php?t=298302&page=5>
-- 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.

@ -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

@ -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

@ -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()

@ -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
--[[--

@ -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

@ -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

@ -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

@ -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

@ -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)

Loading…
Cancel
Save