Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adding pmw3360 modules #797

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open

Conversation

peterwhitesell
Copy link

@peterwhitesell peterwhitesell commented May 17, 2023

Adds pmw3360 trackball module for the pmw3360 motion sensor

PMW3360

For using PMW3360 motion sensor for pointer, scrolling and volume. The default behavior converts sensor XY movement into cursor XY movement.

from kmk.modules.PMW3360 import PMW3360
keyboard.modules.append(PMW3360(
    cs=board.GP0,
    sclk=board.GP2,
    miso=board.GP4,
    mosi=board.GP3,
    invert_x=False,
    invert_y=True,
    flip_xy=False,
    lift_config=0x04,
    on_move=lambda keyboard: None,
    scroll_layers=[1, 2],
    volume_layers=[3],
))

The firmware for this sensor has to be placed in kmk\modules\PMW3360_firmware.py

firmware = (
    b'\x01'
    b'\x04'
    ...
)

Scrolling and Volume

Scrolling and Volumne control can be enabled either in key event handlers, e.g.

...
pmw3360=PWM3360(...)
def ball_scroll_enable(key, keyboard, *args):
    pmw3360.set_scroll(True)
    return True
def ball_scroll_disable(key, keyboard, *args):
    pmw3360.set_scroll(False)
    return True
def ball_volume_enable(key, keyboard, *args):
    pmw3360.start_volume_control()
    return True
def ball_volume_disable(key, keyboard, *args):
    pmw3360.start_volume_control(False)
    return True
KC.A.before_press_handler(ball_scroll_enable)
KC.A.before_release_handler(ball_scroll_disable)
KC.B.before_press_handler(ball_volume_enable)
KC.B.before_release_handler(ball_volume_disable)

or via layers, e.g.

pmw3360=PWM3360(
    scroll_layers=[1, 2],
    volume_layers=[3]
)

Note
The default Mouse device with KMK is kept minimal so it can work to support running on smaller micro controllers. To enable horizontal scrolling, support for panning (mouse wheel left/right) has to be explicitly enabled in boot.py with the bootcfg module.

Constructor parameters

Param Default Description
cs Chip Select pin
sclk SPI Clock pin
miso MISO pin
mosi MOSI pin
invert_x False Invert x axis movement
invert_y False Invert y axis movement
flip_xy False Swap X and Y axes
lift_config 0x04 Adjust for sensor distance
on_move lambda keyboard: None Add move event behavior
scroll_layers [] Movement is treated as scrolling on these layers
volume_layers [] Movement is treated as volume change on these layers

@bullwinkle3000
Copy link

Really glad to see this PR and just wanted to note that I originally forked this from https://github.com/kbjunky/Dactyl5 when he was experiencing weirdness in the driver with debugging enabled. I "fixed" that by adding the same workaround as QMK uses in their pmw33xx_common.c driver. At line 203, a conditional checks for an odd state and resets burst mode which uses the recovery logic in QMK's driver.

@xs5871
Copy link
Collaborator

xs5871 commented May 18, 2023

I'd also like to see this merged. Skimming over the code, we may need a couple of iterations of review to get there. Let's start with linting. Run make fix-isort fix-formatting for automatic corrections and before every push make sure make test passes.
At the end of the code review, we'll need documentation; you can hold off on that for now.

@peterwhitesell
Copy link
Author

Hey @xs5871, I did the isort and formatting fixes and pushed. make test didn't pass for me, but they are not passing for me on master either, and it's the same 12 failures on this branch.

@xs5871
Copy link
Collaborator

xs5871 commented May 18, 2023

Please share the error output.

@peterwhitesell
Copy link
Author

peterwhitesell commented May 18, 2023

Of course! first error:

kmk_firmware $ make test
./kmk/modules/autoshift.py:33:32: W601 .has_key() is deprecated, use 'in'
make: *** [lint] Error 1

if I fix that in autoshift.py, replacing keyboard._hid_helper.has_key(KC.LSHIFT) with KC.LSHIFT in keyboard._hid_helper.has_key, I get to the test errors:

