├── .gitignore ├── Examples ├── example_asyncio.py ├── example_asyncio_class.py └── example_simple.py ├── Images ├── Thumbs.db ├── pyboard.jpg ├── pyboardd.jpg ├── raspberrypipico.jpg └── tinypico.jpg ├── LICENSE ├── README.md ├── package.json ├── rotary.py ├── rotary_irq_esp.py ├── rotary_irq_pyb.py └── rotary_irq_rp2.py /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | .pydevproject 3 | oscilloscope 4 | -------------------------------------------------------------------------------- /Examples/example_asyncio.py: -------------------------------------------------------------------------------- 1 | # MIT License (MIT) 2 | # Copyright (c) 2021 Junliang Yan 3 | # https://opensource.org/licenses/MIT 4 | 5 | # example for MicroPython rotary encoder 6 | # - uasyncio implementation 7 | 8 | import sys 9 | if sys.platform == 'esp8266' or sys.platform == 'esp32': 10 | from rotary_irq_esp import RotaryIRQ 11 | elif sys.platform == 'pyboard': 12 | from rotary_irq_pyb import RotaryIRQ 13 | elif sys.platform == 'rp2': 14 | from rotary_irq_rp2 import RotaryIRQ 15 | else: 16 | print('Warning: The Rotary module has not been tested on this platform') 17 | 18 | import uasyncio as asyncio 19 | 20 | 21 | # Use heartbeat to keep event loop not empty 22 | async def heartbeat(): 23 | while True: 24 | await asyncio.sleep_ms(10) 25 | 26 | event = asyncio.Event() 27 | 28 | 29 | def callback(): 30 | event.set() 31 | 32 | 33 | async def main(): 34 | r = RotaryIRQ(pin_num_clk=13, 35 | pin_num_dt=14) 36 | r.add_listener(callback) 37 | 38 | asyncio.create_task(heartbeat()) 39 | while True: 40 | await event.wait() 41 | print('result =', r.value()) 42 | event.clear() 43 | 44 | try: 45 | asyncio.run(main()) 46 | except (KeyboardInterrupt, Exception) as e: 47 | print('Exception {} {}\n'.format(type(e).__name__, e)) 48 | finally: 49 | ret = asyncio.new_event_loop() # Clear retained uasyncio state 50 | -------------------------------------------------------------------------------- /Examples/example_asyncio_class.py: -------------------------------------------------------------------------------- 1 | # MIT License (MIT) 2 | # Copyright (c) 2021 Mike Teachman 3 | # https://opensource.org/licenses/MIT 4 | 5 | # example for MicroPython rotary encoder 6 | # - uasyncio implementation 7 | # - 2 independent rotary encoders 8 | # - register callbacks with the rotary objects 9 | # - shows the use of an Observer pattern in Python 10 | 11 | import sys 12 | if sys.platform == 'esp8266' or sys.platform == 'esp32': 13 | from rotary_irq_esp import RotaryIRQ 14 | elif sys.platform == 'pyboard': 15 | from rotary_irq_pyb import RotaryIRQ 16 | elif sys.platform == 'rp2': 17 | from rotary_irq_rp2 import RotaryIRQ 18 | else: 19 | print('Warning: The Rotary module has not been tested on this platform') 20 | 21 | import uasyncio as asyncio 22 | 23 | 24 | # example of a class that uses one rotary encoder 25 | class Application1(): 26 | def __init__(self, r1): 27 | self.r1 = r1 28 | self.myevent = asyncio.Event() 29 | asyncio.create_task(self.action()) 30 | r1.add_listener(self.callback) 31 | 32 | def callback(self): 33 | self.myevent.set() 34 | 35 | async def action(self): 36 | while True: 37 | await self.myevent.wait() 38 | print('App 1: rotary 1 = {}'. format(self.r1.value())) 39 | # do something with the encoder result ... 40 | self.myevent.clear() 41 | 42 | 43 | # example of a class that uses two rotary encoders 44 | class Application2(): 45 | def __init__(self, r1, r2): 46 | self.r1 = r1 47 | self.r2 = r2 48 | self.myevent = asyncio.Event() 49 | asyncio.create_task(self.action()) 50 | r1.add_listener(self.callback) 51 | r2.add_listener(self.callback) 52 | 53 | def callback(self): 54 | self.myevent.set() 55 | 56 | async def action(self): 57 | while True: 58 | await self.myevent.wait() 59 | print('App 2: rotary 1 = {}, rotary 2 = {}'. format( 60 | self.r1.value(), self.r2.value())) 61 | # do something with the encoder results ... 62 | self.myevent.clear() 63 | 64 | 65 | async def main(): 66 | rotary_encoder_1 = RotaryIRQ(pin_num_clk=13, 67 | pin_num_dt=14, 68 | min_val=0, 69 | max_val=5, 70 | reverse=False, 71 | range_mode=RotaryIRQ.RANGE_WRAP) 72 | 73 | rotary_encoder_2 = RotaryIRQ(pin_num_clk=18, 74 | pin_num_dt=19, 75 | min_val=0, 76 | max_val=20, 77 | reverse=False, 78 | range_mode=RotaryIRQ.RANGE_WRAP) 79 | 80 | # create tasks that use the rotary encoders 81 | app1 = Application1(rotary_encoder_1) 82 | app2 = Application2(rotary_encoder_1, rotary_encoder_2) 83 | 84 | # keep the event loop active 85 | while True: 86 | await asyncio.sleep_ms(10) 87 | 88 | try: 89 | asyncio.run(main()) 90 | except (KeyboardInterrupt, Exception) as e: 91 | print('Exception {} {}\n'.format(type(e).__name__, e)) 92 | finally: 93 | ret = asyncio.new_event_loop() # Clear retained uasyncio state 94 | -------------------------------------------------------------------------------- /Examples/example_simple.py: -------------------------------------------------------------------------------- 1 | # MIT License (MIT) 2 | # Copyright (c) 2021 Mike Teachman 3 | # https://opensource.org/licenses/MIT 4 | 5 | # example for MicroPython rotary encoder 6 | 7 | import sys 8 | if sys.platform == 'esp8266' or sys.platform == 'esp32': 9 | from rotary_irq_esp import RotaryIRQ 10 | elif sys.platform == 'pyboard': 11 | from rotary_irq_pyb import RotaryIRQ 12 | elif sys.platform == 'rp2': 13 | from rotary_irq_rp2 import RotaryIRQ 14 | else: 15 | print('Warning: The Rotary module has not been tested on this platform') 16 | 17 | import time 18 | 19 | 20 | r = RotaryIRQ(pin_num_clk=13, 21 | pin_num_dt=14, 22 | min_val=0, 23 | max_val=5, 24 | reverse=False, 25 | range_mode=RotaryIRQ.RANGE_WRAP) 26 | 27 | val_old = r.value() 28 | while True: 29 | val_new = r.value() 30 | 31 | if val_old != val_new: 32 | val_old = val_new 33 | print('result =', val_new) 34 | 35 | time.sleep_ms(50) 36 | -------------------------------------------------------------------------------- /Images/Thumbs.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miketeachman/micropython-rotary/b4c8ce53ee2413bdad41d24a2fcf91fac6ab2d32/Images/Thumbs.db -------------------------------------------------------------------------------- /Images/pyboard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miketeachman/micropython-rotary/b4c8ce53ee2413bdad41d24a2fcf91fac6ab2d32/Images/pyboard.jpg -------------------------------------------------------------------------------- /Images/pyboardd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miketeachman/micropython-rotary/b4c8ce53ee2413bdad41d24a2fcf91fac6ab2d32/Images/pyboardd.jpg -------------------------------------------------------------------------------- /Images/raspberrypipico.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miketeachman/micropython-rotary/b4c8ce53ee2413bdad41d24a2fcf91fac6ab2d32/Images/raspberrypipico.jpg -------------------------------------------------------------------------------- /Images/tinypico.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miketeachman/micropython-rotary/b4c8ce53ee2413bdad41d24a2fcf91fac6ab2d32/Images/tinypico.jpg -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Mike Teachman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MicroPython Rotary Encoder Driver 2 | A MicroPython driver to read a rotary encoder. Works with Pyboard, Raspberry Pi Pico, ESP8266, and ESP32 development boards. This is a robust implementation providing effective debouncing of encoder contacts. It uses two GPIO pins configured to trigger interrupts, following Ben Buxton's implementation: 3 | * http://www.buxtronix.net/2011/10/rotary-encoders-done-properly.html 4 | * https://github.com/buxtronix/arduino/tree/master/libraries/Rotary 5 | 6 | 7 | 8 | ## Key Implementation Features 9 | #### Interrupt based 10 | Whenever encoder pins DT and CLK change value a hardware interrupt is generated. This interrupt causes a python-based interrupt service routine (ISR) to run. The ISR interrupts normal code execution to process state changes in the encoder pins. 11 | 12 | #### Transition state machine 13 | A gray code based transition state table is used to process the DT and CLK changes. The use of the state table leads to accurate encoder counts and effective switch debouncing. Credit: Ben Buxton 14 | 15 | ## File Installation 16 | Two files are needed to use this module 17 | * platform-independent file `rotary.py` - a core file for all development boards 18 | * platform-specific file: 19 | * `rotary_irq_esp.py` Platform-specific code for ESP8266 and ESP32 development boards 20 | * `rotary_irq_pyb.py` Platform-specific code for Pyboard development boards 21 | * `rotary_irq_rp2.py` Platform-specific code for Raspberry Pi Pico development boards 22 | 23 | ### Copying files to development boards 24 | Copy files to the internal MicroPython filesystem using a utility such as `ampy` or `rshell` 25 | Ampy example below for Pyboards. Note: `-d1` option is often needed for ESP8266 boards 26 | ``` 27 | ampy -pCOMx put rotary.py 28 | ampy -pCOMx put rotary_irq_pyb.py 29 | ``` 30 | 31 | ### mip install 32 | 33 | Starting with MicroPython 1.20.0, it can be installed from [mip](https://docs.micropython.org/en/latest/reference/packages.html#installing-packages-with-mip) via: 34 | 35 | ``` 36 | >>> import mip 37 | >>> mip.install("github:miketeachman/micropython-rotary") 38 | ``` 39 | 40 | Or from mpremote via: 41 | 42 | ```bash 43 | mpremote mip install github:miketeachman/micropython-rotary 44 | ``` 45 | 46 | ## Class `RotaryIRQ` 47 | ### Constructor 48 | 49 | ```python 50 | RotaryIRQ( 51 | pin_num_clk, 52 | pin_num_dt, 53 | min_val=0, 54 | max_val=10, 55 | incr=1, 56 | reverse=False, 57 | range_mode=RotaryIRQ.RANGE_UNBOUNDED, 58 | pull_up=False, 59 | half_step=False, 60 | invert=False) 61 | ``` 62 | | argument | description | value | 63 | |-------------|-------------|---------| 64 | | pin_num_clk | GPIO pin connected to encoder CLK pin| integer | 65 | | pin_num_dt | GPIO pin connected to encoder DT pin | integer | 66 | | min_val | minimum value in the encoder range. Also the starting value | integer | 67 | | max_val | maximum value in the encoder range (not used when range_mode = RANGE_UNBOUNDED) | integer | 68 | | incr | amount count changes with each encoder click | integer (default=1)| 69 | | reverse | reverse count direction | True or False(default) | 70 | | range_mode | count behavior at min_val and max_val | RotaryIRQ.RANGE_UNBOUNDED(default) RotaryIRQ.RANGE_WRAP RotaryIRQ.RANGE_BOUNDED | 71 | | pull_up | enable internal pull up resistors. Use when rotary encoder hardware lacks pull up resistors | True or False(default) | 72 | | half_step | half-step mode | True or False(default) | 73 | | invert | invert the CLK and DT signals. Use when encoder resting value is CLK, DT = 00 | True or False(default) | 74 | 75 | | range_mode | description | 76 | | ------------- | ------------- | 77 | | RotaryIRQ.RANGE_UNBOUNDED | encoder has no bounds on the counting range | 78 | | RotaryIRQ.RANGE_WRAP | encoder will count up to max_val then wrap to minimum value (similar behaviour for count down) | 79 | | RotaryIRQ.RANGE_BOUNDED | encoder will count up to max_val then stop. Count down stops at min_val | 80 | 81 | ### Methods 82 | `value()` Return the encoder value 83 | *** 84 | `set(value=None, min_val=None, max_val=None, incr=None, reverse=None, range_mode=None)` 85 | Set encoder value and internal configuration parameters. See constructor for argument descriptions. `None` indicates no change to the configuration parameter 86 | 87 | Examples: 88 | * `set(min_val=0, max_val=59)` change encoder bounds - useful to set minutes on a clock display 89 | * `set(value=6)` change encoder value to `6`. calling `value()` will now return `6` 90 | *** 91 | `reset()` set encoder value to `min_val`. Redundant with the addition of the `set()` method. Retained for backwards compatibility) 92 | *** 93 | `add_listener(function)` add a callback function that will be called on each change of encoder count 94 | *** 95 | `remove_listener(function)` remove a previously added callback function 96 | *** 97 | `close()` deactivate microcontroller pins used to read encoder 98 | 99 | Note: None of the arguments are checked for configuration errors. 100 | 101 | ## Example 102 | * CLK pin attached to GPIO12 103 | * DT pin attached to GPIO13 104 | * GND pin attached to GND 105 | * \+ pin attached to 3.3V 106 | * Range mode = RotaryIRQ.RANGE_WRAP 107 | * Range 0...5 108 | 109 | ```python 110 | import time 111 | from rotary_irq_esp import RotaryIRQ 112 | 113 | r = RotaryIRQ(pin_num_clk=12, 114 | pin_num_dt=13, 115 | min_val=0, 116 | max_val=5, 117 | reverse=False, 118 | range_mode=RotaryIRQ.RANGE_WRAP) 119 | 120 | val_old = r.value() 121 | while True: 122 | val_new = r.value() 123 | 124 | if val_old != val_new: 125 | val_old = val_new 126 | print('result =', val_new) 127 | 128 | time.sleep_ms(50) 129 | ``` 130 | 131 | * For clockwise turning the encoder will count 0,1,2,3,4,5,0,1 ... 132 | * For counter-clockwise turning the encoder will count 0,5,4,3,2,1,0,5,4 .... 133 | 134 | ### Tested With: 135 | #### Development Boards 136 | * Pyboard D 137 | * PYBv1.1 138 | * TinyPico 139 | * Lolin D32 (ESP32) 140 | * Lolin D32 Pro (ESP32 with 4MB PSRAM) 141 | * Adafruit Feather Huzzah ESP8266 142 | * Adafruit Feather Huzzah ESP32 143 | * Raspberry Pi Pico 144 | * Raspberry Pi Pico W 145 | 146 | #### Rotary Encoders 147 | * KY-040 rotary encoder 148 | * Fermion: EC11 Rotary Encoder Module (thanks @sfblackwell) 149 | 150 | ### Wiring for KY-040 encoder 151 | | Encoder Pin | Connection | 152 | | ------------- |:-------------:| 153 | | + | 3.3V | 154 | | GND | Ground | 155 | | DT | GPIO pin | 156 | | CLK | GPIO pin | 157 | 158 | ### Recommended ESP8266 input pins 159 | This Rotary module requires pins that support interrupts. The following ESP8266 GPIO pins are recommended for this rotary encoder module 160 | * GPIO4 161 | * GPIO5 162 | * GPIO12 163 | * GPIO13 164 | * GPIO14 165 | 166 | The following ESP8266 GPIO pins should be **used with caution**. There is a risk that the state of the CLK and DT signals can affect the boot sequence. When possible, use other GPIO pins. 167 | * GPIO0 - used to detect boot-mode. Bootloader runs when pin is low during powerup. 168 | * GPIO2 - used to detect boot-mode. Attached to pull-up resistor. 169 | * GPIO15 - used to detect boot-mode. Attached to pull-down resistor. 170 | 171 | One pin does not support interrupts. 172 | * GPIO16 - does not support interrupts. 173 | 174 | ### Recommended ESP32 input pins 175 | This Rotary module requires pins that support interrupts. All ESP32 GPIO pins support interrupts. 176 | 177 | The following ESP32 GPIO strapping pins should be **used with caution**. There is a risk that the state of the CLK and DT signals can affect the boot sequence. When possible, use other GPIO pins. 178 | * GPIO0 - used to detect boot-mode. Bootloader runs when pin is low during powerup. Internal pull-up resistor. 179 | * GPIO2 - used to enter serial bootloader. Internal pull-down resistor. 180 | * GPIO4 - technical reference indicates this is a strapping pin, but usage is not described. Internal pull-down resistor. 181 | * GPIO5 - used to configure SDIO Slave. Internal pull-up resistor. 182 | * GPIO12 - used to select flash voltage. Internal pull-down resistor. 183 | * GPIO15 - used to configure silencing of boot messages. Internal pull-up resistor. 184 | 185 | ### Examples 186 | MicroPython example code is contained in the [Examples](Examples) folder 187 | [simple example](Examples/example_simple.py) 188 | [uasyncio example](Examples/example_asyncio.py) 189 | [uasyncio with classes example](Examples/example_asyncio_class.py) 190 | 191 | ### Oscilloscope Captures 192 | CLK and DT transitions captured on an oscilloscope. CLK = Yellow. DT = Blue 193 | 194 | One clockwise step 195 | ![cw](https://user-images.githubusercontent.com/12716600/46682621-e44a1180-cba2-11e8-98b5-9dc2368e1635.png) 196 | 197 | One counter-clockwise step 198 | ![ccw](https://user-images.githubusercontent.com/12716600/46682653-fe83ef80-cba2-11e8-9a2a-b6ee1f3bdee7.png) 199 | 200 | ### Board Hall of Fame 201 | Testing with Pyboard D, Pyboard v1.1, TinyPico, and Raspberry Pi Pico development boards 202 | ![pyboard d](Images/pyboardd.jpg) 203 | ![pyboard b](Images/pyboard.jpg) 204 | ![tiny pico](Images/tinypico.jpg) 205 | ![raspberry pi pico](Images/raspberrypipico.jpg) 206 | 207 | ## Acknowlegements 208 | This MicroPython implementation is an adaptation of Ben Buxton's C++ work: 209 | * https://github.com/buxtronix/arduino/tree/master/libraries/Rotary 210 | 211 | Other implementation ideas and techniques taken from: 212 | * https://github.com/SpotlightKid/micropython-stm-lib/tree/master/encoder 213 | * https://www.youtube.com/watch?v=BJHftzjNjkw 214 | * https://github.com/dhylands/python_lcd 215 | 216 | ## Future Ambitions 217 | * argument error checking 218 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "urls": [ 3 | ["rotary.py", "github:miketeachman/micropython-rotary/rotary.py"], 4 | ["rotary_irq_esp.py", "github:miketeachman/micropython-rotary/rotary_irq_esp.py"], 5 | ["rotary_irq_pyb.py", "github:miketeachman/micropython-rotary/rotary_irq_pyb.py"], 6 | ["rotary_irq_rp2.py", "github:miketeachman/micropython-rotary/rotary_irq_rp2.py"] 7 | ], 8 | "version": "1.0" 9 | } 10 | -------------------------------------------------------------------------------- /rotary.py: -------------------------------------------------------------------------------- 1 | # MIT License (MIT) 2 | # Copyright (c) 2022 Mike Teachman 3 | # https://opensource.org/licenses/MIT 4 | 5 | # Platform-independent MicroPython code for the rotary encoder module 6 | 7 | # Documentation: 8 | # https://github.com/MikeTeachman/micropython-rotary 9 | 10 | import micropython 11 | 12 | _DIR_CW = const(0x10) # Clockwise step 13 | _DIR_CCW = const(0x20) # Counter-clockwise step 14 | 15 | # Rotary Encoder States 16 | _R_START = const(0x0) 17 | _R_CW_1 = const(0x1) 18 | _R_CW_2 = const(0x2) 19 | _R_CW_3 = const(0x3) 20 | _R_CCW_1 = const(0x4) 21 | _R_CCW_2 = const(0x5) 22 | _R_CCW_3 = const(0x6) 23 | _R_ILLEGAL = const(0x7) 24 | 25 | _transition_table = [ 26 | 27 | # |------------- NEXT STATE -------------| |CURRENT STATE| 28 | # CLK/DT CLK/DT CLK/DT CLK/DT 29 | # 00 01 10 11 30 | [_R_START, _R_CCW_1, _R_CW_1, _R_START], # _R_START 31 | [_R_CW_2, _R_START, _R_CW_1, _R_START], # _R_CW_1 32 | [_R_CW_2, _R_CW_3, _R_CW_1, _R_START], # _R_CW_2 33 | [_R_CW_2, _R_CW_3, _R_START, _R_START | _DIR_CW], # _R_CW_3 34 | [_R_CCW_2, _R_CCW_1, _R_START, _R_START], # _R_CCW_1 35 | [_R_CCW_2, _R_CCW_1, _R_CCW_3, _R_START], # _R_CCW_2 36 | [_R_CCW_2, _R_START, _R_CCW_3, _R_START | _DIR_CCW], # _R_CCW_3 37 | [_R_START, _R_START, _R_START, _R_START]] # _R_ILLEGAL 38 | 39 | _transition_table_half_step = [ 40 | [_R_CW_3, _R_CW_2, _R_CW_1, _R_START], 41 | [_R_CW_3 | _DIR_CCW, _R_START, _R_CW_1, _R_START], 42 | [_R_CW_3 | _DIR_CW, _R_CW_2, _R_START, _R_START], 43 | [_R_CW_3, _R_CCW_2, _R_CCW_1, _R_START], 44 | [_R_CW_3, _R_CW_2, _R_CCW_1, _R_START | _DIR_CW], 45 | [_R_CW_3, _R_CCW_2, _R_CW_3, _R_START | _DIR_CCW], 46 | [_R_START, _R_START, _R_START, _R_START], 47 | [_R_START, _R_START, _R_START, _R_START]] 48 | 49 | _STATE_MASK = const(0x07) 50 | _DIR_MASK = const(0x30) 51 | 52 | 53 | def _wrap(value, incr, lower_bound, upper_bound): 54 | range = upper_bound - lower_bound + 1 55 | value = value + incr 56 | 57 | if value < lower_bound: 58 | value += range * ((lower_bound - value) // range + 1) 59 | 60 | return lower_bound + (value - lower_bound) % range 61 | 62 | 63 | def _bound(value, incr, lower_bound, upper_bound): 64 | return min(upper_bound, max(lower_bound, value + incr)) 65 | 66 | 67 | def _trigger(rotary_instance): 68 | for listener in rotary_instance._listener: 69 | listener() 70 | 71 | 72 | class Rotary(object): 73 | 74 | RANGE_UNBOUNDED = const(1) 75 | RANGE_WRAP = const(2) 76 | RANGE_BOUNDED = const(3) 77 | 78 | def __init__(self, min_val, max_val, incr, reverse, range_mode, half_step, invert): 79 | self._min_val = min_val 80 | self._max_val = max_val 81 | self._incr = incr 82 | self._reverse = -1 if reverse else 1 83 | self._range_mode = range_mode 84 | self._value = min_val 85 | self._state = _R_START 86 | self._half_step = half_step 87 | self._invert = invert 88 | self._listener = [] 89 | 90 | def set(self, value=None, min_val=None, incr=None, 91 | max_val=None, reverse=None, range_mode=None): 92 | # disable DT and CLK pin interrupts 93 | self._hal_disable_irq() 94 | 95 | if value is not None: 96 | self._value = value 97 | if min_val is not None: 98 | self._min_val = min_val 99 | if max_val is not None: 100 | self._max_val = max_val 101 | if incr is not None: 102 | self._incr = incr 103 | if reverse is not None: 104 | self._reverse = -1 if reverse else 1 105 | if range_mode is not None: 106 | self._range_mode = range_mode 107 | self._state = _R_START 108 | 109 | # enable DT and CLK pin interrupts 110 | self._hal_enable_irq() 111 | 112 | def value(self): 113 | return self._value 114 | 115 | def reset(self): 116 | self._value = 0 117 | 118 | def close(self): 119 | self._hal_close() 120 | 121 | def add_listener(self, l): 122 | self._listener.append(l) 123 | 124 | def remove_listener(self, l): 125 | if l not in self._listener: 126 | raise ValueError('{} is not an installed listener'.format(l)) 127 | self._listener.remove(l) 128 | 129 | def _process_rotary_pins(self, pin): 130 | old_value = self._value 131 | clk_dt_pins = (self._hal_get_clk_value() << 132 | 1) | self._hal_get_dt_value() 133 | 134 | if self._invert: 135 | clk_dt_pins = ~clk_dt_pins & 0x03 136 | 137 | # Determine next state 138 | if self._half_step: 139 | self._state = _transition_table_half_step[self._state & 140 | _STATE_MASK][clk_dt_pins] 141 | else: 142 | self._state = _transition_table[self._state & 143 | _STATE_MASK][clk_dt_pins] 144 | direction = self._state & _DIR_MASK 145 | 146 | incr = 0 147 | if direction == _DIR_CW: 148 | incr = self._incr 149 | elif direction == _DIR_CCW: 150 | incr = -self._incr 151 | 152 | incr *= self._reverse 153 | 154 | if self._range_mode == self.RANGE_WRAP: 155 | self._value = _wrap( 156 | self._value, 157 | incr, 158 | self._min_val, 159 | self._max_val) 160 | elif self._range_mode == self.RANGE_BOUNDED: 161 | self._value = _bound( 162 | self._value, 163 | incr, 164 | self._min_val, 165 | self._max_val) 166 | else: 167 | self._value = self._value + incr 168 | 169 | try: 170 | if old_value != self._value and len(self._listener) != 0: 171 | _trigger(self) 172 | except: 173 | pass 174 | -------------------------------------------------------------------------------- /rotary_irq_esp.py: -------------------------------------------------------------------------------- 1 | # MIT License (MIT) 2 | # Copyright (c) 2020 Mike Teachman 3 | # https://opensource.org/licenses/MIT 4 | 5 | # Platform-specific MicroPython code for the rotary encoder module 6 | # ESP8266/ESP32 implementation 7 | 8 | # Documentation: 9 | # https://github.com/MikeTeachman/micropython-rotary 10 | 11 | from machine import Pin 12 | from rotary import Rotary 13 | from sys import platform 14 | 15 | _esp8266_deny_pins = [16] 16 | 17 | 18 | class RotaryIRQ(Rotary): 19 | 20 | def __init__(self, pin_num_clk, pin_num_dt, min_val=0, max_val=10, incr=1, 21 | reverse=False, range_mode=Rotary.RANGE_UNBOUNDED, pull_up=False, half_step=False, invert=False): 22 | 23 | if platform == 'esp8266': 24 | if pin_num_clk in _esp8266_deny_pins: 25 | raise ValueError( 26 | '%s: Pin %d not allowed. Not Available for Interrupt: %s' % 27 | (platform, pin_num_clk, _esp8266_deny_pins)) 28 | if pin_num_dt in _esp8266_deny_pins: 29 | raise ValueError( 30 | '%s: Pin %d not allowed. Not Available for Interrupt: %s' % 31 | (platform, pin_num_dt, _esp8266_deny_pins)) 32 | 33 | super().__init__(min_val, max_val, incr, reverse, range_mode, half_step, invert) 34 | 35 | if pull_up == True: 36 | self._pin_clk = Pin(pin_num_clk, Pin.IN, Pin.PULL_UP) 37 | self._pin_dt = Pin(pin_num_dt, Pin.IN, Pin.PULL_UP) 38 | else: 39 | self._pin_clk = Pin(pin_num_clk, Pin.IN) 40 | self._pin_dt = Pin(pin_num_dt, Pin.IN) 41 | 42 | self._enable_clk_irq(self._process_rotary_pins) 43 | self._enable_dt_irq(self._process_rotary_pins) 44 | 45 | def _enable_clk_irq(self, callback=None): 46 | self._pin_clk.irq( 47 | trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, 48 | handler=callback) 49 | 50 | def _enable_dt_irq(self, callback=None): 51 | self._pin_dt.irq( 52 | trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, 53 | handler=callback) 54 | 55 | def _disable_clk_irq(self): 56 | self._pin_clk.irq(handler=None) 57 | 58 | def _disable_dt_irq(self): 59 | self._pin_dt.irq(handler=None) 60 | 61 | def _hal_get_clk_value(self): 62 | return self._pin_clk.value() 63 | 64 | def _hal_get_dt_value(self): 65 | return self._pin_dt.value() 66 | 67 | def _hal_enable_irq(self): 68 | self._enable_clk_irq(self._process_rotary_pins) 69 | self._enable_dt_irq(self._process_rotary_pins) 70 | 71 | def _hal_disable_irq(self): 72 | self._disable_clk_irq() 73 | self._disable_dt_irq() 74 | 75 | def _hal_close(self): 76 | self._hal_disable_irq() 77 | -------------------------------------------------------------------------------- /rotary_irq_pyb.py: -------------------------------------------------------------------------------- 1 | # MIT License (MIT) 2 | # Copyright (c) 2020 Mike Teachman 3 | # https://opensource.org/licenses/MIT 4 | 5 | # Platform-specific MicroPython code for the rotary encoder module 6 | # Pyboard implementation 7 | 8 | # Documentation: 9 | # https://github.com/MikeTeachman/micropython-rotary 10 | 11 | import os 12 | from pyb import Pin 13 | from pyb import ExtInt 14 | from rotary import Rotary 15 | 16 | 17 | class RotaryIRQ(Rotary): 18 | 19 | def __init__(self, pin_num_clk, pin_num_dt, min_val=0, max_val=10, incr=1, 20 | reverse=False, range_mode=Rotary.RANGE_UNBOUNDED, pull_up=False, half_step=False, invert=False): 21 | super().__init__(min_val, max_val, incr, reverse, range_mode, half_step, invert) 22 | 23 | if pull_up == True: 24 | self._pin_clk = Pin(pin_num_clk, Pin.IN, Pin.PULL_UP) 25 | self._pin_dt = Pin(pin_num_dt, Pin.IN, Pin.PULL_UP) 26 | self._pin_clk_irq = ExtInt( 27 | pin_num_clk, 28 | ExtInt.IRQ_RISING_FALLING, 29 | Pin.PULL_UP, 30 | self._process_rotary_pins) 31 | self._pin_dt_irq = ExtInt( 32 | pin_num_dt, 33 | ExtInt.IRQ_RISING_FALLING, 34 | Pin.PULL_UP, 35 | self._process_rotary_pins) 36 | else: 37 | self._pin_clk = Pin(pin_num_clk, Pin.IN) 38 | self._pin_dt = Pin(pin_num_dt, Pin.IN) 39 | self._pin_clk_irq = ExtInt( 40 | pin_num_clk, 41 | ExtInt.IRQ_RISING_FALLING, 42 | Pin.PULL_NONE, 43 | self._process_rotary_pins) 44 | self._pin_dt_irq = ExtInt( 45 | pin_num_dt, 46 | ExtInt.IRQ_RISING_FALLING, 47 | Pin.PULL_NONE, 48 | self._process_rotary_pins) 49 | 50 | # turn on 3.3V output to power the rotary encoder (pyboard D only) 51 | if 'PYBD' in os.uname().machine: 52 | Pin('EN_3V3').value(1) 53 | 54 | def _enable_clk_irq(self): 55 | self._pin_clk_irq.enable() 56 | 57 | def _enable_dt_irq(self): 58 | self._pin_dt_irq.enable() 59 | 60 | def _disable_clk_irq(self): 61 | self._pin_clk_irq.disable() 62 | 63 | def _disable_dt_irq(self): 64 | self._pin_dt_irq.disable() 65 | 66 | def _hal_get_clk_value(self): 67 | return self._pin_clk.value() 68 | 69 | def _hal_get_dt_value(self): 70 | return self._pin_dt.value() 71 | 72 | def _hal_enable_irq(self): 73 | self._enable_clk_irq() 74 | self._enable_dt_irq() 75 | 76 | def _hal_disable_irq(self): 77 | self._disable_clk_irq() 78 | self._disable_dt_irq() 79 | 80 | def _hal_close(self): 81 | self._hal_disable_irq() 82 | -------------------------------------------------------------------------------- /rotary_irq_rp2.py: -------------------------------------------------------------------------------- 1 | # MIT License (MIT) 2 | # Copyright (c) 2020 Mike Teachman 3 | # Copyright (c) 2021 Eric Moyer 4 | # https://opensource.org/licenses/MIT 5 | 6 | # Platform-specific MicroPython code for the rotary encoder module 7 | # Raspberry Pi Pico implementation 8 | 9 | # Documentation: 10 | # https://github.com/MikeTeachman/micropython-rotary 11 | 12 | from machine import Pin 13 | from rotary import Rotary 14 | 15 | IRQ_RISING_FALLING = Pin.IRQ_RISING | Pin.IRQ_FALLING 16 | 17 | 18 | class RotaryIRQ(Rotary): 19 | def __init__( 20 | self, 21 | pin_num_clk, 22 | pin_num_dt, 23 | min_val=0, 24 | max_val=10, 25 | incr=1, 26 | reverse=False, 27 | range_mode=Rotary.RANGE_UNBOUNDED, 28 | pull_up=False, 29 | half_step=False, 30 | invert=False 31 | ): 32 | super().__init__(min_val, max_val, incr, reverse, range_mode, half_step, invert) 33 | 34 | if pull_up: 35 | self._pin_clk = Pin(pin_num_clk, Pin.IN, Pin.PULL_UP) 36 | self._pin_dt = Pin(pin_num_dt, Pin.IN, Pin.PULL_UP) 37 | else: 38 | self._pin_clk = Pin(pin_num_clk, Pin.IN) 39 | self._pin_dt = Pin(pin_num_dt, Pin.IN) 40 | 41 | self._hal_enable_irq() 42 | 43 | def _enable_clk_irq(self): 44 | self._pin_clk.irq(self._process_rotary_pins, IRQ_RISING_FALLING) 45 | 46 | def _enable_dt_irq(self): 47 | self._pin_dt.irq(self._process_rotary_pins, IRQ_RISING_FALLING) 48 | 49 | def _disable_clk_irq(self): 50 | self._pin_clk.irq(None, 0) 51 | 52 | def _disable_dt_irq(self): 53 | self._pin_dt.irq(None, 0) 54 | 55 | def _hal_get_clk_value(self): 56 | return self._pin_clk.value() 57 | 58 | def _hal_get_dt_value(self): 59 | return self._pin_dt.value() 60 | 61 | def _hal_enable_irq(self): 62 | self._enable_clk_irq() 63 | self._enable_dt_irq() 64 | 65 | def _hal_disable_irq(self): 66 | self._disable_clk_irq() 67 | self._disable_dt_irq() 68 | 69 | def _hal_close(self): 70 | self._hal_disable_irq() 71 | --------------------------------------------------------------------------------