from threading import Thread, Event
from traceback import format_exc
from time import sleep
from copy import copy
import importlib
import inspect
import atexit
import Queue
from actions import Action
from helpers import setup_logger, KEY_RELEASED, KEY_HELD, KEY_PRESSED
from hotplug import DeviceManager
logger = setup_logger(__name__, "warning")
class CallbackException(Exception):
def __init__(self, errno=0, message=""):
self.errno = errno
self.message = message
class InputProcessor(object):
"""A class which listens for input device events and processes the callbacks
set in the InputProxy instance for the currently active context."""
stop_flag = None
thread_index = 0
backlight_cb = None
current_proxy = None
proxy_methods = ["listen", "stop_listen"]
proxy_attrs = ["available_keys"]
proxies = []
def __init__(self, init_drivers, context_manager):
self.global_keymap = {}
self.cm = context_manager
self.queue = Queue.Queue()
self.available_keys = {}
self.drivers = {}
self.initial_drivers = {}
for driver in init_drivers:
name = self.attach_driver(driver)
self.initial_drivers[name] = driver
atexit.register(self.atexit)
def receive_key(self, key):
"""
Receives keypresses from drivers and puts them into ``self.queue``
for ``self.event_loop`` to process.
"""
try:
self.queue.put(key)
except:
raise #Just collecting possible exceptions for now
def attach_driver(self, driver):
"""
Attaches the driver to ``InputProcessor``.
"""
# Generating an unique yet human-readable name
counter = 0
driver_name = driver.__module__.rsplit('.', 1)[-1]
name = "{}-{}".format(driver_name, counter)
while name in self.drivers:
counter += 1
name = "{}-{}".format(driver_name, counter)
logger.info("Attaching driver: {}".format(name))
self.drivers[name] = driver
driver._old_send_key = driver.send_key
# Overriding the send_key method so that keycodes get sent to InputListener
driver.send_key = self.receive_key
self.available_keys[name] = driver.available_keys
self.update_all_proxy_attrs()
driver.start()
return name
def detach_driver(self, name):
"""
Detaches a driver from the ``InputProcessor``.
"""
logger.info("Detaching driver: {}".format(name))
if name in self.initial_drivers.values():
raise ValueError("Driver {} is from config.json, not removing for safety purposes".format(name))
driver = self.drivers.pop(name)
driver.send_key = driver._old_send_key
driver.stop()
self.available_keys.pop(name)
self.update_all_proxy_attrs()
def list_drivers(self):
"""
Returns a list of drivers description lists, containing items as follows:
* Driver name (auto-generated, in form of "driver_name-number")
* Driver object
* Available keys
* ``True`` if driver is supplied from ``config.json``, else ``False``
"""
return [[name, driver, self.available_keys[name], driver in self.initial_drivers.values()]
for name, driver in self.drivers.items()]
def attach_new_proxy(self, proxy):
"""
Calls ``detach_proxy``, then ``attach_proxy`` - just a convenience wrapper.
"""
self.detach_current_proxy()
self.attach_proxy(proxy)
def attach_proxy(self, proxy):
"""
This method is to be called from the ``ContextManager``. Saves a proxy
internally, so that when a callback is received, its keymap can be
referenced.
"""
if self.current_proxy:
raise ValueError("A proxy is already attached!")
logger.info("Attaching proxy for context: {}".format(proxy.context_alias))
self.current_proxy = proxy
def detach_current_proxy(self):
"""
This method is to be called from the ContextManager. Saves a proxy
internally, so that when a callback is received, its keymap can be
referenced.
"""
if self.current_proxy:
logger.info("Detaching proxy for context: {}".format(self.current_proxy.context_alias))
self.current_proxy = None
def get_current_proxy(self):
return self.current_proxy
def set_global_callback(self, key, callback):
"""
Sets a global callback for a key. That global callback will be processed
before the backlight callback or any proxy callbacks.
"""
logger.info("Setting a global callback for key {}".format(key))
if key in self.global_keymap.keys():
#Key is already used in the global keymap
raise CallbackException(4, "Global callback for {} can't be set because it's already in the keymap!".format(key_name))
self.global_keymap[key] = callback
def receive_key(self, key, state = None):
"""
This is the method that receives keypresses from drivers and puts
them into ``self.queue``, to be processed by ``self.event_loop``
Will block with full queue until the queue has a free spot.
"""
if state is not None:
self.queue.put((key, state))
else:
self.queue.put(key)
def event_loop(self, index):
"""
Blocking event loop which just calls ``process_key`` once a key
is received in the ``self.queue``. Also has some mechanisms that
make sure the existing event_loop will exit once flag is set, even
if other event_loop has already started (thought an event_loop can't
exit if it's still processing a callback.)
"""
logger.debug("Starting event loop "+str(index))
self.stop_flag = Event()
stop_flag = self.stop_flag # Saving a reference.
# stop_flag is an object that will signal the current input thread to exit or not exit once it's done processing a callback.
# It'll be called just before self.stop_flag will be overwritten. However, we've got a reference to it and now can check the exact flag this thread itself constructed.
# Praise the holy garbage collector.
stop_flag.clear()
while not stop_flag.isSet():
if self.get_current_proxy() is not None:
try:
data = self.queue.get(False, 0.1)
except Queue.Empty:
# here an active event_loop spends most of the time
sleep(0.1)
except AttributeError:
# typically happens upon program termination
pass
else:
# here event_loop is usually busy
self.process_key(data)
else:
# No current proxy set yet, not processing anything
sleep(0.1)
logger.debug("Stopping event loop "+str(index))
def global_key_processed_by_proxy(self, key, state, global_cb):
"""
Checks whether the global callback execution should be skipped in favor
of a proxy callback. For example, globally, pressing the green ("ANSWER")
button should switch you into the "make a call" menu, in other words,
switch context into the Phone app. However, once you're there, that key should
not cause a context switch yet again, but instead trigger the "call the
entered number" action from the proxy keymap!
At the moment, this mechanism does involve setting non-maskable callbacks
in the proxy, though.
"""
current_proxy = self.get_current_proxy()
# Key force-processed globally
if isinstance(global_cb, Action):
if getattr(global_cb, "force_global_key_processing", False):
return False
# No proxy set at the moment, weird but OK
if not current_proxy:
return False
if key in current_proxy.nonmaskable_keymap:
return True
def process_key(self, data):
"""
This function receives a keyname, finds the corresponding callback/action
and handles it. The lookup order is as follows:
* Global callbacks - set on the InputProcessor itself
* Proxy non-maskable callbacks
* Backlight callback (doesn't do anything with the keyname, but dismisses the keypress if it turned on the backlight)
* Proxy simple callbacks
* Proxy maskable callbacks
* Streaming callback (if set, just sends the key to it)
As soon as a match is found, processes the associated callback and returns.
"""
if isinstance(data, (tuple, list)) and len(data) == 2:
key, state = data
logger.debug("Received key: {}, state: {}".format(key, state))
elif isinstance(data, basestring):
key = data
state = None
logger.debug("Received key: {}".format(key))
else:
raise ValueError("Received unsupported object in place of a key/key+state: {}".format(data))
# Global and nonmaskable callbacks are supposed to work
# even when the screen backlight is off
#
# First, checking whether the global callbacks apply.
if key in self.global_keymap:
global_cb = self.global_keymap[key]
if not self.global_key_processed_by_proxy(key, state, global_cb):
self.handle_callback(global_cb, key, state, type="global")
return
# Now, all the callbacks are either proxy callbacks or backlight-related
# Saving a reference to current_proxy, in case it changes during the lookup
current_proxy = self.get_current_proxy()
if current_proxy and key in current_proxy.nonmaskable_keymap:
callback = current_proxy.nonmaskable_keymap[key]
self.handle_callback(callback, key, state, type="nonmaskable", context_name=current_proxy.context_alias)
return
# Checking backlight state, turning it on if necessary
if callable(self.backlight_cb):
try:
# backlight_cb turns on the backlight as an (expected) side effect
backlight_was_off = self.backlight_cb()
except:
logger.exception("Exception while calling the backlight check callback!")
else:
# If backlight was off, ignore the keypress
if backlight_was_off is True:
return
# Now, all the other callbacks of the proxy:
# Simple callbacks
if current_proxy and key in current_proxy.keymap:
callback = current_proxy.keymap[key]
self.handle_callback(callback, key, state, context_name=current_proxy.context_alias)
#Maskable callbacks
elif current_proxy and key in current_proxy.maskable_keymap:
callback = current_proxy.maskable_keymap[key]
self.handle_callback(callback, key, state, type="maskable", context_name=current_proxy.context_alias)
#Keycode streaming
elif current_proxy and callable(current_proxy.streaming):
self.handle_callback(current_proxy.streaming, key, state, pass_key=True, type="streaming", context_name=current_proxy.context_alias)
else:
logger.debug("Key {} has no handlers - ignored!".format(key))
pass #No handler for the key
def handle_callback(self, callback, key, state, pass_key=False, type="simple", context_name=None):
try:
if context_name:
logger.info("Processing a {} callback for key {} with state {}, context {}".format(type, key, state, context_name))
else:
logger.info("Processing a {} callback for key {}".format(type, key))
logger.debug("pass_key = {}".format(pass_key))
logger.debug("callback name: {}".format(callback.__name__))
# Checking whether the callback wants key state
if isinstance(callback, Action):
callback = callback.cb
keystate_cb_name = "zpui_icb_pass_key_state"
if hasattr(callback, "__func__"):
cb_needs_state = getattr(callback.__func__, keystate_cb_name, False)
else:
cb_needs_state = getattr(callback, keystate_cb_name, False)
# 4 calling conventions - need to pick the right one
if cb_needs_state is True:
if pass_key:
callback(key, state)
else:
callback(state)
else:
# We might also get None for a state if an input driver doesn't support states
if state == KEY_PRESSED or state is None:
if pass_key:
callback(key)
else:
callback()
else:
pass # Not calling the callback if the key is held or released
except Exception as e:
locals = inspect.trace()[-1][0].f_locals
context_alias = getattr(self.get_current_proxy(), "context_alias", None)
logger.error("Exception {} caused by callback {} when key {} with state {} was received, context: {}".format(e.__str__() or e.__class__, callback, key, state, context_alias))
logger.error(format_exc())
logger.error("Locals of the callback:")
logger.error(locals)
finally:
return
def listen(self):
"""Start event_loop in a thread. Nonblocking."""
self.processor_thread = Thread(target = self.event_loop, name="InputThread-"+str(self.thread_index), args=(self.thread_index, ))
self.thread_index += 1
self.processor_thread.daemon = True
self.processor_thread.start()
def stop_listen(self):
"""This sets a flag for ``event_loop`` to stop. If the ``event_loop()`` is
currently executing a callback, it will exit as soon as the callback will
finish executing."""
if self.stop_flag is not None:
self.stop_flag.set()
def atexit(self):
"""Exits driver (if necessary) if something wrong happened or ZPUI exits. Also, stops the InputProcessor, and all the associated drivers."""
self.stop_listen()
for driver in self.drivers.values():
driver.stop()
if hasattr(driver, "atexit"):
driver.atexit()
try:
self.processor_thread.join()
except AttributeError:
pass
def proxy_method(self, method_name, context_alias, *args, **kwargs):
if context_alias == self.cm.get_current_context():
logger.debug("Calling method \"{}\" for proxy \"{}\"".format(method_name, context_alias))
getattr(self, method_name)(*args, **kwargs)
else:
logger.debug("Not calling method \"{}\" for proxy \"{}\" since it's not current".format(method_name, context_alias))
pass #Ignoring method calls from non-current proxies for now
def register_proxy(self, proxy):
context_alias = proxy.context_alias
self.proxies.append(proxy)
self.set_proxy_methods(proxy, context_alias)
self.set_proxy_attrs(proxy)
def set_proxy_methods(self, proxy, alias):
for method_name in self.proxy_methods:
setattr(proxy, method_name, lambda x=method_name, y=alias, *a, **k: self.proxy_method(x, y, *a, **k))
def set_proxy_attrs(self, proxy):
for attr_name in self.proxy_attrs:
setattr(proxy, attr_name, copy(getattr(self, attr_name)))
def update_all_proxy_attrs(self):
"""
Updates all the proxied attributes for proxies, to be triggered when
one of the attributes is changed.
"""
for proxy in self.proxies:
self.set_proxy_attrs(proxy)
def init(driver_configs, context_manager):
""" This function is called by main.py to read the input configuration,
pick the corresponding drivers and initialize InputProcessor. Returns
the InputProcessor instance created.`"""
drivers = []
for driver_config in driver_configs:
driver_name = driver_config["driver"]
driver_module = importlib.import_module("input.drivers."+driver_name)
args = driver_config.get("args", [])
kwargs = driver_config.get("kwargs", {})
driver = driver_module.InputDevice(*args, **kwargs)
drivers.append(driver)
i = InputProcessor(drivers, context_manager)
dm = DeviceManager(i)
return i, dm
if __name__ == "__main__":
import doctest
doctest.testmod()