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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/en/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Before you look further, you probably want to start with our [getting started gu
- [ADNS9800](adns9800.md): Controlling ADNS9800 optical sensor
- [Encoder](encoder.md): Handling rotary encoders
- [Pimoroni trackball](pimoroni_trackball.md): Handling a small I2C trackball made by Pimoroni
- [PMW3360](pmw3360.md): Pointer support for PMW3360 motion sensor.

## [Extensions](extensions.md)

Expand Down
1 change: 1 addition & 0 deletions docs/en/modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ These modules are for specific hardware and may require additional libraries to
- [Encoder](encoder.md): Handling rotary encoders.
- [Pimoroni trackball](pimoroni_trackball.md): Handling a small I2C trackball made by Pimoroni.
- [AS5013 aka EasyPoint](easypoint.md): Handling a small I2C magnetic position sensor made by AMS.
- [PMW3360](pmw3360.md): Pointer support for PMW3360 motion sensor.
79 changes: 79 additions & 0 deletions docs/en/pmw3360.md
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 |
292 changes: 292 additions & 0 deletions kmk/modules/pmw3360.py
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
Comment on lines +16 to +18
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.

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
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.

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
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.

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?

self.pmw3360_start()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does pmw3360_start and pmw3360_stop actually do anything?

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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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)
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 "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)
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.

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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
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_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
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.

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:
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.

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')
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.

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
Loading