Files
koreader/frontend/device/kobo/device.lua
NiLuJe 4264d915b1 Kobo: Refactor suspend in order to be able to catch input events sent during the 2s window of ntx madness (#12330)
I don't even remember how badly things broke (at least on old devices) without it, despite it making absolutely no sense at all (state-extended just flips a global that dictates whether some things get flagged as wakeup sources or not).

So, don't rock the boat too much: we don't remove it, but instead of using a sleep, we use a task deadline instead, which ensures we'll keep processing input events in the right order in the meantime. We'll already have neutered input by this point, so we'll only process power events anyway.

That means that the only iffy things are potentially *when* and *where* we have to potentially cancel that task. Resume makes sense, of course, and we log an info message to make the log flow clear; but we also do so in suspend... just in case. With a warning log because that probably indicates something fishy went on.

Also cleanup the comments while I'm there, and actually rewrite the wakeup_count stuff properly so it could actually theoretically be used if ntx kernels were actually reliable. Spoiler alert: they're not, this is still horribly broken on at least Mk < 7. Works just fine on a Forma, though, so, yay.

Fix #12325
2024-08-12 03:47:43 +02:00

1777 lines
68 KiB
Lua

local Generic = require("device/generic/device")
local Geom = require("ui/geometry")
local UIManager
local WakeupMgr = require("device/wakeupmgr")
local ffiUtil = require("ffi/util")
local lfs = require("libs/libkoreader-lfs")
local logger = require("logger")
local time = require("ui/time")
local util = require("util")
local _ = require("gettext")
-- We're going to need a few <linux/fb.h> & <linux/input.h> constants...
local ffi = require("ffi")
local C = ffi.C
require("ffi/linux_fb_h")
require("ffi/linux_input_h")
require("ffi/posix_h")
require("ffi/fbink_input_h")
local function yes() return true end
local function no() return false end
local function NOP() return end
local function koboEnableWifi(toggle)
if toggle == true then
logger.info("Kobo Wi-Fi: enabling Wi-Fi")
os.execute("./enable-wifi.sh")
else
logger.info("Kobo Wi-Fi: disabling Wi-Fi")
os.execute("./disable-wifi.sh")
end
end
-- checks if standby is available on the device
local function checkStandby(target_state)
logger.dbg("Kobo: checking if standby is possible ...")
local f = io.open("/sys/power/state")
if not f then
return no
end
local mode = f:read()
f:close()
logger.dbg("Kobo: available power states:", mode)
if mode and mode:find(target_state) then
logger.dbg("Kobo: target standby state '" .. target_state .. "' is supported")
return yes
end
logger.dbg("Kobo: target standby state '" .. target_state .. "' is unsupported")
return no
end
-- Return the highest core number
local function getCPUCount()
local fd = io.open("/sys/devices/system/cpu/possible", "re")
if fd then
local str = fd:read("*line")
fd:close()
-- Format is n-N, where n is the first core, and N the last (e.g., 0-3)
return tonumber(str:match("%d+$")) or 1
else
return 1
end
end
local function getCPUGovernor(knob)
local fd = io.open(knob, "re")
if fd then
local str = fd:read("*line")
fd:close()
-- If we're currently using the userspace governor, fudge that to conservative, as we won't ever standby with Wi-Fi on.
-- (userspace is only used on i.MX5 for DVFS shenanigans when Wi-Fi is enabled)
if str == "userspace" then
str = "conservative"
end
return str
else
return nil
end
end
local function getRTCName()
local fd = io.open("/sys/class/rtc/rtc0/name", "re")
if fd then
local str = fd:read("*line")
fd:close()
return str
else
return nil
end
end
local Kobo = Generic:extend{
model = "Kobo",
ota_model = "kobo",
isKobo = yes,
isTouchDevice = yes, -- all of them are
hasOTAUpdates = yes,
hasFastWifiStatusQuery = yes,
hasWifiManager = yes,
hasWifiRestore = yes,
canStandby = no, -- will get updated by checkStandby()
canReboot = yes,
canPowerOff = yes,
canSuspend = yes,
supportsScreensaver = yes,
-- most Kobos are MT-capable
hasMultitouch = yes,
-- most Kobos have X/Y switched for the touch screen
touch_switch_xy = true,
-- most Kobos have also mirrored X coordinates
touch_mirrored_x = true,
-- but a few mirror on the Y axis instead
touch_mirrored_y = false,
-- enforce portrait mode on Kobos
--- @note: In practice, the check that is used for in ffi/framebuffer is no longer relevant,
--- since, in almost every case, we enforce a hardware Portrait rotation via fbdepth on startup by default ;).
--- We still want to keep it in case an unfortunate soul on an older device disables the bitdepth switch...
isAlwaysPortrait = yes,
-- we don't need an extra refreshFull on resume, thank you very much.
needsScreenRefreshAfterResume = no,
-- some devices have coloured frontlights
hasNaturalLight = no,
hasNaturalLightMixer = no,
-- HW inversion is generally safe on Kobo, except on a few boards/kernels
canHWInvert = yes,
home_dir = "/mnt/onboard",
canToggleMassStorage = yes,
-- New devices *may* be REAGL-aware, but generally don't expect explicit REAGL requests, default to not.
isREAGL = no,
-- Mark 7 devices sport an updated driver.
isMk7 = no,
-- MXCFB_WAIT_FOR_UPDATE_COMPLETE ioctls are generally reliable
hasReliableMxcWaitFor = yes,
-- AllWinner SoCs require a completely different fb backend...
isSunxi = no,
-- The fb backend also needs to know if we're on a MediaTek SoC.
isMTK = no,
-- On sunxi, "native" panel layout used to compute the G2D rotation handle (e.g., deviceQuirks.nxtBootRota in FBInk).
boot_rota = nil,
-- Standard sysfs path to the battery directory
battery_sysfs = "/sys/class/power_supply/mc13892_bat",
-- Stable path to the NTX input device
ntx_dev = "/dev/input/event0",
ntx_fd = nil,
-- Stable path to the Touch input device
touch_dev = "/dev/input/event1",
-- Event code to use to detect contact pressure
pressure_event = nil,
-- Device features multiple CPU cores
isSMP = no,
-- Device supports "eclipse" waveform modes (i.e., optimized for nightmode).
hasEclipseWfm = no,
-- Device ships with various hardware revisions under the same device code, requiring automatic hardware detection (PMIC & FL)...
automagic_sysfs = false,
-- The standard "standby" power state
standby_state = "standby",
unexpected_wakeup_count = 0,
}
local KoboTrilogyA = Kobo:extend{
model = "Kobo_trilogy_A",
-- Unlike its B brethren, this one doesn't do the weird translation dance when ABS_PRESSURE is 0...
hasKeys = yes,
hasMultitouch = no,
}
-- Kobo Touch B:
local KoboTrilogyB = Kobo:extend{
model = "Kobo_trilogy_B",
touch_kobo_mk3_protocol = true,
hasKeys = yes,
hasMultitouch = no,
}
-- Kobo Touch C:
local KoboTrilogyC = Kobo:extend{
model = "Kobo_trilogy_C",
hasKeys = yes,
hasMultitouch = no,
}
-- Kobo Mini:
local KoboPixie = Kobo:extend{
model = "Kobo_pixie",
display_dpi = 200,
hasMultitouch = no,
-- bezel:
viewport = Geom:new{x=0, y=2, w=596, h=794},
}
-- Kobo Aura One:
local KoboDaylight = Kobo:extend{
model = "Kobo_daylight",
hasFrontlight = yes,
touch_phoenix_protocol = true,
display_dpi = 300,
hasNaturalLight = yes,
frontlight_settings = {
frontlight_white = "/sys/class/backlight/lm3630a_led1b",
frontlight_red = "/sys/class/backlight/lm3630a_led1a",
frontlight_green = "/sys/class/backlight/lm3630a_ledb",
},
}
-- Kobo Aura H2O:
local KoboDahlia = Kobo:extend{
model = "Kobo_dahlia",
canToggleChargingLED = yes,
led_uses_channel_3 = true,
hasFrontlight = yes,
touch_phoenix_protocol = true,
-- There's no slot 0, the first finger gets assigned slot 1, and the second slot 2.
-- NOTE: Could be queried at runtime via EVIOCGABS on C.ABS_MT_TRACKING_ID (minimum field).
-- Used to be handled via an adjustTouchAlyssum hook that just mangled ABS_MT_TRACKING_ID values.
main_finger_slot = 1,
display_dpi = 265,
-- the bezel covers the top 11 pixels:
viewport = Geom:new{x=0, y=11, w=1080, h=1429},
}
-- Kobo Aura HD:
local KoboDragon = Kobo:extend{
model = "Kobo_dragon",
hasFrontlight = yes,
hasMultitouch = no,
display_dpi = 265,
}
-- Kobo Glo:
local KoboKraken = Kobo:extend{
model = "Kobo_kraken",
hasFrontlight = yes,
hasMultitouch = no,
display_dpi = 212,
}
-- Kobo Aura:
local KoboPhoenix = Kobo:extend{
model = "Kobo_phoenix",
hasFrontlight = yes,
touch_phoenix_protocol = true,
display_dpi = 212,
-- The bezel covers 10 pixels at the bottom:
viewport = Geom:new{x=0, y=0, w=758, h=1014},
-- NOTE: AFAICT, the Aura was the only one explicitly requiring REAGL requests...
isREAGL = yes,
-- NOTE: May have a buggy kernel, according to the nightmode hack...
canHWInvert = no,
}
-- Kobo Aura H2O2:
local KoboSnow = Kobo:extend{
model = "Kobo_snow",
hasFrontlight = yes,
touch_snow_protocol = true,
touch_mirrored_x = false,
display_dpi = 265,
hasNaturalLight = yes,
frontlight_settings = {
frontlight_white = "/sys/class/backlight/lm3630a_ledb",
frontlight_red = "/sys/class/backlight/lm3630a_led",
frontlight_green = "/sys/class/backlight/lm3630a_leda",
},
}
-- Kobo Aura H2O2, Rev2:
--- @fixme Check if the Clara fix actually helps here... (#4015)
local KoboSnowRev2 = Kobo:extend{
model = "Kobo_snow_r2",
isMk7 = yes,
hasFrontlight = yes,
touch_snow_protocol = true,
display_dpi = 265,
hasNaturalLight = yes,
frontlight_settings = {
frontlight_white = "/sys/class/backlight/lm3630a_ledb",
frontlight_red = "/sys/class/backlight/lm3630a_leda",
},
}
-- Kobo Aura second edition:
local KoboStar = Kobo:extend{
model = "Kobo_star",
hasFrontlight = yes,
touch_phoenix_protocol = true,
display_dpi = 212,
}
-- Kobo Aura second edition, Rev 2:
local KoboStarRev2 = Kobo:extend{
model = "Kobo_star_r2",
isMk7 = yes,
hasFrontlight = yes,
touch_phoenix_protocol = true,
display_dpi = 212,
}
-- Kobo Glo HD:
local KoboAlyssum = Kobo:extend{
model = "Kobo_alyssum",
hasFrontlight = yes,
touch_phoenix_protocol = true,
main_finger_slot = 1,
display_dpi = 300,
}
-- Kobo Touch 2.0:
local KoboPika = Kobo:extend{
model = "Kobo_pika",
touch_phoenix_protocol = true,
main_finger_slot = 1,
}
-- Kobo Clara HD:
local KoboNova = Kobo:extend{
model = "Kobo_nova",
isMk7 = yes,
canToggleChargingLED = yes,
hasFrontlight = yes,
touch_snow_protocol = true,
display_dpi = 300,
hasNaturalLight = yes,
frontlight_settings = {
frontlight_white = "/sys/class/backlight/mxc_msp430.0/brightness",
frontlight_mixer = "/sys/class/backlight/lm3630a_led/color",
nl_min = 0,
nl_max = 10,
nl_inverted = true,
},
}
-- Kobo Forma:
-- NOTE: Right now, we enforce Portrait orientation on startup to avoid getting touch coordinates wrong,
-- no matter the rotation we were started from (c.f., platform/kobo/koreader.sh).
-- NOTE: For the FL, assume brightness is WO, and actual_brightness is RO!
-- i.e., we could have a real KoboPowerD:frontlightIntensityHW() by reading actual_brightness ;).
-- NOTE: Rotation events *may* not be enabled if Nickel has never been brought up in that power cycle.
-- i.e., this will affect KSM users.
-- c.f., https://github.com/koreader/koreader/pull/4414#issuecomment-449652335
-- There's also a CM_ROTARY_ENABLE command, but which seems to do as much nothing as the STATUS one...
local KoboFrost = Kobo:extend{
model = "Kobo_frost",
isMk7 = yes,
canToggleChargingLED = yes,
hasFrontlight = yes,
hasKeys = yes,
hasGSensor = yes,
touch_snow_protocol = true,
display_dpi = 300,
hasNaturalLight = yes,
frontlight_settings = {
frontlight_white = "/sys/class/backlight/mxc_msp430.0/brightness",
frontlight_mixer = "/sys/class/backlight/tlc5947_bl/color",
-- Warmth goes from 0 to 10 on the device's side (our own internal scale is still normalized to [0...100])
-- NOTE: Those three extra keys are *MANDATORY* if frontlight_mixer is set!
nl_min = 0,
nl_max = 10,
nl_inverted = true,
},
}
-- Kobo Libra:
-- NOTE: Assume the same quirks as the Forma apply.
local KoboStorm = Kobo:extend{
model = "Kobo_storm",
isMk7 = yes,
canToggleChargingLED = yes,
hasFrontlight = yes,
hasKeys = yes,
hasGSensor = yes,
touch_snow_protocol = true,
display_dpi = 300,
hasNaturalLight = yes,
frontlight_settings = {
frontlight_white = "/sys/class/backlight/mxc_msp430.0/brightness",
frontlight_mixer = "/sys/class/backlight/lm3630a_led/color",
-- Warmth goes from 0 to 10 on the device's side (our own internal scale is still normalized to [0...100])
-- NOTE: Those three extra keys are *MANDATORY* if frontlight_mixer is set!
nl_min = 0,
nl_max = 10,
nl_inverted = true,
},
-- NOTE: The Libra apparently suffers from a mysterious issue where completely innocuous WAIT_FOR_UPDATE_COMPLETE ioctls
-- will mysteriously fail with a timeout (5s)...
-- This obviously leads to *terrible* user experience,
-- so we've tried a few things over the years to attempt to deal with it.
-- c.f., https://github.com/koreader/koreader/issues/7340 for the genesis of all that.
-- NOTE: On a possibly related note, on NXP devices (even earlier ones), Nickel will *always* wait for markers in pairs:
-- the "expected" marker to wait for, and the *previous* one right before that.
-- Of course, that first wait will mostly always return early, because that refresh is usually much older and already dealt with.
-- This weird quirk was dropped on sunxi & MTK, FWIW.
hasReliableMxcWaitFor = no,
}
-- Kobo Nia:
local KoboLuna = Kobo:extend{
model = "Kobo_luna",
isMk7 = yes,
canToggleChargingLED = yes,
hasFrontlight = yes,
touch_phoenix_protocol = true,
display_dpi = 212,
hasReliableMxcWaitFor = no, -- Board is similar to the Libra 2, but it's such an unpopular device that reports are scarce.
-- Handle the HW revision w/ a BD71828 PMIC
automagic_sysfs = true,
}
-- Kobo Elipsa
local KoboEuropa = Kobo:extend{
model = "Kobo_europa",
isSunxi = yes,
hasEclipseWfm = yes,
canToggleChargingLED = yes,
led_uses_channel_3 = true,
hasFrontlight = yes,
hasGSensor = yes,
pressure_event = C.ABS_MT_PRESSURE,
display_dpi = 227,
boot_rota = C.FB_ROTATE_CCW,
battery_sysfs = "/sys/class/power_supply/battery",
isSMP = yes,
}
-- Kobo Sage
local KoboCadmus = Kobo:extend{
model = "Kobo_cadmus",
isSunxi = yes,
hasEclipseWfm = yes,
canToggleChargingLED = yes,
led_uses_channel_3 = true,
hasFrontlight = yes,
hasKeys = yes,
hasGSensor = yes,
pressure_event = C.ABS_MT_PRESSURE,
display_dpi = 300,
hasNaturalLight = yes,
frontlight_settings = {
frontlight_white = "/sys/class/backlight/mxc_msp430.0/brightness",
frontlight_mixer = "/sys/class/leds/aw99703-bl_FL1/color",
-- Warmth goes from 0 to 10 on the device's side (our own internal scale is still normalized to [0...100])
-- NOTE: Those three extra keys are *MANDATORY* if frontlight_mixer is set!
nl_min = 0,
nl_max = 10,
nl_inverted = false,
},
boot_rota = C.FB_ROTATE_CW,
battery_sysfs = "/sys/class/power_supply/battery",
hasAuxBattery = yes,
aux_battery_sysfs = "/sys/class/misc/cilix",
isSMP = yes,
-- Much like the Libra 2, there are at least two different HW revisions, with different PMICs...
automagic_sysfs = true,
}
-- Kobo Libra 2:
local KoboIo = Kobo:extend{
model = "Kobo_io",
isMk7 = yes,
hasEclipseWfm = yes,
canToggleChargingLED = yes,
hasFrontlight = yes,
hasKeys = yes,
hasGSensor = yes,
pressure_event = C.ABS_MT_PRESSURE,
touch_mirrored_x = false,
display_dpi = 300,
hasNaturalLight = yes,
frontlight_settings = {
frontlight_white = "/sys/class/backlight/mxc_msp430.0/brightness",
-- Warmth goes from 0 to 10 on the device's side (our own internal scale is still normalized to [0...100])
-- NOTE: Those three extra keys are *MANDATORY* if frontlight_mixer is set!
nl_min = 0,
nl_max = 10,
nl_inverted = true,
},
-- It would appear that the Libra 2 inherited its ancestor's quirks, and more...
-- c.f., https://github.com/koreader/koreader/issues/8414 & https://github.com/koreader/koreader/issues/8664
hasReliableMxcWaitFor = no,
-- NOTE: There are at least two hardware revisions of this device (*without* a device code change, this time),
-- with *significant* hardware changes, so we'll handle this by making the sysfs path discovery automagic.
-- c.f., https://github.com/koreader/koreader/issues/9218
automagic_sysfs = true,
}
-- Kobo Clara 2E:
local KoboGoldfinch = Kobo:extend{
model = "Kobo_goldfinch",
isMk7 = yes,
hasEclipseWfm = yes,
canToggleChargingLED = yes,
led_uses_channel_3 = true,
hasFrontlight = yes,
display_dpi = 300,
hasNaturalLight = yes,
frontlight_settings = {
frontlight_white = "/sys/class/backlight/mxc_msp430.0/brightness",
frontlight_mixer = "/sys/class/leds/aw99703-bl_FL1/color",
nl_min = 0,
nl_max = 10,
nl_inverted = true,
},
battery_sysfs = "/sys/class/power_supply/battery",
-- Board is eerily similar to the Libra 2, so, it inherits the same quirks...
-- c.f., https://github.com/koreader/koreader/issues/9552#issuecomment-1293000313
hasReliableMxcWaitFor = no,
}
-- Kobo Elipsa 2E:
local KoboCondor = Kobo:extend{
model = "Kobo_condor",
isMTK = yes,
hasEclipseWfm = yes,
canToggleChargingLED = yes,
hasFrontlight = yes,
hasGSensor = yes,
display_dpi = 227,
pressure_event = C.ABS_MT_PRESSURE,
touch_mirrored_x = false,
touch_mirrored_y = true,
hasNaturalLight = yes,
frontlight_settings = {
frontlight_white = "/sys/class/backlight/mxc_msp430.0/brightness",
frontlight_mixer = "/sys/class/leds/aw99703-bl_FL1/color",
nl_min = 0,
nl_max = 10,
nl_inverted = true,
},
battery_sysfs = "/sys/class/power_supply/bd71827_bat",
isSMP = yes,
}
-- Kobo Libra Colour:
local KoboMonza = Kobo:extend{
model = "Kobo_monza",
isMTK = yes,
hasEclipseWfm = yes,
canToggleChargingLED = yes,
hasFrontlight = yes,
hasKeys = yes,
hasGSensor = yes,
display_dpi = 300,
pressure_event = C.ABS_MT_PRESSURE,
touch_mirrored_x = false,
touch_mirrored_y = true,
hasNaturalLight = yes,
frontlight_settings = {
frontlight_white = "/sys/class/backlight/mxc_msp430.0/brightness",
frontlight_mixer = "/sys/class/backlight/lm3630a_led/color",
nl_min = 0,
nl_max = 10,
nl_inverted = true,
},
battery_sysfs = "/sys/class/power_supply/bd71827_bat",
isSMP = yes,
hasColorScreen = yes,
}
-- Kobo Clara B/W:
local KoboSpaBW = Kobo:extend{
model = "Kobo_spaBW",
isMTK = yes,
hasEclipseWfm = yes,
canToggleChargingLED = yes,
hasFrontlight = yes,
touch_snow_protocol = true,
display_dpi = 300,
hasNaturalLight = yes,
frontlight_settings = {
frontlight_white = "/sys/class/backlight/mxc_msp430.0/brightness",
frontlight_mixer = "/sys/class/backlight/lm3630a_led/color",
nl_min = 0,
nl_max = 10,
nl_inverted = true,
},
battery_sysfs = "/sys/class/power_supply/bd71827_bat",
}
-- Kobo Clara Colour:
local KoboSpaColour = Kobo:extend{
model = "Kobo_spaColour",
isMTK = yes,
hasEclipseWfm = yes,
canToggleChargingLED = yes,
hasFrontlight = yes,
touch_snow_protocol = true,
display_dpi = 300,
hasNaturalLight = yes,
frontlight_settings = {
frontlight_white = "/sys/class/backlight/mxc_msp430.0/brightness",
frontlight_mixer = "/sys/class/backlight/lm3630a_led/color",
nl_min = 0,
nl_max = 10,
nl_inverted = true,
},
battery_sysfs = "/sys/class/power_supply/bd71827_bat",
isSMP = yes,
hasColorScreen = yes,
}
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() and not self.powerd:isAuxCharged())
else
self:toggleChargingLED(self.powerd:isCharging() and not self.powerd:isCharged())
end
end
end
function Kobo:getKeyRepeat()
-- Sanity check (mostly for the testsuite's benefit...)
if not self.ntx_fd then
return false
end
self.key_repeat = ffi.new("unsigned int[?]", C.REP_CNT)
if C.ioctl(self.ntx_fd, C.EVIOCGREP, self.key_repeat) < 0 then
local err = ffi.errno()
logger.warn("Device:getKeyRepeat: EVIOCGREP ioctl on fd", self.ntx_fd, "failed:", ffi.string(C.strerror(err)))
return false
else
logger.dbg("Key repeat is set up to repeat every", self.key_repeat[C.REP_PERIOD], "ms after a delay of", self.key_repeat[C.REP_DELAY], "ms")
self.canKeyRepeat = yes
return true
end
end
function Kobo:disableKeyRepeat()
-- NOTE: LuaJIT zero inits, and PERIOD == 0 with DELAY == 0 disables repeats ;).
local key_repeat = ffi.new("unsigned int[?]", C.REP_CNT)
if C.ioctl(self.ntx_fd, C.EVIOCSREP, key_repeat) < 0 then
local err = ffi.errno()
logger.warn("Device:disableKeyRepeat: EVIOCSREP ioctl on fd", self.ntx_fd, "failed:", ffi.string(C.strerror(err)))
end
end
function Kobo:restoreKeyRepeat()
if C.ioctl(self.ntx_fd, C.EVIOCSREP, self.key_repeat) < 0 then
local err = ffi.errno()
logger.warn("Device:restoreKeyRepeat: EVIOCSREP ioctl on fd", self.ntx_fd, "failed:", ffi.string(C.strerror(err)))
end
end
function Kobo:toggleKeyRepeat(toggle)
local key_repeat = ffi.new("unsigned int[?]", C.REP_CNT)
if toggle == true then
-- Use the defaults from a Sage, as we can't guarantee the state of the setup on startup, so we can't just use self.key_repeat
key_repeat[C.REP_DELAY] = 400
key_repeat[C.REP_PERIOD] = 80
elseif toggle == false then
key_repeat[C.REP_DELAY] = 0
key_repeat[C.REP_PERIOD] = 0
else
-- Check the current (kernel) state to know what to do
if C.ioctl(self.ntx_fd, C.EVIOCGREP, key_repeat) < 0 then
local err = ffi.errno()
logger.warn("Device:toggleKeyRepeat: EVIOCGREP ioctl on fd", self.ntx_fd, "failed:", ffi.string(C.strerror(err)))
return false
else
if key_repeat[C.REP_DELAY] == 0 and key_repeat[C.REP_PERIOD] == 0 then
return self:toggleKeyRepeat(true)
else
return self:toggleKeyRepeat(false)
end
end
end
if C.ioctl(self.ntx_fd, C.EVIOCSREP, key_repeat) < 0 then
local err = ffi.errno()
logger.warn("Device:toggleKeyRepeat: EVIOCSREP ioctl on fd", self.ntx_fd, "failed:", ffi.string(C.strerror(err)))
return false
end
return true
end
function Kobo:init()
-- Check if we need to disable MXCFB_WAIT_FOR_UPDATE_COMPLETE ioctls...
local mxcfb_bypass_wait_for
if G_reader_settings:has("mxcfb_bypass_wait_for") then
mxcfb_bypass_wait_for = G_reader_settings:isTrue("mxcfb_bypass_wait_for")
else
mxcfb_bypass_wait_for = not self:hasReliableMxcWaitFor()
end
if self:isSunxi() then
self.screen = require("ffi/framebuffer_sunxi"):new{
device = self,
debug = logger.dbg,
is_always_portrait = self.isAlwaysPortrait(),
mxcfb_bypass_wait_for = mxcfb_bypass_wait_for,
boot_rota = self.boot_rota,
}
-- Sunxi means no HW inversion :(
self.canHWInvert = no
else
self.screen = require("ffi/framebuffer_mxcfb"):new{
device = self,
debug = logger.dbg,
is_always_portrait = self.isAlwaysPortrait(),
mxcfb_bypass_wait_for = mxcfb_bypass_wait_for,
no_cfa_post_processing = G_reader_settings:isTrue("no_cfa_post_processing"),
}
if self.screen.fb_bpp == 32 and self.screen._vinfo.red.offset ~= 0 then
-- Ensure we decode images properly, as our framebuffer is BGRA...
logger.info("Enabling Kobo @ 32bpp BGR tweaks")
self.hasBGRFrameBuffer = yes
end
end
-- So far, MTK kernels do not export a per-request inversion flag
if self:isMTK() then
-- Instead, there's a global flag that we can *set* (but not *get*) via a procfs knob...
-- Overload the HWNightMode stuff to implement that properly, like we do on Kindle
function self.screen:setHWNightmode(toggle)
-- No getter, so, keep track of our own state
self.hw_night_mode = toggle
-- Flip the global invert_fb flag
ffiUtil.writeToSysfs(toggle and "night_mode 4" or "night_mode 0", "/proc/hwtcon/cmd")
end
function self.screen:getHWNightmode()
-- Return false on nil for reader.lua's sake, mostly.
-- (We want to disable this on exit, always, as it will never be used by Nickel, which does SW inversion).
return self.hw_night_mode == true
end
end
-- Just to be safe, we absolutely don't want to call open on this, so just use stat
self.has_wakeup_count = util.pathExists("/sys/power/wakeup_count")
-- Automagic sysfs discovery
if self.automagic_sysfs then
-- Battery
if util.pathExists("/sys/class/power_supply/battery") then
-- Newer devices (circa sunxi)
self.battery_sysfs = "/sys/class/power_supply/battery"
elseif util.fileExists("/sys/class/power_supply/bd71827_bat") then
self.battery_sysfs = "/sys/class/power_supply/bd71827_bat"
else
self.battery_sysfs = "/sys/class/power_supply/mc13892_bat"
end
-- Frontlight
if self:hasNaturalLight() then
if util.fileExists("/sys/class/leds/aw99703-bl_FL1/color") then
-- HWConfig FL_PWM is AW99703x2
self.frontlight_settings.frontlight_mixer = "/sys/class/leds/aw99703-bl_FL1/color"
elseif util.fileExists("/sys/class/backlight/lm3630a_led/color") then
-- HWConfig FL_PWM is LM3630
self.frontlight_settings.frontlight_mixer = "/sys/class/backlight/lm3630a_led/color"
elseif util.fileExists("/sys/class/backlight/tlc5947_bl/color") then
-- HWConfig FL_PWM is TLC5947
self.frontlight_settings.frontlight_mixer = "/sys/class/backlight/tlc5947_bl/color"
end
end
end
-- NOTE: i.MX5 devices have a wonky RTC that doesn't like alarms set further away that UINT16_MAX seconds from now...
-- (c.f., WakeupMgr for more details).
-- NOTE: getRTCName is currently hardcoded to rtc0 (which is also WakeupMgr's default).
local dodgy_rtc = false
if getRTCName() == "pmic_rtc" then
-- This *should* match the 'RTC' (46) NTX HWConfig field being set to 'MSP430' (0).
dodgy_rtc = true
end
-- Detect the various CPU governor sysfs knobs...
if util.pathExists("/sys/devices/system/cpu/cpufreq/policy0") then
self.cpu_governor_knob = "/sys/devices/system/cpu/cpufreq/policy0/scaling_governor"
else
self.cpu_governor_knob = "/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor"
end
self.default_cpu_governor = getCPUGovernor(self.cpu_governor_knob)
-- NOP unsupported methods
if not self.default_cpu_governor then
self.performanceCPUGovernor = NOP
self.defaultCPUGovernor = NOP
end
-- And while we're on CPU-related endeavors...
self.cpu_count = self:isSMP() and getCPUCount() or 1
-- NOP unsupported methods
if self.cpu_count == 1 then
self.enableCPUCores = NOP
end
-- Automagically set this so we never have to remember to do it manually ;p
if self:hasNaturalLight() and self.frontlight_settings and self.frontlight_settings.frontlight_mixer then
self.hasNaturalLightMixer = yes
end
-- Ditto
if self:isMk7() or self:isMTK() then
self.canHWDither = yes
end
-- Enable Kaleido waveform modes on supported devices
if self:hasColorScreen() and self:isMTK() then
self.hasKaleidoWfm = yes
end
-- NOTE: Devices with an AW99703 frontlight PWM controller feature a hardware smooth ramp when setting the frontlight intensity.
--- A side-effect of this behavior is that if you queue a series of intensity changes ending at 0,
--- it won't ramp *at all*, jumping straight to zero instead.
--- So we delay the final ramp off step to prevent (both) the native and our ramping from being optimized out.
if self:hasNaturalLightMixer() and self.frontlight_settings.frontlight_mixer:find("aw99703", 12, true) then
self.frontlight_settings.ramp_off_delay = 0.5
end
-- I don't know how this PWM controller behaves on earlier devices, but it's... not great here.
if self:hasNaturalLightMixer() and self:isMTK() and self.frontlight_settings.frontlight_mixer:find("lm3630a_led", 12, true) then
-- First, we need a delay between ioctls
self.frontlight_settings.ramp_delay = 0.025
-- Second, it *really* doesn't like being interleaved with screen refreshes
self.frontlight_settings.delay_ramp_start = true
end
self.powerd = require("device/kobo/powerd"):new{
device = self,
battery_sysfs = self.battery_sysfs,
aux_battery_sysfs = self.aux_battery_sysfs,
}
-- NOTE: For the Forma, with the buttons on the right, 193 is Top, 194 Bottom.
self.input = require("device/input"):new{
device = self,
event_map = {
[35] = "SleepCover", -- KEY_H, Elipsa
[59] = "SleepCover",
[90] = "LightButton",
[102] = "Home",
[116] = "Power",
[193] = "RPgBack",
[194] = "RPgFwd",
[331] = "Eraser",
[332] = "Highlighter",
},
event_map_adapter = {
SleepCover = function(ev)
if self.input:isEvKeyPress(ev) then
return "SleepCoverClosed"
elseif self.input:isEvKeyRelease(ev) then
return "SleepCoverOpened"
end
end,
LightButton = function(ev)
if self.input:isEvKeyRelease(ev) then
return "Light"
end
end,
},
main_finger_slot = self.main_finger_slot or 0,
pressure_event = self.pressure_event,
}
self.wakeup_mgr = WakeupMgr:new{
dodgy_rtc = dodgy_rtc,
}
-- Input handling on Kobo is a thing of nightmares, start by setting up the actual evdev handler...
self:setTouchEventHandler()
-- And then handle the extra shenanigans if necessary.
self:initEventAdjustHooks()
-- Auto-detect input devices (via FBInk's fbink_input_scan)
local ok, FBInkInput = pcall(ffi.load, "fbink_input")
if not ok then
-- NOP fallback for the testsuite...
FBInkInput = { fbink_input_scan = NOP }
end
local dev_count = ffi.new("size_t[1]")
-- We care about: the touchscreen, the stylus, the power button, the sleep cover, and pagination buttons
-- (and technically rotation events, but we'll get it with the device that provides the buttons on NTX).
-- We exclude keyboards to play nice with the ExternalKeyboard plugin, which will handle potential keyboards on its own.
local match_mask = bit.bor(C.INPUT_TOUCHSCREEN, C.INPUT_TABLET, C.INPUT_POWER_BUTTON, C.INPUT_SLEEP_COVER, C.INPUT_PAGINATION_BUTTONS)
local devices = FBInkInput.fbink_input_scan(match_mask, C.INPUT_KEYBOARD, 0, dev_count)
if devices ~= nil then
for i = 0, tonumber(dev_count[0]) - 1 do
local dev = devices[i]
if dev.matched then
-- We need to single out whichever device provides pagination buttons or sleep cover events, as we'll want to tweak key repeat there...
-- The first one will do, as it's extremely likely to be event0, and that's pretty fairly set in stone on NTX boards.
if (bit.band(dev.type, C.INPUT_PAGINATION_BUTTONS) ~= 0 or bit.band(dev.type, C.INPUT_SLEEP_COVER) ~= 0) and not self.ntx_fd then
self.ntx_fd = self.input.fdopen(tonumber(dev.fd), ffi.string(dev.path), ffi.string(dev.name))
else
self.input.fdopen(tonumber(dev.fd), ffi.string(dev.path), ffi.string(dev.name))
end
end
end
C.free(devices)
else
-- Auto-detection failed, warn and fall back to defaults
logger.warn("We failed to auto-detect the proper input devices, input handling may be inconsistent!")
-- Various HW Buttons, Switches & Synthetic NTX events
self.ntx_fd = self.input.open(self.ntx_dev)
-- Touch panel
self.input.open(self.touch_dev)
end
-- NOTE: On devices with a gyro, there may be a dedicated input device outputting the raw accelerometer data
-- (3-Axis Orientation/Motion Detection).
-- We skip it because we don't need it (synthetic rotation change events are sent to the main ntx input device),
-- and it's usually *extremely* verbose, so it'd just be a waste of processing power.
-- fake_events is only used for usb plug & charge events so far (generated via uevent, c.f., input/iput-kobo.h in base).
-- NOTE: usb hotplug event is also available in /tmp/nickel-hardware-status (... but only when Nickel is running ;p)
self.input.open("fake_events")
-- See if the device supports key repeat
-- This is *not* behind a hasKeys check, because we mainly use it to stop SleepCover chatter,
-- and sleep covers are available on a number of devices without keys ;).
self:getKeyRepeat()
if not self:canKeyRepeat() then
-- NOP unsupported methods
self.disableKeyRepeat = NOP
self.restoreKeyRepeat = NOP
self.toggleKeyRepeat = NOP
end
-- Detect the NTX charging LED sysfs knob
if util.pathExists("/sys/class/leds/LED") then
self.charging_led_sysfs_knob = "/sys/class/leds/LED/brightness"
elseif util.pathExists("/sys/class/leds/GLED") then
self.charging_led_sysfs_knob = "/sys/class/leds/GLED/brightness"
elseif util.pathExists("/sys/class/leds/bd71828-green-led") then
self.charging_led_sysfs_knob = "/sys/class/leds/bd71828-green-led/brightness"
elseif util.pathExists("/sys/devices/platform/ntx_led/lit") then
self.ntx_lit_sysfs_knob = "/sys/devices/platform/ntx_led/lit"
elseif util.pathExists("/sys/devices/platform/pmic_light.1/lit") then
self.ntx_lit_sysfs_knob = "/sys/devices/platform/pmic_light.1/lit"
else
self.canToggleChargingLED = no
end
-- Switch to the simple standard implementation if available
if self.charging_led_sysfs_knob then
self.charging_led_imp = self._LinuxChargingLEDToggle
else
self.charging_led_imp = self._NTXChargingLEDToggle
end
-- NOP unsupported methods
if not self:canToggleChargingLED() then
self.toggleChargingLED = NOP
self.setupChargingLED = NOP
end
-- We have no way of querying the current state of the charging LED, so, start from scratch.
-- Much like Nickel, start by turning it off.
self:toggleChargingLED(false)
self:setupChargingLED()
-- Only enable a single core on startup
self:enableCPUCores(1)
-- On MTK, the "standby" power state is unavailable, and Nickel instead uses "mem" (and /sys/power/mem_sleep doesn't exist either)
if self:isMTK() then
self.standby_state = "mem"
end
self.canStandby = checkStandby(self.standby_state)
if self.canStandby() and (self:isMk7() or self:isSunxi()) then
-- NOTE: Do *NOT* enable this on MTK. What happens if you do can only be described as "shit hits the fan".
-- (Nickel doesn't).
self.canPowerSaveWhileCharging = yes
end
-- Check if the device has a Neonode IR grid (to tone down the chatter on resume ;)).
if lfs.attributes("/sys/devices/virtual/input/input1/neocmd", "mode") == "file" then
-- As found on (at least), the Aura H2O
self.hasIRGridSysfsKnob = "/sys/devices/virtual/input/input1/neocmd"
elseif lfs.attributes("/sys/devices/platform/imx-i2c.1/i2c-1/1-0050/neocmd", "mode") == "file" then
-- As found on (at least) the Glo HD (c.f., https://github.com/koreader/koreader/pull/9377#issuecomment-1213544478)
self.hasIRGridSysfsKnob = "/sys/devices/platform/imx-i2c.1/i2c-1/1-0050/neocmd"
end
-- Disable key repeat if requested
if G_reader_settings:isTrue("input_no_key_repeat") then
self:toggleKeyRepeat(false)
end
-- Finally, Let Generic properly setup the standard stuff.
-- (Of particular import, this needs to come *after* we've set our input hooks, so that the viewport translation runs last).
Generic.init(self)
end
function Kobo:exit()
-- Re-enable key repeat on exit, that's the default state
self:toggleKeyRepeat(true)
Generic.exit(self)
end
function Kobo:setDateTime(year, month, day, hour, min, sec)
if hour == nil or min == nil then return true end
local command
if year and month and day then
command = string.format("date -s '%d-%d-%d %d:%d:%d'", year, month, day, hour, min, sec)
else
command = string.format("date -s '%d:%d'",hour, min)
end
if os.execute(command) == 0 then
os.execute("hwclock -u -w")
return true
else
return false
end
end
function Kobo:initNetworkManager(NetworkMgr)
function NetworkMgr:turnOffWifi(complete_callback)
self:releaseIP()
koboEnableWifi(false)
if complete_callback then
complete_callback()
end
end
function NetworkMgr:turnOnWifi(complete_callback, interactive)
koboEnableWifi(true)
return self:reconnectOrShowNetworkMenu(complete_callback, interactive)
end
local net_if = os.getenv("INTERFACE") or "eth0"
function NetworkMgr:getNetworkInterfaceName()
return net_if
end
NetworkMgr:setWirelessBackend(
"wpa_supplicant", {ctrl_interface = "/var/run/wpa_supplicant/" .. net_if})
function NetworkMgr:obtainIP()
os.execute("./obtain-ip.sh")
end
function NetworkMgr:releaseIP()
os.execute("./release-ip.sh")
end
function NetworkMgr:restoreWifiAsync()
os.execute("./restore-wifi-async.sh")
end
NetworkMgr.isWifiOn = NetworkMgr.sysfsWifiOn
NetworkMgr.isConnected = NetworkMgr.ifHasAnAddress
-- Usually handled in NetworkMgr:init, but we'll need it *now*
NetworkMgr.interface = net_if
-- Kill Wi-Fi if NetworkMgr:isWifiOn() and NOT NetworkMgr:isConnected()
-- (i.e., if the launcher left the Wi-Fi in an inconsistent state: modules loaded, but no route to gateway).
if NetworkMgr:isWifiOn() and not NetworkMgr:isConnected() then
logger.info("Kobo Wi-Fi: Left in an inconsistent state by launcher!")
NetworkMgr:turnOffWifi()
end
end
function Kobo:setTouchEventHandler()
if self.touch_snow_protocol then
self.input.snow_protocol = true
elseif self.touch_phoenix_protocol then
self.input.handleTouchEv = self.input.handleTouchEvPhoenix
elseif not self:hasMultitouch() then
self.input.handleTouchEv = self.input.handleTouchEvLegacy
if self.touch_kobo_mk3_protocol then
self.input.touch_kobo_mk3_protocol = true
end
end
-- Accelerometer
if self:hasGSensor() then
self.input.handleMiscEv = function(this, ev)
-- As generated by gyroTranslation below
if ev.code == C.MSC_GYRO then
return this:handleGyroEv(ev)
end
end
end
end
-- HAL for gyro orientation switches (NTX's EV_MSC:MSC_RAW w/ custom values to EV_MSC:MSC_GYRO w/ our own custom values)
local function gyroTranslation(ev)
-- c.f., include/uapi/linux/input.h,
-- implementations in drivers/hwmon/mma8x5x.c & drivers/input/touchscreen/kx122.c
local MSC_RAW_GSENSOR_PORTRAIT_DOWN = 0x17
local MSC_RAW_GSENSOR_PORTRAIT_UP = 0x18
local MSC_RAW_GSENSOR_LANDSCAPE_RIGHT = 0x19
local MSC_RAW_GSENSOR_LANDSCAPE_LEFT = 0x1a
-- Not that we care about those, but they are reported, and accurate ;).
--[[
local MSC_RAW_GSENSOR_BACK = 0x1b
local MSC_RAW_GSENSOR_FRONT = 0x1c
--]]
if ev.value == MSC_RAW_GSENSOR_PORTRAIT_UP then
-- i.e., UR
ev.code = C.MSC_GYRO
ev.value = C.DEVICE_ROTATED_UPRIGHT
elseif ev.value == MSC_RAW_GSENSOR_LANDSCAPE_RIGHT then
-- i.e., CW
ev.code = C.MSC_GYRO
ev.value = C.DEVICE_ROTATED_CLOCKWISE
elseif ev.value == MSC_RAW_GSENSOR_PORTRAIT_DOWN then
-- i.e., UD
ev.code = C.MSC_GYRO
ev.value = C.DEVICE_ROTATED_UPSIDE_DOWN
elseif ev.value == MSC_RAW_GSENSOR_LANDSCAPE_LEFT then
-- i.e., CCW
ev.code = C.MSC_GYRO
ev.value = C.DEVICE_ROTATED_COUNTER_CLOCKWISE
end
end
function Kobo:initEventAdjustHooks()
-- Build a single composite hook, to avoid duplicated branches...
local koboInputMangling
-- NOTE: touch_switch_xy is *always* true, but not touch_mirrored_x or touch_mirrored_y...
if self.touch_switch_xy and self.touch_mirrored_x then
local max_x = self.screen:getWidth() - 1
koboInputMangling = function(this, ev)
if ev.type == C.EV_ABS then
this:adjustABS_SwitchAxesAndMirrorX(ev, max_x)
elseif ev.type == C.EV_MSC and ev.code == C.MSC_RAW then
gyroTranslation(ev)
end
end
elseif self.touch_switch_xy and self.touch_mirrored_y then
local max_y = self.screen:getHeight() - 1
koboInputMangling = function(this, ev)
if ev.type == C.EV_ABS then
this:adjustABS_SwitchAxesAndMirrorY(ev, max_y)
elseif ev.type == C.EV_MSC and ev.code == C.MSC_RAW then
gyroTranslation(ev)
end
end
elseif self.touch_switch_xy and not self.touch_mirrored_x and not self.touch_mirrored_y then
koboInputMangling = function(this, ev)
if ev.type == C.EV_ABS then
this:adjustABS_SwitchXY(ev)
elseif ev.type == C.EV_MSC and ev.code == C.MSC_RAW then
gyroTranslation(ev)
end
end
end
if koboInputMangling then
self.input:registerEventAdjustHook(koboInputMangling)
end
end
local function getCodeName()
-- Try to get it from the env first
local codename = os.getenv("PRODUCT")
-- If that fails, run the script ourselves
if not codename then
local std_out = io.popen("/bin/kobo_config.sh 2>/dev/null", "re")
if std_out then
codename = std_out:read("*line")
std_out:close()
end
end
return codename
end
function Kobo:getFirmwareVersion()
local version_file = io.open("/mnt/onboard/.kobo/version", "re")
if not version_file then
self.firmware_rev = "none"
return
end
local version_str = version_file:read("*line")
version_file:close()
local i = 1
for field in version_str:gmatch("([^,]+)") do
if i == 3 then
self.firmware_rev = field
break
end
i = i + 1
end
end
local function getProductId()
-- Try to get it from the env first (KSM only)
local product_id = os.getenv("MODEL_NUMBER")
-- If that fails, devise it ourselves
if not product_id then
local version_file = io.open("/mnt/onboard/.kobo/version", "re")
if not version_file then
return "000"
end
local version_str = version_file:read("*line")
version_file:close()
product_id = string.sub(version_str, -3, -1)
end
return product_id
end
-- NOTE: We overload this to make sure checkUnexpectedWakeup doesn't trip *before* the newly scheduled suspend
function Kobo:rescheduleSuspend()
UIManager:unschedule(self.suspend)
UIManager:unschedule(self._doSuspend)
UIManager:unschedule(self.checkUnexpectedWakeup)
UIManager:scheduleIn(self.suspend_wait_timeout, self.suspend, self)
end
function Kobo:scheduleUnexpectedWakeupGuard()
self.unexpected_wakeup_count = self.unexpected_wakeup_count + 1
logger.dbg("Kobo suspend: scheduling unexpected wakeup guard")
UIManager:scheduleIn(15, self.checkUnexpectedWakeup, self)
end
function Kobo:checkUnexpectedWakeup()
-- 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
-- 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 > 20 then
-- If we've failed to put the device back to sleep over 20 consecutive times, we give up.
-- Broadcast a specific event, so that AutoSuspend can pick up the baton...
local Event = require("ui/event")
UIManager:broadcastEvent(Event:new("UnexpectedWakeupLimit"))
return
end
logger.err("Kobo suspend: putting device back to sleep after", self.unexpected_wakeup_count, "unexpected wakeups.")
self:suspend()
end
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)
-- On MTK, any suspend/standby attempt while plugged-in will hang the kernel... -_-"
-- NOTE: isCharging is still true while isCharged!
if self:isMTK() and self.powerd:isCharging() then
logger.info("Kobo standby: skipping the standby request for now: device is plugged in and would otherwise crash!")
return
end
-- NOTE: Switch to the performance CPU governor, in order to speed up the resume process so as to lower its latency cost...
-- (It won't have any impact on power efficiency *during* suspend, so there's not really any drawback).
self:performanceCPUGovernor()
--[[
-- On most devices, attempting to PM with a Wi-Fi module loaded will horribly crash the kernel, so, don't?
-- NOTE: Much like suspend, our caller should ensure this never happens, hence this being commented out ;).
if NetworkMgr:isWifiOn() then
-- AutoSuspend relies on NetworkMgr:getWifiState to prevent this, so, if we ever trip this, it's a bug ;).
logger.err("Kobo standby: cannot standby with Wi-Fi modules loaded! (NetworkMgr is confused: this is a bug)")
return
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, standby_alarm)
end
logger.dbg("Kobo standby: asking to enter standby . . .")
local standby_time = time.boottime_or_realtime_coarse()
-- The odd Sunxi needs some time to settle before entering standby.
-- This will avoid the screen puzzling effect documented in
-- https://github.com/koreader/koreader/pull/10306#issue-1659242042 not only for
-- WiFi toggle, but (almost) everywhere.
ffiUtil.usleep(90000) -- sleep 0.09s (0.08s would also work)
local ret = ffiUtil.writeToSysfs(self.standby_state, "/sys/power/state")
self.last_standby_time = time.boottime_or_realtime_coarse() - standby_time
self.total_standby_time = self.total_standby_time + self.last_standby_time
if ret then
logger.dbg("Kobo standby: zZz zZz zZz zZz... And woke up!")
if G_reader_settings:isTrue("pm_debug_entry_failure") then
-- NOTE: This is a debug option where we coopt the charging LED, hence us not using setupChargingLED here.
-- (It's called on resume anyway).
self:toggleChargingLED(false)
end
else
logger.warn("Kobo standby: the kernel refused to enter standby!")
if G_reader_settings:isTrue("pm_debug_entry_failure") then
self:toggleChargingLED(true)
end
end
if max_duration then
-- 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
-- And restore the standard CPU scheduler once we're done dealing with the wakeup event.
UIManager:tickAfterNext(self.defaultCPUGovernor, self)
end
function Kobo:suspend()
-- If there's a _doSuspend still scheduled, something is going seriously wrong
-- (e.g., we caught multiple Suspend events without a Resume in between)...
if UIManager:unschedule(self._doSuspend) then
logger.warn("Kobo suspend: cancelled a pending suspend request via *suspend*. This is most likely a bug.")
end
-- On MTK, any suspend/standby attempt while plugged-in will hang the kernel... -_-"
-- NOTE: isCharging is still true while isCharged!
if self:isMTK() and self.powerd:isCharging() then
logger.info("Kobo suspend: skipping the suspend request for now: device is plugged in and would otherwise crash!")
-- Do the usual scheduling dance, so we get a chance to fire the UnexpectedWakeupLimit event...
UIManager:unschedule(self.checkUnexpectedWakeup)
self:scheduleUnexpectedWakeupGuard()
return
end
logger.info("Kobo suspend: going to sleep . . .")
UIManager:unschedule(self.checkUnexpectedWakeup)
-- NOTE: Sleep as little as possible here, sleeping has a tendency to make
-- everything mysteriously hang...
-- 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
local ret = ffiUtil.writeToSysfs("1", "/sys/power/state-extended")
if ret then
logger.dbg("Kobo suspend: successfully asked the kernel to put subsystems to sleep")
else
logger.err("Kobo suspend: the kernel refused to flag subsystems for suspend, aborting this attempt!")
-- We'd be going to standby instead of suspend, so, just try again later.
self:scheduleUnexpectedWakeupGuard()
return
end
-- NOTE: As nonsensical as it looks given that the above just flips a global,
-- I have traumatic memories of things going awry if we don't sleep between the two writes...
logger.dbg("Kobo suspend: waiting for 2s because of reasons...")
-- We keep polling for input in order to be able to catch power events with extremely unlucky timing (#12325)...
UIManager:scheduleIn(2, self._doSuspend, self)
end
function Kobo:_doSuspend()
os.execute("sync")
logger.dbg("Kobo suspend: synced FS")
-- Depending on device/FW version, some kernels do not support wakeup_count, account for that.
-- NOTE: ...and of course, it appears to be broken on older devices,
-- which probably explains why nickel doesn't use this facility there...
-- (By broken, I mean that the system wakes up right away despite the successful write).
-- As we can't really divine where and when it'll work properly, unconditionally disable it.
--[[
if self.has_wakeup_count then
self.curr_wakeup_count = self.powerd:read_int_file("/sys/power/wakeup_count")
logger.dbg("Kobo suspend: Current WakeUp count:", self.curr_wakeup_count)
local ret = ffiUtil.writeToSysfs(self.curr_wakeup_count, "/sys/power/wakeup_count")
if ret then
logger.dbg("Kobo suspend: WakeUp count matched")
else
logger.err("Kobo suspend: WakeUp count mismatch, aborting this suspend attempt!")
-- This means that there was at least one wakeup event since our read,
-- abort this attempt (i.e., don't write to state for now) and just schedule the wakeup guard.
self:scheduleUnexpectedWakeupGuard()
return
end
end
--]]
logger.dbg("Kobo suspend: asking for a suspend to RAM . . .")
local suspend_time = time.boottime_or_realtime_coarse()
local ret = ffiUtil.writeToSysfs("mem", "/sys/power/state")
-- NOTE: At this point, we *should* be in suspend to RAM, as such,
-- execution should only resume on wakeup...
self.last_suspend_time = time.boottime_or_realtime_coarse() - suspend_time
self.total_suspend_time = self.total_suspend_time + self.last_suspend_time
if ret then
logger.info("Kobo suspend: ZzZ ZzZ ZzZ... And woke up!")
if G_reader_settings:isTrue("pm_debug_entry_failure") then
self:toggleChargingLED(false)
end
else
-- Most of the potential failures ought to be -EBUSY
-- (usually, because of the EPDC or touch panel).
-- NOTE: On recent enough kernels, with debugfs enabled and mounted, see also
-- /sys/kernel/debug/suspend_stats & /sys/kernel/debug/wakeup_sources
logger.warn("Kobo suspend: the kernel refused to enter suspend!")
-- NOTE: Despite it making little sense,
-- we reset state-extended back to 0 to mimic Nickel's own
-- 1 -> mem -> 0 loop in case of suspend failures...
-- c.f., nickel_suspend_strace.txt for more details.
ffiUtil.writeToSysfs("0", "/sys/power/state-extended")
if G_reader_settings:isTrue("pm_debug_entry_failure") then
self:toggleChargingLED(true)
end
end
-- NOTE: We unflag /sys/power/state-extended in Kobo:resume() to keep
-- things tidy and easier to follow
-- Kobo:resume() will also reset unexpected_wakeup_count and unschedule the check to signal a sane wakeup.
self:scheduleUnexpectedWakeupGuard()
end
function Kobo:resume()
logger.dbg("Kobo resume: clean up after wakeup")
-- Reset unexpected_wakeup_count ASAP
self.unexpected_wakeup_count = 0
-- Unschedule the checkUnexpectedWakeup shenanigans.
UIManager:unschedule(self.checkUnexpectedWakeup)
UIManager:unschedule(self.suspend)
-- Cancel any pending suspend request
if UIManager:unschedule(self._doSuspend) then
logger.info("Kobo resume: cancelled a pending suspend request")
end
-- 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
-- Among other things, this sets up the wakeup pins (e.g., resume on input).
local ret = ffiUtil.writeToSysfs("0", "/sys/power/state-extended")
if ret then
logger.dbg("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
ffiUtil.usleep(100000)
if self.hasIRGridSysfsKnob then
-- cf. #1862, I can reliably break IR touch input on resume...
-- cf. also #1943 for the rationale behind applying this workaround in every case...
-- c.f., neo_ctl @ drivers/input/touchscreen/zforce_i2c.c,
-- basically, a is wakeup (for activate), d is sleep (for deactivate), and we don't care about s (set res),
-- and l (led signal level, actually a NOP on NTX kernels).
ffiUtil.writeToSysfs("a", self.hasIRGridSysfsKnob)
end
-- A full suspend may have toggled the LED off.
self:setupChargingLED()
end
function Kobo:usbPlugOut()
-- Rewind the unexpected wakeup counter, since we're no longer charging, meaning power savings are now critical again ;).
-- NOTE: We don't reset it to 0 because, semantically, only resume should ever be allowed to do so.
if self.unexpected_wakeup_count > 0 then
self.unexpected_wakeup_count = 1
end
end
function Kobo:saveSettings()
-- save frontlight state to G_reader_settings (and NickelConf if needed)
self.powerd:saveSettings()
end
function Kobo:powerOff()
-- Much like Nickel itself, disable the RTC alarm before powering down.
self.wakeup_mgr:unsetWakeupAlarm()
--- @todo: Check on MTK
if self:isSunxi() or self:isMTK() then
-- On sunxi, apparently, we *do* go through init
os.execute("sleep 1 && poweroff &")
else
-- Then shut down without init's help
os.execute("sleep 1 && poweroff -f &")
end
end
function Kobo:reboot()
os.execute("sleep 1 && reboot &")
end
function Kobo:_NTXChargingLEDToggle(toggle)
-- NOTE: While most/all Kobos actually have a charging LED, and it can usually be fiddled with in a similar fashion,
-- we've seen *extremely* weird behavior in the past when playing with it on older devices (c.f., #5479).
-- In fact, Nickel itself doesn't provide this feature on said older devices
-- (when it does, it's an option in the Energy saving settings),
-- which is why we also limit ourselves to "true" on devices where this was tested.
-- c.f., drivers/misc/ntx_misc_light.c
local fd = C.open(self.ntx_lit_sysfs_knob, 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 `" .. self.ntx_lit_sysfs_knob .. "`:", ffi.string(C.strerror(ffi.errno())))
return false
end
-- c.f., strace -fittTvyy -e trace=ioctl,file,signal,ipc,desc -s 256 -o /tmp/nickel.log -p $(pidof -s nickel) &
-- This was observed on a Forma, so I'm mildly hopeful that it's safe on other Mk. 7 devices ;).
-- NOTE: ch stands for channel, cur for current, dc for duty cycle. c.f., the driver source.
if toggle == true then
-- NOTE: Technically, Nickel forces a toggle off before that, too.
-- But since we do that on startup, it shouldn't be necessary here...
for ch = self.led_uses_channel_3 and 3 or 4, 4 do
C.write(fd, "ch " .. tostring(ch), 4)
C.write(fd, "cur 1", 5)
C.write(fd, "dc 63", 5)
end
else
for ch = 3, 5 do
C.write(fd, "ch " .. tostring(ch), 4)
C.write(fd, "cur 1", 5)
C.write(fd, "dc 0", 4)
end
end
C.close(fd)
end
function Kobo:_LinuxChargingLEDToggle(toggle)
-- max_brightness usually says 255 for those, but 1 does the same (and matches Nickel's behavior)
ffiUtil.writeToSysfs(toggle and "1" or "0", self.charging_led_sysfs_knob)
end
function Kobo:toggleChargingLED(toggle)
-- We have no way of querying the current state from the HW!
if toggle == nil then
return
end
-- Don't do anything if the state is already correct
-- NOTE: What happens to the LED when attempting/successfully entering PM is... kind of a mess.
-- On a H2O, even *attempting* to enter PM will kill the light (and it'll stay off).
-- On a Forma, a failed attempt will *not* affect the light, but a successful one *will* kill it,
-- be that standby or suspend, but it'll be restored on wakeup...
-- On sunxi, PM appears to have zero effect on the LED.
if self.charging_led_state == toggle then
return
end
self.charging_led_state = toggle
logger.dbg("Kobo: Turning the charging LED", toggle and "on" or "off")
return self:charging_led_imp(toggle)
end
-- Return the highest core number
function Kobo:getCPUCount()
local fd = io.open("/sys/devices/system/cpu/possible", "re")
if fd then
local str = fd:read("*line")
fd:close()
-- Format is n-N, where n is the first core, and N the last (e.g., 0-3)
return tonumber(str:match("%d+$")) or 1
else
return 1
end
end
function Kobo:enableCPUCores(amount)
-- CPU0 is *always* online ;).
for n = 1, self.cpu_count do
local path = "/sys/devices/system/cpu/cpu" .. n .. "/online"
local up
if n >= amount then
up = "0"
else
up = "1"
end
ffiUtil.writeToSysfs(up, path)
end
end
function Kobo:performanceCPUGovernor()
ffiUtil.writeToSysfs("performance", self.cpu_governor_knob)
end
function Kobo:defaultCPUGovernor()
ffiUtil.writeToSysfs(self.default_cpu_governor, self.cpu_governor_knob)
end
function Kobo:isStartupScriptUpToDate()
-- Compare the hash of the *active* script (i.e., the one in /tmp) to the *potential* one (i.e., the one in KOREADER_DIR)
local current_script = "/tmp/koreader.sh"
local new_script = os.getenv("KOREADER_DIR") .. "/" .. "koreader.sh"
local md5 = require("ffi/MD5")
return md5.sumFile(current_script) == md5.sumFile(new_script)
end
function Kobo:UIManagerReady(uimgr)
UIManager = uimgr
end
function Kobo:setEventHandlers(uimgr)
-- We do not want auto suspend procedure to waste battery during
-- suspend. So let's unschedule it when suspending, and restart it after
-- resume. Done via the plugin's onSuspend/onResume handlers.
UIManager.event_handlers.Suspend = function()
self:onPowerEvent("Suspend")
end
UIManager.event_handlers.Resume = function()
self:onPowerEvent("Resume")
end
UIManager.event_handlers.PowerPress = function()
-- Always schedule power off.
-- Press the power button for 2+ seconds to shutdown directly from suspend.
UIManager:scheduleIn(2, UIManager.poweroff_action)
end
UIManager.event_handlers.PowerRelease = function()
if not UIManager._entered_poweroff_stage then
UIManager:unschedule(UIManager.poweroff_action)
-- resume if we were suspended
if self.screen_saver_mode then
if self.screen_saver_lock then
UIManager.event_handlers.Suspend()
else
UIManager.event_handlers.Resume()
end
else
UIManager.event_handlers.Suspend()
end
end
end
UIManager.event_handlers.Light = function()
self:getPowerDevice():toggleFrontlight()
end
-- USB plug events with a power-only charger
UIManager.event_handlers.Charging = function()
self:_beforeCharging()
-- NOTE: Plug/unplug events will wake the device up, which is why we put it back to sleep.
if self.screen_saver_mode and not self.screen_saver_lock then
UIManager.event_handlers.Suspend()
end
end
UIManager.event_handlers.NotCharging = function()
-- We need to put the device into suspension, other things need to be done before it.
self:usbPlugOut()
self:_afterNotCharging()
if self.screen_saver_mode and not self.screen_saver_lock then
UIManager.event_handlers.Suspend()
end
end
-- USB plug events with a data-aware host
UIManager.event_handlers.UsbPlugIn = function()
self:_beforeCharging()
-- NOTE: Plug/unplug events will wake the device up, which is why we put it back to sleep.
if self.screen_saver_mode and not self.screen_saver_lock then
UIManager.event_handlers.Suspend()
elseif not self.screen_saver_lock then
-- Potentially start an USBMS session
local MassStorage = require("ui/elements/mass_storage")
MassStorage:start()
end
end
UIManager.event_handlers.UsbPlugOut = function()
-- We need to put the device into suspension, other things need to be done before it.
self:usbPlugOut()
self:_afterNotCharging()
if self.screen_saver_mode and not self.screen_saver_lock then
UIManager.event_handlers.Suspend()
elseif not self.screen_saver_lock then
-- Potentially dismiss the USBMS ConfirmBox
local MassStorage = require("ui/elements/mass_storage")
MassStorage:dismiss()
end
end
-- Sleep Cover handling
if G_reader_settings:isTrue("ignore_power_sleepcover") then
-- NOTE: The hardware event itself will wake the kernel up if it's in suspend (:/).
-- Let the unexpected wakeup guard handle that.
UIManager.event_handlers.SleepCoverClosed = nil
UIManager.event_handlers.SleepCoverOpened = nil
elseif G_reader_settings:isTrue("ignore_open_sleepcover") then
-- Just ignore wakeup events, and do NOT set is_cover_closed,
-- so device/generic/device will let us use the power button to wake ;).
UIManager.event_handlers.SleepCoverClosed = function()
UIManager.event_handlers.Suspend()
end
UIManager.event_handlers.SleepCoverOpened = function()
self.is_cover_closed = false
end
else
UIManager.event_handlers.SleepCoverClosed = function()
self.is_cover_closed = true
UIManager.event_handlers.Suspend()
end
UIManager.event_handlers.SleepCoverOpened = function()
self.is_cover_closed = false
UIManager.event_handlers.Resume()
end
end
end
-------------- device probe ------------
local codename = getCodeName()
local product_id = getProductId()
if codename == "dahlia" then
return KoboDahlia
elseif codename == "dragon" then
return KoboDragon
elseif codename == "kraken" then
return KoboKraken
elseif codename == "phoenix" then
return KoboPhoenix
elseif codename == "trilogy" and product_id == "310" then
-- This is where things get interesting...
-- The early 'A' variant (the actual model name being N905, without any letter suffix, unlike the two other variants)
-- does *NOT* feature an internal SD card, and is manufactured in China instead of Taiwan... because it is *NOT* an NTX board.
-- cf. https://github.com/koreader/koreader/issues/9742
if os.getenv("PLATFORM") == "freescale" then
return KoboTrilogyA
else
return KoboTrilogyB
end
elseif codename == "trilogy" and product_id == "320" then
return KoboTrilogyC
elseif codename == "pixie" then
return KoboPixie
elseif codename == "alyssum" then
return KoboAlyssum
elseif codename == "pika" then
return KoboPika
elseif codename == "star" and product_id == "379" then
return KoboStarRev2
elseif codename == "star" then
return KoboStar
elseif codename == "daylight" then
return KoboDaylight
elseif codename == "snow" and product_id == "378" then
return KoboSnowRev2
elseif codename == "snow" then
return KoboSnow
elseif codename == "nova" then
return KoboNova
elseif codename == "frost" then
return KoboFrost
elseif codename == "storm" then
return KoboStorm
elseif codename == "luna" then
return KoboLuna
elseif codename == "europa" then
return KoboEuropa
elseif codename == "cadmus" then
return KoboCadmus
elseif codename == "io" then
return KoboIo
elseif codename == "goldfinch" then
return KoboGoldfinch
elseif codename == "condor" then
return KoboCondor
elseif codename == "monza" or codename == "monzaTolino" then
return KoboMonza
elseif codename == "spaBW" or codename == "spaTolinoBW" then
return KoboSpaBW
elseif codename == "spaColour" or codename == "spaTolinoColour" then
return KoboSpaColour
else
error("unrecognized Kobo model ".. codename .. " with device id " .. product_id)
end