Skip to content

Commit 5856c18

Browse files
committed
feature/hardware: Add Rotary class
Signed-off-by: lbuque <[email protected]>
1 parent d52f841 commit 5856c18

File tree

7 files changed

+290
-0
lines changed

7 files changed

+290
-0
lines changed

m5stack/libs/driver/manifest.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
"neopixel/__init__.py",
1313
"neopixel/sk6812.py",
1414
"neopixel/ws2812.py",
15+
"rotary/__init__.py",
16+
"rotary/rotary_irq_esp.py",
17+
"rotary/rotary.py",
1518
"ads1100.py",
1619
"asr650x.py",
1720
"bh1750.py",

m5stack/libs/driver/rotary/__init__.py

Whitespace-only changes.

m5stack/libs/driver/rotary/rotary.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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+
# |------------- NEXT STATE -------------| |CURRENT STATE|
27+
# CLK/DT CLK/DT CLK/DT CLK/DT
28+
# 00 01 10 11
29+
[_R_START, _R_CCW_1, _R_CW_1, _R_START], # _R_START
30+
[_R_CW_2, _R_START, _R_CW_1, _R_START], # _R_CW_1
31+
[_R_CW_2, _R_CW_3, _R_CW_1, _R_START], # _R_CW_2
32+
[_R_CW_2, _R_CW_3, _R_START, _R_START | _DIR_CW], # _R_CW_3
33+
[_R_CCW_2, _R_CCW_1, _R_START, _R_START], # _R_CCW_1
34+
[_R_CCW_2, _R_CCW_1, _R_CCW_3, _R_START], # _R_CCW_2
35+
[_R_CCW_2, _R_START, _R_CCW_3, _R_START | _DIR_CCW], # _R_CCW_3
36+
[_R_START, _R_START, _R_START, _R_START],
37+
] # _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+
50+
_STATE_MASK = const(0x07)
51+
_DIR_MASK = const(0x30)
52+
53+
54+
def _wrap(value, incr, lower_bound, upper_bound):
55+
range = upper_bound - lower_bound + 1
56+
value = value + incr
57+
58+
if value < lower_bound:
59+
value += range * ((lower_bound - value) // range + 1)
60+
61+
return lower_bound + (value - lower_bound) % range
62+
63+
64+
def _bound(value, incr, lower_bound, upper_bound):
65+
return min(upper_bound, max(lower_bound, value + incr))
66+
67+
68+
def _trigger(rotary_instance):
69+
for listener in rotary_instance._listener:
70+
listener()
71+
72+
73+
class Rotary(object):
74+
75+
RANGE_UNBOUNDED = const(1)
76+
RANGE_WRAP = const(2)
77+
RANGE_BOUNDED = const(3)
78+
79+
def __init__(self, min_val, max_val, incr, reverse, range_mode, half_step, invert):
80+
self._min_val = min_val
81+
self._max_val = max_val
82+
self._incr = incr
83+
self._reverse = -1 if reverse else 1
84+
self._range_mode = range_mode
85+
self._value = min_val
86+
self._state = _R_START
87+
self._half_step = half_step
88+
self._invert = invert
89+
self._listener = []
90+
91+
def set(
92+
self, value=None, min_val=None, incr=None, max_val=None, reverse=None, range_mode=None
93+
):
94+
# disable DT and CLK pin interrupts
95+
self._hal_disable_irq()
96+
97+
if value is not None:
98+
self._value = value
99+
if min_val is not None:
100+
self._min_val = min_val
101+
if max_val is not None:
102+
self._max_val = max_val
103+
if incr is not None:
104+
self._incr = incr
105+
if reverse is not None:
106+
self._reverse = -1 if reverse else 1
107+
if range_mode is not None:
108+
self._range_mode = range_mode
109+
self._state = _R_START
110+
111+
# enable DT and CLK pin interrupts
112+
self._hal_enable_irq()
113+
114+
def value(self):
115+
return self._value
116+
117+
def reset(self):
118+
self._value = 0
119+
120+
def close(self):
121+
self._hal_close()
122+
123+
def add_listener(self, l):
124+
self._listener.append(l)
125+
126+
def remove_listener(self, l):
127+
if l not in self._listener:
128+
raise ValueError("{} is not an installed listener".format(l))
129+
self._listener.remove(l)
130+
131+
def _process_rotary_pins(self, pin):
132+
old_value = self._value
133+
clk_dt_pins = (self._hal_get_clk_value() << 1) | self._hal_get_dt_value()
134+
135+
if self._invert:
136+
clk_dt_pins = ~clk_dt_pins & 0x03
137+
138+
# Determine next state
139+
if self._half_step:
140+
self._state = _transition_table_half_step[self._state & _STATE_MASK][clk_dt_pins]
141+
else:
142+
self._state = _transition_table[self._state & _STATE_MASK][clk_dt_pins]
143+
direction = self._state & _DIR_MASK
144+
145+
incr = 0
146+
if direction == _DIR_CW:
147+
incr = self._incr
148+
elif direction == _DIR_CCW:
149+
incr = -self._incr
150+
151+
incr *= self._reverse
152+
153+
if self._range_mode == self.RANGE_WRAP:
154+
self._value = _wrap(self._value, incr, self._min_val, self._max_val)
155+
elif self._range_mode == self.RANGE_BOUNDED:
156+
self._value = _bound(self._value, incr, self._min_val, self._max_val)
157+
else:
158+
self._value = self._value + incr
159+
160+
try:
161+
if old_value != self._value and len(self._listener) != 0:
162+
_trigger(self)
163+
except:
164+
pass
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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+
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+
33+
if platform == "esp8266":
34+
if pin_num_clk in _esp8266_deny_pins:
35+
raise ValueError(
36+
"%s: Pin %d not allowed. Not Available for Interrupt: %s"
37+
% (platform, pin_num_clk, _esp8266_deny_pins)
38+
)
39+
if pin_num_dt in _esp8266_deny_pins:
40+
raise ValueError(
41+
"%s: Pin %d not allowed. Not Available for Interrupt: %s"
42+
% (platform, pin_num_dt, _esp8266_deny_pins)
43+
)
44+
45+
super().__init__(min_val, max_val, incr, reverse, range_mode, half_step, invert)
46+
47+
if pull_up == True:
48+
self._pin_clk = Pin(pin_num_clk, Pin.IN, Pin.PULL_UP)
49+
self._pin_dt = Pin(pin_num_dt, Pin.IN, Pin.PULL_UP)
50+
else:
51+
self._pin_clk = Pin(pin_num_clk, Pin.IN)
52+
self._pin_dt = Pin(pin_num_dt, Pin.IN)
53+
54+
self._enable_clk_irq(self._process_rotary_pins)
55+
self._enable_dt_irq(self._process_rotary_pins)
56+
57+
def _enable_clk_irq(self, callback=None):
58+
self._pin_clk.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=callback)
59+
60+
def _enable_dt_irq(self, callback=None):
61+
self._pin_dt.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=callback)
62+
63+
def _disable_clk_irq(self):
64+
self._pin_clk.irq(handler=None)
65+
66+
def _disable_dt_irq(self):
67+
self._pin_dt.irq(handler=None)
68+
69+
def _hal_get_clk_value(self):
70+
return self._pin_clk.value()
71+
72+
def _hal_get_dt_value(self):
73+
return self._pin_dt.value()
74+
75+
def _hal_enable_irq(self):
76+
self._enable_clk_irq(self._process_rotary_pins)
77+
self._enable_dt_irq(self._process_rotary_pins)
78+
79+
def _hal_disable_irq(self):
80+
self._disable_clk_irq()
81+
self._disable_dt_irq()
82+
83+
def _hal_close(self):
84+
self._hal_disable_irq()

