#!/usr/bin/python import time from rgbkbd.core import StaticLighting, KeyboardMode, EventHandler from rgbkbd.color import Color, Colors from rgbkbd.geometry import Keys # Lighting animation class TypingSpeedAnimation(EventHandler): """Displays typing speeds (current, peak, sustained) in 10's of WPM along the number row of keys. Current speed is shown by a bar of white keys with a fading effect. Peak speed is shown by a single yellow key, unless it exceeds the width of the bar, in which case it will show as red on the highest value key. Sustained speed is shown by a single green key, unless it exceeds the width of the bar, in which case it will be shown as a single blue key on the highest value key. When there is a conflict on colors to display, the sustained value overrides the peak value which overrides the current value. """ tick_rate = 0.1 light_decay_seconds = 0.3 # Number of seconds of data to use for the current speed value running_avg_seconds = 2 # Number of seconds of current speed in which to track peak speed peak_seconds = 10 # Number of seconds to use for the sustained speed. The intention is that # this should be long enough time that it doesn't vary much on bursts of # keystrokes. It should reflect what you would be doing if you were typing # non-stop such as composing text or doing data entry. sustained_avg_seconds = 60 # Keys to use for the display of typing speed. # '1' = >=10WPM, '2' = >=20WPM, ... '0' = >=100WPM, '=' = >=120WPM # So when the '=' key goes red, that means you've peaked at over 130WPM; # and if it goes blue, you have sustained over 130WPM for at least a # minute. display_bar = list("1234567890") + ["minus", "equal"] def __init__(self, keyboard): super(TypingSpeedAnimation, self).__init__(keyboard) self.last_tick = time.time() self.key_history = {} def tick(self): """Handler for tick events""" # Display in units of 10WPM so we can represent a reasonable # touch-typing range of WPM self.display_rates([r/10. for r in self.calc_wpm_rates()]) def event(self, key, state): """Handler for key events""" #print "event: %s %s" % (key, state) now = round(time.time(), 1) # tenth-second granularity if state == '+': keystrokes = self.key_history.get(now, 0) if key in ['bspace', 'del']: delta = -1 elif key in Keys.TYPING: delta = 1 else: delta = 0 self.key_history[now] = keystrokes + delta self.tick() def calc_cpm_rates(self): """Returns a list of rates in characters per minute. The lowest index is the most recent rate. """ seconds_of_history = max(self.running_avg_seconds + max(self.light_decay_seconds, self.peak_seconds), self.sustained_avg_seconds+1) now = time.time() total_keystrokes = [0,] * int(seconds_of_history / self.tick_rate) for timestamp, keystrokes in sorted(self.key_history.items()): if timestamp <= now - seconds_of_history: # Age off old data # We can modify the underlying dictionary because we have # created a list of the items in the setup of the for loop del self.key_history[timestamp] else: # This one counts age = now-timestamp # how many seconds old it is bucket = int(age / self.tick_rate) total_keystrokes[bucket] += keystrokes return [c/self.tick_rate*60. for c in total_keystrokes] def calc_wpm_rates(self): """Returns rates in words per minute. 1 WPM is defined as 5 CPM.""" return [c/5. for c in self.calc_cpm_rates()] def running_average(self, rate_data, amount): """Utility function to calculate a running average of a given width over a set of data. """ return [sum(rate_data[n:n+amount])/amount for n in range(len(rate_data)-amount)] def display_rates(self, rate_data): """Update the keyboard lights to display the given rate data.""" # Calculate a running average of the data; otherwise the output is too # volatile; hitting a few keys in very rapid succession can make for an # misleading peak value. sustained_rate = int(max(self.running_average(rate_data, int(self.sustained_avg_seconds / self.tick_rate)) + [0])) rate_data = self.running_average(rate_data, int(self.running_avg_seconds / self.tick_rate)) #print max(rate_data), rate_data[:30], '...' # DEBUG # intensities are in the range [0-1]; an intensity for each key in the display bar intensities = [0,] * len(self.display_bar) # Iterate over the data starting at the oldest data for rate in reversed(rate_data[:int(self.light_decay_seconds/self.tick_rate)]): # linear decay intensities = [max(0.0, i-self.tick_rate/self.light_decay_seconds) for i in intensities] for i in range(min(int(rate), len(self.display_bar))): intensities[i] = 1.0 # peg the new data # Determine preliminary colors for the keys based on the current typing speed key_colors = {} for intensity, key in zip(intensities, self.display_bar): color = self.intensity_to_color(intensity) key_colors[key] = color # Indicate peak speed, if >0 peak = int(max(rate_data[:int(self.peak_seconds/self.tick_rate)])) if peak: key = self.display_bar[min(peak, len(self.display_bar))-1] if peak > len(self.display_bar): # Blow-out! color = Colors.RED else: color = Colors.YELLOW key_colors[key] = color # Indicate sustained speed, if >0 if sustained_rate: key = self.display_bar[min(sustained_rate, len(self.display_bar))-1] if sustained_rate > len(self.display_bar): # Blow-out! color = Colors.BLUE else: color = Colors.GREEN key_colors[key] = color for key, color in key_colors.items(): self.keyboard.set_keys(key, color) #print intensities, peak # DEBUG def intensity_to_color(self, intensity): """Convert intensity, a value in [0, 1], into a shade of grey.""" color_value = min(255, intensity * 256) return Color(red=color_value, green=color_value, blue=color_value) # Keyboard Mode def TypingMode(manager, keyboard): profile = [ (Keys.ALL, Color.fromstring('e0e0e0')), (Keys.HOME, Colors.BLUE), (Keys.NUM_PAD, Colors.GREEN), (Keys.FUNCTION, Colors.RED), (Keys.MEDIA, Colors.DARKCYAN), (Keys.NAV, Colors.YELLOW), (Keys.MOD, Colors.PURPLE), ("lock", Colors.BLACK), # Windows Lock key ("light", Colors.BLACK), # Light key ("caps", Colors.BLACK), # Caps Lock key ] return KeyboardMode(manager, keyboard, static_lighting=StaticLighting(keyboard, profile=profile), animations = [ TypingSpeedAnimation(keyboard), ])