summaryrefslogtreecommitdiffstats
path: root/keyboards/ergodox/keymaps/algernon/tools/log-to-heatmap.py
diff options
context:
space:
mode:
authorGergely Nagy <algernon@madhouse-project.org>2016-08-24 13:17:01 +0200
committerGergely Nagy <algernon@madhouse-project.org>2016-08-24 23:22:57 +0200
commitf512179e66e4d4dfbcc92c6ba1aaa1b6cf1ef12b (patch)
tree982b6483c74c01ef5bad18cdf585290397e278c4 /keyboards/ergodox/keymaps/algernon/tools/log-to-heatmap.py
parentad206155aa6f87de3fb9f0aefe60685ec12905a8 (diff)
ergodox: Update algernon's keymap to v1.6
Major changes include: Base layer changes ------------------ * The parentheses & bracket keys have been merged: tapping them results in `[` or `{` (if it was shifted), double tapping leads to `(`. * The `:;` and `-_` keys are now available on the base layer, on their **ADORE** location, too, just below `[{(`/`]})`. * The `Apps` key has been replaced by `F12`. * The `-`/`_` is no longer a tap-dance key. ADORE layer changes ------------------- * Adjustments were made to the **ADORE** layer, to separate some inconvenient combinations. Miscellaneous changes --------------------- * `LEAD u` now starts the symbolic unicode input system, instead of the OS-one. * The mouse acceleration keys on the **Navigation and Media* layer have been turned into toggles: tap them once to turn them on, until tapped again. Tapping an accelerator button will turn all the others off. * When the **ARROW** layer is on, the *red* and *blue* LEDs light up now. Heatmap ------- * The built-in keylogger has been greatly enhanced, it now outputs the pressed state, and the layer (Dvorak or ADORE). As such, the `ADORE_AUTOLOG` option has been removed, instead there is `AUTOLOG_ENABLE` now, which when enabled, makes the keylogger start when the keyboard boots. It defaults to off. * The heatmap generator received a lot of updates. Signed-off-by: Gergely Nagy <algernon@madhouse-project.org>
Diffstat (limited to 'keyboards/ergodox/keymaps/algernon/tools/log-to-heatmap.py')
-rwxr-xr-xkeyboards/ergodox/keymaps/algernon/tools/log-to-heatmap.py368
1 files changed, 236 insertions, 132 deletions
diff --git a/keyboards/ergodox/keymaps/algernon/tools/log-to-heatmap.py b/keyboards/ergodox/keymaps/algernon/tools/log-to-heatmap.py
index a13d12e81a..68585843be 100755
--- a/keyboards/ergodox/keymaps/algernon/tools/log-to-heatmap.py
+++ b/keyboards/ergodox/keymaps/algernon/tools/log-to-heatmap.py
@@ -3,143 +3,247 @@ import json
import os
import sys
import re
+import argparse
from math import floor
+from os.path import dirname
+
+class Heatmap(object):
+ coords = [
+ [
+ # Row 0
+ [ 4, 0], [ 4, 2], [ 2, 0], [ 1, 0], [ 2, 2], [ 3, 0], [ 3, 2],
+ [ 3, 4], [ 3, 6], [ 2, 4], [ 1, 2], [ 2, 6], [ 4, 4], [ 4, 6],
+ ],
+ [
+ # Row 1
+ [ 8, 0], [ 8, 2], [ 6, 0], [ 5, 0], [ 6, 2], [ 7, 0], [ 7, 2],
+ [ 7, 4], [ 7, 6], [ 6, 4], [ 5, 2], [ 6, 6], [ 8, 4], [ 8, 6],
+ ],
+ [
+ # Row 2
+ [12, 0], [12, 2], [10, 0], [ 9, 0], [10, 2], [11, 0], [ ],
+ [ ], [11, 2], [10, 4], [ 9, 2], [10, 6], [12, 4], [12, 6],
+ ],
+ [
+ # Row 3
+ [17, 0], [17, 2], [15, 0], [14, 0], [15, 2], [16, 0], [13, 0],
+ [13, 2], [16, 2], [15, 4], [14, 2], [15, 6], [17, 4], [17, 6],
+ ],
+ [
+ # Row 4
+ [20, 0], [20, 2], [19, 0], [18, 0], [19, 2], [], [], [], [],
+ [19, 4], [18, 2], [19, 6], [20, 4], [20, 6],
+ ],
+ [
+ # Row 5
+ [ ], [23, 0], [22, 2], [22, 0], [22, 4], [21, 0], [21, 2],
+ [24, 0], [24, 2], [25, 0], [25, 4], [25, 2], [26, 0], [ ],
+ ],
+ ]
+
+ def set_attr_at(self, block, n, attr, fn, val):
+ blk = self.heatmap[block][n]
+ if attr in blk:
+ blk[attr] = fn(blk[attr], val)
+ else:
+ blk[attr] = fn(None, val)
+
+ def coord(self, col, row):
+ return self.coords[row][col]
+
+ @staticmethod
+ def set_attr(orig, new):
+ return new
+
+ def set_bg(self, (block, n), color):
+ self.set_attr_at(block, n, "c", self.set_attr, color)
+ #self.set_attr_at(block, n, "g", self.set_attr, False)
+
+ def set_tap_info(self, (block, n), count, cap):
+ def _set_tap_info(o, _count, _cap):
+ ns = 4 - o.count ("\n")
+ return o + "\n" * ns + "%.02f%%" % (float(_count) / float(_cap) * 100)
+
+ if not cap:
+ cap = 1
+ self.heatmap[block][n + 1] = _set_tap_info (self.heatmap[block][n + 1], count, cap)
+
+ @staticmethod
+ def heatmap_color (v):
+ colors = [ [0.3, 0.3, 1], [0.3, 1, 0.3], [1, 1, 0.3], [1, 0.3, 0.3]]
+ fb = 0
+ if v <= 0:
+ idx1, idx2 = 0, 0
+ elif v >= 1:
+ idx1, idx2 = len(colors) - 1, len(colors) - 1
+ else:
+ val = v * (len(colors) - 1)
+ idx1 = int(floor(val))
+ idx2 = idx1 + 1
+ fb = val - float(idx1)
+
+ r = (colors[idx2][0] - colors[idx1][0]) * fb + colors[idx1][0]
+ g = (colors[idx2][1] - colors[idx1][1]) * fb + colors[idx1][1]
+ b = (colors[idx2][2] - colors[idx1][2]) * fb + colors[idx1][2]
+
+ r, g, b = [x * 255 for x in r, g, b]
+ return "#%02x%02x%02x" % (r, g, b)
+
+ def __init__(self, layout):
+ self.log = {}
+ self.total = 0
+ self.max_cnt = 0
+ self.layout = layout
+
+ def update_log(self, (c, r)):
+ if not (c, r) in self.log:
+ self.log[(c, r)] = 0
+ self.log[(c, r)] = self.log[(c, r)] + 1
+ self.total = self.total + 1
+ if self.max_cnt < self.log[(c, r)]:
+ self.max_cnt = self.log[(c, r)]
+
+ def get_heatmap(self):
+ with open("%s/heatmap-layout.%s.json" % (dirname(sys.argv[0]), self.layout), "r") as f:
+ self.heatmap = json.load (f)
+
+ ## Reset colors
+ for row in self.coords:
+ for coord in row:
+ if coord != []:
+ self.set_bg (coord, "#d9dae0")
+
+ for (c, r) in self.log:
+ coords = self.coord(c, r)
+ b, n = coords
+ cap = self.max_cnt
+ if cap == 0:
+ cap = 1
+ v = float(self.log[(c, r)]) / cap
+ self.set_bg (coords, self.heatmap_color (v))
+ self.set_tap_info (coords, self.log[(c, r)], self.total)
+ return self.heatmap
+
+ def get_stats(self):
+ usage = [
+ # left hand
+ [0, 0, 0, 0, 0],
+ # right hand
+ [0, 0, 0, 0, 0]
+ ]
+ finger_map = [0, 0, 1, 2, 3, 4, 4]
+ for (c, r) in self.log:
+ if r == 5: # thumb cluster
+ if c <= 6: # left side
+ usage[0][4] = usage[0][4] + self.log[(c, r)]
+ else:
+ usage[1][4] = usage[1][4] + self.log[(c, r)]
+ else:
+ fc = c
+ hand = 0
+ if fc >= 7:
+ fc = fc - 7
+ hand = 1
+ fm = finger_map[fc]
+ usage[hand][fm] = usage[hand][fm] + self.log[(c, r)]
+ hand_usage = [0, 0]
+ for f in usage[0]:
+ hand_usage[0] = hand_usage[0] + f
+ for f in usage[1]:
+ hand_usage[1] = hand_usage[1] + f
+
+ total = self.total
+ if total == 0:
+ total = 1
+ stats = {
+ "hands": {
+ "left": {
+ "usage": float(hand_usage[0]) / total * 100,
+ "fingers": {
+ "0 - pinky": 0,
+ "1 - ring": 0,
+ "2 - middle": 0,
+ "3 - index": 0,
+ "4 - thumb": 0,
+ }
+ },
+ "right": {
+ "usage": float(hand_usage[1]) / total * 100,
+ "fingers": {
+ "0 - thumb": 0,
+ "1 - index": 0,
+ "2 - middle": 0,
+ "3 - ring": 0,
+ "4 - pinky": 0,
+ }
+ },
+ }
+ }
+
+ hmap = ['left', 'right']
+ fmap = ['0 - pinky', '1 - ring', '2 - middle', '3 - index', '4 - thumb',
+ '0 - thumb', '1 - index', '2 - middle', '3 - ring', '4 - pinky']
+ for hand_idx in range(len(usage)):
+ hand = usage[hand_idx]
+ for finger_idx in range(len(hand)):
+ stats['hands'][hmap[hand_idx]]['fingers'][fmap[finger_idx + hand_idx * 5]] = float(hand[finger_idx]) / total * 100
+ return stats
+
+def dump_all(out_dir, heatmaps):
+ for layer in heatmaps.keys():
+ if len(heatmaps[layer].log) == 0:
+ continue
-cr_coord_map = [
- [
- # Row 0
- [ 4, 0], [ 4, 2], [ 2, 0], [ 1, 0], [ 2, 2], [ 3, 0], [ 3, 2],
- [ 3, 4], [ 3, 6], [ 2, 4], [ 1, 2], [ 2, 6], [ 4, 4], [ 4, 6],
- ],
- [
- # Row 1
- [ 8, 0], [ 8, 2], [ 6, 0], [ 5, 0], [ 6, 2], [ 7, 0], [ 7, 2],
- [ 7, 4], [ 7, 6], [ 6, 4], [ 5, 2], [ 6, 6], [ 8, 4], [ 8, 6],
- ],
- [
- # Row 2
- [12, 0], [12, 2], [10, 0], [ 9, 0], [10, 2], [11, 0], [ ],
- [ ], [11, 2], [10, 4], [ 9, 2], [10, 6], [12, 4], [12, 6],
- ],
- [
- # Row 3
- [17, 0], [17, 2], [15, 0], [14, 0], [15, 2], [16, 0], [13, 0],
- [13, 2], [16, 2], [15, 4], [14, 2], [15, 6], [17, 4], [17, 6],
- ],
- [
- # Row 4
- [20, 0], [20, 2], [19, 0], [18, 0], [19, 2], [], [], [], [],
- [19, 4], [18, 2], [19, 6], [20, 4], [20, 6],
- ],
- [
- # Row 5
- [ ], [23, 0], [22, 2], [22, 0], [22, 4], [21, 0], [21, 2],
- [24, 0], [24, 2], [25, 0], [25, 4], [25, 2], [26, 0], [ ],
- ],
-]
-
-def set_attr_at(j, b, n, attr, fn, val):
- blk = j[b][n]
- if attr in blk:
- blk[attr] = fn(blk[attr], val)
- else:
- blk[attr] = fn(None, val)
-
-def coord(col, row):
- return cr_coord_map[row][col]
-
-def set_attr(orig, new):
- return new
-
-def set_bg(j, (b, n), color):
- set_attr_at(j, b, n, "c", set_attr, color)
- #set_attr_at(j, b, n, "g", set_attr, False)
-
-def _set_tap_info(o, count, cap):
- ns = 4 - o.count ("\n")
- return o + "\n" * ns + "%.02f%%" % (float(count) / float(cap) * 100)
-
-def set_tap_info(j, (b, n), count, cap):
- j[b][n + 1] = _set_tap_info (j[b][n + 1], count, cap)
-
-def heatmap_color (v):
- colors = [ [0.3, 0.3, 1], [0.3, 1, 0.3], [1, 1, 0.3], [1, 0.3, 0.3]]
- fb = 0
- if v <= 0:
- idx1, idx2 = 0, 0
- elif v >= 1:
- idx1, idx2 = len(colors) - 1, len(colors) - 1
- else:
- val = v * (len(colors) - 1)
- idx1 = int(floor(val))
- idx2 = idx1 + 1
- fb = val - float(idx1)
-
- r = (colors[idx2][0] - colors[idx1][0]) * fb + colors[idx1][0]
- g = (colors[idx2][1] - colors[idx1][1]) * fb + colors[idx1][1]
- b = (colors[idx2][2] - colors[idx1][2]) * fb + colors[idx1][2]
-
- r, g, b = [x * 255 for x in r, g, b]
- return "#%02x%02x%02x" % (r, g, b)
-
-# Load the keylog
-def load_keylog(fname, restrict_row):
- keylog = {}
- total = 0
- with open(fname, "r") as f:
- lines = f.readlines()
- for line in lines:
- m = re.search ('KL: col=(\d+), row=(\d+)', line)
+ with open ("%s/%s.json" % (out_dir, layer), "w") as f:
+ json.dump(heatmaps[layer].get_heatmap(), f)
+ print >>sys.stderr, "%s stats:" % (layer)
+ json.dump (heatmaps[layer].get_stats(), sys.stderr,
+ indent = 4, sort_keys = True)
+ print >>sys.stderr, ""
+ print >>sys.stderr, ""
+
+def main(opts):
+
+ heatmaps = {"Dvorak": Heatmap("Dvorak"),
+ "ADORE": Heatmap("ADORE")
+ }
+ cnt = 0
+ restrict_row = opts.restrict_row
+ out_dir = opts.outdir
+
+ while True:
+ line = sys.stdin.readline()
+ if not line:
+ break
+ m = re.search ('KL: col=(\d+), row=(\d+), pressed=(\d+), layer=(.*)', line)
if not m:
continue
- (c, r) = (int(m.group (2)), int(m.group (1)))
- if restrict_row != None and r != int(restrict_row):
+
+ cnt = cnt + 1
+ (c, r, l) = (int(m.group (2)), int(m.group (1)), m.group (4))
+ if restrict_row != -1 and r != restrict_row:
+ continue
+ if c in opts.ignore_columns:
continue
- if (c, r) in keylog:
- keylog[(c, r)] = keylog[(c, r)] + 1
- else:
- keylog[(c, r)] = 1
- total = total + 1
- return total / 2, keylog
-
-def l_flat(s):
- f = s.split("\n")
- return ", ".join (f)
-
-def main(base_fn, log_fn, restrict_row = None):
-
- with open(base_fn, "r") as f:
- layout = json.load (f)
-
- ## Reset colors
- for row in cr_coord_map:
- for col in row:
- if col != []:
- set_bg (layout, col, "#d9dae0")
- #set_attr_at (layout, col[0], col[1], "g", set_attr, True)
-
- total, log = load_keylog (log_fn, restrict_row)
- max_cnt = 0
- for (c, r) in log:
- max_cnt = max(max_cnt, log[(c, r)])
-
- # Create the heatmap
- for (c, r) in log:
- coords = coord(c, r)
- b, n = coords
- cap = max_cnt
- v = float(log[(c, r)]) / cap
- print >> sys.stderr, "%s => %d/%d => %f = %s" % (l_flat(layout[b][n+1]), log[(c,r)], cap, v, heatmap_color(v))
- set_bg (layout, coord(c, r), heatmap_color (v))
- set_tap_info (layout, coord (c, r), log[(c, r)], total)
-
- print json.dumps(layout)
-if __name__ == "__main__":
- if len(sys.argv) < 3:
- print """Log to Heatmap -- creates a heatmap out of keyboard logs
+ heatmaps[l].update_log ((c, r))
+
+ if opts.dump_interval != -1 and cnt >= opts.dump_interval:
+ cnt = 0
+ dump_all(out_dir, heatmaps)
-Usage: log-to-heatmap.py base-layout.json logfile [row] >layout.json"""
- sys.exit (1)
- main(*sys.argv[1:])
+ dump_all (out_dir, heatmaps)
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser (description = "keylog to heatmap processor")
+ parser.add_argument ('outdir', action = 'store',
+ help = 'Output directory')
+ parser.add_argument ('--row', dest = 'restrict_row', action = 'store', type = int,
+ default = -1, help = 'Restrict processing to this row only')
+ parser.add_argument ('--dump-interval', dest = 'dump_interval', action = 'store', type = int,
+ default = 100, help = 'Dump stats and heatmap at every Nth event, -1 for dumping at EOF only')
+ parser.add_argument ('--ignore-column', dest = 'ignore_columns', action = 'append', type = int,
+ default = [], help = 'Ignore the specified columns')
+ args = parser.parse_args()
+ main(args)