#!/usr/bin/python import os import time from color import Colors from geometry import Keys class Keyboard(object): """Object representing an attached keyboard; communicates with the ckb-daemon process. """ def __init__(self, num=None, device=None): if device is not None: self.num = int(device[-1]) self.cmdfile = '%s/cmd' % device elif num is not None: self.num = num self.cmdfile = '/dev/input/ckb%s/cmd' % num def _cmd(self, command): # FIXME: handle the driver daemon shutting down and eliminating this pipe open(self.cmdfile, 'w').write(command + '\n') def clear(self, color=Colors.BLACK): """Clears the colors of the keyboard to a solid color; black by default. """ self._cmd("rgb %s" % color) def lights(self, on=True): """Turns the keyboard lights on or off""" if on: self._cmd("rgb on") else: self._cmd("rgb off") def unbind(self, keys="all"): self._cmd("unbind %s" % keys) def rebind(self, keys="all"): self._cmd("rebind %s" % keys) def set_keys(self, keys, color): """Sets the color of a set of keys""" self._cmd('rgb %s:%s' % (keys, color)) def set_many_keys(self, key_colors): """Sets the colors of multiple set of keys""" self._cmd('rgb %s' % ' '.join('%s:%s' % c for c in key_colors)) def alloc_notify(self): """Allocate a notification pipe from ckb-daemon""" n = 1 while os.path.exists('/dev/input/ckb%s/notify%s' % (self.num, n)): n += 1 if n > 9: # Catch the driver limitation raise Exception("Too many notification nodes in use") notify_filename = '/dev/input/ckb%s/notify%s' % (self.num, n) self._cmd('notifyon %s' % n) self._cmd('@%s notify all:on' % n) timeout = time.time() + 2 while not os.path.exists(notify_filename) and time.time() < timeout: time.sleep(0.01) if not os.path.exists(notify_filename): raise Exception("Failed to create notification pipe") return (n, notify_filename) def free_notify(self, num): """Release a notification pipe from ckb-daemon""" self._cmd('notifyoff %s' % num) # Lighting stuff class StaticLighting(object): """Keys have a set color""" def __init__(self, keyboard, profile=None): self.keyboard = keyboard if profile is None: profile = [] self.profile = profile def start(self): """Initializes key colors""" if self.profile is not None: self.keyboard.set_many_keys(self.profile) class Monochrome(StaticLighting): """Static lighting where all keys are one color""" def __init__(self, keyboard, color=Colors.WHITE): super(Monochrome, self).__init__(keyboard, profile =[(Keys.ALL, color)]) class ColorPattern(object): """Object which returns a color given a time Abstract base class; subclasses must implement color(time). """ def __init__(self, colors, period=5): self.colors = colors self.period = period def foreground(self): """Utility function to get the foreground color""" return self.colors[0] def background(self): """Utility function to get the background color, defaulting to black""" if len(self.colors) > 1: background = self.colors[1] else: background = Colors.BLACK return background def phase(self, t): """Returns a float value in [0, 1] indicating where in the period the given time is. """ return (t % self.period) / self.period def color(self, t): """Given a time t, return a color""" raise NotImplementedError class MotionPattern(object): """Object which returns a time offset given an x,y position Abstract base class; subclasses must implement phase_offset(x, y). """ def __init__(self, period=5): self.period = period def t_offset(self, x, y): """Turns a phase offset into a time offset""" return self.phase_offset(x, y) * self.period def phase_offset(self, x, y): raise NotImplementedError class EventHandler(object): """Object to receive keyboard events from the Manager object By default does nothing with events. """ tick_rate = 0.03 # seconds def __init__(self, keyboard): self.keyboard = keyboard def tick(self): """Override this method for periodic events""" pass def event(self, key, state): """Override this method for key up/down events""" pass def chord_event(self, chord): """Override this method for key-chord events chord is a list of keys pressed in the chord, in order single keys are sent as a single element list """ pass # Keyboard Modes class KeyboardMode(object): """A keyboard mode represents a set of behaviors for the keyboard, including lighting schemes and reactions to key presses. """ def __init__(self, manager, keyboard, static_lighting, animations=None): self.manager = manager self.keyboard = keyboard self.static_lighting = static_lighting if animations is None: animations = [] self.animations = animations self.lights_on = True # Use fastest tick rate of the provided animations tick_rate = None for animation in animations: if animation.tick_rate is not None: if tick_rate is None: tick_rate = animation.tick_rate else: tick_rate = min(tick_rate, animation.tick_rate) if not tick_rate: tick_rate = None self.tick_rate = tick_rate #print "Tick rate of animations is: %r" % tick_rate # DEBUG def start(self, previous_mode=None): """Initializes the lighting effects and resets the key bindings.""" self.keyboard._cmd("rebind all") self.static_lighting.start() for animation in reversed(self.animations): animation.tick() def tick(self): """Passes tick events on to each animation in its stack of animations. """ for animation in reversed(self.animations): animation.tick() def is_command_toggle(self, chord): """Utility function to check if we're supposed to break out to command mode. """ value = sorted(chord) == ['light', 'lock'] #print "is_command_toggle: %s" % value # DEBUG if value: #print "active modes: %r" % self.manager.active_modes # DEBUG pass return value def event(self, key, state): """Passes key events on to each animation in its stack of animations. """ for animation in reversed(self.animations): animation.event(key, state) def chord_event(self, chord): """Passes chord events on to each animation in its stack of animations. Also handles the light toggle and breaking out to command mode """ #print "chord %s" % chord # DEBUG for animation in reversed(self.animations): animation.chord_event(chord) if chord == ["light"]: self.lights_on = not self.lights_on self.keyboard.lights(self.lights_on) elif self.is_command_toggle(chord): self.manager.mode_return()