kmk_firmware $ make test
EEEEEEEEEEEE
======================================================================
ERROR: tests.test_autoshift (unittest.loader._FailedTest)
----------------------------------------------------------------------
ImportError: Failed to import test module: tests.test_autoshift
Traceback (most recent call last):
  File "/Users/peter/.asdf/installs/python/3.6.15/lib/python3.6/unittest/loader.py", line 428, in _find_test_path
    module = self._get_module_from_name(name)
  File "/Users/peter/.asdf/installs/python/3.6.15/lib/python3.6/unittest/loader.py", line 369, in _get_module_from_name
    __import__(name)
  File "/Users/peter/Dev/kmk_firmware/tests/test_autoshift.py", line 3, in <module>
    from kmk.keys import KC
  File "/Users/peter/Dev/kmk_firmware/kmk/keys.py", line 495, in <module>
    class Key:
  File "/Users/peter/Dev/kmk_firmware/kmk/keys.py", line 508, in Key
    meta: object = object(),
TypeError: 'type' object is not subscriptable


======================================================================
ERROR: tests.test_capsword (unittest.loader._FailedTest)
----------------------------------------------------------------------
ImportError: Failed to import test module: tests.test_capsword
Traceback (most recent call last):
  File "/Users/peter/.asdf/installs/python/3.6.15/lib/python3.6/unittest/loader.py", line 428, in _find_test_path
    module = self._get_module_from_name(name)
  File "/Users/peter/.asdf/installs/python/3.6.15/lib/python3.6/unittest/loader.py", line 369, in _get_module_from_name
    __import__(name)
  File "/Users/peter/Dev/kmk_firmware/tests/test_capsword.py", line 3, in <module>
    from kmk.keys import KC
  File "/Users/peter/Dev/kmk_firmware/kmk/keys.py", line 495, in <module>
    class Key:
  File "/Users/peter/Dev/kmk_firmware/kmk/keys.py", line 508, in Key
    meta: object = object(),
TypeError: 'type' object is not subscriptable


======================================================================
ERROR: tests.test_combos (unittest.loader._FailedTest)
----------------------------------------------------------------------
ImportError: Failed to import test module: tests.test_combos
Traceback (most recent call last):
  File "/Users/peter/.asdf/installs/python/3.6.15/lib/python3.6/unittest/loader.py", line 428, in _find_test_path
    module = self._get_module_from_name(name)
  File "/Users/peter/.asdf/installs/python/3.6.15/lib/python3.6/unittest/loader.py", line 369, in _get_module_from_name
    __import__(name)
  File "/Users/peter/Dev/kmk_firmware/tests/test_combos.py", line 3, in <module>
    from kmk.keys import KC
  File "/Users/peter/Dev/kmk_firmware/kmk/keys.py", line 495, in <module>
    class Key:
  File "/Users/peter/Dev/kmk_firmware/kmk/keys.py", line 508, in Key
    meta: object = object(),
TypeError: 'type' object is not subscriptable


======================================================================
ERROR: tests.test_holdtap (unittest.loader._FailedTest)
----------------------------------------------------------------------
ImportError: Failed to import test module: tests.test_holdtap
Traceback (most recent call last):
  File "/Users/peter/.asdf/installs/python/3.6.15/lib/python3.6/unittest/loader.py", line 428, in _find_test_path
    module = self._get_module_from_name(name)
  File "/Users/peter/.asdf/installs/python/3.6.15/lib/python3.6/unittest/loader.py", line 369, in _get_module_from_name
    __import__(name)
  File "/Users/peter/Dev/kmk_firmware/tests/test_holdtap.py", line 3, in <module>
    from kmk.keys import KC
  File "/Users/peter/Dev/kmk_firmware/kmk/keys.py", line 495, in <module>
    class Key:
  File "/Users/peter/Dev/kmk_firmware/kmk/keys.py", line 508, in Key
    meta: object = object(),
TypeError: 'type' object is not subscriptable


======================================================================
ERROR: tests.test_kmk_extension_stringy_keymaps (unittest.loader._FailedTest)
----------------------------------------------------------------------
ImportError: Failed to import test module: tests.test_kmk_extension_stringy_keymaps
Traceback (most recent call last):
  File "/Users/peter/.asdf/installs/python/3.6.15/lib/python3.6/unittest/loader.py", line 428, in _find_test_path
    module = self._get_module_from_name(name)
  File "/Users/peter/.asdf/installs/python/3.6.15/lib/python3.6/unittest/loader.py", line 369, in _get_module_from_name
    __import__(name)
  File "/Users/peter/Dev/kmk_firmware/tests/test_kmk_extension_stringy_keymaps.py", line 3, in <module>
    from kmk.extensions.stringy_keymaps import StringyKeymaps
  File "/Users/peter/Dev/kmk_firmware/kmk/extensions/stringy_keymaps.py", line 2, in <module>
    from kmk.keys import KC
  File "/Users/peter/Dev/kmk_firmware/kmk/keys.py", line 495, in <module>
    class Key:
  File "/Users/peter/Dev/kmk_firmware/kmk/keys.py", line 508, in Key
    meta: object = object(),
