2
0
mirror of https://github.com/koreader/koreader synced 2024-11-10 01:10:34 +00:00
koreader/frontend/device/kobo/device.lua
NiLuJe d59c837714
Kobo: Refactor various aspects of the Kaleido/MTK support (#12221)
* UIManager: Let the fb backend deal with Kaleido wfm promotion. This fixes a number of quirks that poisoned the refresh queue with spurious full-screen refreshes. See https://github.com/koreader/koreader-base/pull/1865 for more details.
* This also means we now disable Kaleido waveform modes when color rendering is disabled (remember to trash your thumbnail cache if you don't want to mix color w/ grayscale thumbnails, though).
* UIManager: Merge refreshes that share an edge, because that was driving me nuts (and would have most likely been merged by the kernel anyway). A perfect test-case is the FM, which trips two separate refreshes because of its title bar.
* ReaderFlipping: Use sensible dimensions, so that we only refresh the icon's region.
* ReaderBookmark: Only refresh the dogear instead of the whole page when toggling bookmarks.
* NetworkSetting: Make it a real boy, so it consistently refreshes properly on dismiss instead of relying on UIManager saving the day.
* Kobo: Aggressively prevent *both* suspend & standby while MTK devices are plugged-in, as both will horribly implode the kernel (we previously only prevent standby while charging).
* Kobo: Switch to 8bpp on B&W MTK devices (or when color rendering is disabled on Kaleido panels).
2024-07-28 01:19:40 +02:00

1792 lines
67 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
-- 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.checkUnexpectedWakeup)
UIManager:scheduleIn(self.suspend_wait_timeout, self.suspend, 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()
-- 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.unexpected_wakeup_count = self.unexpected_wakeup_count + 1
logger.dbg("Kobo suspend: scheduling unexpected wakeup guard")
UIManager:scheduleIn(15, self.checkUnexpectedWakeup, self)
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...
-- Depending on device/FW version, some kernels do not support
-- wakeup_count, account for that
--
-- NOTE: ... and of course, it appears to be broken, which probably
-- explains why nickel doesn't use this facility...
-- (By broken, I mean that the system wakes up right away).
-- So, unless that changes, unconditionally disable it.
--[[
local f, re, err_msg, err_code
local has_wakeup_count = false
f = io.open("/sys/power/wakeup_count", "re")
if f ~= nil then
f:close()
has_wakeup_count = true
end
-- Clear the kernel ring buffer... (we're missing a proper -C flag...)
--dmesg -c >/dev/null
-- Go to sleep
local curr_wakeup_count
if has_wakeup_count then
curr_wakeup_count = "$(cat /sys/power/wakeup_count)"
logger.dbg("Kobo suspend: Current WakeUp count:", curr_wakeup_count)
end
-]]
-- 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.warn("Kobo suspend: the kernel refused to flag subsystems for suspend!")
end
ffiUtil.sleep(2)
logger.dbg("Kobo suspend: waited for 2s because of reasons...")
os.execute("sync")
logger.dbg("Kobo suspend: synced FS")
--[[
if has_wakeup_count then
f = io.open("/sys/power/wakeup_count", "we")
if not f then
logger.err("cannot open /sys/power/wakeup_count")
return false
end
re, err_msg, err_code = f:write(tostring(curr_wakeup_count), "\n")
logger.dbg("Kobo suspend: wrote WakeUp count:", curr_wakeup_count)
if not re then
logger.err("Kobo suspend: failed to write WakeUp count:",
err_msg,
err_code)
end
f:close()
end
--]]
logger.dbg("Kobo suspend: asking for a suspend to RAM . . .")
local suspend_time = time.boottime_or_realtime_coarse()
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
logger.warn("Kobo suspend: the kernel refused to enter suspend!")
-- Reset state-extended back to 0 since we are giving up.
ffiUtil.writeToSysfs("0", "/sys/power/state-extended")
if G_reader_settings:isTrue("pm_debug_entry_failure") then
self:toggleChargingLED(true)
end
end
-- NOTE: Ideally, we'd need a way to warn the user that suspending
-- gloriously failed at this point...
-- We can safely assume that just from a non-zero return code, without
-- looking at the detailed stderr message
-- (most of the failures we'll see are -EBUSY anyway)
-- For reference, when that happens to nickel, it appears to keep retrying
-- to wakeup & sleep ad nauseam,
-- which is where the non-sensical 1 -> mem -> 0 loop idea comes from...
-- cf. nickel_suspend_strace.txt for more details.
-- NOTE: On recent enough kernels, with debugfs enabled and mounted, see also
-- /sys/kernel/debug/suspend_stats & /sys/kernel/debug/wakeup_sources
--[[
if has_wakeup_count then
logger.dbg("wakeup count: $(cat /sys/power/wakeup_count)")
end
-- Print tke kernel log since our attempt to sleep...
--dmesg -c
--]]
-- NOTE: We unflag /sys/power/state-extended in Kobo:resume() to keep
-- things tidy and easier to follow
-- Kobo:resume() will reset unexpected_wakeup_count and unschedule the check to signal a sane wakeup.
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: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)
-- 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