diff --git a/frontend/device/input.lua b/frontend/device/input.lua index 80a5e41a3..ccb781f46 100644 --- a/frontend/device/input.lua +++ b/frontend/device/input.lua @@ -176,6 +176,14 @@ local Input = { }, }, + fake_event_set = { + IntoSS = true, OutOfSS = true, + UsbPlugIn = true, UsbPlugOut = true, + Charging = true, NotCharging = true, + WakeupFromSuspend = true, ReadyToSuspend = true, + UsbDevicePlugIn = true, UsbDevicePlugOut = true, + }, + -- NOTE: When looking at the device in Portrait mode, that's assuming PgBack is on TOP, and PgFwd on the BOTTOM rotation_map = { [framebuffer.ORIENTATION_PORTRAIT] = {}, @@ -194,6 +202,7 @@ local Input = { Ctrl = false, Shift = false, Sym = false, + Meta = false, }, -- repeat state: @@ -256,6 +265,8 @@ function Input:init() self.event_map[10021] = "NotCharging" self.event_map[10030] = "WakeupFromSuspend" self.event_map[10031] = "ReadyToSuspend" + self.event_map[10040] = "UsbDevicePlugIn" + self.event_map[10041] = "UsbDevicePlugOut" -- user custom event map local custom_event_map_location = string.format( @@ -527,11 +538,7 @@ function Input:handleKeyBoardEv(ev) keycode = self.rotation_map[self.device.screen:getRotationMode()][keycode] end - -- fake events - if keycode == "IntoSS" or keycode == "OutOfSS" - or keycode == "UsbPlugIn" or keycode == "UsbPlugOut" - or keycode == "Charging" or keycode == "NotCharging" - or keycode == "WakeupFromSuspend" or keycode == "ReadyToSuspend" then + if self.fake_event_set[keycode] then return keycode end @@ -638,11 +645,7 @@ function Input:handlePowerManagementOnlyEv(ev) return keycode end - -- Fake events - if keycode == "IntoSS" or keycode == "OutOfSS" - or keycode == "UsbPlugIn" or keycode == "UsbPlugOut" - or keycode == "Charging" or keycode == "NotCharging" - or keycode == "WakeupFromSuspend" or keycode == "ReadyToSuspend" then + if self.fake_event_set[keycode] then return keycode end diff --git a/frontend/ui/elements/filemanager_menu_order.lua b/frontend/ui/elements/filemanager_menu_order.lua index 59b074c07..eb2eb83bd 100644 --- a/frontend/ui/elements/filemanager_menu_order.lua +++ b/frontend/ui/elements/filemanager_menu_order.lua @@ -43,6 +43,7 @@ local order = { }, device = { "keyboard_layout", + "external_keyboard", "font_ui_fallbacks", "----------------------------", "time", diff --git a/frontend/ui/elements/reader_menu_order.lua b/frontend/ui/elements/reader_menu_order.lua index 87d17f74b..399fcf4b1 100644 --- a/frontend/ui/elements/reader_menu_order.lua +++ b/frontend/ui/elements/reader_menu_order.lua @@ -86,6 +86,7 @@ local order = { }, device = { "keyboard_layout", + "external_keyboard", "font_ui_fallbacks", "----------------------------", "time", diff --git a/frontend/ui/uimanager.lua b/frontend/ui/uimanager.lua index 660dd0e73..19917c430 100644 --- a/frontend/ui/uimanager.lua +++ b/frontend/ui/uimanager.lua @@ -56,6 +56,13 @@ function UIManager:init() Power = function(input_event) Device:onPowerEvent(input_event) end, + -- This is for OTG input devices + UsbDevicePlugIn = function() + self:broadcastEvent(Event:new("UsbDevicePlugIn")) + end, + UsbDevicePlugOut = function() + self:broadcastEvent(Event:new("UsbDevicePlugOut")) + end, } self.poweroff_action = function() self._entered_poweroff_stage = true @@ -329,6 +336,46 @@ function UIManager:tickAfterNext(action) end --]] +function UIManager:debounce(seconds, immediate, action) + -- Ported from underscore.js + local args = nil + local previous_call_at = nil + local is_scheduled = false + local result = nil + + local scheduled_action + scheduled_action = function() + local passed_from_last_call = time:now() - previous_call_at + if seconds > passed_from_last_call then + self:scheduleIn(seconds - passed_from_last_call, scheduled_action) + is_scheduled = true + else + is_scheduled = false + if not immediate then + result = action(unpack(args)) + end + if not is_scheduled then + -- This check is needed because action can recursively call debounced_action_wrapper + args = nil + end + end + end + local debounced_action_wrapper = function(...) + args = table.pack(...) + previous_call_at = time:now() + if not is_scheduled then + self:scheduleIn(seconds, scheduled_action) + is_scheduled = true + if immediate then + result = action(unpack(args)) + end + end + return result + end + + return debounced_action_wrapper +end + --[[-- Unschedules a previously scheduled task. diff --git a/frontend/ui/widget/focusmanager.lua b/frontend/ui/widget/focusmanager.lua index 947a7dc6d..e967cdc6d 100644 --- a/frontend/ui/widget/focusmanager.lua +++ b/frontend/ui/widget/focusmanager.lua @@ -32,11 +32,17 @@ local FocusManager = InputContainer:extend{ movement_allowed = { x = true, y = true }, } --- Only build the default mappings once, we'll make copies during instantiation. -local KEY_EVENTS = {} -local BUILTIN_KEY_EVENTS = {} -local EXTRA_KEY_EVENTS = {} -do +-- Only build the default mappings once on initialization, or when an external keyboard is (dis-)/connected. +-- We'll make copies during instantiation. +local KEY_EVENTS +local BUILTIN_KEY_EVENTS +local EXTRA_KEY_EVENTS + +local function populateEventMappings() + KEY_EVENTS = {} + BUILTIN_KEY_EVENTS = {} + EXTRA_KEY_EVENTS = {} + if Device:hasDPad() then local event_keys = {} -- these will all generate the same event, just with different arguments @@ -93,6 +99,8 @@ do end end +populateEventMappings() + function FocusManager:_init() InputContainer._init(self) @@ -250,6 +258,33 @@ function FocusManager:onFocusMove(args) return true end +function FocusManager:onPhysicalKeyboardConnected() + -- Re-initialize with new keys info. + populateEventMappings() + -- We can't just call FocusManager._init because it will *reset* the mappings, losing our widget-specific ones (if any), + -- and it'll call InputContainer._init, which *also* resets the touch zones. + -- Instead, we'll just do a merge ourselves. + util.tableMerge(self.key_events, KEY_EVENTS) + -- populateEventMappings replaces these, so, update our refs + self.builtin_key_events = BUILTIN_KEY_EVENTS + self.extra_key_events = EXTRA_KEY_EVENTS +end + +function FocusManager:onPhysicalKeyboardDisconnected() + local prev_key_events = KEY_EVENTS + populateEventMappings() + + -- Remove what disappeared from KEY_EVENTS from self.key_events (if any). + -- NOTE: This is slightly overkill, we could very well live with a few unreachable mappings for the rest of this widget's life ;). + for k, _ in pairs(prev_key_events) do + if not KEY_EVENTS[k] then + self.key_events[k] = nil + end + end + self.builtin_key_events = BUILTIN_KEY_EVENTS + self.extra_key_events = EXTRA_KEY_EVENTS +end + -- constant, used to reset focus widget after layout recreation -- not send Unfocus event FocusManager.NOT_UNFOCUS = 1 diff --git a/frontend/ui/widget/inputtext.lua b/frontend/ui/widget/inputtext.lua index 1595cfbea..faa3555b5 100644 --- a/frontend/ui/widget/inputtext.lua +++ b/frontend/ui/widget/inputtext.lua @@ -72,9 +72,7 @@ function InputText:initEventListener() end function InputText:onFocus() end function InputText:onUnfocus() end --- only use PhysicalKeyboard if the device does not have touch screen -if Device:isTouchDevice() or Device:hasDPad() then - Keyboard = require("ui/widget/virtualkeyboard") +local function initTouchEvents() if Device:isTouchDevice() then function InputText:initEventListener() self.ges_events = { @@ -284,6 +282,9 @@ if Device:isTouchDevice() or Device:hasDPad() then return false end end +end + +local function initDPadEvents() if Device:hasDPad() then function InputText:onFocus() -- Event called by the focusmanager @@ -302,10 +303,23 @@ if Device:isTouchDevice() or Device:hasDPad() then return true end end -else - Keyboard = require("ui/widget/physicalkeyboard") end +-- only use PhysicalKeyboard if the device does not support touch input +function InputText.initInputEvents() + FocusManagerInstance = nil + + if Device:isTouchDevice() or Device:hasDPad() then + Keyboard = require("ui/widget/virtualkeyboard") + initTouchEvents() + initDPadEvents() + else + Keyboard = require("ui/widget/physicalkeyboard") + end +end + +InputText.initInputEvents() + function InputText:checkTextEditability() -- The split of the 'text' string to a table of utf8 chars may not be -- reversible to the same string, if 'text' comes from a binary file diff --git a/plugins/externalkeyboard.koplugin/_meta.lua b/plugins/externalkeyboard.koplugin/_meta.lua new file mode 100644 index 000000000..f8624d66e --- /dev/null +++ b/plugins/externalkeyboard.koplugin/_meta.lua @@ -0,0 +1,6 @@ +local _ = require("gettext") +return { + name = "externalkeyboard", + fullname = _("External Keyboard"), + description = _([[Manages USB OTG and configures keyboard.]]), +} diff --git a/plugins/externalkeyboard.koplugin/event_map_keyboard.lua b/plugins/externalkeyboard.koplugin/event_map_keyboard.lua new file mode 100644 index 000000000..35e646c72 --- /dev/null +++ b/plugins/externalkeyboard.koplugin/event_map_keyboard.lua @@ -0,0 +1,72 @@ +--[[ +event map for full-size keyboards. +--]] + +return { + [2] = "1", [3] = "2", [4] = "3", [5] = "4", [6] = "5", [7] = "6", [8] = "7", [9] = "8", [10] = "9", [11] = "0", + [16] = "Q", [17] = "W", [18] = "E", [19] = "R", [20] = "T", [21] = "Y", [22] = "U", [23] = "I", [24] = "O", [25] = "P", + [30] = "A", [31] = "S", [32] = "D", [33] = "F", [34] = "G", [35] = "H", [36] = "J", [37] = "K", [38] = "L", [39] = ":", [40] = "'", + [44] = "Z", [45] = "X", [46] = "C", [47] = "V", [48] = "B", [49] = "N", [50] = "M", [51] = ",", [52] = ".", [53] = "/", + + [14] = "Backspace", + [15] = "Tab", + [28] = "Press", -- Enter + [29] = "LCtrl", + [42] = "Shift", -- Left Shift + [43] = "\\", + [54] = "Shift", -- Right Shift + [56] = "LAlt", + [58] = "CapsLock", + [57] = " ", + [59] = "F1", + [60] = "F2", + [61] = "F3", + [62] = "F4", + [63] = "F5", + [64] = "F6", + [65] = "F7", + [66] = "F8", + [67] = "F9", + [68] = "F10", + [69] = "NumLock", + [70] = "ScrollLock", + + [71] = "KP7", + [72] = "KP8", + [73] = "KP9", + [74] = "KPMinus", + [75] = "KP4", + [76] = "KP5", + [77] = "KP6", + [78] = "KPPlus", + [79] = "KP1", + [80] = "KP2", + [81] = "KP3", + [82] = "KP0", + [87] = "F11", + [88] = "F12", + [96] = "Press", -- KPEnter + + [97] = "Ctrl", -- Right Ctrl + [98] = "Home", + [99] = "PrintScr", -- Also SysRq + [100] = "RAlt", + [102] = "Home", + [103] = "Up", + [104] = "LPgBack", -- PageUp + [105] = "Left", + [106] = "Right", + [107] = "End", + [108] = "Down", + [109] = "LPgFwd", -- PageDown + [110] = "Ins", + [111] = "Del", + [114] = "VMinus", + [115] = "VPlus", + [116] = "Power", + [119] = "Pause", + [125] = "LMeta", -- Meta, Win, Cmd, etc. + [126] = "RMeta", + [127] = "Compose", + [139] = "Menu", +} diff --git a/plugins/externalkeyboard.koplugin/find-keyboard.lua b/plugins/externalkeyboard.koplugin/find-keyboard.lua new file mode 100644 index 000000000..01ef5b07b --- /dev/null +++ b/plugins/externalkeyboard.koplugin/find-keyboard.lua @@ -0,0 +1,102 @@ +local bit = require("bit") +local ffi = require("ffi") +local lfs = require("libs/libkoreader-lfs") + +-- Constants from the linux kernel input-event-codes.h +local KEY_UP = 103 +local BTN_DPAD_UP = 0x220 + +local FindKeyboard = {} + +local function count_set_bits(n) + -- Brian Kernighan's algorithm + local count = 0 + while n ~= 0 do + count = count + 1 + n = bit.band(n, n - 1) + end + return count +end + +local function capabilities_str_to_long_bitmap_array(str) + -- The format for capabilities is at include/linux/mod_devicetable.h. + -- They are long's split by spaces. See linux/drivers/input/input.c::input_print_bitmap. + local long_bitmap_arr = {} + for c in str:gmatch "([0-9a-fA-F]+)" do + local long_bitmap = tonumber(c, 16) + table.insert(long_bitmap_arr, 1, long_bitmap) + end + return long_bitmap_arr +end + +local function count_set_bits_in_array(arr) + local count = 0 + for __, number in ipairs(arr) do + local count_in_number = count_set_bits(number) + count = count + count_in_number + end + + return count +end + +local function is_capabilities_bit_set(long_bitmap_arr, bit_offset) + local long_bitsize = ffi.sizeof("long") * 8 + local arr_index = math.floor(bit_offset / long_bitsize) + local long_mask = bit.lshift(1, bit_offset % long_bitsize) + local long_bitmap = long_bitmap_arr[arr_index + 1] -- Array index starts from 1 in Lua + + if long_bitmap then + return bit.band(long_bitmap, long_mask) ~= 0 + else + return false + end +end + +local function read_key_capabilities(sys_event_path) + local key_path = sys_event_path .. "/device/capabilities/key" + local file = io.open(key_path, "r") + if not file then + -- This should not happen - the kernel creates key capabilities file for all devices. + return nil + end + local keys_bitmap_str = file:read("l") + file:close() + + return capabilities_str_to_long_bitmap_array(keys_bitmap_str) +end + +local function analyze_key_capabilities(long_bitmap_arr) + -- The heuristic is that a keyboard has at least as many keys as there are alphabet letters and some more. + local keyboard_min_number_keys = 64 + local keys_count = count_set_bits_in_array(long_bitmap_arr) + + local is_keyboard = keys_count >= keyboard_min_number_keys + local has_dpad = is_capabilities_bit_set(long_bitmap_arr, KEY_UP) or + is_capabilities_bit_set(long_bitmap_arr, BTN_DPAD_UP) + + return { + is_keyboard = is_keyboard, + has_dpad = has_dpad, + } +end + +function FindKeyboard:find() + local keyboards = {} + for event_file_name in lfs.dir("/sys/class/input/") do + if event_file_name:match("event.*") then + local capabilities_long_bitmap_arr = read_key_capabilities("/sys/class/input/" .. event_file_name) + if capabilities_long_bitmap_arr then + local keyboard_info = analyze_key_capabilities(capabilities_long_bitmap_arr) + if keyboard_info.is_keyboard then + table.insert(keyboards, { + event_path = "/dev/input/" .. event_file_name, + has_dpad = keyboard_info.has_dpad + }) + end + end + end + end + return keyboards +end + +return FindKeyboard diff --git a/plugins/externalkeyboard.koplugin/main.lua b/plugins/externalkeyboard.koplugin/main.lua new file mode 100644 index 000000000..479633eab --- /dev/null +++ b/plugins/externalkeyboard.koplugin/main.lua @@ -0,0 +1,325 @@ +local Event = require("ui/event") +local FindKeyboard = require("find-keyboard") +local Device = require("device") +local InfoMessage = require("ui/widget/infomessage") +local InputText = require("ui/widget/inputtext") +local lfs = require("libs/libkoreader-lfs") +local logger = require("logger") +local UIManager = require("ui/uimanager") +local WidgetContainer = require("ui/widget/container/widgetcontainer") +local event_map_keyboard = require("event_map_keyboard") +local util = require("util") +local _ = require("gettext") + +-- The include/linux/usb/role.h calls the USB roles "host" and "device". +local USB_ROLE_DEVICE = "device" +local USB_ROLE_HOST = "host" +-- The Chipidea driver calls them "host" and "gadget". +-- This plugin sticks to Linux naming except when interacting with drivers. +local CHIPIDEA_TO_USB = { + host = USB_ROLE_HOST, + gadget = USB_ROLE_DEVICE, +} +local USB_TO_CHIPIDEA = { + [USB_ROLE_HOST] = "host", + [USB_ROLE_DEVICE] = "gadget", +} +-- sunxi just adds a "usb_" prefix +local SUNXI_TO_USB = { + usb_host = USB_ROLE_HOST, + usb_device = USB_ROLE_DEVICE, +} +local USB_TO_SUNXI = { + [USB_ROLE_HOST] = "usb_host", + [USB_ROLE_DEVICE] = "usb_device", +} + +-- This path exists on Kobo Clara and newer. Other devices w/ Chipidea drivers should have it too. +-- Also, the kernel must be compiled with CONFIG_DEBUG_FS and the debugfs must be mounted (we'll ensure the latter). +local OTG_CHIPIDEA_ROLE_PATH = "/sys/kernel/debug/ci_hdrc.0/role" +-- This one is for devices on a sunxi SoC (tested on a B300, as found on the Kobo Elipsa & Sage). +-- It does not require debugfs, but the point is moot as debugfs is mounted by default on those, +-- as Nickel relies on it for PM interaction with the display driver. +local OTG_SUNXI_ROLE_PATH = "/sys/devices/platform/soc/usbc0/otg_role" + +local function setupDebugFS() + local mounts = io.open("/proc/mounts", "re") + if not mounts then + return false + end + + local found = false + for line in mounts:lines() do + if line:find("^none /sys/kernel/debug debugfs") then + found = true + break + end + end + mounts:close() + + if not found then + if os.execute("mount -t debugfs none /sys/kernel/debug") ~= 0 then + logger.warn("ExternalKeyboard: Failed to mount debugfs") + return false + end + end + + return true +end + +if lfs.attributes("/sys/kernel/debug", "mode") == "directory" then + -- This should be in init() but the check must come first. So this part of initialization is here. + -- It is quick and harmless enough to be in a check. + if not setupDebugFS() then + return { disabled = true } + end + if lfs.attributes(OTG_CHIPIDEA_ROLE_PATH, "mode") ~= "file" and + lfs.attributes(OTG_SUNXI_ROLE_PATH, "mode") ~= "file" then + return { disabled = true } + end +else + return { disabled = true } +end + +local function yes() return true end +local function no() return false end -- luacheck: ignore + +local ExternalKeyboard = WidgetContainer:extend{ + name = "external_keyboard", + is_doc_only = false, + original_device_values = nil, + keyboard_fds = {}, +} + +function ExternalKeyboard:init() + self.ui.menu:registerToMainMenu(self) + + -- Check if we should go with the sunxi otg manager, or the chipidea driver... + if lfs.attributes(OTG_SUNXI_ROLE_PATH, "mode") == "file" then + self.getOTGRole = self.sunxiGetOTGRole + self.setOTGRole = self.sunxiSetOTGRole + else + self.getOTGRole = self.chipideaGetOTGRole + self.setOTGRole = self.chipideaSetOTGRole + end + + local role = self:getOTGRole() + logger.dbg("ExternalKeyboard: role", role) + + if role == USB_ROLE_DEVICE and G_reader_settings:isTrue("external_keyboard_otg_mode_on_start") then + self:setOTGRole(USB_ROLE_HOST) + role = USB_ROLE_HOST + end + if role == USB_ROLE_HOST then + self:findAndSetupKeyboard() + end +end + +function ExternalKeyboard:addToMainMenu(menu_items) + menu_items.external_keyboard = { + text = _("External Keyboard"), + sub_item_table = { + { + text = _("Enable OTG mode to connect peripherals"), + keep_menu_open = true, + checked_func = function() + return self:getOTGRole() == USB_ROLE_HOST + end, + callback = function(touchmenu_instance) + local role = self:getOTGRole() + local new_role = (role == USB_ROLE_DEVICE) and USB_ROLE_HOST or USB_ROLE_DEVICE + self:setOTGRole(new_role) + touchmenu_instance:updateItems() + end, + }, + { + text = _("Always enable OTG mode"), + keep_menu_open = true, + checked_func = function() + return G_reader_settings:isTrue("external_keyboard_otg_mode_on_start") + end, + callback = function(touchmenu_instance) + G_reader_settings:flipNilOrFalse("external_keyboard_otg_mode_on_start") + end, + }, + { + text = _("Help"), + keep_menu_open = true, + callback = function() + self:showHelp() + end, + }, + } + } +end + +function ExternalKeyboard:chipideaGetOTGRole() + local role = USB_ROLE_DEVICE + local file = io.open(OTG_CHIPIDEA_ROLE_PATH, "re") + + -- Do not throw exception if the file for role does not exist. + -- If it does not exist, the USB must be in the default device mode. + if file then + local chipidea_role = file:read("l") + file:close() + return CHIPIDEA_TO_USB[chipidea_role] or role + end + return role +end + +function ExternalKeyboard:sunxiGetOTGRole() + local file = io.open(OTG_SUNXI_ROLE_PATH, "re") + + -- File should always be present + if file then + local sunxi_role = file:read("l") + file:close() + return SUNXI_TO_USB[sunxi_role] + end +end + +function ExternalKeyboard:getOTGRole() end + +function ExternalKeyboard:chipideaSetOTGRole(role) + -- Writing role to file will fail if the role is the same as the current role. + -- Check current role before calling. + logger.dbg("ExternalKeyboard:chipideaSetOTGRole setting to", role) + local file = io.open(OTG_CHIPIDEA_ROLE_PATH, "we") + if file then + file:write(USB_TO_CHIPIDEA[role]) + file:close() + end +end + +function ExternalKeyboard:sunxiSetOTGRole(role) + -- Sunxi being what it is, there's no sanity check at all, it'll happily reset USB to set the same role again. + logger.dbg("ExternalKeyboard:sunxiSetOTGRole setting to", role) + local file = io.open(OTG_SUNXI_ROLE_PATH, "we") + if file then + file:write(USB_TO_SUNXI[role]) + file:close() + end +end + +function ExternalKeyboard:setOTGRole(role) end + +-- Keep in mind this fires every time we tear down the FM or Reader, too. +-- Then again, so does init when the new view spins up, +-- which ensures everything works out when switching between the FM & the Reader, +-- as long as `external_keyboard_otg_mode_on_start` is enabled. +function ExternalKeyboard:onCloseWidget() + logger.dbg("ExternalKeyboard:onCloseWidget") + local role = self:getOTGRole() + if role == USB_ROLE_HOST then + self:setOTGRole(USB_ROLE_DEVICE) + end +end + +ExternalKeyboard.onUsbDevicePlugIn = UIManager:debounce(0.5, false, function(self) + self:findAndSetupKeyboard() +end) + +ExternalKeyboard.onUsbDevicePlugOut = UIManager:debounce(0.5, false, function(self) + logger.dbg("ExternalKeyboard: onUsbDevicePlugOut") + local is_any_disconnected = false + -- Check that a keyboard really was disconnected. Another USB device could've been unplugged. + for event_path, fd in pairs(ExternalKeyboard.keyboard_fds) do + local event_file_attrs = lfs.attributes(event_path, "mode") + logger.dbg("ExternalKeyboard: checked if event file exists. path:", event_path, "file mode:", tostring(event_file_attrs)) + if event_file_attrs == nil then + is_any_disconnected = true + end + end + + if not is_any_disconnected then + return + end + + logger.dbg("ExternalKeyboard: USB keyboard was disconnected") + + ExternalKeyboard.keyboard_fds = {} + if ExternalKeyboard.original_device_values then + Device.input.event_map = ExternalKeyboard.original_device_values.event_map + Device.keyboard_layout = ExternalKeyboard.original_device_values.keyboard_layout + Device.hasKeyboard = ExternalKeyboard.original_device_values.hasKeyboard + Device.hasDPad = ExternalKeyboard.original_device_values.hasDPad + ExternalKeyboard.original_device_values = nil + end + + -- Broadcasting events throught UIManager would only get to InputText if there is an active widget on the window stack. + -- So, calling a static function is the only choice. + -- InputText.setKeyboard(require("ui/widget/virtualkeyboard")) + -- Update the existing input widgets. It must be issued after the static state of InputText is updated. + InputText.initInputEvents() + UIManager:broadcastEvent(Event:new("PhysicalKeyboardDisconnected")) +end) + +-- The keyboard events with the same key codes would override the original events. +-- That may cause embedded buttons to lose their original function and produce letters. +-- Can we tell from which device a key press comes? The koreader-base passes values of input_event which do not have file descriptors. +function ExternalKeyboard:findAndSetupKeyboard() + local keyboards = FindKeyboard:find() + local is_new_keyboard_setup = false + local has_dpad_func = Device.hasDPad + + -- A USB keyboard may be recognized as several devices under a hub. And several of them may + -- have keyboard capabilities set. Yet, only one would emit the events. The solution is to open all of them. + for __, keyboard_info in ipairs(keyboards) do + logger.dbg("ExternalKeyboard:findAndSetupKeyboard found event path", keyboard_info.event_path, "has_dpad", keyboard_info.has_dpad) + -- Check if the event file already was open. + if ExternalKeyboard.keyboard_fds[keyboard_info.event_path] == nil then + local ok, fd = pcall(Device.input.open, keyboard_info.event_path) + if not ok then + UIManager:show(InfoMessage:new{ + text = "Error opening the keyboard device " .. keyboard_info.event_path .. ":\n" .. tostring(fd), + }) + return + end + + is_new_keyboard_setup = true + ExternalKeyboard.keyboard_fds[keyboard_info.event_path] = fd + + if keyboard_info.has_dpad then + has_dpad_func = yes + end + end + end + + if is_new_keyboard_setup then + -- The setting for input_invert_page_turn_keys wouldn't mess up the new event map. Device module applies it on initialization, not dynamically. + ExternalKeyboard.original_device_values = { + event_map = Device.input.event_map, + keyboard_layout = Device.keyboard_layout, + hasKeyboard = Device.hasKeyboard, + hasDPad = Device.hasDPad, + } + + -- Using a new table avoids mutating the original event map. + local event_map = {} + util.tableMerge(event_map, Device.input.event_map) + util.tableMerge(event_map, event_map_keyboard) + Device.input.event_map = event_map + Device.hasKeyboard = yes + Device.hasDPad = has_dpad_func + + UIManager:show(InfoMessage:new{ + text = _("Keyboard connected"), + timeout = 1, + }) + InputText.initInputEvents() + UIManager:broadcastEvent(Event:new("PhysicalKeyboardConnected")) + end +end + +function ExternalKeyboard:showHelp() + UIManager:show(InfoMessage:new { + text = _([[ +Note that in the OTG mode the device would not be recognized as a USB drive by a computer. + +Troubleshooting: +- If the keyboard is not recognized after plugging it in, try switching the USB mode to regular and back to OTG again. +]]), + }) +end + +return ExternalKeyboard