TypeError: 'type' object is not subscriptable


======================================================================
ERROR: tests.test_kmk_keyboard (unittest.loader._FailedTest)
----------------------------------------------------------------------
ImportError: Failed to import test module: tests.test_kmk_keyboard
Traceback (most recent call last):
  File "/Users/peter/.asdf/installs/python/3.6.15/lib/python3.6/unittest/loader.py", line 428, in _find_test_path
    module = self._get_module_from_name(name)
  File "/Users/peter/.asdf/installs/python/3.6.15/lib/python3.6/unittest/loader.py", line 369, in _get_module_from_name
    __import__(name)
  File "/Users/peter/Dev/kmk_firmware/tests/test_kmk_keyboard.py", line 3, in <module>
    from kmk.keys import KC
  File "/Users/peter/Dev/kmk_firmware/kmk/keys.py", line 495, in <module>
    class Key:
  File "/Users/peter/Dev/kmk_firmware/kmk/keys.py", line 508, in Key
    meta: object = object(),
TypeError: 'type' object is not subscriptable


======================================================================
ERROR: tests.test_kmk_keys (unittest.loader._FailedTest)
----------------------------------------------------------------------
ImportError: Failed to import test module: tests.test_kmk_keys
Traceback (most recent call last):
  File "/Users/peter/.asdf/installs/python/3.6.15/lib/python3.6/unittest/loader.py", line 428, in _find_test_path
    module = self._get_module_from_name(name)
  File "/Users/peter/.asdf/installs/python/3.6.15/lib/python3.6/unittest/loader.py", line 369, in _get_module_from_name
    __import__(name)
  File "/Users/peter/Dev/kmk_firmware/tests/test_kmk_keys.py", line 3, in <module>
    from kmk.keys import KC, Key, ModifierKey, make_key
  File "/Users/peter/Dev/kmk_firmware/kmk/keys.py", line 495, in <module>
    class Key:
  File "/Users/peter/Dev/kmk_firmware/kmk/keys.py", line 508, in Key
    meta: object = object(),
TypeError: 'type' object is not subscriptable


======================================================================
ERROR: tests.test_layers (unittest.loader._FailedTest)
----------------------------------------------------------------------
ImportError: Failed to import test module: tests.test_layers
Traceback (most recent call last):
  File "/Users/peter/.asdf/installs/python/3.6.15/lib/python3.6/unittest/loader.py", line 428, in _find_test_path
    module = self._get_module_from_name(name)
  File "/Users/peter/.asdf/installs/python/3.6.15/lib/python3.6/unittest/loader.py", line 369, in _get_module_from_name
    __import__(name)
  File "/Users/peter/Dev/kmk_firmware/tests/test_layers.py", line 3, in <module>
    from kmk.keys import KC
  File "/Users/peter/Dev/kmk_firmware/kmk/keys.py", line 495, in <module>
    class Key:
  File "/Users/peter/Dev/kmk_firmware/kmk/keys.py", line 508, in Key
    meta: object = object(),
TypeError: 'type' object is not subscriptable


======================================================================
ERROR: tests.test_oneshot (unittest.loader._FailedTest)
----------------------------------------------------------------------
ImportError: Failed to import test module: tests.test_oneshot
Traceback (most recent call last):
  File "/Users/peter/.asdf/installs/python/3.6.15/lib/python3.6/unittest/loader.py", line 428, in _find_test_path
    module = self._get_module_from_name(name)
  File "/Users/peter/.asdf/installs/python/3.6.15/lib/python3.6/unittest/loader.py", line 369, in _get_module_from_name
    __import__(name)
  File "/Users/peter/Dev/kmk_firmware/tests/test_oneshot.py", line 3, in <module>
    from kmk.keys import KC
  File "/Users/peter/Dev/kmk_firmware/kmk/keys.py", line 495, in <module>
    class Key:
  File "/Users/peter/Dev/kmk_firmware/kmk/keys.py", line 508, in Key
    meta: object = object(),
TypeError: 'type' object is not subscriptable


