Skip to content

Commit c9fd698

Browse files
vectorstormTrevor Powelltzarcsigprof
authored
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 <[email protected]> Co-authored-by: Trevor Powell <[email protected]> Co-authored-by: Nick Brassel <[email protected]> Co-authored-by: Sergey Vlasov <[email protected]>
1 parent 36d123e commit c9fd698

File tree

3 files changed

+99
-28
lines changed

3 files changed

+99
-28
lines changed

docs/feature_wpm.md

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,23 @@ For split keyboards using soft serial, the computed WPM score will be available
1010

1111
## Configuration
1212

13-
|Define |Default | Description |
14-
|-----------------------------|--------------|------------------------------------------------------------------------------------------|
15-
|`WPM_SMOOTHING` |`0.0487` | Sets the smoothing to about 40 keystrokes |
16-
|`WPM_ESTIMATED_WORD_SIZE` |`5` | This is the value used when estimating average word size (for regression and normal use) |
17-
|`WPM_ALLOW_COUNT_REGRESSOIN` |_Not defined_ | If defined allows the WPM to be decreased when hitting Delete or Backspace |
13+
| Define | Default | Description |
14+
|------------------------------|---------------|------------------------------------------------------------------------------------------|
15+
| `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_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+
24+
Increasing 'WPM_SAMPLE_SECONDS' will give more smoothly changing WPM values at the expense of slightly more latency to the WPM calculation.
25+
26+
Increasing '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+
28+
If '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+
1830
## Public Functions
1931

2032
|Function |Description |

quantum/wpm.c

Lines changed: 77 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,37 @@
2121

2222
// WPM Stuff
2323
static uint8_t current_wpm = 0;
24-
static uint16_t wpm_timer = 0;
24+
static uint32_t wpm_timer = 0;
25+
#ifndef WPM_UNFILTERED
26+
static uint32_t smoothing_timer = 0;
27+
#endif
2528

26-
// This smoothing is 40 keystrokes
27-
static const float wpm_smoothing = WPM_SMOOTHING;
29+
/* The WPM calculation works by specifying a certain number of 'periods' inside
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)
45+
static int8_t period_presses[MAX_PERIODS] = {0};
46+
static uint8_t current_period = 0;
47+
static uint8_t periods = 1;
2848

29-
void set_current_wpm(uint8_t new_wpm) { current_wpm = new_wpm; }
49+
#if !defined(WPM_UNFILTERED)
50+
static uint8_t prev_wpm = 0;
51+
static uint8_t next_wpm = 0;
52+
#endif
3053

54+
void set_current_wpm(uint8_t new_wpm) { current_wpm = new_wpm; }
3155
uint8_t get_current_wpm(void) { return current_wpm; }
3256

3357
bool wpm_keycode(uint16_t keycode) { return wpm_keycode_kb(keycode); }
@@ -68,33 +92,65 @@ __attribute__((weak)) uint8_t wpm_regress_count(uint16_t keycode) {
6892
}
6993
#endif
7094

95+
// Outside 'raw' mode we smooth results over time.
96+
7197
void update_wpm(uint16_t keycode) {
7298
if (wpm_keycode(keycode)) {
73-
if (wpm_timer > 0) {
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();
99+
period_presses[current_period]++;
81100
}
82101
#ifdef WPM_ALLOW_COUNT_REGRESSION
83102
uint8_t regress = wpm_regress_count(keycode);
84103
if (regress) {
85-
if (current_wpm < regress) {
86-
current_wpm = 0;
87-
} else {
88-
current_wpm -= regress;
89-
}
90-
wpm_timer = timer_read();
104+
period_presses[current_period]--;
91105
}
92106
#endif
93107
}
94108

95109
void decay_wpm(void) {
96-
if (timer_elapsed(wpm_timer) > 1000) {
97-
current_wpm += (-current_wpm) * wpm_smoothing;
98-
wpm_timer = timer_read();
110+
int32_t presses = period_presses[0];
111+
for (int i = 1; i <= periods; i++) {
112+
presses += period_presses[i];
113+
}
114+
if (presses < 0) {
115+
presses = 0;
99116
}
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
100156
}

quantum/wpm.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,11 @@
2222
#ifndef WPM_ESTIMATED_WORD_SIZE
2323
# define WPM_ESTIMATED_WORD_SIZE 5
2424
#endif
25-
#ifndef WPM_SMOOTHING
26-
# define WPM_SMOOTHING 0.0487
25+
#ifndef WPM_SAMPLE_SECONDS
26+
# define WPM_SAMPLE_SECONDS 5
27+
#endif
28+
#ifndef WPM_SAMPLE_PERIODS
29+
# define WPM_SAMPLE_PERIODS 50
2730
#endif
2831

2932
bool wpm_keycode(uint16_t keycode);

0 commit comments

Comments
 (0)