Files
koreader/frontend/device/kobo/device.lua
NiLuJe fea0d8dbaf Another set of fixes (#4083)
* Make findcalibre actually useful

By setting SEARCH_LIBRARY_PATH when a Calibre tree is found, so that
book paths can properly be constructed.

Users should still very much set it themselves, but at least everything
works as intended when not, instead of mysteriously half-breaking later.

Drop the SetDefaults bits, it appeared to have been added to fix no-ops
detected by Luacheck, and it's actually non-functional, because
SetDefaults doesn't handle saving variables it did not itself assign.
So this was just causing the "Do you want to save new defaults" popup to
show up on exit, but it couldn't actually do anything useful (like, say,
save the new SEARCH_LIBRARY_PATH value).

fix #4082

* Better comments about the state of NaturalLight on the Clara, and how this might translate to the H2O²r2.

re #4015

* Make ScrolltextWidget refresh as "partial" only on actual Scroll events

Moving the cursor should stay "ui", or things gets annoying really fast
;).

re #4084

* Bump base to pickup ZMQ fixes (fix #4086)
2018-07-21 21:51:25 +02:00

665 lines
20 KiB
Lua

local Generic = require("device/generic/device")
local TimeVal = require("ui/timeval")
local Geom = require("ui/geometry")
local util = require("ffi/util")
local _ = require("gettext")
local logger = require("logger")
local function yes() return true end
local function no() return false end
local function koboEnableWifi(toggle)
if toggle == 1 then
logger.info("Kobo WiFi: enabling WiFi")
os.execute("./enable-wifi.sh")
else
logger.info("Kobo WiFi: disabling WiFi")
os.execute("./disable-wifi.sh")
end
end
local Kobo = Generic:new{
model = "Kobo",
isKobo = yes,
isTouchDevice = yes, -- all of them are
hasBGRFrameBuffer = yes, -- has always been the case, even on 16bpp FWs
-- 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,
-- enforce protrait mode on Kobos:
isAlwaysPortrait = yes,
-- the internal storage mount point users can write to
internal_storage_mount_point = "/mnt/onboard/",
-- currently only Aura One has coloured frontlight
hasNaturalLight = no,
frontlight_settings = {},
}
-- TODO: hasKeys for some devices?
-- Kobo Touch:
local KoboTrilogy = Kobo:new{
model = "Kobo_trilogy",
needsTouchScreenProbe = yes,
touch_switch_xy = false,
-- Some Kobo Touch models' kernel does not generate touch event with epoch
-- timestamp. This flag will probe for those models and setup event adjust
-- hook accordingly
touch_probe_ev_epoch_time = true,
hasKeys = yes,
}
-- Kobo Mini:
local KoboPixie = Kobo:new{
model = "Kobo_pixie",
display_dpi = 200,
-- bezel:
viewport = Geom:new{x=0, y=2, w=596, h=794},
}
-- Kobo Aura One:
local KoboDaylight = Kobo:new{
model = "Kobo_daylight",
hasFrontlight = yes,
touch_probe_ev_epoch_time = true,
touch_phoenix_protocol = true,
display_dpi = 300,
hasNaturalLight = yes,
}
-- Kobo Aura H2O:
local KoboDahlia = Kobo:new{
model = "Kobo_dahlia",
hasFrontlight = yes,
touch_phoenix_protocol = true,
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:new{
model = "Kobo_dragon",
hasFrontlight = yes,
display_dpi = 265,
}
-- Kobo Glo:
local KoboKraken = Kobo:new{
model = "Kobo_kraken",
hasFrontlight = yes,
display_dpi = 212,
}
-- Kobo Aura:
local KoboPhoenix = Kobo:new{
model = "Kobo_phoenix",
hasFrontlight = yes,
touch_phoenix_protocol = true,
display_dpi = 212,
-- the bezel covers 12 pixels at the bottom:
viewport = Geom:new{x=0, y=0, w=758, h=1012},
}
-- Kobo Aura H2O2:
local KoboSnow = Kobo:new{
model = "Kobo_snow",
hasFrontlight = yes,
touch_snow_protocol = true,
touch_mirrored_x = false,
touch_probe_ev_epoch_time = true,
display_dpi = 265,
-- the bezel covers the top 11 pixels:
viewport = Geom:new{x=0, y=11, w=1080, h=1429},
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:new{
model = "Kobo_snow",
hasFrontlight = yes,
touch_snow_protocol = true,
display_dpi = 265,
-- the bezel covers the top 11 pixels:
viewport = Geom:new{x=0, y=11, w=1080, h=1429},
hasNaturalLight = yes,
frontlight_settings = {
frontlight_white = "/sys/class/backlight/lm3630a_ledb",
frontlight_red = "/sys/class/backlight/lm3630a_leda",
frontlight_green = "/dev/null",
},
}
-- Kobo Aura second edition:
local KoboStar = Kobo:new{
model = "Kobo_star",
hasFrontlight = yes,
touch_probe_ev_epoch_time = true,
touch_phoenix_protocol = true,
display_dpi = 212,
-- the bezel covers 1-2 pixels on each side:
viewport = Geom:new{x=1, y=0, w=756, h=1024},
}
-- Kobo Aura second edition, Rev 2:
-- FIXME: Confirm that this is accurate? If it is, and matches the Rev1, ditch the special casing.
local KoboStarRev2 = Kobo:new{
model = "Kobo_star",
hasFrontlight = yes,
touch_probe_ev_epoch_time = true,
touch_phoenix_protocol = true,
display_dpi = 212,
-- the bezel covers 1-2 pixels on each side:
viewport = Geom:new{x=1, y=0, w=756, h=1024},
}
-- Kobo Glo HD:
local KoboAlyssum = Kobo:new{
model = "Kobo_alyssum",
hasFrontlight = yes,
touch_phoenix_protocol = true,
touch_alyssum_protocol = true,
display_dpi = 300,
}
-- Kobo Touch 2.0:
local KoboPika = Kobo:new{
model = "Kobo_pika",
touch_phoenix_protocol = true,
touch_alyssum_protocol = true,
}
-- Kobo Clara HD:
local KoboNova = Kobo:new{
model = "Kobo_nova",
hasFrontlight = yes,
touch_snow_protocol = true,
display_dpi = 300,
hasNaturalLight = yes,
frontlight_settings = {
frontlight_white = "/sys/class/backlight/lm3630a_ledb",
frontlight_red = "/sys/class/backlight/lm3630a_leda",
-- NOTE: There doesn't appear to be a dedicated "green" LED, instead,
-- there's a knob that mixes the white & red ones together (/sys/class/backlight/lm3630a_led).
-- c.f., https://www.mobileread.com/forums/showpost.php?p=3728236&postcount=2947
-- Because I'm not familiar with sysfs_light.lua, just throw green into the void, and hope for the best...
frontlight_green = "/dev/null",
},
}
function Kobo:init()
self.screen = require("ffi/framebuffer_mxcfb"):new{device = self, debug = logger.dbg}
self.powerd = require("device/kobo/powerd"):new{device = self}
self.input = require("device/input"):new{
device = self,
event_map = {
[59] = "SleepCover",
[90] = "LightButton",
[102] = "Home",
[116] = "Power",
},
event_map_adapter = {
SleepCover = function(ev)
if self.input:isEvKeyPress(ev) then
return "SleepCoverClosed"
else
return "SleepCoverOpened"
end
end,
LightButton = function(ev)
if self.input:isEvKeyRelease(ev) then
return "Light"
end
end,
}
}
Generic.init(self)
-- event2 is for MMA7660 sensor (3-Axis Orientation/Motion Detection)
self.input.open("/dev/input/event0") -- Light button and sleep slider
self.input.open("/dev/input/event1")
-- fake_events is only used for usb plug event so far
-- NOTE: usb hotplug event is also available in /tmp/nickel-hardware-status
self.input.open("fake_events")
if not self.needsTouchScreenProbe() then
self:initEventAdjustHooks()
else
-- if touch probe is required, we postpone EventAdjustHook
-- initialization to when self:touchScreenProbe is called
self.touchScreenProbe = function()
-- if user has not set KOBO_TOUCH_MIRRORED yet
if KOBO_TOUCH_MIRRORED == nil then
local switch_xy = G_reader_settings:readSetting("kobo_touch_switch_xy")
-- and has no probe before
if switch_xy == nil then
local TouchProbe = require("tools/kobo_touch_probe")
local UIManager = require("ui/uimanager")
UIManager:show(TouchProbe:new{})
UIManager:run()
-- assuming TouchProbe sets kobo_touch_switch_xy config
switch_xy = G_reader_settings:readSetting("kobo_touch_switch_xy")
end
self.touch_switch_xy = switch_xy
end
self:initEventAdjustHooks()
end
end
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)
koboEnableWifi(0)
if complete_callback then
complete_callback()
end
end
function NetworkMgr:turnOnWifi(complete_callback)
koboEnableWifi(1)
self:showNetworkMenu(complete_callback)
end
local net_if = os.getenv("INTERFACE")
if not net_if then
net_if = "eth0"
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
-- NOTE: Cheap-ass way of checking if WiFi seems to be enabled...
-- Since the crux of the issues lies in race-y module unloading, this is perfectly fine for our usage.
function NetworkMgr:isWifiOn()
return 0 == os.execute("lsmod | grep -q sdio_wifi_pwr")
end
end
function Kobo:supportsScreensaver() return true end
local probeEvEpochTime
-- this function will update itself after the first touch event
probeEvEpochTime = function(self, ev)
local now = TimeVal:now()
-- This check should work as long as main UI loop is not blocked for more
-- than 10 minute before handling the first touch event.
if ev.time.sec <= now.sec - 600 then
-- time is seconds since boot, force it to epoch
probeEvEpochTime = function(_, _ev)
_ev.time = TimeVal:now()
end
ev.time = now
else
-- time is already epoch time, no need to do anything
probeEvEpochTime = function(_, _) end
end
end
local ABS_MT_TRACKING_ID = 57
local EV_ABS = 3
local adjustTouchAlyssum = function(self, ev)
ev.time = TimeVal:now()
if ev.type == EV_ABS and ev.code == ABS_MT_TRACKING_ID then
ev.value = ev.value - 1
end
end
function Kobo:initEventAdjustHooks()
-- it's called KOBO_TOUCH_MIRRORED in defaults.lua, but what it
-- actually did in its original implementation was to switch X/Y.
-- NOTE: for kobo touch, adjustTouchSwitchXY needs to be called before
-- adjustTouchMirrorX
if (self.touch_switch_xy and not KOBO_TOUCH_MIRRORED)
or (not self.touch_switch_xy and KOBO_TOUCH_MIRRORED)
then
self.input:registerEventAdjustHook(self.input.adjustTouchSwitchXY)
end
if self.touch_mirrored_x then
self.input:registerEventAdjustHook(
self.input.adjustTouchMirrorX,
-- FIXME: what if we change the screen protrait mode?
self.screen:getWidth()
)
end
if self.touch_alyssum_protocol then
self.input:registerEventAdjustHook(adjustTouchAlyssum)
end
if self.touch_snow_protocol then
self.input.snow_protocol = true
end
if self.touch_probe_ev_epoch_time then
self.input:registerEventAdjustHook(function(_, ev)
probeEvEpochTime(_, ev)
end)
end
if self.touch_phoenix_protocol then
self.input.handleTouchEv = self.input.handleTouchEvPhoenix
end
end
function Kobo: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", "r")
codename = std_out:read()
std_out:close()
end
return codename
end
function Kobo:getFirmwareVersion()
local version_file = io.open("/mnt/onboard/.kobo/version", "r")
if not version_file then
self.firmware_rev = "none"
end
local version_str = version_file:read()
version_file:close()
local i = 0
for field in util.gsplit(version_str, ",", false, false) do
i = i + 1
if (i == 3) then
self.firmware_rev = field
end
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", "r")
if not version_file then
return "000"
end
local version_str = version_file:read()
version_file:close()
product_id = string.sub(version_str, -3, -1)
end
return product_id
end
local unexpected_wakeup_count = 0
local function check_unexpected_wakeup()
logger.dbg("Kobo suspend: checking unexpected wakeup:",
unexpected_wakeup_count)
if unexpected_wakeup_count == 0 or unexpected_wakeup_count > 20 then
-- Don't put device back to sleep under the following two cases:
-- 1. a resume event triggered Kobo:resume() function
-- 2. trying to put device back to sleep more than 20 times after unexpected wakeup
return
end
logger.err("Kobo suspend: putting device back to sleep, unexpected wakeups:",
unexpected_wakeup_count)
-- just in case other events like SleepCoverClosed also scheduled a suspend
require("ui/uimanager"):unschedule(Kobo.suspend)
Kobo.suspend()
end
function Kobo:getUnexpectedWakeup() return unexpected_wakeup_count end
function Kobo:suspend()
logger.info("Kobo suspend: going to sleep . . .")
local UIManager = require("ui/uimanager")
UIManager:unschedule(check_unexpected_wakeup)
local f, re, err_msg, err_code
-- 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 has_wakeup_count = false
f = io.open("/sys/power/wakeup_count", "r")
if f ~= nil then
io.close(f)
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.info("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
-- cf. kernel/power/main.c @ L#207
f = io.open("/sys/power/state-extended", "w")
if not f then
logger.err("Cannot open /sys/power/state-extended for writing!")
return false
end
re, err_msg, err_code = f:write("1\n")
io.close(f)
logger.info("Kobo suspend: asked the kernel to put subsystems to sleep, ret:", re)
if not re then
logger.err('write error: ', err_msg, err_code)
end
util.sleep(2)
logger.info("Kobo suspend: waited for 2s because of reasons...")
os.execute("sync")
logger.info("Kobo suspend: synced FS")
--[[
if has_wakeup_count then
f = io.open("/sys/power/wakeup_count", "w")
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.info("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
io.close(f)
end
--]]
logger.info("Kobo suspend: asking for a suspend to RAM . . .")
f = io.open("/sys/power/state", "w")
if not f then
-- reset state-extend back to 0 since we are giving up
local ext_fd = io.open("/sys/power/state-extended", "w")
if not ext_fd then
logger.err("cannot open /sys/power/state-extended for writing!")
else
ext_fd:write("0\n")
io.close(ext_fd)
end
return false
end
re, err_msg, err_code = f:write("mem\n")
-- NOTE: At this point, we *should* be in suspend to RAM, as such,
-- execution should only resume on wakeup...
logger.info("Kobo suspend: ZzZ ZzZ ZzZ? Write syscall returned: ", re)
if not re then
logger.err('write error: ', err_msg, err_code)
end
io.close(f)
-- 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.
logger.info("Kobo suspend: woke up!")
--[[
if has_wakeup_count then
logger.info("wakeup count: $(cat /sys/power/wakeup_count)")
end
-- Print tke kernel log since our attempt to sleep...
--dmesg -c
--]]
-- NOTE: We unflag /sys/power/state-extended in Kobo:resume() to keep
-- things tidy and easier to follow
-- Kobo:resume() will reset unexpected_wakeup_count = 0 to signal an
-- expected wakeup, which gets checked in check_unexpected_wakeup().
unexpected_wakeup_count = unexpected_wakeup_count + 1
-- assuming Kobo:resume() will be called in 15 seconds
logger.dbg("Kobo suspend: scheduing unexpected wakeup guard")
UIManager:scheduleIn(15, check_unexpected_wakeup)
end
function Kobo:resume()
logger.info("Kobo resume: clean up after wakeup")
-- reset unexpected_wakeup_count ASAP
unexpected_wakeup_count = 0
require("ui/uimanager"):unschedule(check_unexpected_wakeup)
-- 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
local f = io.open("/sys/power/state-extended", "w")
if not f then
logger.err("cannot open /sys/power/state-extended for writing!")
return false
end
local re, err_msg, err_code = f:write("0\n")
io.close(f)
logger.info("Kobo resume: unflagged kernel subsystems for resume, ret:", re)
if not re then
logger.err('write error: ', err_msg, err_code)
end
-- HACK: wait a bit (0.1 sec) for the kernel to catch up
util.usleep(100000)
-- cf. #1862, I can reliably break IR touch input on resume...
-- cf. also #1943 for the rationale behind applying this workaorund in every case...
f = io.open("/sys/devices/virtual/input/input1/neocmd", "w")
if f ~= nil then
f:write("a\n")
io.close(f)
end
end
function Kobo:saveSettings()
-- save frontlight state to G_reader_settings (and NickelConf if needed)
self.powerd:saveSettings()
end
function Kobo:powerOff()
os.execute("poweroff")
end
function Kobo:reboot()
os.execute("reboot")
end
-------------- device probe ------------
local codename = Kobo: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" then
return KoboTrilogy
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
else
error("unrecognized Kobo model "..codename)
end