======================================================================
ERROR: tests.test_sticky_mod (unittest.loader._FailedTest)
----------------------------------------------------------------------
ImportError: Failed to import test module: tests.test_sticky_mod
Traceback (most recent call last):
  File "/Users/peter/.asdf/installs/python/3.6.15/lib/python3.6/unittest/loader.py", line 428, in _find_test_path
    module = self._get_module_from_name(name)
  File "/Users/peter/.asdf/installs/python/3.6.15/lib/python3.6/unittest/loader.py", line 369, in _get_module_from_name
    __import__(name)
  File "/Users/peter/Dev/kmk_firmware/tests/test_sticky_mod.py", line 3, in <module>
    from kmk.keys import KC
  File "/Users/peter/Dev/kmk_firmware/kmk/keys.py", line 495, in <module>
    class Key:
  File "/Users/peter/Dev/kmk_firmware/kmk/keys.py", line 508, in Key
    meta: object = object(),
TypeError: 'type' object is not subscriptable


======================================================================
ERROR: tests.test_string_substitution (unittest.loader._FailedTest)
----------------------------------------------------------------------
ImportError: Failed to import test module: tests.test_string_substitution
Traceback (most recent call last):
  File "/Users/peter/.asdf/installs/python/3.6.15/lib/python3.6/unittest/loader.py", line 428, in _find_test_path
    module = self._get_module_from_name(name)
  File "/Users/peter/.asdf/installs/python/3.6.15/lib/python3.6/unittest/loader.py", line 369, in _get_module_from_name
    __import__(name)
  File "/Users/peter/Dev/kmk_firmware/tests/test_string_substitution.py", line 3, in <module>
    from kmk.keys import ALL_ALPHAS, ALL_NUMBERS, KC
  File "/Users/peter/Dev/kmk_firmware/kmk/keys.py", line 495, in <module>
    class Key:
  File "/Users/peter/Dev/kmk_firmware/kmk/keys.py", line 508, in Key
    meta: object = object(),
TypeError: 'type' object is not subscriptable


======================================================================
ERROR: tests.test_tapdance (unittest.loader._FailedTest)
----------------------------------------------------------------------
ImportError: Failed to import test module: tests.test_tapdance
Traceback (most recent call last):
  File "/Users/peter/.asdf/installs/python/3.6.15/lib/python3.6/unittest/loader.py", line 428, in _find_test_path
    module = self._get_module_from_name(name)
  File "/Users/peter/.asdf/installs/python/3.6.15/lib/python3.6/unittest/loader.py", line 369, in _get_module_from_name
    __import__(name)
  File "/Users/peter/Dev/kmk_firmware/tests/test_tapdance.py", line 3, in <module>
    from kmk.keys import KC
  File "/Users/peter/Dev/kmk_firmware/kmk/keys.py", line 495, in <module>
    class Key:
  File "/Users/peter/Dev/kmk_firmware/kmk/keys.py", line 508, in Key
    meta: object = object(),
TypeError: 'type' object is not subscriptable


----------------------------------------------------------------------
Ran 12 tests in 0.001s

FAILED (errors=12)
make: *** [unit-tests] Error 1

Do you not see these symptoms? maybe my dev env isn't set up correctly. I'm using python 3.6.15:

kmk_firmware $ pipenv run python --version
Python 3.6.15

@xs5871
Copy link
Collaborator

xs5871 commented May 20, 2023

3.6 is ancient, try a current version.

@peterwhitesell
Copy link
Author

3.6 is ancient, try a current version.

Indeed it is. Saw it in the docs somewhere. I also tried 3.9, as seen in the dockerfile, with the same results. What version do you recommend?

@xs5871
Copy link
Collaborator

xs5871 commented May 21, 2023

