mirror of https://github.com/koreader/koreader
[feat, Kobo] Autoshutdown (#5335)
The methods used here will likely work on most embedded devices, which is why I put them in their own WakeupMgr interface/scheduler module, separate from Kobo. See https://www.mobileread.com/forums/showthread.php?p=3886403#post3886403 for more context. Fixes #3806.pull/5368/head
parent
9163a85b3c
commit
e257c4e45e
@ -0,0 +1,187 @@
|
||||
--[[--
|
||||
RTC wakeup interface.
|
||||
|
||||
Many devices can schedule hardware wakeups with a real time clock alarm.
|
||||
On embedded devices this can typically be easily manipulated by the user
|
||||
through `/sys/class/rtc/rtc0/wakealarm`. Some, like the Kobo Aura H2O,
|
||||
can only schedule wakeups through ioctl.
|
||||
|
||||
See @{ffi.rtc} for implementation details.
|
||||
|
||||
See also: <https://linux.die.net/man/4/rtc>.
|
||||
--]]
|
||||
|
||||
local RTC = require("ffi/rtc")
|
||||
local logger = require("logger")
|
||||
|
||||
--[[--
|
||||
WakeupMgr base class.
|
||||
|
||||
@table WakeupMgr
|
||||
--]]
|
||||
local WakeupMgr = {
|
||||
dev_rtc = "/dev/rtc0", -- RTC device
|
||||
_task_queue = {}, -- Table with epoch at which to schedule the task and the function to be scheduled.
|
||||
}
|
||||
|
||||
--[[--
|
||||
Initiate a WakeupMgr instance.
|
||||
|
||||
@usage
|
||||
local WakeupMgr = require("device/wakeupmgr")
|
||||
local wakeup_mgr = WakeupMgr:new{
|
||||
-- The default is `/dev/rtc0`, but some devices have more than one RTC.
|
||||
-- You might therefore need to use `/dev/rtc1`, etc.
|
||||
dev_rtc = "/dev/rtc0",
|
||||
}
|
||||
--]]
|
||||
function WakeupMgr:new(o)
|
||||
o = o or {}
|
||||
setmetatable(o, self)
|
||||
self.__index = self
|
||||
if o.init then o:init() end
|
||||
return o
|
||||
end
|
||||
|
||||
--[[--
|
||||
Add a task to the queue.
|
||||
|
||||
@todo Group by type to avoid useless wakeups.
|
||||
For example, maintenance, sync, and shutdown.
|
||||
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
|
||||
|
||||
local epoch = RTC:secondsFromNowToEpoch(seconds_from_now)
|
||||
|
||||
local old_upcoming_task = (self._task_queue[1] or {}).epoch
|
||||
|
||||
table.insert(self._task_queue, {
|
||||
epoch = epoch,
|
||||
callback = callback,
|
||||
})
|
||||
--- @todo Binary insert? This table should be so small that performance doesn't matter.
|
||||
-- It might be useful to have that available as a utility function regardless.
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
||||
--[[--
|
||||
Remove task from queue.
|
||||
|
||||
This method removes a task by either index, scheduled time or callback.
|
||||
|
||||
@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.
|
||||
--]]
|
||||
function WakeupMgr:removeTask(idx, epoch, callback)
|
||||
if not type(idx) == "number"
|
||||
and not type(epoch) == "number"
|
||||
and not type(callback) == "function" then return end
|
||||
|
||||
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
|
||||
table.remove(self._task_queue, k)
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--[[--
|
||||
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.
|
||||
|
||||
@treturn bool
|
||||
--]]
|
||||
function WakeupMgr:wakeupAction()
|
||||
if #self._task_queue > 0 then
|
||||
local task = self._task_queue[1]
|
||||
if self:validateWakeupAlarmByProximity(task.epoch) then
|
||||
task.callback()
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
--[[--
|
||||
Set wakeup alarm.
|
||||
|
||||
Simple wrapper for @{ffi.rtc.setWakeupAlarm}.
|
||||
--]]
|
||||
function WakeupMgr:setWakeupAlarm(epoch, enabled)
|
||||
return RTC:setWakeupAlarm(epoch, enabled)
|
||||
end
|
||||
|
||||
--[[--
|
||||
Unset wakeup alarm.
|
||||
|
||||
Simple wrapper for @{ffi.rtc.unsetWakeupAlarm}.
|
||||
--]]
|
||||
function WakeupMgr:unsetWakeupAlarm()
|
||||
return RTC:unsetWakeupAlarm()
|
||||
end
|
||||
|
||||
--[[--
|
||||
Get wakealarm as set by us.
|
||||
|
||||
Simple wrapper for @{ffi.rtc.getWakeupAlarm}.
|
||||
--]]
|
||||
function WakeupMgr:getWakeupAlarm()
|
||||
return RTC:getWakeupAlarm()
|
||||
end
|
||||
|
||||
--[[--
|
||||
Get RTC wakealarm from system.
|
||||
|
||||
Simple wrapper for @{ffi.rtc.getWakeupAlarm}.
|
||||
--]]
|
||||
function WakeupMgr:getWakeupAlarmSys()
|
||||
return RTC:getWakeupAlarmSys()
|
||||
end
|
||||
|
||||
--[[--
|
||||
Validate wakeup alarm.
|
||||
|
||||
Checks if we set the alarm.
|
||||
|
||||
Simple wrapper for @{ffi.rtc.validateWakeupAlarmByProximity}.
|
||||
--]]
|
||||
function WakeupMgr:validateWakeupAlarmByProximity()
|
||||
return RTC:validateWakeupAlarmByProximity()
|
||||
end
|
||||
|
||||
--[[--
|
||||
Check if a wakeup is scheduled.
|
||||
|
||||
Simple wrapper for @{ffi.rtc.isWakeupAlarmScheduled}.
|
||||
--]]
|
||||
function WakeupMgr:isWakeupAlarmScheduled()
|
||||
local wakeup_scheduled = RTC:isWakeupAlarmScheduled()
|
||||
logger.dbg("isWakeupAlarmScheduled", wakeup_scheduled)
|
||||
return wakeup_scheduled
|
||||
end
|
||||
|
||||
return WakeupMgr
|
@ -1,75 +1,120 @@
|
||||
describe("AutoSuspend widget tests", function()
|
||||
describe("AutoSuspend", function()
|
||||
setup(function()
|
||||
require("commonrequire")
|
||||
package.unloadAll()
|
||||
require("document/canvascontext"):init(require("device"))
|
||||
end)
|
||||
|
||||
before_each(function()
|
||||
local Device = require("device")
|
||||
stub(Device, "isKobo")
|
||||
Device.isKobo.returns(true)
|
||||
Device.input.waitEvent = function() end
|
||||
local UIManager = require("ui/uimanager")
|
||||
stub(UIManager, "suspend")
|
||||
UIManager._run_forever = true
|
||||
G_reader_settings:saveSetting("auto_suspend_timeout_seconds", 10)
|
||||
require("mock_time"):install()
|
||||
end)
|
||||
describe("suspend", function()
|
||||
before_each(function()
|
||||
local Device = require("device")
|
||||
stub(Device, "isKobo")
|
||||
Device.isKobo.returns(true)
|
||||
Device.input.waitEvent = function() end
|
||||
local UIManager = require("ui/uimanager")
|
||||
stub(UIManager, "suspend")
|
||||
UIManager._run_forever = true
|
||||
G_reader_settings:saveSetting("auto_suspend_timeout_seconds", 10)
|
||||
require("mock_time"):install()
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
require("device").isKobo:revert()
|
||||
require("ui/uimanager").suspend:revert()
|
||||
G_reader_settings:delSetting("auto_suspend_timeout_seconds")
|
||||
require("mock_time"):uninstall()
|
||||
end)
|
||||
after_each(function()
|
||||
require("device").isKobo:revert()
|
||||
require("ui/uimanager").suspend:revert()
|
||||
G_reader_settings:delSetting("auto_suspend_timeout_seconds")
|
||||
require("mock_time"):uninstall()
|
||||
end)
|
||||
|
||||
it("should be able to execute suspend when timing out", function()
|
||||
local mock_time = require("mock_time")
|
||||
local widget_class = dofile("plugins/autosuspend.koplugin/main.lua")
|
||||
local widget = widget_class:new() --luacheck: ignore
|
||||
local UIManager = require("ui/uimanager")
|
||||
mock_time:increase(5)
|
||||
UIManager:handleInput()
|
||||
assert.stub(UIManager.suspend).was.called(0)
|
||||
mock_time:increase(6)
|
||||
UIManager:handleInput()
|
||||
assert.stub(UIManager.suspend).was.called(1)
|
||||
mock_time:uninstall()
|
||||
end)
|
||||
it("should be able to execute suspend when timing out", function()
|
||||
local mock_time = require("mock_time")
|
||||
local widget_class = dofile("plugins/autosuspend.koplugin/main.lua")
|
||||
local widget = widget_class:new() --luacheck: ignore
|
||||
local UIManager = require("ui/uimanager")
|
||||
mock_time:increase(5)
|
||||
UIManager:handleInput()
|
||||
assert.stub(UIManager.suspend).was.called(0)
|
||||
mock_time:increase(6)
|
||||
UIManager:handleInput()
|
||||
assert.stub(UIManager.suspend).was.called(1)
|
||||
mock_time:uninstall()
|
||||
end)
|
||||
|
||||
it("should be able to initialize several times", function()
|
||||
local mock_time = require("mock_time")
|
||||
-- AutoSuspend plugin set the last_action_sec each time it is initialized.
|
||||
local widget_class = dofile("plugins/autosuspend.koplugin/main.lua")
|
||||
local widget1 = widget_class:new() --luacheck: ignore
|
||||
-- So if one more initialization happens, it won't sleep after another 5 seconds.
|
||||
mock_time:increase(5)
|
||||
local widget2 = widget_class:new() --luacheck: ignore
|
||||
local UIManager = require("ui/uimanager")
|
||||
mock_time:increase(6)
|
||||
UIManager:handleInput()
|
||||
assert.stub(UIManager.suspend).was.called(1)
|
||||
mock_time:uninstall()
|
||||
it("should be able to deprecate last task", function()
|
||||
local mock_time = require("mock_time")
|
||||
local widget_class = dofile("plugins/autosuspend.koplugin/main.lua")
|
||||
local widget = widget_class:new()
|
||||
mock_time:increase(5)
|
||||
local UIManager = require("ui/uimanager")
|
||||
UIManager:handleInput()
|
||||
assert.stub(UIManager.suspend).was.called(0)
|
||||
widget:onInputEvent()
|
||||
widget:onSuspend()
|
||||
widget:onResume()
|
||||
mock_time:increase(6)
|
||||
UIManager:handleInput()
|
||||
assert.stub(UIManager.suspend).was.called(0)
|
||||
mock_time:increase(5)
|
||||
UIManager:handleInput()
|
||||
assert.stub(UIManager.suspend).was.called(1)
|
||||
mock_time:uninstall()
|
||||
end)
|
||||
end)
|
||||
|
||||
it("should be able to deprecate last task", function()
|
||||
local mock_time = require("mock_time")
|
||||
local widget_class = dofile("plugins/autosuspend.koplugin/main.lua")
|
||||
local widget = widget_class:new()
|
||||
mock_time:increase(5)
|
||||
local UIManager = require("ui/uimanager")
|
||||
UIManager:handleInput()
|
||||
assert.stub(UIManager.suspend).was.called(0)
|
||||
widget:onInputEvent()
|
||||
widget:onSuspend()
|
||||
widget:onResume()
|
||||
mock_time:increase(6)
|
||||
UIManager:handleInput()
|
||||
assert.stub(UIManager.suspend).was.called(0)
|
||||
mock_time:increase(5)
|
||||
UIManager:handleInput()
|
||||
assert.stub(UIManager.suspend).was.called(1)
|
||||
mock_time:uninstall()
|
||||
describe("shutdown", function()
|
||||
--- @todo duplicate with above, elegant way to DRY?
|
||||
before_each(function()
|
||||
local Device = require("device")
|
||||
stub(Device, "isKobo")
|
||||
Device.isKobo.returns(true)
|
||||
stub(Device, "canPowerOff")
|
||||
Device.canPowerOff.returns(true)
|
||||
Device.input.waitEvent = function() end
|
||||
local UIManager = require("ui/uimanager")
|
||||
stub(UIManager, "poweroff_action")
|
||||
UIManager._run_forever = true
|
||||
G_reader_settings:saveSetting("autoshutdown_timeout_seconds", 10)
|
||||
require("mock_time"):install()
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
require("device").isKobo:revert()
|
||||
require("ui/uimanager").poweroff_action:revert()
|
||||
G_reader_settings:delSetting("autoshutdown_timeout_seconds")
|
||||
require("mock_time"):uninstall()
|
||||
end)
|
||||
|
||||
it("should be able to execute suspend when timing out", function()
|
||||
local mock_time = require("mock_time")
|
||||
local widget_class = dofile("plugins/autosuspend.koplugin/main.lua")
|
||||
local widget = widget_class:new() --luacheck: ignore
|
||||
local UIManager = require("ui/uimanager")
|
||||
mock_time:increase(5)
|
||||
UIManager:handleInput()
|
||||
assert.stub(UIManager.poweroff_action).was.called(0)
|
||||
mock_time:increase(6)
|
||||
UIManager:handleInput()
|
||||
assert.stub(UIManager.poweroff_action).was.called(1)
|
||||
mock_time:uninstall()
|
||||
end)
|
||||
|
||||
it("should be able to deprecate last task", function()
|
||||
local mock_time = require("mock_time")
|
||||
local widget_class = dofile("plugins/autosuspend.koplugin/main.lua")
|
||||
local widget = widget_class:new()
|
||||
mock_time:increase(5)
|
||||
local UIManager = require("ui/uimanager")
|
||||
UIManager:handleInput()
|
||||
assert.stub(UIManager.poweroff_action).was.called(0)
|
||||
widget:onInputEvent()
|
||||
widget:onSuspend()
|
||||
widget:onResume()
|
||||
mock_time:increase(6)
|
||||
UIManager:handleInput()
|
||||
assert.stub(UIManager.poweroff_action).was.called(0)
|
||||
mock_time:increase(5)
|
||||
UIManager:handleInput()
|
||||
assert.stub(UIManager.poweroff_action).was.called(1)
|
||||
mock_time:uninstall()
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
@ -0,0 +1,55 @@
|
||||
describe("WakeupMgr", function()
|
||||
local RTC
|
||||
local WakeupMgr
|
||||
local epoch1, epoch2, epoch3
|
||||
|
||||
setup(function()
|
||||
require("commonrequire")
|
||||
package.unloadAll()
|
||||
RTC = require("ffi/rtc")
|
||||
WakeupMgr = require("device/wakeupmgr")
|
||||
-- We could theoretically test this by running the tests as root locally.
|
||||
stub(WakeupMgr, "setWakeupAlarm")
|
||||
WakeupMgr.validateWakeupAlarmByProximity = spy.new(function() return true end)
|
||||
|
||||
epoch1 = RTC:secondsFromNowToEpoch(1234)
|
||||
epoch2 = RTC:secondsFromNowToEpoch(123)
|
||||
epoch3 = RTC:secondsFromNowToEpoch(9999)
|
||||
end)
|
||||
|
||||
it("should add a task", function()
|
||||
WakeupMgr:addTask(1234, function() end)
|
||||
assert.is_equal(epoch1, WakeupMgr._task_queue[1].epoch)
|
||||
assert.stub(WakeupMgr.setWakeupAlarm).was.called(1)
|
||||
end)
|
||||
it("should add a task in order", function()
|
||||
WakeupMgr:addTask(9999, function() end)
|
||||
assert.is_equal(epoch1, WakeupMgr._task_queue[1].epoch)
|
||||
assert.stub(WakeupMgr.setWakeupAlarm).was.called(1)
|
||||
|
||||
WakeupMgr:addTask(123, function() end)
|
||||
assert.is_equal(epoch2, WakeupMgr._task_queue[1].epoch)
|
||||
assert.stub(WakeupMgr.setWakeupAlarm).was.called(2)
|
||||
end)
|
||||
it("should execute top task", function()
|
||||
assert.is_true(WakeupMgr:wakeupAction())
|
||||
end)
|
||||
it("should have removed executed task from stack", function()
|
||||
assert.is_equal(epoch1, WakeupMgr._task_queue[1].epoch)
|
||||
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)
|
||||
end)
|
||||
it("should remove arbitrary task from stack", function()
|
||||
WakeupMgr:removeTask(2)
|
||||
assert.is_equal(epoch1, WakeupMgr._task_queue[1].epoch)
|
||||
assert.is_equal(nil, WakeupMgr._task_queue[2])
|
||||
end)
|
||||
it("should execute last task", 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)
|
||||
end)
|
||||
end)
|
Loading…
Reference in New Issue