2
0
mirror of https://github.com/koreader/koreader synced 2024-11-13 19:11:25 +00:00
koreader/frontend/device/kobo/powerd.lua
NiLuJe abc86323fc [Kobo/Cervantes] Always use setBrightness to set the brightness (#5429)
sysfs_light handles every case sanely already, no need to be cute ;).

This prevents always setting the warmth when setting the FL on NL
devices with a mixer, because, besides being useless, it's also slow
(lua does mmap'ed io, so every handle requires an fstat + mmap).

It was subtly affecting the fluidity of the toggle ramp, and much less
subtly wreaking havoc on #5373 ;).

KA1 owners might want to investigate keeping the fds around one way or
another, like we do for the fl ioctl, to alleviate the overhead of doing
that for not one but three or four fds...
I won't be doing that, since I lack the hardware to test it, and FL+NL
stuff is pain.

* Do the same on Cervantes

* Bump base to pickup related ioctl tweaks
2019-09-26 23:59:31 +02:00

412 lines
18 KiB
Lua

local BasePowerD = require("device/generic/powerd")
local NickelConf = require("device/kobo/nickel_conf")
local PluginShare = require("pluginshare")
local SysfsLight = require ("device/sysfs_light")
local batt_state_folder =
"/sys/devices/platform/pmic_battery.1/power_supply/mc13892_bat/"
-- Here, we only deal with the real hw intensity.
-- Dealing with toggling and remembering/restoring
-- previous intensity when toggling/untoggling is done
-- by BasePowerD.
local KoboPowerD = BasePowerD:new{
fl_min = 0, fl_max = 100,
fl = nil,
batt_capacity_file = batt_state_folder .. "capacity",
is_charging_file = batt_state_folder .. "status",
fl_warmth_min = 0, fl_warmth_max = 100,
fl_warmth = nil,
auto_warmth = false,
max_warmth_hour = 23,
fl_was_on = nil,
}
--- @todo Remove KOBO_LIGHT_ON_START
function KoboPowerD:_syncKoboLightOnStart()
local new_intensity = nil
local is_frontlight_on = nil
local new_warmth = nil
local auto_warmth = nil
local kobo_light_on_start = tonumber(KOBO_LIGHT_ON_START)
if kobo_light_on_start then
if kobo_light_on_start > 0 then
new_intensity = math.min(kobo_light_on_start, 100)
is_frontlight_on = true
elseif kobo_light_on_start == 0 then
new_intensity = 0
is_frontlight_on = false
elseif kobo_light_on_start == -2 then -- get values from NickelConf
new_intensity = NickelConf.frontLightLevel.get()
is_frontlight_on = NickelConf.frontLightState:get()
if self.fl_warmth ~= nil then
local new_color = NickelConf.colorSetting.get()
if new_color ~= nil then
-- ColorSetting is stored as a color temperature scale in Kelvin,
-- from 1500 to 6400
-- so normalize this to [0,100] on our end.
new_warmth = (100 - math.floor((new_color - 1500) / 49))
end
auto_warmth = NickelConf.autoColorEnabled.get()
end
if is_frontlight_on == nil then
-- this device does not support frontlight toggle,
-- we set the value based on frontlight intensity.
if new_intensity > 0 then
is_frontlight_on = true
else
is_frontlight_on = false
end
end
if is_frontlight_on == false and new_intensity == 0 then
-- frontlight was toggled off in nickel, and we have no
-- level-before-toggle-off (firmware without "FrontLightState"):
-- use the one from koreader settings
new_intensity = G_reader_settings:readSetting("frontlight_intensity")
end
else -- if kobo_light_on_start == -1 or other unexpected value then
-- As we can't read value from the OS or hardware, use last values
-- stored in koreader settings
new_intensity = G_reader_settings:readSetting("frontlight_intensity")
is_frontlight_on = G_reader_settings:readSetting("is_frontlight_on")
if self.fl_warmth ~= nil then
new_warmth = G_reader_settings:readSetting("frontlight_warmth")
auto_warmth = G_reader_settings:readSetting("frontlight_auto_warmth")
end
end
end
if new_intensity ~= nil then
self.hw_intensity = new_intensity
end
if is_frontlight_on ~= nil then
-- will only be used to give initial state to BasePowerD:_decideFrontlightState()
self.initial_is_fl_on = is_frontlight_on
end
-- This is always read from G_reader_settings, since we do not
-- support reading 'BedTime' from NickelConf.
local max_warmth_hour =
G_reader_settings:readSetting("frontlight_max_warmth_hour")
if max_warmth_hour then
self.max_warmth_hour = max_warmth_hour
end
if auto_warmth then
self.auto_warmth = true
self:calculateAutoWarmth()
elseif new_warmth ~= nil then
self.fl_warmth = new_warmth
end
-- In any case frontlight is off, ensure intensity is non-zero so untoggle works
if self.initial_is_fl_on == false and self.hw_intensity == 0 then
self.hw_intensity = 1
end
end
function KoboPowerD:init()
-- Default values in case self:_syncKoboLightOnStart() does not find
-- any previously saved setting (and for unit tests where it will
-- not be called)
self.hw_intensity = 20
self.initial_is_fl_on = true
self.autowarmth_job_running = false
if self.device:hasFrontlight() then
-- If this device has natural light (currently only KA1 & Forma)
-- Use the SysFS interface, and ioctl otherwise.
-- NOTE: On the Forma, nickel still appears to prefer using ntx_io to handle the FL,
-- but it does use sysfs for the NL...
if self.device:hasNaturalLight() then
local nl_config = G_reader_settings:readSetting("natural_light_config")
if nl_config then
for key,val in pairs(nl_config) do
self.device.frontlight_settings[key] = val
end
end
-- Does this device's NaturalLight use a custom scale?
self.fl_warmth_min = self.device.frontlight_settings.nl_min or self.fl_warmth_min
self.fl_warmth_max = self.device.frontlight_settings.nl_max or self.fl_warmth_max
-- If this device has a mixer, we can use the ioctl for brightness control, as it's much lower latency.
if self.device:hasNaturalLightMixer() then
local kobolight = require("ffi/kobolight")
local ok, light = pcall(kobolight.open)
if ok then
self.device.frontlight_settings.frontlight_ioctl = light
end
end
self.fl = SysfsLight:new(self.device.frontlight_settings)
self.fl_warmth = 0
self:_syncKoboLightOnStart()
else
local kobolight = require("ffi/kobolight")
local ok, light = pcall(kobolight.open)
if ok then
self.fl = light
self:_syncKoboLightOnStart()
end
end
-- See discussion in https://github.com/koreader/koreader/issues/3118#issuecomment-334995879
-- for the reasoning behind this bit of insanity.
if self:isFrontlightOnHW() then
-- On devices with a mixer, setIntensity will *only* set the FL, so, ensure we honor the warmth, too.
if self.device:hasNaturalLightMixer() then
self:setWarmth(self.fl_warmth)
end
-- Use setIntensity to ensure it sets fl_intensity, and because we don't want the ramping behavior of turnOn
self:setIntensity(self:frontlightIntensityHW())
else
-- Use setBrightness so as *NOT* to set hw_intensity, so toggle will still (mostly) work.
self.fl:setBrightness(0)
-- And make sure the fact that we started with the FL off propagates as best as possible.
self.initial_is_fl_on = false
-- NOTE: BasePowerD's init sets fl_intensity to hw_intensity right after this,
-- so, instead of simply setting hw_intensity to either 1 or fl_min or fl_intensity, depending on user preference,
-- we jump through a couple of hoops in turnOnFrontlightHW to recover from the first quirky toggle...
end
end
end
function KoboPowerD:saveSettings()
if self.device:hasFrontlight() then
-- Store BasePowerD values into settings (and not our hw_intensity, so
-- that if frontlight was toggled off, we save and restore the previous
-- untoggled intensity and toggle state at next startup)
local cur_intensity = self.fl_intensity
-- If we're shutting down straight from suspend then the frontlight won't
-- be turned on but we still want to save its state.
local cur_is_fl_on = self.is_fl_on or self.fl_was_on or false
local cur_warmth = self.fl_warmth
local cur_auto_warmth = self.auto_warmth
local cur_max_warmth_hour = self.max_warmth_hour
-- Save intensity to koreader settings
G_reader_settings:saveSetting("frontlight_intensity", cur_intensity)
G_reader_settings:saveSetting("is_frontlight_on", cur_is_fl_on)
if cur_warmth ~= nil then
G_reader_settings:saveSetting("frontlight_warmth", cur_warmth)
G_reader_settings:saveSetting("frontlight_auto_warmth", cur_auto_warmth)
G_reader_settings:saveSetting("frontlight_max_warmth_hour", cur_max_warmth_hour)
end
-- And to "Kobo eReader.conf" if needed
if KOBO_SYNC_BRIGHTNESS_WITH_NICKEL then
if NickelConf.frontLightState.get() ~= nil then
if NickelConf.frontLightState.get() ~= cur_is_fl_on then
NickelConf.frontLightState.set(cur_is_fl_on)
end
else -- no support for frontlight state
if not cur_is_fl_on then -- if toggled off, save intensity as 0
cur_intensity = self.fl_min
end
end
if NickelConf.frontLightLevel.get() ~= cur_intensity then
NickelConf.frontLightLevel.set(cur_intensity)
end
if cur_warmth ~= nil then
local warmth_rescaled = (100 - cur_warmth) * 49 + 1500
if NickelConf.colorSetting.get() ~= warmth_rescaled then
NickelConf.colorSetting.set(warmth_rescaled)
end
if NickelConf.autoColorEnabled.get() ~= cur_auto_warmth then
NickelConf.autoColorEnabled.set(cur_auto_warmth)
end
end
end
end
end
function KoboPowerD:frontlightIntensityHW()
return self.hw_intensity
end
function KoboPowerD:isFrontlightOnHW()
if self.initial_is_fl_on ~= nil then -- happens only once after init()
-- give initial state to BasePowerD, which will
-- reset our self.hw_intensity to 0 if self.initial_is_fl_on is false
local ret = self.initial_is_fl_on
self.initial_is_fl_on = nil
return ret
end
return self.hw_intensity > 0
end
function KoboPowerD:setIntensityHW(intensity)
if self.fl == nil then return end
self.fl:setBrightness(intensity)
self.hw_intensity = intensity
-- Now that we have set intensity, we need to let BasePowerD
-- know about possibly changed frontlight state (if we came
-- from light toggled off to some intensity > 0).
self:_decideFrontlightState()
end
function KoboPowerD:setWarmth(warmth)
if self.fl == nil then return end
if not warmth and self.auto_warmth then
self:calculateAutoWarmth()
end
self.fl_warmth = warmth or self.fl_warmth
-- Don't turn the light back on on legacy NaturalLight devices just for the sake of setting the warmth!
-- That's because we can only set warmth independently of brightness on devices with a mixer.
-- On older ones, calling setWarmth *will* actually set the brightness, too!
if self.device:hasNaturalLightMixer() or self:isFrontlightOnHW() then
self.fl:setWarmth(self.fl_warmth)
end
end
-- Sets fl_warmth according to current hour and max_warmth_hour
-- and starts background job if necessary.
function KoboPowerD:calculateAutoWarmth()
local current_time = os.date("%H") + os.date("%M")/60
local max_hour = self.max_warmth_hour
local diff_time = max_hour - current_time
if diff_time < 0 then
diff_time = diff_time + 24
end
if diff_time < 12 then
-- We are before bedtime. Use a slower progression over 5h.
self.fl_warmth = math.max(20 * (5 - diff_time), 0)
elseif diff_time > 22 then
-- Keep warmth at maximum for two hours after bedtime.
self.fl_warmth = 100
else
-- Between 2-4h after bedtime, return to zero.
self.fl_warmth = math.max(100 - 50 * (22 - diff_time), 0)
end
self.fl_warmth = math.floor(self.fl_warmth + 0.5)
-- Make sure sysfs_light actually picks that new value up without an explicit setWarmth call...
-- This avoids having to bypass the ramp-up on resume w/ an explicit setWarmth call on devices where brightness & warmth
-- are linked (i.e., when there's no mixer) ;).
-- NOTE: A potentially saner solution would be to ditch the internal sysfs_light current_* values,
-- and just pass it a pointer to this powerd instance, so it has access to fl_warmth & hw_intensity.
-- It seems harmless enough for warmth, but brightness might be a little trickier because of the insanity
-- that is hw_intensity handling because we can't actually *read* the frontlight status...
-- (Technically, we could, on Mk. 7 devices, but we don't,
-- because this is already messy enough without piling on special cases.)
if self.fl then
self.fl.current_warmth = self.fl_warmth
end
-- Enable background job for setting Warmth, if not already done.
if not self.autowarmth_job_running then
table.insert(PluginShare.backgroundJobs, {
when = 180,
repeated = true,
executable = function()
if self.auto_warmth then
self:setWarmth()
end
end,
})
self.autowarmth_job_running = true
end
end
function KoboPowerD:getCapacityHW()
return self:read_int_file(self.batt_capacity_file)
end
function KoboPowerD:isChargingHW()
return self:read_str_file(self.is_charging_file) == "Charging\n"
end
function KoboPowerD:turnOffFrontlightHW()
if not self:isFrontlightOnHW() then
return
end
local util = require("ffi/util")
util.runInSubProcess(function()
for i = 1,5 do
self:_setIntensity(math.floor(self.fl_intensity - ((self.fl_intensity / 5) * i)))
--- @note: Newer devices appear to block slightly longer on FL ioctls/sysfs, so only sleep on older devices,
--- otherwise we get a jump and not a ramp ;).
if not self.device:hasNaturalLight() then
if (i < 5) then
util.usleep(35 * 1000)
end
end
end
end, false, true)
-- NOTE: This is essentially what _setIntensity does, except we don't actually touch the FL,
-- we only sync the state of the main process with the final state of what we're doing in the forks.
-- And update hw_intensity in our actual process ;).
self.hw_intensity = self.fl_min
-- NOTE: And don't forget to update sysfs_light, too, as a real _setIntensity would via setBrightness
if self.fl then
self.fl.current_brightness = self.fl_min
end
self:_decideFrontlightState()
-- And let the footer know of the change
if package.loaded["ui/uimanager"] ~= nil then
local Event = require("ui/event")
local UIManager = require("ui/uimanager")
UIManager:broadcastEvent(Event:new("FrontlightStateChanged"))
end
end
function KoboPowerD:turnOnFrontlightHW()
-- NOTE: Insane workaround for the first toggle after a startup with the FL off.
-- The light is actually off, but hw_intensity couldn't have been set to a sane value because of a number of interactions.
-- So, fix it now, so we pass the isFrontlightOnHW check (which checks if hw_intensity > fl_min).
if (self.is_fl_on == false and self.hw_intensity > self.fl_min and self.hw_intensity == self.fl_intensity) then
self.hw_intensity = self.fl_min
end
if self:isFrontlightOnHW() then
return
end
local util = require("ffi/util")
util.runInSubProcess(function()
for i = 1,5 do
self:_setIntensity(math.ceil(self.fl_min + ((self.fl_intensity / 5) * i)))
--- @note: Newer devices appear to block slightly longer on FL ioctls/sysfs, so only sleep on older devices,
--- otherwise we get a jump and not a ramp ;).
if not self.device:hasNaturalLight() then
if (i < 5) then
util.usleep(35 * 1000)
end
end
end
end, false, true)
-- NOTE: This is essentially what _setIntensity does, except we don't actually touch the FL,
-- we only sync the state of the main process with the final state of what we're doing in the forks.
-- And update hw_intensity in our actual process ;).
self.hw_intensity = self.fl_intensity
-- NOTE: And don't forget to update sysfs_light, too, as a real _setIntensity would via setBrightness
if self.fl then
self.fl.current_brightness = self.fl_intensity
end
self:_decideFrontlightState()
-- And let the footer know of the change
if package.loaded["ui/uimanager"] ~= nil then
local Event = require("ui/event")
local UIManager = require("ui/uimanager")
UIManager:broadcastEvent(Event:new("FrontlightStateChanged"))
end
end
-- Turn off front light before suspend.
function KoboPowerD:beforeSuspend()
if self.fl == nil then return end
-- Remember the current frontlight state
self.fl_was_on = self.is_fl_on
-- Turn off the frontlight
self:turnOffFrontlight()
end
-- Restore front light state after resume.
function KoboPowerD:afterResume()
if self.fl == nil then return end
-- Don't bother if the light was already off on suspend
if not self.fl_was_on then return end
-- Update AutoWarmth state
if self.fl_warmth ~= nil and self.auto_warmth then
self:calculateAutoWarmth()
-- And we need an explicit setWarmth if the device has a mixer, because turnOn won't touch the warmth on those ;).
if self.device:hasNaturalLightMixer() then
self:setWarmth(self.fl_warmth)
end
end
-- Turn the frontlight back on
self:turnOnFrontlight()
end
return KoboPowerD