It worked just before you tried to use it (see #801). My guess is flake8 very recently dropped python 3.9 support

@kbjunky
Copy link
Contributor

kbjunky commented May 23, 2023

Thanks for fixing my code and happy to see this merged into KMK.

@xs5871
Copy link
Collaborator

xs5871 commented May 30, 2023

Any updates on making the linter happy with the updated library version requirements?

@peterwhitesell
Copy link
Author

hey @xs5871, make fix-isort and make fix-formatting are passing for me now (🥳). I see make test is still failing, with actual test failures now, though they are failing on master as well. they are AssertionError: reports don't match up failures, and they seem flakey. here's an example failure with debug_enabled turned on for the tests that were failing:

@peterwhitesell
Copy link
Author

..........match: 2 combo, within timeout
assert mods: 0 == 0 keys: {'X'} == {'X'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
match: 3 combo, within timeout, shuffled
assert mods: 0 == 0 keys: {'Y'} == {'Y'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
match: 2 combo + overlap, after timeout
assert mods: 0 == 0 keys: {'X'} == {'X'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'C'} == {'C'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
match: 2 combo + overlap, interleaved, after timeout
assert mods: 0 == 0 keys: {'X'} == {'X'} matching?: True
assert mods: 0 == 0 keys: {'C', 'X'} == {'C', 'X'} matching?: True
assert mods: 0 == 0 keys: {'C'} == {'C'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
match: 2 combo hold + other, interleaved, after timeout
assert mods: 0 == 0 keys: {'X'} == {'X'} matching?: True
assert mods: 0 == 0 keys: {'X', 'E'} == {'X', 'E'} matching?: True
assert mods: 0 == 0 keys: {'X'} == {'X'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
match: 2 combo hold + overlap, interleaved, after timeout
assert mods: 0 == 0 keys: {'X'} == {'X'} matching?: True
assert mods: 0 == 0 keys: {'C', 'X'} == {'C', 'X'} matching?: True
assert mods: 0 == 0 keys: {'X'} == {'X'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
match: other + 2 combo, after timeout
assert mods: 0 == 0 keys: {'E'} == {'E'} matching?: True
assert mods: 0 == 0 keys: {'X', 'E'} == {'X', 'E'} matching?: True
assert mods: 0 == 0 keys: {'E'} == {'E'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
match: 2 combo + other, after timeout
assert mods: 0 == 0 keys: {'X'} == {'X'} matching?: True
assert mods: 0 == 0 keys: {'X', 'E'} == {'X', 'E'} matching?: True
assert mods: 0 == 0 keys: {'E'} == {'E'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
match: 2 combo, partial release and repeat
assert mods: 0 == 0 keys: {'X'} == {'X'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'X'} == {'X'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
match: 2 combo, partial release and repeat
assert mods: 0 == 0 keys: {'Y'} == {'Y'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'Y'} == {'Y'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
match: 2 combo + 2 combo, after timeout
assert mods: 0 == 0 keys: {'X'} == {'X'} matching?: True
assert mods: 0 == 0 keys: {'X', 'Z'} == {'X', 'Z'} matching?: True
assert mods: 0 == 0 keys: {'Z'} == {'Z'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
match: 2 combo hold + 2 combo, after timeout
assert mods: 0 == 0 keys: {'X'} == {'X'} matching?: True
assert mods: 0 == 0 keys: {'X', 'Z'} == {'X', 'Z'} matching?: True
assert mods: 0 == 0 keys: {'X'} == {'X'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
no match: partial combo, after timeout
assert mods: 0 == 0 keys: {'A'} == {'A'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
no match: partial combo, repeated
assert mods: 0 == 0 keys: {'A'} == {'A'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'A'} == {'A'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'A'} == {'A'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
no match: partial combo, repeated
assert mods: 0 == 0 keys: {'A'} == {'A'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'B'} == {'B'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'A'} == {'A'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
no match: 3 combo after timout
assert mods: 0 == 0 keys: {'A'} == {'A'} matching?: True
assert mods: 0 == 0 keys: {'A', 'C'} == {'A', 'C'} matching?: True
assert mods: 0 == 0 keys: {'A', 'B', 'C'} == {'A', 'B', 'C'} matching?: True
assert mods: 0 == 0 keys: {'A', 'C'} == {'A', 'C'} matching?: True
assert mods: 0 == 0 keys: {'C'} == {'C'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
no match: other + 2 combo within timeout
assert mods: 0 == 0 keys: {'E'} == {'E'} matching?: True
assert mods: 0 == 0 keys: {'A', 'E'} == {'A', 'E'} matching?: True
assert mods: 0 == 0 keys: {'A', 'B', 'E'} == {'A', 'B', 'E'} matching?: True
assert mods: 0 == 0 keys: {'A', 'E'} == {'A', 'E'} matching?: True
assert mods: 0 == 0 keys: {'A'} == {'A'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
no match: 2 combo + other within timeout
assert mods: 0 == 0 keys: {'A'} == {'A'} matching?: True
assert mods: 0 == 0 keys: {'A', 'B'} == {'A', 'B'} matching?: True
assert mods: 0 == 0 keys: {'A', 'B', 'E'} == {'A', 'B', 'E'} matching?: True
assert mods: 0 == 0 keys: {'A', 'E'} == {'A', 'E'} matching?: True
assert mods: 0 == 0 keys: {'A'} == {'A'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
no match: 2 combo after timeout
assert mods: 0 == 0 keys: {'A'} == {'A'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'B'} == {'B'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
no match: Combo + other, within timeout
assert mods: 0 == 0 keys: {'A'} == {'A'} matching?: True
assert mods: 0 == 0 keys: {'A', 'B'} == {'A', 'B'} matching?: True
assert mods: 0 == 0 keys: {'A', 'B', 'E'} == {'A', 'B', 'E'} matching?: True
assert mods: 0 == 0 keys: {'B', 'E'} == {'B', 'E'} matching?: True
assert mods: 0 == 0 keys: {'E'} == {'E'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
no match: Combo + other, within timeout
assert mods: 0 == 0 keys: {'A'} == {'A'} matching?: True
assert mods: 0 == 0 keys: {'A', 'E'} == {'A', 'E'} matching?: True
assert mods: 0 == 0 keys: {'A', 'B', 'E'} == {'A', 'B', 'E'} matching?: True
assert mods: 0 == 0 keys: {'B', 'E'} == {'B', 'E'} matching?: True
assert mods: 0 == 0 keys: {'E'} == {'E'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
match: Combo containing layer switch, within timeout
assert mods: 0 == 0 keys: {'Z'} == {'Z'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
no match: Combo containing layer switch + other, within timeout
assert mods: 0 == 0 keys: {'1'} == {'1'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
match: Other pressed and released before combo, delay after other press but within the combo timeout
assert mods: 0 == 0 keys: {'B'} == {'B'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'Z'} == {'Z'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
match: Other pressed and released before combo, delay after other release but within the combo timeout
assert mods: 0 == 0 keys: {'B'} == {'B'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'Z'} == {'Z'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
match: Other pressed and released before combo, delay after other pressed but within the combo timeout, other is part of another combo
assert mods: 0 == 0 keys: {'A'} == {'A'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'Z'} == {'Z'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
match: Other pressed and released before combo, delay after other release but within the combo timeout, other is part of another combo
assert mods: 0 == 0 keys: {'A'} == {'A'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'Z'} == {'Z'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
.match: leader sequence, within timeout
assert mods: 0 == 0 keys: {'V'} == {'V'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
match: 2 sequence, within timeout
assert mods: 0 == 0 keys: {'X'} == {'X'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
match: 2 sequence, within long timeout
assert mods: 0 == 0 keys: {'Z'} == {'Z'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
match: 3 sequence, within timeout
assert mods: 0 == 0 keys: {'1'} == {'Y'} matching?: False
assert mods: 0 == 0 keys: set() == set() matching?: False
assert mods: 0 == None keys: {'2'} == {None} matching?: False
assert mods: 0 == None keys: set() == {None} matching?: False
assert mods: 0 == None keys: {'HASH'} == {None} matching?: False
assert mods: 0 == None keys: set() == {None} matching?: False
FF.................................................multi-character key, single-character value
assert mods: 0 == 0 keys: {'A'} == {'A'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'BACKSPACE'} == {'BACKSPACE'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'B'} == {'B'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
multi-character value, single-character key
assert mods: 0 == 0 keys: {'A'} == {'A'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'A'} == {'A'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
shifted alphanumeric or symbol in key and/or value
assert mods: 2 == 2 keys: set() == set() matching?: True
assert mods: 2 == 2 keys: {'2'} == {'2'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
backspace is only tapped as many times as necessary to delete the difference between the key and value
assert mods: 0 == 0 keys: {'D'} == {'D'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'C'} == {'C'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'C'} == {'C'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'BACKSPACE'} == {'BACKSPACE'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'B'} == {'B'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'B'} == {'B'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
the presence of non-shift modifiers prevents a multi-character match
assert mods: 1 == 1 keys: set() == set() matching?: True
assert mods: 1 == 1 keys: {'A'} == {'A'} matching?: True
assert mods: 1 == 1 keys: set() == set() matching?: True
assert mods: 1 == 1 keys: {'A'} == {'A'} matching?: True
assert mods: 1 == 1 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
the presence of non-shift modifiers prevents a single-character match
assert mods: 1 == 1 keys: set() == set() matching?: True
assert mods: 1 == 1 keys: {'B'} == {'B'} matching?: True
assert mods: 1 == 1 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
the presence of non-shift modifiers resets current potential matches
assert mods: 0 == 0 keys: {'A'} == {'A'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 1 == 1 keys: set() == set() matching?: True
assert mods: 1 == 1 keys: {'A'} == {'A'} matching?: True
assert mods: 1 == 1 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
match found and replaced when there are preceding characters
assert mods: 0 == 0 keys: {'C'} == {'C'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'A'} == {'A'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'BACKSPACE'} == {'BACKSPACE'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'B'} == {'B'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
match found and replaced when there are trailing characters, and the trailing characters are sent
assert mods: 0 == 0 keys: {'A'} == {'A'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'BACKSPACE'} == {'BACKSPACE'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'B'} == {'B'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'C'} == {'C'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
no match
assert mods: 0 == 0 keys: {'A'} == {'A'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'1'} == {'1'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
multiple backspaces
assert mods: 0 == 0 keys: {'D'} == {'D'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'A'} == {'A'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'D'} == {'D'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'A'} == {'A'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'D'} == {'D'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'A'} == {'A'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'D'} == {'D'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'BACKSPACE'} == {'BACKSPACE'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'BACKSPACE'} == {'BACKSPACE'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'BACKSPACE'} == {'BACKSPACE'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'BACKSPACE'} == {'BACKSPACE'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'BACKSPACE'} == {'BACKSPACE'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'B'} == {'B'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'A'} == {'A'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'A'} == {'A'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'A'} == {'A'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: 0 == 0 keys: {'A'} == {'A'} matching?: True
assert mods: 0 == 0 keys: set() == set() matching?: True
assert mods: None == 0 keys: {None} == {'B'} matching?: False
assert mods: None == 0 keys: {None} == set() matching?: False
F..........
======================================================================
FAIL: test_sequence (tests.test_combos.TestCombo.test_sequence)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/peter/Dev/kmk_firmware/tests/test_combos.py", line 464, in test_sequence
    keyboard.test(
  File "/Users/peter/.asdf/installs/python/3.11.3/lib/python3.11/unittest/mock.py", line 1369, in patched
    return func(*newargs, **newkeywargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/peter/Dev/kmk_firmware/tests/keyboard_test.py", line 131, in test
    assert matching, "reports don't match up"
AssertionError: reports don't match up

======================================================================
FAIL: test_holdtap (tests.test_holdtap.TestHoldTap.test_holdtap)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/peter/Dev/kmk_firmware/tests/test_holdtap.py", line 98, in test_holdtap
    keyboard.test(
  File "/Users/peter/.asdf/installs/python/3.11.3/lib/python3.11/unittest/mock.py", line 1369, in patched
    return func(*newargs, **newkeywargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/peter/Dev/kmk_firmware/tests/keyboard_test.py", line 131, in test
    assert matching, "reports don't match up"
AssertionError: reports don't match up

======================================================================
FAIL: test_keyboard_events_are_correct (tests.test_string_substitution.TestStringSubstitution.test_keyboard_events_are_correct)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/peter/Dev/kmk_firmware/tests/test_string_substitution.py", line 184, in test_keyboard_events_are_correct
    self.keyboard.test(
  File "/Users/peter/.asdf/installs/python/3.11.3/lib/python3.11/unittest/mock.py", line 1369, in patched
    return func(*newargs, **newkeywargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/peter/Dev/kmk_firmware/tests/keyboard_test.py", line 131, in test
    assert matching, "reports don't match up"
AssertionError: reports don't match up

----------------------------------------------------------------------
Ran 73 tests in 4.599s

FAILED (failures=3)
make: *** [unit-tests] Error 1

@peterwhitesell
Copy link
Author

the test failures are unrelated to this changeset (as they fail on master as well), but just being diligent here, running make test for each commit

@peterwhitesell
Copy link
Author

hey @xs5871, I was planning to start writing documentation for the module. Is there anything major interface-wise that would be good to address before that? I believe the linting is all good at this point

@xs5871
Copy link
Collaborator

xs5871 commented Jun 3, 2023

Unfortunately, yes, there's quite a bit that can be improved, code quality wise. Since this is a big chunk of a module, it'll take me a while to go through all of it. I'm positive we can work through it together.

@peterwhitesell
Copy link
Author

peterwhitesell commented Jun 3, 2023

@xs5871 totally understand it'll take a while to review 😃 just making sure I'm not holding up the review on my end. for full transparency I copied this code from bullwinkle3000's fork of kbjunky's original code, and then hacked it a bit myself for my needs, but I think I understand it well enough at this point to address any feedback.

I'll look forward to your feedbacks as you have a chance to review. not sure if you like to do comprehensive or iterative reviews, but I'm happy to address points along the way if that's helpful

@peterwhitesell
Copy link
Author

I went ahead and pushed up some initial docs for the module as well

Comment on lines +16 to +18
class REG:
Product_ID = 0x0
Revision_ID = 0x1
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
class REG:
Product_ID = 0x0
Revision_ID = 0x1
_PRODUCT_ID = const(0x0)
_REVISION_ID = const(0x1)
# etc.

Leading underscore + const + top level variable (doesn't work with class members!) makes this the mircopython equivalent of a pre-processor macro. Saves space and runtime lookups. You may prepend register constants with "_REG" and bit masks with "_MSK", but that's not strictly necessary.

Comment on lines +58 to +60
self.cs = digitalio.DigitalInOut(cs)
self.cs.direction = digitalio.Direction.OUTPUT
self.spi = busio.SPI(clock=sclk, MOSI=mosi, MISO=miso)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Peripheral init (io, bus and so on) are preferable done in during_bootup.

return comp

def pmw3360_read_motion(self):
result = bytearray(12)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make a module level bytearray for reading and writing -- try to avoid runtime allocations.

Comment on lines +96 to +97
while not self.spi.try_lock():
pass
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
while not self.spi.try_lock():
pass
if not self.spi.try_lock():
return

This'll lock up the rest of the firmware if for some reason SPI doesn't work. Refactor every occurance of this pattern.

while not self.spi.try_lock():
pass
try:
self.spi.configure(baudrate=self.baud, polarity=self.cpol, phase=self.cpha)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it really necessary to configure SPI everytime you read or write?

Comment on lines +187 to +191
self.pmw3360_read(REG.Motion)
self.pmw3360_read(REG.Delta_X_L)
self.pmw3360_read(REG.Delta_X_H)
self.pmw3360_read(REG.Delta_Y_L)
self.pmw3360_read(REG.Delta_Y_H)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading without storing the result.

Comment on lines +179 to +180
debug('firmware during_bootup() called')
debug('Debugging not enabled')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove debugging remnants left over from development.

# self.pmw3360_write(REG.Config2, 0) # set to wired mouse mode
self.pmw3360_write(REG.Angle_Tune, -25) # set to wired mouse mode
self.pmw3360_write(REG.Lift_Config, self.lift_config) # set to wired mouse mode
if keyboard.debug_enabled:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if keyboard.debug_enabled:
if debug.debug_enabled:

Didn't notice this earlier: Every debug statement has to be guarded by a if debug.debug_enabled: statement.

motion = self.pmw3360_read_motion()
if motion[0] & 0x80:
if motion[0] & 0x07:
debug('Motion weirdness')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a helpfull debug message.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this, what is in there? Where did you get it? Is it something proprietary or closed source? Needs a description answering these basic questions.

Also: if you import this huge binary blob it'll stay in memory indefinitely. Unless you're up for doing some questionable things to the global and local namespaces in order to ensure that the memory gets freed again, you have to find another way to load it.

@peterwhitesell
Copy link
Author

hey @xs5871 thank you for the review and feedback. It may take me some time to address, but just writing now to acknowledge receipt and thank you for the time

@xs5871
Copy link
Collaborator

xs5871 commented Aug 31, 2023

It's my fault this took so long. A bunch of my reviews got stuck for some reason and I only noticed yesterday. I actually wrote this over a month ago... apologies.

@lucacri
Copy link

lucacri commented May 29, 2024

Is there any update on this? I would like to test it and debug it. Can I just use this module or is it now too obsolete?

@peterwhitesell
Copy link
Author

Hi @lucacri, sorry I haven't attended to this PR for so long. I've changed tactics a bit, and haven't had a chance to new PR for it. What I do now is I use the whimsee/CircuitPython_PMW3360 library, with a much thinner kmk module built on that. If you want to try it out, it's pretty simple and you're more than welcome to open a PR for it if you get to that before I do.

Steps to use it are as follows, assuming you already have kmk setup on your board:

  1. install whimsee/CircuitPython_PMW3360 (either follow instructions in their readme, or just copy pmw3360.py from the root of their repo into <your board>/lib/pmw3360.py
  2. copy kmk/modules/pmw3660.py from my branch into <your board>/kmk/modules/pmw3660.py
  3. configure your board with the module:
from kmk.modules.pmw3360 import PMW3360
...
keyboard.modules.append(PMW3360(
  sck_pin, mosi_pin, miso_pin, cs_pin, # assuming you have defined these variables
  cpi=800,
  scroll_layers=[2, 4],
))

@meteozond
Copy link

@peterwhitesell Thanks for your instruction!
I've finally managed to get trackball working with my SuperMini NRF52840 powered Charybdis.
But it suddenly it stops working if any of RGB non static animations enabled.
Do you have any point how to fix it, may be?

@tikimo
Copy link

tikimo commented Sep 10, 2024

Possibly blocking the main thread?

@meteozond
Copy link

Looks like default RGB refresh rate is too high and block any activity :(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants