From: Melissa LeBlanc-Williams Date: Thu, 30 Jan 2025 19:20:21 +0000 (-0800) Subject: Add rotaryio module X-Git-Tag: 8.53.0^2~1 X-Git-Url: https://git.ayoreis.com/Adafruit_Blinka-hackapet.git/commitdiff_plain/a66c05d143cab78dc36ea119e7de17897d75bfe2?hp=-c Add rotaryio module --- a66c05d143cab78dc36ea119e7de17897d75bfe2 diff --git a/src/rotaryio.py b/src/rotaryio.py new file mode 100644 index 0000000..7c30332 --- /dev/null +++ b/src/rotaryio.py @@ -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