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 ## Configuration -|Define |Default | Description | -|-----------------------------|--------------|------------------------------------------------------------------------------------------| -|`WPM_SMOOTHING` |`0.0487` | Sets the smoothing to about 40 keystrokes | -|`WPM_ESTIMATED_WORD_SIZE` |`5` | This is the value used when estimating average word size (for regression and normal use) | -|`WPM_ALLOW_COUNT_REGRESSOIN` |_Not defined_ | If defined allows the WPM to be decreased when hitting Delete or Backspace | +| Define | Default | Description | +|------------------------------|---------------|------------------------------------------------------------------------------------------| +| `WPM_ESTIMATED_WORD_SIZE` | `5` | This is the value used when estimating average word size (for regression and normal use) | +| `WPM_ALLOW_COUNT_REGRESSION` | _Not defined_ | If defined allows the WPM to be decreased when hitting Delete or Backspace | +| `WPM_UNFILTERED` | _Not defined_ | If undefined (the default), WPM values will be smoothed to avoid sudden changes in value | +| `WPM_SAMPLE_SECONDS` | `5` | This defines how many seconds of typing to average, when calculating WPM | +| `WPM_SAMPLE_PERIODS` | `50` | This defines how many sampling periods to use when calculating WPM | +| `WPM_LAUNCH_CONTROL` | _Not defined_ | If defined, WPM values will be calculated using partial buffers when typing begins | + +'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. + +Increasing 'WPM_SAMPLE_SECONDS' will give more smoothly changing WPM values at the expense of slightly more latency to the WPM calculation. + +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. + +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. + ## Public Functions |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 @@ // WPM Stuff static uint8_t current_wpm = 0; -static uint16_t wpm_timer = 0; +static uint32_t wpm_timer = 0; +#ifndef WPM_UNFILTERED +static uint32_t smoothing_timer = 0; +#endif -// This smoothing is 40 keystrokes -static const float wpm_smoothing = WPM_SMOOTHING; +/* The WPM calculation works by specifying a certain number of 'periods' inside + * a ring buffer, and we count the number of keypresses which occur in each of + * those periods. Then to calculate WPM, we add up all of the keypresses in + * the whole ring buffer, divide by the number of keypresses in a 'word', and + * then adjust for how much time is captured by our ring buffer. Right now + * the ring buffer is hardcoded below to be six half-second periods, accounting + * for a total WPM sampling period of up to three seconds of typing. + * + * Whenever our WPM drops to absolute zero due to no typing occurring within + * any contiguous three seconds, we reset and start measuring fresh, + * which lets our WPM immediately reach the correct value even before a full + * three second sampling buffer has been filled. + */ +#define MAX_PERIODS (WPM_SAMPLE_PERIODS) +#define PERIOD_DURATION (1000 * WPM_SAMPLE_SECONDS / MAX_PERIODS) +#define LATENCY (100) +static int8_t period_presses[MAX_PERIODS] = {0}; +static uint8_t current_period = 0; +static uint8_t periods = 1; -void set_current_wpm(uint8_t new_wpm) { current_wpm = new_wpm; } +#if !defined(WPM_UNFILTERED) +static uint8_t prev_wpm = 0; +static uint8_t next_wpm = 0; +#endif +void set_current_wpm(uint8_t new_wpm) { current_wpm = new_wpm; } uint8_t get_current_wpm(void) { return current_wpm; } 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) { } #endif +// Outside 'raw' mode we smooth results over time. + void update_wpm(uint16_t keycode) { if (wpm_keycode(keycode)) { - if (wpm_timer > 0) { - uint16_t latest_wpm = 60000 / timer_elapsed(wpm_timer) / WPM_ESTIMATED_WORD_SIZE; - if (latest_wpm > UINT8_MAX) { - latest_wpm = UINT8_MAX; - } - current_wpm += ceilf((latest_wpm - current_wpm) * wpm_smoothing); - } - wpm_timer = timer_read(); + period_presses[current_period]++; } #ifdef WPM_ALLOW_COUNT_REGRESSION uint8_t regress = wpm_regress_count(keycode); if (regress) { - if (current_wpm < regress) { - current_wpm = 0; - } else { - current_wpm -= regress; - } - wpm_timer = timer_read(); + period_presses[current_period]--; } #endif } void decay_wpm(void) { - if (timer_elapsed(wpm_timer) > 1000) { - current_wpm += (-current_wpm) * wpm_smoothing; - wpm_timer = timer_read(); + int32_t presses = period_presses[0]; + for (int i = 1; i <= periods; i++) { + presses += period_presses[i]; } + if (presses < 0) { + presses = 0; + } + int32_t elapsed = timer_elapsed32(wpm_timer); + uint32_t duration = (((periods)*PERIOD_DURATION) + elapsed); + uint32_t wpm_now = (60000 * presses) / (duration * WPM_ESTIMATED_WORD_SIZE); + wpm_now = (wpm_now > 240) ? 240 : wpm_now; + + if (elapsed > PERIOD_DURATION) { + current_period = (current_period + 1) % MAX_PERIODS; + period_presses[current_period] = 0; + periods = (periods < MAX_PERIODS - 1) ? periods + 1 : MAX_PERIODS - 1; + elapsed = 0; + /* if (wpm_timer == 0) { */ + wpm_timer = timer_read32(); + /* } else { */ + /* wpm_timer += PERIOD_DURATION; */ + /* } */ + } + if (presses < 2) // don't guess high WPM based on a single keypress. + wpm_now = 0; + +#if defined WPM_LAUNCH_CONTROL + if (presses == 0) { + current_period = 0; + periods = 0; + wpm_now = 0; + } +#endif // WPM_LAUNCH_CONTROL + +#ifndef WPM_UNFILTERED + int32_t latency = timer_elapsed32(smoothing_timer); + if (latency > LATENCY) { + smoothing_timer = timer_read32(); + prev_wpm = current_wpm; + next_wpm = wpm_now; + } + + current_wpm = prev_wpm + (latency * ((int)next_wpm - (int)prev_wpm) / LATENCY); +#else + current_wpm = wpm_now; +#endif } 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 @@ #ifndef WPM_ESTIMATED_WORD_SIZE # define WPM_ESTIMATED_WORD_SIZE 5 #endif -#ifndef WPM_SMOOTHING -# define WPM_SMOOTHING 0.0487 +#ifndef WPM_SAMPLE_SECONDS +# define WPM_SAMPLE_SECONDS 5 +#endif +#ifndef WPM_SAMPLE_PERIODS +# define WPM_SAMPLE_PERIODS 50 #endif bool wpm_keycode(uint16_t keycode);