-
Notifications
You must be signed in to change notification settings - Fork 488
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
base: master
Are you sure you want to change the base?
Changes from all commits
54ec555
1d37fa2
b402422
ab05a81
eaa0c91
073ad4f
9967bb5
751eb6f
db7dd09
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
# PMW3360 | ||
For using PMW3360 motion sensor for pointer, scrolling and volume. The default behavior converts sensor XY movement into cursor XY movement. | ||
|
||
```python | ||
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` | ||
```python | ||
firmware = ( | ||
b'\x01' | ||
b'\x04' | ||
... | ||
) | ||
``` | ||
|
||
## Scrolling and Volume | ||
Scrolling and Volumne control can be enabled either in key event handlers, e.g. | ||
```python | ||
... | ||
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. | ||
```python | ||
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](boot.md#panning). | ||
|
||
## 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 | |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,292 @@ | ||||||||||
import busio | ||||||||||
import digitalio | ||||||||||
import microcontroller | ||||||||||
|
||||||||||
import math | ||||||||||
import time | ||||||||||
|
||||||||||
from kmk.keys import AX, KC | ||||||||||
from kmk.modules import Module | ||||||||||
from kmk.modules.pmw3360_firmware import firmware | ||||||||||
from kmk.utils import Debug | ||||||||||
|
||||||||||
debug = Debug(__name__) | ||||||||||
|
||||||||||
|
||||||||||
class REG: | ||||||||||
Product_ID = 0x0 | ||||||||||
Revision_ID = 0x1 | ||||||||||
Motion = 0x02 | ||||||||||
Delta_X_L = 0x03 | ||||||||||
Delta_X_H = 0x04 | ||||||||||
Delta_Y_L = 0x05 | ||||||||||
Delta_Y_H = 0x06 | ||||||||||
Config1 = 0x0F | ||||||||||
Config2 = 0x10 | ||||||||||
Angle_Tune = 0x11 | ||||||||||
SROM_Enable = 0x13 | ||||||||||
Observation = 0x24 | ||||||||||
SROM_ID = 0x2A | ||||||||||
Power_Up_Reset = 0x3A | ||||||||||
Motion_Burst = 0x50 | ||||||||||
SROM_Load_Burst = 0x62 | ||||||||||
Lift_Config = 0x63 | ||||||||||
|
||||||||||
|
||||||||||
class PMW3360(Module): | ||||||||||
tsww = tswr = 180 | ||||||||||
baud = 2000000 | ||||||||||
cpol = 1 | ||||||||||
cpha = 1 | ||||||||||
DIR_WRITE = 0x80 | ||||||||||
DIR_READ = 0x7F | ||||||||||
|
||||||||||
def __init__( | ||||||||||
self, | ||||||||||
cs, | ||||||||||
sclk, | ||||||||||
miso, | ||||||||||
mosi, | ||||||||||
invert_x=False, | ||||||||||
invert_y=False, | ||||||||||
flip_xy=False, | ||||||||||
lift_config=0x04, | ||||||||||
on_move=lambda keyboard: None, | ||||||||||
scroll_layers=[], | ||||||||||
volume_layers=[], | ||||||||||
): | ||||||||||
self.cs = digitalio.DigitalInOut(cs) | ||||||||||
self.cs.direction = digitalio.Direction.OUTPUT | ||||||||||
self.spi = busio.SPI(clock=sclk, MOSI=mosi, MISO=miso) | ||||||||||
Comment on lines
+58
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Peripheral init (io, bus and so on) are preferable done in |
||||||||||
self.invert_x = invert_x | ||||||||||
self.invert_y = invert_y | ||||||||||
self.flip_xy = flip_xy | ||||||||||
self.v_scroll_enabled = False | ||||||||||
self.h_scroll_enabled = False | ||||||||||
self.volume_control = False | ||||||||||
self.v_scroll_ctr = 0 | ||||||||||
self.h_scroll_ctr = 0 | ||||||||||
self.scroll_res = 10 | ||||||||||
self.on_move = on_move | ||||||||||
self.lift_config = lift_config | ||||||||||
self.scroll_layers = scroll_layers | ||||||||||
self.volume_layers = volume_layers | ||||||||||
debug(f'lift_config: {lift_config}') | ||||||||||
|
||||||||||
def start_v_scroll(self, enabled=True): | ||||||||||
self.v_scroll_enabled = enabled | ||||||||||
|
||||||||||
def start_h_scroll(self, enabled=True): | ||||||||||
self.h_scroll_enabled = enabled | ||||||||||
|
||||||||||
def set_scroll(self, enabled=True): | ||||||||||
self.v_scroll_enabled = enabled | ||||||||||
self.h_scroll_enabled = enabled | ||||||||||
|
||||||||||
def start_volume_control(self, enabled=True): | ||||||||||
self.volume_control = enabled | ||||||||||
|
||||||||||
def pmw3360_start(self): | ||||||||||
self.cs.value = False | ||||||||||
|
||||||||||
def pmw3360_stop(self): | ||||||||||
self.cs.value = True | ||||||||||
|
||||||||||
def pmw3360_write(self, reg, data): | ||||||||||
while not self.spi.try_lock(): | ||||||||||
pass | ||||||||||
Comment on lines
+96
to
+97
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
This'll lock up the rest of the firmware if for some reason SPI doesn't work. Refactor every occurance of this pattern. |
||||||||||
try: | ||||||||||
self.spi.configure(baudrate=self.baud, polarity=self.cpol, phase=self.cpha) | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it really necessary to configure SPI everytime you read or write? |
||||||||||
self.pmw3360_start() | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does |
||||||||||
self.spi.write(bytes([reg | self.DIR_WRITE, data])) | ||||||||||
# microcontroller.delay_us(35) | ||||||||||
except Exception as e: | ||||||||||
debug(e) | ||||||||||
Comment on lines
+103
to
+104
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not necessary. If you don't handle an exception, you don't have to catch it. It'll be caught and printed to debug console by upstream code. |
||||||||||
finally: | ||||||||||
self.spi.unlock() | ||||||||||
self.pmw3360_stop() | ||||||||||
microcontroller.delay_us(self.tswr) | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's "tswr"? Not a big fan of all the synchronous delays. It locks up the entire firmware. |
||||||||||
|
||||||||||
def pmw3360_read(self, reg): | ||||||||||
result = bytearray(1) | ||||||||||
while not self.spi.try_lock(): | ||||||||||
pass | ||||||||||
try: | ||||||||||
self.spi.configure(baudrate=self.baud, polarity=self.cpol, phase=self.cpha) | ||||||||||
self.pmw3360_start() | ||||||||||
self.spi.write(bytes([reg & self.DIR_READ])) | ||||||||||
microcontroller.delay_us(160) | ||||||||||
self.spi.readinto(result) | ||||||||||
microcontroller.delay_us(1) | ||||||||||
finally: | ||||||||||
self.spi.unlock() | ||||||||||
self.pmw3360_stop() | ||||||||||
microcontroller.delay_us(19) | ||||||||||
return result[0] | ||||||||||
|
||||||||||
def pwm3360_upload_srom(self): | ||||||||||
debug('Uploading pmw3360 FW') | ||||||||||
self.pmw3360_write(REG.Config2, 0x0) | ||||||||||
self.pmw3360_write(REG.SROM_Enable, 0x1D) | ||||||||||
time.sleep(0.01) | ||||||||||
self.pmw3360_write(REG.SROM_Enable, 0x18) | ||||||||||
while not self.spi.try_lock(): | ||||||||||
pass | ||||||||||
try: | ||||||||||
self.spi.configure(baudrate=self.baud, polarity=self.cpol, phase=self.cpha) | ||||||||||
self.pmw3360_start() | ||||||||||
self.spi.write(bytes([REG.SROM_Load_Burst | self.DIR_WRITE])) | ||||||||||
microcontroller.delay_us(15) | ||||||||||
for b in firmware: | ||||||||||
self.spi.write(bytes([b])) | ||||||||||
microcontroller.delay_us(15) | ||||||||||
except Exception as e: | ||||||||||
debug('Received error on firmware write') | ||||||||||
debug(e) | ||||||||||
finally: | ||||||||||
debug('Firmware done') | ||||||||||
microcontroller.delay_us(200) | ||||||||||
self.spi.unlock() | ||||||||||
self.pmw3360_stop() | ||||||||||
|
||||||||||
self.pmw3360_read(REG.SROM_ID) | ||||||||||
self.pmw3360_write(REG.Config2, 0) # set to wired mouse mode | ||||||||||
microcontroller.delay_us(1) | ||||||||||
|
||||||||||
def delta_to_int(self, high, low): | ||||||||||
comp = (high << 8) | low | ||||||||||
if comp & 0x8000: | ||||||||||
return (-1) * (0xFFFF + 1 - comp) | ||||||||||
return comp | ||||||||||
|
||||||||||
def pmw3360_read_motion(self): | ||||||||||
result = bytearray(12) | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||||||||||
while not self.spi.try_lock(): | ||||||||||
pass | ||||||||||
try: | ||||||||||
self.spi.configure(baudrate=self.baud, polarity=self.cpol, phase=self.cpha) | ||||||||||
self.pmw3360_start() | ||||||||||
self.spi.write(bytes([REG.Motion_Burst & self.DIR_READ])) | ||||||||||
microcontroller.delay_us(35) | ||||||||||
self.spi.readinto(result) | ||||||||||
finally: | ||||||||||
self.spi.unlock() | ||||||||||
self.pmw3360_stop() | ||||||||||
microcontroller.delay_us(20) | ||||||||||
return result | ||||||||||
Comment on lines
+164
to
+176
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You already implemented wrappers for SPI reading/writing. Why not use them here? |
||||||||||
|
||||||||||
def during_bootup(self, keyboard): | ||||||||||
debug('firmware during_bootup() called') | ||||||||||
debug('Debugging not enabled') | ||||||||||
Comment on lines
+179
to
+180
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove debugging remnants left over from development. |
||||||||||
self.pmw3360_start() | ||||||||||
microcontroller.delay_us(40) | ||||||||||
self.pmw3360_stop() | ||||||||||
microcontroller.delay_us(40) | ||||||||||
self.pmw3360_write(REG.Power_Up_Reset, 0x5A) | ||||||||||
time.sleep(0.1) | ||||||||||
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) | ||||||||||
Comment on lines
+187
to
+191
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reading without storing the result. |
||||||||||
self.pwm3360_upload_srom() | ||||||||||
time.sleep(0.1) | ||||||||||
self.pmw3360_write(REG.Config1, 0x06) # set x/y resolution to 700 cpi | ||||||||||
# 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: | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Didn't notice this earlier: Every |
||||||||||
debug('PMW3360 Product ID ', hex(self.pmw3360_read(REG.Product_ID))) | ||||||||||
debug('PMW3360 Revision ID ', hex(self.pmw3360_read(REG.Revision_ID))) | ||||||||||
if self.pmw3360_read(REG.Observation) & 0x40: | ||||||||||
debug('PMW3360: Sensor is running SROM') | ||||||||||
debug('PMW3360: SROM ID: ', hex(self.pmw3360_read(REG.SROM_ID))) | ||||||||||
else: | ||||||||||
debug('PMW3360: Sensor is not running SROM!') | ||||||||||
debug('Finished with firmware download') | ||||||||||
|
||||||||||
def before_matrix_scan(self, keyboard): | ||||||||||
return | ||||||||||
|
||||||||||
def after_matrix_scan(self, keyboard): | ||||||||||
return | ||||||||||
|
||||||||||
def before_hid_send(self, keyboard): | ||||||||||
return | ||||||||||
|
||||||||||
def after_hid_send(self, keyboard): | ||||||||||
motion = self.pmw3360_read_motion() | ||||||||||
if motion[0] & 0x80: | ||||||||||
if motion[0] & 0x07: | ||||||||||
debug('Motion weirdness') | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a helpfull debug message. |
||||||||||
self.pmw3360_write(REG.Motion_Burst, 0) | ||||||||||
return | ||||||||||
if self.flip_xy: | ||||||||||
delta_x = self.delta_to_int(motion[5], motion[4]) | ||||||||||
delta_y = self.delta_to_int(motion[3], motion[2]) | ||||||||||
else: | ||||||||||
delta_x = self.delta_to_int(motion[3], motion[2]) | ||||||||||
delta_y = self.delta_to_int(motion[5], motion[4]) | ||||||||||
if self.invert_x: | ||||||||||
delta_x *= -1 | ||||||||||
if self.invert_y: | ||||||||||
delta_y *= -1 | ||||||||||
if delta_x == 0 and delta_y == 0: | ||||||||||
return | ||||||||||
if keyboard.active_layers[0] in self.scroll_layers: | ||||||||||
self.v_scroll(keyboard, delta_y) | ||||||||||
self.h_scroll(keyboard, delta_x) | ||||||||||
elif self.v_scroll_enabled or self.h_scroll_enabled: | ||||||||||
if self.v_scroll_enabled: | ||||||||||
self.v_scroll(keyboard, delta_y) | ||||||||||
if self.h_scroll_enabled: | ||||||||||
self.h_scroll(keyboard, delta_x) | ||||||||||
elif self.volume_control or keyboard.active_layers[0] in self.volume_layers: | ||||||||||
self.v_scroll_ctr += 1 | ||||||||||
if self.v_scroll_ctr >= self.scroll_res: | ||||||||||
if delta_y > 0: | ||||||||||
keyboard.tap_key(KC.VOLD) | ||||||||||
if delta_y < 0: | ||||||||||
keyboard.tap_key(KC.VOLU) | ||||||||||
self.v_scroll_ctr = 0 | ||||||||||
else: | ||||||||||
if delta_x: | ||||||||||
AX.X.move(keyboard, self._scale_mouse_move(delta_x)) | ||||||||||
if delta_y: | ||||||||||
AX.Y.move(keyboard, self._scale_mouse_move(delta_y)) | ||||||||||
if self.on_move is not None: | ||||||||||
self.on_move(keyboard) | ||||||||||
|
||||||||||
def v_scroll(self, keyboard, delta): | ||||||||||
self.v_scroll_ctr += delta | ||||||||||
if self.v_scroll_ctr >= self.scroll_res: | ||||||||||
AX.W.move(keyboard, -1) | ||||||||||
self.v_scroll_ctr = 0 | ||||||||||
if self.v_scroll_ctr <= -self.scroll_res: | ||||||||||
AX.W.move(keyboard, 1) | ||||||||||
self.v_scroll_ctr = 0 | ||||||||||
|
||||||||||
def h_scroll(self, keyboard, delta): | ||||||||||
self.h_scroll_ctr += delta | ||||||||||
if self.h_scroll_ctr >= self.scroll_res: | ||||||||||
AX.P.move(keyboard, 1) | ||||||||||
self.h_scroll_ctr = 0 | ||||||||||
if self.h_scroll_ctr <= -self.scroll_res: | ||||||||||
AX.P.move(keyboard, -1) | ||||||||||
self.h_scroll_ctr = 0 | ||||||||||
|
||||||||||
def on_powersave_enable(self, keyboard): | ||||||||||
return | ||||||||||
|
||||||||||
def on_powersave_disable(self, keyboard): | ||||||||||
return | ||||||||||
|
||||||||||
def _scale_mouse_move(self, val): | ||||||||||
return val | ||||||||||
sign = math.copysign(1, val) | ||||||||||
sqrd = abs(val**1.5) | ||||||||||
scaled = sqrd // 4 | ||||||||||
ensured = max(scaled, 1) | ||||||||||
signed = math.copysign(ensured, sign) | ||||||||||
typed = int(signed) | ||||||||||
return typed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.