]> vault307.fbx.one Git - ir_remote.git/blob - primitives/pushbutton.py
infrared remote
[ir_remote.git] / primitives / pushbutton.py
1 # pushbutton.py
2
3 # Copyright (c) 2018-2022 Peter Hinch
4 # Released under the MIT License (MIT) - see LICENSE file
5
6 import uasyncio as asyncio
7 import utime as time
8 from . import launch, Delay_ms
9
10 try:
11 from machine import TouchPad
12 except ImportError:
13 pass
14
15
16 class Pushbutton:
17 debounce_ms = 50
18 long_press_ms = 1000
19 double_click_ms = 400
20
21 def __init__(self, pin, suppress=False, sense=None):
22 self._pin = pin # Initialise for input
23 self._supp = suppress
24 self._dblpend = False # Doubleclick waiting for 2nd click
25 self._dblran = False # Doubleclick executed user function
26 self._tf = False
27 self._ff = False
28 self._df = False
29 self._ld = False # Delay_ms instance for long press
30 self._dd = False # Ditto for doubleclick
31 # Convert from electrical to logical value
32 self._sense = pin.value() if sense is None else sense
33 self._state = self.rawstate() # Initial state
34 self._run = asyncio.create_task(self._go()) # Thread runs forever
35
36 async def _go(self):
37 while True:
38 self._check(self.rawstate())
39 # Ignore state changes until switch has settled. Also avoid hogging CPU.
40 # See https://github.com/peterhinch/micropython-async/issues/69
41 await asyncio.sleep_ms(Pushbutton.debounce_ms)
42
43 def _check(self, state):
44 if state == self._state:
45 return
46 # State has changed: act on it now.
47 self._state = state
48 if state: # Button pressed: launch pressed func
49 if self._tf:
50 launch(self._tf, self._ta)
51 if self._ld: # There's a long func: start long press delay
52 self._ld.trigger(Pushbutton.long_press_ms)
53 if self._df:
54 if self._dd(): # Second click: timer running
55 self._dd.stop()
56 self._dblpend = False
57 self._dblran = True # Prevent suppressed launch on release
58 launch(self._df, self._da)
59 else:
60 # First click: start doubleclick timer
61 self._dd.trigger(Pushbutton.double_click_ms)
62 self._dblpend = True # Prevent suppressed launch on release
63 else: # Button release. Is there a release func?
64 if self._ff:
65 if self._supp:
66 d = self._ld
67 # If long delay exists, is running and doubleclick status is OK
68 if not self._dblpend and not self._dblran:
69 if (d and d()) or not d:
70 launch(self._ff, self._fa)
71 else:
72 launch(self._ff, self._fa)
73 if self._ld:
74 self._ld.stop() # Avoid interpreting a second click as a long push
75 self._dblran = False
76
77 def _ddto(self): # Doubleclick timeout: no doubleclick occurred
78 self._dblpend = False
79 if self._supp and not self._state:
80 if not self._ld or (self._ld and not self._ld()):
81 launch(self._ff, self._fa)
82
83 # ****** API ******
84 def press_func(self, func=False, args=()):
85 if func is None:
86 self.press = asyncio.Event()
87 self._tf = self.press.set if func is None else func
88 self._ta = args
89
90 def release_func(self, func=False, args=()):
91 if func is None:
92 self.release = asyncio.Event()
93 self._ff = self.release.set if func is None else func
94 self._fa = args
95
96 def double_func(self, func=False, args=()):
97 if func is None:
98 self.double = asyncio.Event()
99 func = self.double.set
100 self._df = func
101 self._da = args
102 if func: # If double timer already in place, leave it
103 if not self._dd:
104 self._dd = Delay_ms(self._ddto)
105 else:
106 self._dd = False # Clearing down double func
107
108 def long_func(self, func=False, args=()):
109 if func is None:
110 self.long = asyncio.Event()
111 func = self.long.set
112 if func:
113 if self._ld:
114 self._ld.callback(func, args)
115 else:
116 self._ld = Delay_ms(func, args)
117 else:
118 self._ld = False
119
120 # Current non-debounced logical button state: True == pressed
121 def rawstate(self):
122 return bool(self._pin() ^ self._sense)
123
124 # Current debounced state of button (True == pressed)
125 def __call__(self):
126 return self._state
127
128 def deinit(self):
129 self._run.cancel()
130
131
132 class ESP32Touch(Pushbutton):
133 thresh = (80 << 8) // 100
134
135 @classmethod
136 def threshold(cls, val):
137 if not (isinstance(val, int) and 0 < val < 100):
138 raise ValueError("Threshold must be in range 1-99")
139 cls.thresh = (val << 8) // 100
140
141 def __init__(self, pin, suppress=False):
142 self._thresh = 0 # Detection threshold
143 self._rawval = 0
144 try:
145 self._pad = TouchPad(pin)
146 except ValueError:
147 raise ValueError(pin) # Let's have a bit of information :)
148 super().__init__(pin, suppress, False)
149
150 # Current logical button state: True == touched
151 def rawstate(self):
152 rv = self._pad.read() # ~220μs
153 if rv > self._rawval: # Either initialisation or pad was touched
154 self._rawval = rv # when initialised and has now been released
155 self._thresh = (rv * ESP32Touch.thresh) >> 8
156 return False # Untouched
157 return rv < self._thresh