aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorvectorstorm <github@gridbug.org>2021-11-16 05:40:52 +1100
committerGitHub <noreply@github.com>2021-11-16 05:40:52 +1100
commitc9fd69871165eb889be5421df518d8e35b2be027 (patch)
treedffb4a815809e9860a1111304c9ab6872551df58
parent36d123e9c5a9ce0e29b9bc22ef87661bf479e299 (diff)
downloadqmk_firmware-c9fd69871165eb889be5421df518d8e35b2be027.tar.gz
qmk_firmware-c9fd69871165eb889be5421df518d8e35b2be027.zip
Reimplements WPM feature to be smaller & precise (#13902)
* Reimplements WPM feature. - Now calculates exact WPM over the last up to three seconds of typing. - WPM_SMOOTHING removed, as it's no longer needed. - WPM_SAMPLE_SECONDS added, to specify how long a period to average WPM over, set to 5 seconds by default. - WPM_SAMPLE_PERIODS added, to specify how many sampling buffers we'll use. Each one uses one extra byte of space. Having more will lead to smoother decay of WPM values. Defaults to 50 (we're saving so many bytes of firmware space I felt like being extravagent, and this change is still a big size saving overall) - WPM_UNFILTERED option added (defaults to unset), which disables all filtering within the WPM feature. This saves some space in the firmware and also reduces latency between typing and the WPM calculation measuring it. (saves 70 bytes in my tests) - WPM_LAUNCH_CONTROL added (defaults to unset). When typing begins while the current displayed WPM value is zero, the WPM calculation only considers the time elapsed since typing began, not the whole WPM_SAMPLE_SECONDS buffer. The result of this is that the displayed WPM value much more rapidly reaches an accurate WPM value, even when results are being filtered. (costs 22 bytes in my tests) - Updates documentation to reflect changed options. Saves about 900 bytes, in my tests, compared against the previous implementation, with default settings. * Apply suggestions from code review Co-authored-by: Sergey Vlasov <sigprof@gmail.com> Co-authored-by: Trevor Powell <trevor@vectorstorm.org> Co-authored-by: Nick Brassel <nick@tzarc.org> Co-authored-by: Sergey Vlasov <sigprof@gmail.com>
-rw-r--r--docs/feature_wpm.md22
-rw-r--r--quantum/wpm.c98
-rw-r--r--quantum/wpm.h7
3 files changed, 99 insertions, 28 deletions
diff --git a/docs/feature_wpm.md b/docs/feature_wpm.md
index 7e465e79b..87145c97e 100644
--- a/docs/feature_wpm.md
+++ b/docs/feature_wpm.md
@@ -10,11 +10,23 @@ For split keyboards using soft serial, the computed WPM score will be available
10 10
11## Configuration 11## Configuration
12 12
13|Define |Default | Description | 13| Define | Default | Description |
14|-----------------------------|--------------|------------------------------------------------------------------------------------------| 14|------------------------------|---------------|------------------------------------------------------------------------------------------|
15|`WPM_SMOOTHING` |`0.0487` | Sets the smoothing to about 40 keystrokes | 15| `WPM_ESTIMATED_WORD_SIZE` | `5` | This is the value used when estimating average word size (for regression and normal use) |
16|`WPM_ESTIMATED_WORD_SIZE` |`5` | This is the value used when estimating average word size (for regression and normal use) | 16| `WPM_ALLOW_COUNT_REGRESSION` | _Not defined_ | If defined allows the WPM to be decreased when hitting Delete or Backspace |
17|`WPM_ALLOW_COUNT_REGRESSOIN` |_Not defined_ | If defined allows the WPM to be decreased when hitting Delete or Backspace | 17| `WPM_UNFILTERED` | _Not defined_ | If undefined (the default), WPM values will be smoothed to avoid sudden changes in value |
18| `WPM_SAMPLE_SECONDS` | `5` | This defines how many seconds of typing to average, when calculating WPM |
19| `WPM_SAMPLE_PERIODS` | `50` | This defines how many sampling periods to use when calculating WPM |
20| `WPM_LAUNCH_CONTROL` | _Not defined_ | If defined, WPM values will be calculated using partial buffers when typing begins |
21
22'WPM_UNFILTERED' is potentially useful if you're filtering data in some other way (and also because it reduces the code required for the WPM feature), or if reducing measurement latency to a minimum is important for you.
23
24Increasing 'WPM_SAMPLE_SECONDS' will give more smoothly changing WPM values at the expense of slightly more latency to the WPM calculation.
25
26Increasing 'WPM_SAMPLE_PERIODS' will improve the smoothness at which WPM decays once typing stops, at a cost of approximately this many bytes of firmware space.
27
28If 'WPM_LAUNCH_CONTROL' is defined, whenever WPM drops to zero, the next time typing begins WPM will be calculated based only on the time since that typing began, instead of the whole period of time specified by WPM_SAMPLE_SECONDS. This results in reaching an accurate WPM value much faster, even when filtering is enabled and a large WPM_SAMPLE_SECONDS value is specified.
29
18## Public Functions 30## Public Functions
19 31
20|Function |Description | 32|Function |Description |
diff --git a/quantum/wpm.c b/quantum/wpm.c
index cad4cefd5..925e2c416 100644
--- a/quantum/wpm.c
+++ b/quantum/wpm.c
@@ -21,13 +21,37 @@
21 21
22// WPM Stuff 22// WPM Stuff
23static uint8_t current_wpm = 0; 23static uint8_t current_wpm = 0;
24static uint16_t wpm_timer = 0; 24static uint32_t wpm_timer = 0;
25#ifndef WPM_UNFILTERED
26static uint32_t smoothing_timer = 0;
27#endif
25 28
26// This smoothing is 40 keystrokes 29/* The WPM calculation works by specifying a certain number of 'periods' inside
27static const float wpm_smoothing = WPM_SMOOTHING; 30 * a ring buffer, and we count the number of keypresses which occur in each of
31 * those periods. Then to calculate WPM, we add up all of the keypresses in
32 * the whole ring buffer, divide by the number of keypresses in a 'word', and
33 * then adjust for how much time is captured by our ring buffer. Right now
34 * the ring buffer is hardcoded below to be six half-second periods, accounting
35 * for a total WPM sampling period of up to three seconds of typing.
36 *
37 * Whenever our WPM drops to absolute zero due to no typing occurring within
38 * any contiguous three seconds, we reset and start measuring fresh,
39 * which lets our WPM immediately reach the correct value even before a full
40 * three second sampling buffer has been filled.
41 */
42#define MAX_PERIODS (WPM_SAMPLE_PERIODS)
43#define PERIOD_DURATION (1000 * WPM_SAMPLE_SECONDS / MAX_PERIODS)
44#define LATENCY (100)
45static int8_t period_presses[MAX_PERIODS] = {0};
46static uint8_t current_period = 0;
47static uint8_t periods = 1;
28 48
29void set_current_wpm(uint8_t new_wpm) { current_wpm = new_wpm; } 49#if !defined(WPM_UNFILTERED)
50static uint8_t prev_wpm = 0;
51static uint8_t next_wpm = 0;
52#endif
30 53
54void set_current_wpm(uint8_t new_wpm) { current_wpm = new_wpm; }
31uint8_t get_current_wpm(void) { return current_wpm; } 55uint8_t get_current_wpm(void) { return current_wpm; }
32 56
33bool wpm_keycode(uint16_t keycode) { return wpm_keycode_kb(keycode); } 57bool wpm_keycode(uint16_t keycode) { return wpm_keycode_kb(keycode); }
@@ -68,33 +92,65 @@ __attribute__((weak)) uint8_t wpm_regress_count(uint16_t keycode) {
68} 92}
69#endif 93#endif
70 94
95// Outside 'raw' mode we smooth results over time.
96
71void update_wpm(uint16_t keycode) { 97void update_wpm(uint16_t keycode) {
72 if (wpm_keycode(keycode)) { 98 if (wpm_keycode(keycode)) {
73 if (wpm_timer > 0) { 99 period_presses[current_period]++;
74 uint16_t latest_wpm = 60000 / timer_elapsed(wpm_timer) / WPM_ESTIMATED_WORD_SIZE;
75 if (latest_wpm > UINT8_MAX) {
76 latest_wpm = UINT8_MAX;
77 }
78 current_wpm += ceilf((latest_wpm - current_wpm) * wpm_smoothing);
79 }
80 wpm_timer = timer_read();
81 } 100 }
82#ifdef WPM_ALLOW_COUNT_REGRESSION 101#ifdef WPM_ALLOW_COUNT_REGRESSION
83 uint8_t regress = wpm_regress_count(keycode); 102 uint8_t regress = wpm_regress_count(keycode);
84 if (regress) { 103 if (regress) {
85 if (current_wpm < regress) { 104 period_presses[current_period]--;
86 current_wpm = 0;
87 } else {
88 current_wpm -= regress;
89 }
90 wpm_timer = timer_read();
91 } 105 }
92#endif 106#endif
93} 107}
94 108
95void decay_wpm(void) { 109void decay_wpm(void) {
96 if (timer_elapsed(wpm_timer) > 1000) { 110 int32_t presses = period_presses[0];
97 current_wpm += (-current_wpm) * wpm_smoothing; 111 for (int i = 1; i <= periods; i++) {
98 wpm_timer = timer_read(); 112 presses += period_presses[i];
113 }
114 if (presses < 0) {
115 presses = 0;
99 } 116 }
117 int32_t elapsed = timer_elapsed32(wpm_timer);
118 uint32_t duration = (((periods)*PERIOD_DURATION) + elapsed);
119 uint32_t wpm_now = (60000 * presses) / (duration * WPM_ESTIMATED_WORD_SIZE);
120 wpm_now = (wpm_now > 240) ? 240 : wpm_now;
121
122 if (elapsed > PERIOD_DURATION) {
123 current_period = (current_period + 1) % MAX_PERIODS;
124 period_presses[current_period] = 0;
125 periods = (periods < MAX_PERIODS - 1) ? periods + 1 : MAX_PERIODS - 1;
126 elapsed = 0;
127 /* if (wpm_timer == 0) { */
128 wpm_timer = timer_read32();
129 /* } else { */
130 /* wpm_timer += PERIOD_DURATION; */
131 /* } */
132 }
133 if (presses < 2) // don't guess high WPM based on a single keypress.
134 wpm_now = 0;
135
136#if defined WPM_LAUNCH_CONTROL
137 if (presses == 0) {
138 current_period = 0;
139 periods = 0;
140 wpm_now = 0;
141 }
142#endif // WPM_LAUNCH_CONTROL
143
144#ifndef WPM_UNFILTERED
145 int32_t latency = timer_elapsed32(smoothing_timer);
146 if (latency > LATENCY) {
147 smoothing_timer = timer_read32();
148 prev_wpm = current_wpm;
149 next_wpm = wpm_now;
150 }
151
152 current_wpm = prev_wpm + (latency * ((int)next_wpm - (int)prev_wpm) / LATENCY);
153#else
154 current_wpm = wpm_now;
155#endif
100} 156}
diff --git a/quantum/wpm.h b/quantum/wpm.h
index 4af52d2b9..c8e7d2668 100644
--- a/quantum/wpm.h
+++ b/quantum/wpm.h
@@ -22,8 +22,11 @@
22#ifndef WPM_ESTIMATED_WORD_SIZE 22#ifndef WPM_ESTIMATED_WORD_SIZE
23# define WPM_ESTIMATED_WORD_SIZE 5 23# define WPM_ESTIMATED_WORD_SIZE 5
24#endif 24#endif
25#ifndef WPM_SMOOTHING 25#ifndef WPM_SAMPLE_SECONDS
26# define WPM_SMOOTHING 0.0487 26# define WPM_SAMPLE_SECONDS 5
27#endif
28#ifndef WPM_SAMPLE_PERIODS
29# define WPM_SAMPLE_PERIODS 50
27#endif 30#endif
28 31
29bool wpm_keycode(uint16_t keycode); 32bool wpm_keycode(uint16_t keycode);