source: rgbkbd/trunk/rgbkbd/modes/typingmode.py @ 71

Last change on this file since 71 was 71, checked in by retracile, 9 years ago

Initial import of source code

  • Property svn:executable set to *
File size: 7.1 KB
Line 
1#!/usr/bin/python
2import time
3
4from rgbkbd.core import StaticLighting, KeyboardMode, EventHandler
5from rgbkbd.color import Color, Colors
6from rgbkbd.geometry import Keys
7
8
9# Lighting animation
10class TypingSpeedAnimation(EventHandler):
11    """Displays typing speeds (current, peak, sustained) in 10's of WPM along
12    the number row of keys.
13
14    Current speed is shown by a bar of white keys with a fading effect.
15    Peak speed is shown by a single yellow key, unless it exceeds the width of
16    the bar, in which case it will show as red on the highest value key.
17    Sustained speed is shown by a single green key, unless it exceeds the width
18    of the bar, in which case it will be shown as a single blue key on the
19    highest value key.
20    When there is a conflict on colors to display, the sustained value
21    overrides the peak value which overrides the current value.
22    """
23    tick_rate = 0.1
24    light_decay_seconds = 0.3
25    # Number of seconds of data to use for the current speed value
26    running_avg_seconds = 2
27    # Number of seconds of current speed in which to track peak speed
28    peak_seconds = 10
29    # Number of seconds to use for the sustained speed.  The intention is that
30    # this should be long enough time that it doesn't vary much on bursts of
31    # keystrokes.  It should reflect what you would be doing if you were typing
32    # non-stop such as composing text or doing data entry.
33    sustained_avg_seconds = 60
34    # Keys to use for the display of typing speed.
35    # '1' = >=10WPM, '2' = >=20WPM, ... '0' = >=100WPM, '=' = >=120WPM
36    # So when the '=' key goes red, that means you've peaked at over 130WPM;
37    # and if it goes blue, you have sustained over 130WPM for at least a
38    # minute.
39    display_bar = list("1234567890") + ["minus", "equal"]
40
41    def __init__(self, keyboard):
42        super(TypingSpeedAnimation, self).__init__(keyboard)
43        self.last_tick = time.time()
44        self.key_history = {}
45
46    def tick(self):
47        """Handler for tick events"""
48        # Display in units of 10WPM so we can represent a reasonable
49        # touch-typing range of WPM
50        self.display_rates([r/10. for r in self.calc_wpm_rates()])
51
52    def event(self, key, state):
53        """Handler for key events"""
54        #print "event: %s %s" % (key, state)
55        now = round(time.time(), 1) # tenth-second granularity
56        if state == '+':
57            keystrokes = self.key_history.get(now, 0)
58            if key in ['bspace', 'del']:
59                delta = -1
60            elif key in Keys.TYPING:
61                delta = 1
62            else:
63                delta = 0
64            self.key_history[now] = keystrokes + delta
65        self.tick()
66
67    def calc_cpm_rates(self):
68        """Returns a list of rates in characters per minute.  The lowest index
69        is the most recent rate.
70        """
71        seconds_of_history = max(self.running_avg_seconds + max(self.light_decay_seconds, self.peak_seconds), self.sustained_avg_seconds+1)
72        now = time.time()
73        total_keystrokes = [0,] * int(seconds_of_history / self.tick_rate)
74
75        for timestamp, keystrokes in sorted(self.key_history.items()):
76            if timestamp <= now - seconds_of_history:
77                # Age off old data
78                # We can modify the underlying dictionary because we have
79                # created a list of the items in the setup of the for loop
80                del self.key_history[timestamp]
81            else: # This one counts
82                age = now-timestamp # how many seconds old it is
83                bucket = int(age / self.tick_rate)
84                total_keystrokes[bucket] += keystrokes
85        return [c/self.tick_rate*60. for c in total_keystrokes]
86
87    def calc_wpm_rates(self):
88        """Returns rates in words per minute.  1 WPM is defined as 5 CPM."""
89        return [c/5. for c in self.calc_cpm_rates()]
90
91    def running_average(self, rate_data, amount):
92        """Utility function to calculate a running average of a given width
93        over a set of data.
94        """
95        return [sum(rate_data[n:n+amount])/amount for n in range(len(rate_data)-amount)]
96
97    def display_rates(self, rate_data):
98        """Update the keyboard lights to display the given rate data."""
99        # Calculate a running average of the data; otherwise the output is too
100        # volatile; hitting a few keys in very rapid succession can make for an
101        # misleading peak value.
102        sustained_rate = int(max(self.running_average(rate_data, int(self.sustained_avg_seconds / self.tick_rate)) + [0]))
103        rate_data = self.running_average(rate_data, int(self.running_avg_seconds / self.tick_rate))
104
105        #print max(rate_data), rate_data[:30], '...' # DEBUG
106
107        # intensities are in the range [0-1]; an intensity for each key in the display bar
108        intensities = [0,] * len(self.display_bar)
109        # Iterate over the data starting at the oldest data
110        for rate in reversed(rate_data[:int(self.light_decay_seconds/self.tick_rate)]):
111            # linear decay
112            intensities = [max(0.0, i-self.tick_rate/self.light_decay_seconds) for i in intensities]
113            for i in range(min(int(rate), len(self.display_bar))):
114                intensities[i] = 1.0 # peg the new data
115
116        # Determine preliminary colors for the keys based on the current typing speed
117        key_colors = {}
118        for intensity, key in zip(intensities, self.display_bar):
119            color = self.intensity_to_color(intensity)
120            key_colors[key] = color
121
122        # Indicate peak speed, if >0
123        peak = int(max(rate_data[:int(self.peak_seconds/self.tick_rate)]))
124        if peak:
125            key = self.display_bar[min(peak, len(self.display_bar))-1]
126            if peak > len(self.display_bar): # Blow-out!
127                color = Colors.RED
128            else:
129                color = Colors.YELLOW
130            key_colors[key] = color
131
132        # Indicate sustained speed, if >0
133        if sustained_rate:
134            key = self.display_bar[min(sustained_rate, len(self.display_bar))-1]
135            if sustained_rate > len(self.display_bar): # Blow-out!
136                color = Colors.BLUE
137            else:
138                color = Colors.GREEN
139            key_colors[key] = color
140
141        for key, color in key_colors.items():
142            self.keyboard.set_keys(key, color)
143
144        #print intensities, peak # DEBUG
145
146    def intensity_to_color(self, intensity):
147        """Convert intensity, a value in [0, 1], into a shade of grey."""
148        color_value = min(255, intensity * 256)
149        return Color(red=color_value, green=color_value, blue=color_value)
150
151
152# Keyboard Mode
153def TypingMode(manager, keyboard):
154    profile = [
155        (Keys.ALL, Color.fromstring('e0e0e0')),
156        (Keys.HOME, Colors.BLUE),
157        (Keys.NUM_PAD, Colors.GREEN),
158        (Keys.FUNCTION, Colors.RED),
159        (Keys.MEDIA, Colors.DARKCYAN),
160        (Keys.NAV, Colors.YELLOW),
161        (Keys.MOD, Colors.PURPLE),
162        ("lock", Colors.BLACK), # Windows Lock key
163        ("light", Colors.BLACK), # Light key
164        ("caps", Colors.BLACK), # Caps Lock key
165    ]
166    return KeyboardMode(manager, keyboard,
167        static_lighting=StaticLighting(keyboard, profile=profile),
168        animations = [
169            TypingSpeedAnimation(keyboard),
170        ])
Note: See TracBrowser for help on using the repository browser.