Skip to content

Commit 57f3ea2

Browse files
authored
Refactor RapidFire (#1026)
* Fix RapidFireKey argument expansion * Add basic unit test * Fix calling keyboard.tap() from within a scheduled task * Simplify code and memory management
1 parent 5fccee7 commit 57f3ea2

File tree

2 files changed

+143
-36
lines changed

2 files changed

+143
-36
lines changed

kmk/modules/rapidfire.py

Lines changed: 49 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
1+
from micropython import const
2+
13
from random import randint
24

35
from kmk.keys import Key, make_argumented_key
46
from kmk.modules import Module
7+
from kmk.utils import Debug
8+
9+
debug = Debug(__name__)
10+
11+
_INACTIVE = const(0)
12+
_HOLD = const(1)
13+
_ACTIVE = const(2)
514

615

716
class RapidFireKey(Key):
@@ -13,7 +22,7 @@ def __init__(
1322
enable_interval_randomization=False,
1423
randomization_magnitude=15,
1524
toggle=False,
16-
*kwargs,
25+
**kwargs,
1726
):
1827
super().__init__(**kwargs)
1928
self.key = key
@@ -22,13 +31,11 @@ def __init__(
2231
self.enable_interval_randomization = enable_interval_randomization
2332
self.randomization_magnitude = randomization_magnitude
2433
self.toggle = toggle
34+
self._state = _INACTIVE
35+
self._timeout = None
2536

2637

2738
class RapidFire(Module):
28-
_active_keys = {}
29-
_toggled_keys = []
30-
_waiting_keys = []
31-
3239
def __init__(self):
3340
make_argumented_key(
3441
names=('RF',),
@@ -37,51 +44,57 @@ def __init__(self):
3744
on_release=self._rf_released,
3845
)
3946

40-
def _get_repeat(self, key):
47+
def _on_timer_timeout(self, key, keyboard):
48+
if key._state == _HOLD:
49+
key._state = _ACTIVE
50+
keyboard.remove_key(key.key)
51+
key._timeout = keyboard.set_timeout(
52+
1, lambda: self._on_timer_timeout(key, keyboard)
53+
)
54+
return
55+
56+
keyboard.add_key(key.key)
57+
keyboard.set_timeout(1, lambda: keyboard.remove_key(key.key))
58+
59+
interval = key.interval
4160
if key.enable_interval_randomization:
42-
return key.interval + randint(
61+
interval += randint(
4362
-key.randomization_magnitude, key.randomization_magnitude
4463
)
45-
return key.interval
46-
47-
def _on_timer_timeout(self, key, keyboard):
48-
keyboard.tap_key(key.key)
49-
if key in self._waiting_keys:
50-
self._waiting_keys.remove(key)
51-
if key.toggle and key not in self._toggled_keys:
52-
self._toggled_keys.append(key)
53-
self._active_keys[key] = keyboard.set_timeout(
54-
self._get_repeat(key), lambda: self._on_timer_timeout(key, keyboard)
64+
key._timeout = keyboard.set_timeout(
65+
interval, lambda: self._on_timer_timeout(key, keyboard)
5566
)
5667

68+
if debug.enabled:
69+
debug(key.key, ' @', interval, 'ms')
70+
5771
def _rf_pressed(self, key, keyboard, *args, **kwargs):
58-
if key in self._toggled_keys:
59-
self._toggled_keys.remove(key)
72+
if key._state == _ACTIVE:
6073
self._deactivate_key(key, keyboard)
6174
return
62-
if key.timeout > 0:
63-
keyboard.tap_key(key.key)
64-
self._waiting_keys.append(key)
65-
self._active_keys[key] = keyboard.set_timeout(
66-
key.timeout, lambda: self._on_timer_timeout(key, keyboard)
67-
)
68-
else:
69-
self._on_timer_timeout(key, keyboard)
75+
76+
keyboard.add_key(key.key)
77+
key._state = _HOLD
78+
key._timeout = keyboard.set_timeout(
79+
key.timeout, lambda: self._on_timer_timeout(key, keyboard)
80+
)
7081

7182
def _rf_released(self, key, keyboard, *args, **kwargs):
72-
if key not in self._active_keys:
73-
return
74-
if key in self._toggled_keys:
75-
if key not in self._waiting_keys:
83+
if key._state == _ACTIVE:
84+
if key.toggle:
7685
return
77-
self._toggled_keys.remove(key)
78-
if key in self._waiting_keys:
79-
self._waiting_keys.remove(key)
86+
key._state = _INACTIVE
87+
elif key._state == _INACTIVE:
88+
return
89+
else:
90+
keyboard.remove_key(key.key)
91+
8092
self._deactivate_key(key, keyboard)
8193

8294
def _deactivate_key(self, key, keyboard):
83-
keyboard.cancel_timeout(self._active_keys[key])
84-
self._active_keys.pop(key)
95+
keyboard.cancel_timeout(key._timeout)
96+
key._state = _INACTIVE
97+
key._timeout = None
8598

8699
def during_bootup(self, keyboard):
87100
return

tests/test_rapidfire.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import unittest
2+
3+
from kmk.keys import KC
4+
from kmk.modules.rapidfire import RapidFire
5+
from tests.keyboard_test import KeyboardTest
6+
7+
t_interval = 4 * KeyboardTest.loop_delay_ms
8+
t_timeout = 10 * KeyboardTest.loop_delay_ms
9+
t_hold = t_timeout + 5 * t_interval + KeyboardTest.loop_delay_ms
10+
11+
12+
class TestKeyRepeat(unittest.TestCase):
13+
@classmethod
14+
def setUpClass(cls):
15+
KC.clear()
16+
17+
cls.keyboard = KeyboardTest(
18+
[RapidFire()],
19+
[
20+
[
21+
KC.RF(KC.N0, interval=t_interval, timeout=t_timeout),
22+
KC.RF(KC.N1, interval=t_interval, timeout=t_timeout, toggle=True),
23+
],
24+
],
25+
debug_enabled=False,
26+
)
27+
28+
def test_rapidfire(self):
29+
self.keyboard.test(
30+
'',
31+
[(0, True), (0, False)],
32+
[{KC.N0}, {}],
33+
)
34+
35+
self.keyboard.test(
36+
'',
37+
[(0, True), t_timeout // 2, (0, False)],
38+
[{KC.N0}, {}],
39+
)
40+
41+
self.keyboard.test(
42+
'',
43+
[(0, True), t_timeout + t_interval // 2, (0, False)],
44+
[{KC.N0}, {}, {KC.N0}, {}],
45+
)
46+
47+
self.keyboard.test(
48+
'',
49+
[(0, True), t_timeout + (3 * t_interval) // 2, (0, False)],
50+
[{KC.N0}, {}, {KC.N0}, {}, {KC.N0}, {}],
51+
)
52+
53+
self.keyboard.test(
54+
'',
55+
[(0, True), t_hold, (0, False)],
56+
[
57+
{KC.N0},
58+
{},
59+
{KC.N0},
60+
{},
61+
{KC.N0},
62+
{},
63+
{KC.N0},
64+
{},
65+
{KC.N0},
66+
{},
67+
{KC.N0},
68+
{},
69+
],
70+
)
71+
72+
self.keyboard.test(
73+
'',
74+
[
75+
(1, True),
76+
t_timeout + t_interval // 2,
77+
(1, False),
78+
3 * t_interval,
79+
(1, True),
80+
(1, False),
81+
],
82+
[
83+
{KC.N1},
84+
{},
85+
{KC.N1},
86+
{},
87+
{KC.N1},
88+
{},
89+
{KC.N1},
90+
{},
91+
{KC.N1},
92+
{},
93+
],
94+
)

0 commit comments

Comments
 (0)