]> Repositories - Adafruit_Blinka-hackapet.git/blob - src/adafruit_blinka/microcontroller/generic_linux/rotaryio.py
support renesas rzv2h/rzv2n, bananapi ai2h/ai2n board
[Adafruit_Blinka-hackapet.git] / src / adafruit_blinka / microcontroller / generic_linux / rotaryio.py
1 # SPDX-FileCopyrightText: 2025 Melissa LeBlanc-Williams for Adafruit Industries
2 #
3 # SPDX-License-Identifier: MIT
4 """
5 `rotaryio` - Support for reading rotation sensors
6 ===========================================================
7 See `CircuitPython:rotaryio` in CircuitPython for more details.
8
9 Generic Threading/DigitalIO implementation for Linux
10
11 * Author(s): Melissa LeBlanc-Williams
12 """
13
14 from __future__ import annotations
15 import threading
16 import microcontroller
17 import digitalio
18
19 # Define the state transition table for the quadrature encoder
20 transitions = [
21     0,  # 00 -> 00 no movement
22     -1,  # 00 -> 01 3/4 ccw (11 detent) or 1/4 ccw (00 at detent)
23     +1,  # 00 -> 10 3/4 cw or 1/4 cw
24     0,  # 00 -> 11 non-Gray-code transition
25     +1,  # 01 -> 00 2/4 or 4/4 cw
26     0,  # 01 -> 01 no movement
27     0,  # 01 -> 10 non-Gray-code transition
28     -1,  # 01 -> 11 4/4 or 2/4 ccw
29     -1,  # 10 -> 00 2/4 or 4/4 ccw
30     0,  # 10 -> 01 non-Gray-code transition
31     0,  # 10 -> 10 no movement
32     +1,  # 10 -> 11 4/4 or 2/4 cw
33     0,  # 11 -> 00 non-Gray-code transition
34     +1,  # 11 -> 01 1/4 or 3/4 cw
35     -1,  # 11 -> 10 1/4 or 3/4 ccw
36     0,  # 11 -> 11 no movement
37 ]
38
39
40 class IncrementalEncoder:
41     """
42     IncrementalEncoder determines the relative rotational position based on two series of
43     pulses. It assumes that the encoder’s common pin(s) are connected to ground,and enables
44     pull-ups on pin_a and pin_b.
45
46     Create an IncrementalEncoder object associated with the given pins. It tracks the
47     positional state of an incremental rotary encoder (also known as a quadrature encoder.)
48     Position is relative to the position when the object is constructed.
49     """
50
51     def __init__(
52         self, pin_a: microcontroller.Pin, pin_b: microcontroller.Pin, divisor: int = 4
53     ):
54         """
55         Create an IncrementalEncoder object associated with the given pins. It tracks the
56         positional state of an incremental rotary encoder (also known as a quadrature encoder.)
57         Position is relative to the position when the object is constructed.
58
59         :param microcontroller.Pin pin_a: The first pin connected to the encoder.
60         :param microcontroller.Pin pin_b: The second pin connected to the encoder.
61         :param int divisor: The number of pulses per encoder step. Default is 4.
62         """
63         self._pin_a = digitalio.DigitalInOut(pin_a)
64         self._pin_a.switch_to_input(pull=digitalio.Pull.UP)
65         self._pin_b = digitalio.DigitalInOut(pin_b)
66         self._pin_b.switch_to_input(pull=digitalio.Pull.UP)
67         self._position = 0
68         self._last_state = 0
69         self._divisor = divisor
70         self._sub_count = 0
71         self._poll_thread = threading.Thread(target=self._polling_loop, daemon=True)
72         self._poll_thread.start()
73
74     def deinit(self):
75         """Deinitializes the IncrementalEncoder and releases any hardware resources for reuse."""
76         self._pin_a.deinit()
77         self._pin_b.deinit()
78         if self._poll_thread.is_alive():
79             self._poll_thread.join()
80
81     def __enter__(self) -> IncrementalEncoder:
82         """No-op used by Context Managers."""
83         return self
84
85     def __exit__(self, _type, _value, _traceback):
86         """
87         Automatically deinitializes when exiting a context. See
88         :ref:`lifetime-and-contextmanagers` for more info.
89         """
90         self.deinit()
91
92     @property
93     def divisor(self) -> int:
94         """The divisor of the quadrature signal. Use 1 for encoders without detents, or encoders
95         with 4 detents per cycle. Use 2 for encoders with 2 detents per cycle. Use 4 for encoders
96         with 1 detent per cycle."""
97         return self._divisor
98
99     @divisor.setter
100     def divisor(self, value: int):
101         self._divisor = value
102
103     @property
104     def position(self) -> int:
105         """The current position in terms of pulses. The number of pulses per rotation is defined
106         by the specific hardware and by the divisor."""
107         return self._position
108
109     @position.setter
110     def position(self, value: int):
111         self._position = value
112
113     def _get_pin_state(self) -> int:
114         """Returns the current state of the pins."""
115         return self._pin_a.value << 1 | self._pin_b.value
116
117     def _polling_loop(self):
118         while True:
119             self._poll_encoder()
120
121     def _poll_encoder(self):
122         # Check the state of the pins
123         # if either pin has changed, update the state
124         new_state = self._get_pin_state()
125         if new_state != self._last_state:
126             self._state_update(new_state)
127             self._last_state = new_state
128
129     def _state_update(self, new_state: int):
130         new_state &= 3
131         index = self._last_state << 2 | new_state
132         sub_increment = transitions[index]
133         self._sub_count += sub_increment
134         if self._sub_count >= self._divisor:
135             self._position += 1
136             self._sub_count = 0
137         elif self._sub_count <= -self._divisor:
138             self._position -= 1
139             self._sub_count = 0