m5stack/libs/hardware/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@
99
pass
1010

1111
from .ir import IR
12+
from .rotary import Rotary

m5stack/libs/hardware/manifest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"ir.py",
77
"rgb.py",
88
"sdcard.py",
9+
"rotary.py",
910
),
1011
base_path="..",
1112
opt=0,

m5stack/libs/hardware/rotary.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from driver.rotary.rotary_irq_esp import RotaryIRQ
2+
3+
4+
class Rotary(RotaryIRQ):
5+
def __init__(self, min_val=0, max_val=10) -> None:
6+
# M5Dial’s unique features
7+
super().__init__(
8+
pin_num_clk=41,
9+
pin_num_dt=40,
10+
min_val=min_val,
11+
max_val=max_val,
12+
reverse=False,
13+
range_mode=RotaryIRQ.RANGE_UNBOUNDED,
14+
)
15+
self._last_value = self.value()
16+
17+
def get_rotary_status(self):
18+
val = self.value()
19+
if val != self._last_value:
20+
return True
21+
return False
22+
23+
def get_rotary_value(self):
24+
self._last_value = self.value()
25+
return self.value()
26+
27+
def reset_rotary_vlaue(self):
28+
self._last_value = 0
29+
super().reset()
30+
31+
def set_rotary_vlaue(self, val):
32+
self._last_value = val
33+
super().set(val)
34+
35+
def get_rotary_increments(self):
36+
tmp = self._last_value
37+
return self.get_rotary_value() - tmp

0 commit comments

Comments
 (0)