3131__repo__ = "https://github.com/furbrain/CircuitPython_async_button.git"
3232
3333import asyncio
34+ from asyncio import Task , Event
3435
3536from adafruit_ticks import ticks_add , ticks_less , ticks_ms
3637
3738try :
38- from typing import Dict , Sequence , Awaitable
39+ from typing import Dict , Sequence , Awaitable , Any , Union
3940except ImportError :
4041 pass
4142
@@ -100,6 +101,52 @@ async def released(self):
100101 await asyncio .sleep (self .interval )
101102
102103
104+ class TaskWrapper :
105+ """
106+ Create a task to run coro, then trigger event when finished
107+ Shouldn't really need this but CircuitPython does not have asyncio.wait or Task.result()
108+ """
109+
110+ def __init__ (self , coro : Awaitable , event : Event ):
111+ """
112+ :param Awaitable coro: coroutine to run
113+ :param asyncio.Event event:
114+ """
115+ self ._result = None
116+ self .coro = coro
117+ self .event = event
118+ self .task = asyncio .create_task (self ._wait ())
119+
120+ async def _wait (self ):
121+ self ._result = await self .coro
122+ self .event .set ()
123+
124+ def done (self ):
125+ """
126+ Check whether task has completed
127+
128+ :return: True if task has completed
129+ """
130+ return self .task .done ()
131+
132+ def result (self ):
133+ """
134+ Get return value from task
135+
136+ :return: Whatever the task returned.
137+ """
138+ if not self .done ():
139+ raise AssertionError ("Task has not yet completed" )
140+ return self ._result
141+
142+ def cancel (self ):
143+ """
144+ Cancel the task
145+ :return:
146+ """
147+ self .task .cancel ()
148+
149+
103150class Button :
104151 """
105152 This object will monitor the specified pin for changes and will report
@@ -251,17 +298,12 @@ def _trigger(self, event: int):
251298 evt .set ()
252299 evt .clear ()
253300
254- @staticmethod
255- async def _set_event_when_done (coro : Awaitable , event : asyncio .Event ):
256- await coro
257- event .set ()
258-
259- async def wait (self , click_types : Sequence [int ] = ALL_EVENTS ):
301+ async def wait (self , click_types : Union [int , Sequence [int ]] = ALL_EVENTS ):
260302 """
261303 Wait for the first of the specified events.
262304
263- :param List[int] click_types: One or more events to listen for. Default is to listen
264- for all events
305+ :param ( List[int] | int) click_types: List of events to listen for. You can also pass a
306+ single event type in. Default is to listen for all events.
265307 :return: A list of the clicks that actually happened.
266308
267309 :example:
@@ -274,13 +316,19 @@ async def wait(self, click_types: Sequence[int] = ALL_EVENTS):
274316 >>> # do something
275317
276318 """
277- evts : Dict [int , asyncio .Task ] = {}
319+ if isinstance (click_types , int ):
320+ click_types = [click_types ]
321+ # shortcut for efficiency: if only one click type awaited, just await that
322+ if len (click_types ) == 1 :
323+ await self .events [click_types [0 ]].wait ()
324+ return [click_types [0 ]]
325+
326+ # multiple click types - needs more complex algorithm
327+ evts : Dict [int , TaskWrapper ] = {}
278328 one_event_done = asyncio .Event ()
279329 for evt_type in click_types :
280330 coro = self .events [evt_type ].wait ()
281- evts [evt_type ] = asyncio .create_task (
282- self ._set_event_when_done (coro , one_event_done )
283- )
331+ evts [evt_type ] = TaskWrapper (coro , one_event_done )
284332 await one_event_done .wait ()
285333 if len (evts ) > 1 :
286334 await asyncio .sleep (0 ) # ensure all event types get an opportunity to run
@@ -307,3 +355,58 @@ def deinit(self):
307355 """
308356 self .monitor_task .cancel ()
309357 self .keys .deinit ()
358+
359+
360+ class MultiButton :
361+ """
362+ This class allows you to await the first click from any of two or more buttons
363+ """
364+
365+ def __init__ (self , ** kwargs ):
366+ """
367+
368+ :param kwargs: pass each button that you want to be able to listen to with its name
369+
370+ :example:
371+ .. code-block:: python
372+
373+ >>> multi = MultiButton(a = button_a, b=button_b)
374+ >>> button, result = await multi.wait(a=Button.SINGLE, b= (Button.DOUBLE, Button.LONG))
375+ >>> # Long click on button B
376+ >>> print(button, result) # "b", Button.Long
377+ """
378+ for button in kwargs .values ():
379+ if not isinstance (button , Button ):
380+ raise TypeError ("Must pass in async_button.Button as parameters" )
381+ self .buttons : Dict [Any , Button ] = kwargs
382+
383+ async def wait (self , ** kwargs ):
384+ """
385+ Wait for any specified clicks
386+
387+ :param kwargs: pass by keyword what clicks you want to listen for
388+ :return: button, click type
389+ :example:
390+ .. code-block:: python
391+
392+ >>> multi = MultiButton(a = button_a, b=button_b)
393+ >>> button, result = await multi.wait(a=Button.SINGLE, b= (Button.DOUBLE, Button.LONG))
394+ >>> # Long click on button B
395+ >>> print(button, result) # "b", Button.Long
396+ """
397+ if len (kwargs ) == 1 :
398+ button = list (kwargs )[0 ]
399+ results = await self .buttons [button ].wait (kwargs [button ])
400+ return button , results [0 ]
401+ tasks : Dict [Any , Task ] = {}
402+ click_happened = asyncio .Event ()
403+ for key , value in kwargs .items ():
404+ tasks [key ] = TaskWrapper (self .buttons [key ].wait (value ), click_happened )
405+ await click_happened .wait ()
406+ result = (None , None )
407+ for key , task in tasks .items ():
408+ if task .done ():
409+ result = (key , task .result ()[0 ])
410+ else :
411+ task .cancel ()
412+ return result
0 commit comments