-
Notifications
You must be signed in to change notification settings - Fork 354
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #937 from makermelissa/main
Add rotaryio module
- Loading branch information
Showing
1 changed file
with
137 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
# SPDX-FileCopyrightText: 2025 Melissa LeBlanc-Williams for Adafruit Industries | ||
# | ||
# SPDX-License-Identifier: MIT | ||
""" | ||
`rotaryio` - Support for reading rotation sensors | ||
=========================================================== | ||
See `CircuitPython:rotaryio` in CircuitPython for more details. | ||
* Author(s): Melissa LeBlanc-Williams | ||
""" | ||
|
||
from __future__ import annotations | ||
import threading | ||
import microcontroller | ||
import digitalio | ||
|
||
# Define the state transition table for the quadrature encoder | ||
transitions = [ | ||
0, # 00 -> 00 no movement | ||
-1, # 00 -> 01 3/4 ccw (11 detent) or 1/4 ccw (00 at detent) | ||
+1, # 00 -> 10 3/4 cw or 1/4 cw | ||
0, # 00 -> 11 non-Gray-code transition | ||
+1, # 01 -> 00 2/4 or 4/4 cw | ||
0, # 01 -> 01 no movement | ||
0, # 01 -> 10 non-Gray-code transition | ||
-1, # 01 -> 11 4/4 or 2/4 ccw | ||
-1, # 10 -> 00 2/4 or 4/4 ccw | ||
0, # 10 -> 01 non-Gray-code transition | ||
0, # 10 -> 10 no movement | ||
+1, # 10 -> 11 4/4 or 2/4 cw | ||
0, # 11 -> 00 non-Gray-code transition | ||
+1, # 11 -> 01 1/4 or 3/4 cw | ||
-1, # 11 -> 10 1/4 or 3/4 ccw | ||
0, # 11 -> 11 no movement | ||
] | ||
|
||
|
||
class IncrementalEncoder: | ||
""" | ||
IncrementalEncoder determines the relative rotational position based on two series of | ||
pulses. It assumes that the encoder’s common pin(s) are connected to ground,and enables | ||
pull-ups on pin_a and pin_b. | ||
Create an IncrementalEncoder object associated with the given pins. It tracks the | ||
positional state of an incremental rotary encoder (also known as a quadrature encoder.) | ||
Position is relative to the position when the object is constructed. | ||
""" | ||
|
||
def __init__( | ||
self, pin_a: microcontroller.Pin, pin_b: microcontroller.Pin, divisor: int = 4 | ||
): | ||
""" | ||
Create an IncrementalEncoder object associated with the given pins. It tracks the | ||
positional state of an incremental rotary encoder (also known as a quadrature encoder.) | ||
Position is relative to the position when the object is constructed. | ||
:param microcontroller.Pin pin_a: The first pin connected to the encoder. | ||
:param microcontroller.Pin pin_b: The second pin connected to the encoder. | ||
:param int divisor: The number of pulses per encoder step. Default is 4. | ||
""" | ||
self._pin_a = digitalio.DigitalInOut(pin_a) | ||
self._pin_a.switch_to_input(pull=digitalio.Pull.UP) | ||
self._pin_b = digitalio.DigitalInOut(pin_b) | ||
self._pin_b.switch_to_input(pull=digitalio.Pull.UP) | ||
self._position = 0 | ||
self._last_state = 0 | ||
self._divisor = divisor | ||
self._sub_count = 0 | ||
self._poll_thread = threading.Thread(target=self._polling_loop, daemon=True) | ||
self._poll_thread.start() | ||
|
||
def deinit(self): | ||
"""Deinitializes the IncrementalEncoder and releases any hardware resources for reuse.""" | ||
self._pin_a.deinit() | ||
self._pin_b.deinit() | ||
if self._poll_thread.is_alive(): | ||
self._poll_thread.join() | ||
|
||
def __enter__(self) -> IncrementalEncoder: | ||
"""No-op used by Context Managers.""" | ||
return self | ||
|
||
def __exit__(self, _type, _value, _traceback): | ||
""" | ||
Automatically deinitializes when exiting a context. See | ||
:ref:`lifetime-and-contextmanagers` for more info. | ||
""" | ||
self.deinit() | ||
|
||
@property | ||
def divisor(self) -> int: | ||
"""The divisor of the quadrature signal. Use 1 for encoders without detents, or encoders | ||
with 4 detents per cycle. Use 2 for encoders with 2 detents per cycle. Use 4 for encoders | ||
with 1 detent per cycle.""" | ||
return self._divisor | ||
|
||
@divisor.setter | ||
def divisor(self, value: int): | ||
self._divisor = value | ||
|
||
@property | ||
def position(self) -> int: | ||
"""The current position in terms of pulses. The number of pulses per rotation is defined | ||
by the specific hardware and by the divisor.""" | ||
return self._position | ||
|
||
@position.setter | ||
def position(self, value: int): | ||
self._position = value | ||
|
||
def _get_pin_state(self) -> int: | ||
"""Returns the current state of the pins.""" | ||
return self._pin_a.value << 1 | self._pin_b.value | ||
|
||
def _polling_loop(self): | ||
while True: | ||
self._poll_encoder() | ||
|
||
def _poll_encoder(self): | ||
# Check the state of the pins | ||
# if either pin has changed, update the state | ||
new_state = self._get_pin_state() | ||
if new_state != self._last_state: | ||
self._state_update(new_state) | ||
self._last_state = new_state | ||
|
||
def _state_update(self, new_state: int): | ||
new_state &= 3 | ||
index = self._last_state << 2 | new_state | ||
sub_increment = transitions[index] | ||
self._sub_count += sub_increment | ||
if self._sub_count >= self._divisor: | ||
self._position += 1 | ||
self._sub_count = 0 | ||
elif self._sub_count <= -self._divisor: | ||
self._position -= 1 | ||
self._sub_count = 0 |