Skip to content

Commit fb7f206

Browse files
committed
Add Keyboard primitive.
1 parent ffee282 commit fb7f206

File tree

4 files changed

+135
-28
lines changed

4 files changed

+135
-28
lines changed

v3/docs/EVENTS.md

Lines changed: 77 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
# Synopsis
22

3-
Using `Event` instances rather than callbacks in `uasyncio` device drivers can
3+
Using `Event` instances rather than callbacks in `asyncio` device drivers can
44
simplify their design and standardise their APIs. It can also simplify
55
application logic.
66

7-
This document assumes familiarity with `uasyncio`. See [official docs](http://docs.micropython.org/en/latest/library/uasyncio.html) and
7+
This document assumes familiarity with `asyncio`. See [official docs](http://docs.micropython.org/en/latest/library/asyncio.html) and
88
[unofficial tutorial](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/TUTORIAL.md).
99

1010
# 0. Contents
1111

12-
1. [An alternative to callbacks in uasyncio code](./EVENTS.md#1-an-alternative-to-callbacks-in-uasyncio-code)
12+
1. [An alternative to callbacks in asyncio code](./EVENTS.md#1-an-alternative-to-callbacks-in-asyncio-code)
1313
2. [Rationale](./EVENTS.md#2-rationale)
1414
3. [Device driver design](./EVENTS.md#3-device-driver-design)
1515
4. [Primitives](./EVENTS.md#4-primitives) Facilitating Event-based application logic
@@ -25,10 +25,11 @@ This document assumes familiarity with `uasyncio`. See [official docs](http://do
2525
6.2 [EButton](./EVENTS.md#62-ebutton) Debounced pushbutton with double and long press events
2626
     6.2.1 [The suppress constructor argument](./EVENTS.md#621-the-suppress-constructor-argument)
2727
     6.2.2 [The sense constructor argument](./EVENTS.md#622-the-sense-constructor-argument)
28+
6.3 [Keyboard](./EVENTS.md#63-keyboard) A crosspoint array of pushbuttons.
2829
7. [Ringbuf queue](./EVENTS.md#7-ringbuf-queue) A MicroPython optimised queue primitive.
2930
[Appendix 1 Polling](./EVENTS.md#100-appendix-1-polling)
3031

31-
# 1. An alternative to callbacks in uasyncio code
32+
# 1. An alternative to callbacks in asyncio code
3233

3334
Callbacks have two merits. They are familiar, and they enable an interface
3435
which allows an asynchronous application to be accessed by synchronous code.
@@ -49,7 +50,7 @@ async def handle_messages(input_stream):
4950
Callbacks are not a natural fit in this model. Viewing the declaration of a
5051
synchronous function, it is not evident how the function gets called or in what
5152
context the code runs. Is it an ISR? Is it called from another thread or core?
52-
Or is it a callback running in a `uasyncio` context? You cannot tell without
53+
Or is it a callback running in a `asyncio` context? You cannot tell without
5354
trawling the code. By contrast, a routine such as the above example is a self
5455
contained process whose context and intended behaviour are evident.
5556

@@ -93,15 +94,15 @@ know to access this driver interface is the name of the bound `Event`.
9394
This doc aims to demostrate that the event based approach can simplify
9495
application logic by eliminating the need for callbacks.
9596

96-
The design of `uasyncio` V3 and its `Event` class enables this approach
97+
The design of `asyncio` V3 and its `Event` class enables this approach
9798
because:
9899
1. A task waiting on an `Event` is put on a queue where it consumes no CPU
99100
cycles until the event is triggered.
100-
2. The design of `uasyncio` can support large numbers of tasks (hundreds) on
101+
2. The design of `asyncio` can support large numbers of tasks (hundreds) on
101102
a typical microcontroller. Proliferation of tasks is not a problem, especially
102103
where they are small and spend most of the time paused waiting on queues.
103104

104-
This contrasts with other schedulers (such as `uasyncio` V2) where there was no
105+
This contrasts with other schedulers (such as `asyncio` V2) where there was no
105106
built-in `Event` class; typical `Event` implementations used
106107
[polling](./EVENTS.md#100-appendix-1-polling) and were convenience objects
107108
rather than performance solutions.
@@ -151,7 +152,7 @@ Drivers exposing `Event` instances include:
151152

152153
Applying `Events` to typical logic problems requires two new primitives:
153154
`WaitAny` and `WaitAll`. Each is an ELO. These primitives may be cancelled or
154-
subject to a timeout with `uasyncio.wait_for()`, although judicious use of
155+
subject to a timeout with `asyncio.wait_for()`, although judicious use of
155156
`Delay_ms` offers greater flexibility than `wait_for`.
156157

157158
## 4.1 WaitAny
@@ -325,13 +326,16 @@ async def foo():
325326

326327
This document describes drivers for mechanical switches and pushbuttons. These
327328
have event based interfaces exclusively and support debouncing. The drivers are
328-
simplified alternatives for
329+
simplified alternatives for
329330
[Switch](https://github.com/peterhinch/micropython-async/blob/master/v3/primitives/switch.py)
330331
and [Pushbutton](https://github.com/peterhinch/micropython-async/blob/master/v3/primitives/pushbutton.py),
331332
which also support callbacks.
332333

333334
## 6.1 ESwitch
334335

336+
```python
337+
from primitives import ESwitch
338+
```
335339
This provides a debounced interface to a switch connected to gnd or to 3V3. A
336340
pullup or pull down resistor should be supplied to ensure a valid logic level
337341
when the switch is open. The default constructor arg `lopen=1` is for a switch
@@ -348,7 +352,7 @@ Constructor arguments:
348352
down as appropriate.
349353
2. `lopen=1` Electrical level when switch is open circuit i.e. 1 is 3.3V, 0 is
350354
gnd.
351-
355+
352356
Methods:
353357

354358
1. `__call__` Call syntax e.g. `myswitch()` returns the logical debounced
@@ -363,7 +367,7 @@ Bound objects:
363367
Application code is responsible for clearing the `Event` instances.
364368
Usage example:
365369
```python
366-
import uasyncio as asyncio
370+
import asyncio
367371
from machine import Pin
368372
from primitives import ESwitch
369373
es = ESwitch(Pin("Y1", Pin.IN, Pin.PULL_UP))
@@ -390,7 +394,11 @@ asyncio.run(main())
390394
###### [Contents](./EVENTS.md#0-contents)
391395

392396
## 6.2 EButton
393-
397+
398+
```python
399+
from primitives import EButton
400+
```
401+
394402
This extends the functionality of `ESwitch` to provide additional events for
395403
long and double presses.
396404

@@ -479,12 +487,63 @@ determine whether the button is closed or open.
479487

480488
###### [Contents](./EVENTS.md#0-contents)
481489

490+
## 6.3 Keyboard
491+
492+
```python
493+
from primitives import Keyboard
494+
```
495+
A `Keyboard` provides an interface to a set of pushbuttons arranged as a
496+
crosspoint array. If a key is pressed its array index (scan code) is placed on a
497+
queue. Keypresses are retrieved with `async for`. The driver operates by
498+
polling each row, reading the response of each column. N-key rollover is
499+
supported - this is the case where a key is pressed before the prior key has
500+
been released.
501+
502+
Example usage:
503+
```python
504+
import asyncio
505+
from primitives import Keyboard
506+
from machine import Pin
507+
rowpins = [Pin(p, Pin.OUT) for p in range(10, 14)]
508+
colpins = [Pin(p, Pin.IN, Pin.PULL_DOWN) for p in range(16, 20)]
509+
510+
async def main():
511+
kp = Keyboard(rowpins, colpins)
512+
async for scan_code in kp:
513+
print(scan_code)
514+
if not scan_code:
515+
break # Quit on key with code 0
516+
517+
asyncio.run(main())
518+
```
519+
Constructor mandatory args:
520+
* `rowpins` A list or tuple of initialised output pins.
521+
* `colpins` A list or tuple of initialised input pins (pulled down).
522+
Constructor optional keyword only args:
523+
* `buffer=bytearray(10)` Keyboard buffer.
524+
* `db_delay=50` Debounce delay in ms.
525+
526+
The `Keyboard` class is subclassed from [Ringbuf queue](./EVENTS.md#7-ringbuf-queue)
527+
enabling scan codes to be retrieved with an asynchronous iterator.
528+
529+
In typical use the scan code would be used as the index into a string of
530+
keyboard characters ordered to match the physical layout of the keys. If data
531+
is not removed from the buffer, on overflow the oldest scan code is discarded.
532+
There is no limit on the number of rows or columns however if more than 256 keys
533+
are used, the `buffer` arg would need to be adapted to handle scan codes > 255.
534+
535+
###### [Contents](./EVENTS.md#0-contents)
536+
482537
# 7. Ringbuf Queue
483538

539+
```python
540+
from primitives import RingbufQueue
541+
```
542+
484543
The API of the `Queue` aims for CPython compatibility. This is at some cost to
485544
efficiency. As the name suggests, the `RingbufQueue` class uses a pre-allocated
486545
circular buffer which may be of any mutable type supporting the buffer protocol
487-
e.g. `list`, `array` or `bytearray`.
546+
e.g. `list`, `array` or `bytearray`.
488547

489548
Attributes of `RingbufQueue`:
490549
1. It is of fixed size, `Queue` can grow to arbitrary size.
@@ -515,7 +574,7 @@ Asynchronous methods:
515574
block until space is available.
516575
* `get` Return an object from the queue. If empty, block until an item is
517576
available.
518-
577+
519578
Retrieving items from the queue:
520579

521580
The `RingbufQueue` is an asynchronous iterator. Results are retrieved using
@@ -539,28 +598,27 @@ def add_item(q, data):
539598
except IndexError:
540599
pass
541600
```
542-
543601
###### [Contents](./EVENTS.md#0-contents)
544602

545603
# 100 Appendix 1 Polling
546604

547605
The primitives or drivers referenced here do not use polling with the following
548606
exceptions:
549607
1. Switch and pushbutton drivers. These poll the `Pin` instance for electrical
550-
reasons described below.
608+
reasons described below.
551609
2. `ThreadSafeFlag` and subclass `Message`: these use the stream mechanism.
552610

553611
Other drivers and primitives are designed such that paused tasks are waiting on
554612
queues and are therefore using no CPU cycles.
555613

556614
[This reference][1e] states that bouncing contacts can assume invalid logic
557-
levels for a period. It is a reaonable assumption that `Pin.value()` always
615+
levels for a period. It is a reasonable assumption that `Pin.value()` always
558616
returns 0 or 1: the drivers are designed to cope with any sequence of such
559617
readings. By contrast, the behaviour of IRQ's under such conditions may be
560618
abnormal. It would be hard to prove that IRQ's could never be missed, across
561619
all platforms and input conditions.
562620

563-
Pin polling aims to use minimal resources, the main overhead being `uasyncio`'s
621+
Pin polling aims to use minimal resources, the main overhead being `asyncio`'s
564622
task switching overhead: typically about 250 μs. The default polling interval
565623
is 50 ms giving an overhead of ~0.5%.
566624

v3/docs/THREADING.md

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ $ mpremote mip install github:peterhinch/micropython-async/v3/threadsafe
3838
1.3 [Threaded code on one core](./THREADING.md#13-threaded-code-on-one-core)
3939
1.4 [Threaded code on multiple cores](./THREADING.md#14-threaded-code-on-multiple-cores)
4040
1.5 [Globals](./THREADING.md#15-globals)
41-
1.6 [Debugging](./THREADING.md#16-debugging)
41+
1.6 [Allocation](./THREADING.md#16-allocation)
42+
1.7 [Debugging](./THREADING.md#17-debugging)
4243
2. [Sharing data](./THREADING.md#2-sharing-data)
4344
2.1 [A pool](./THREADING.md#21-a-pool) Sharing a set of variables.
4445
2.2 [ThreadSafeQueue](./THREADING.md#22-threadsafequeue)
@@ -146,7 +147,7 @@ async def foo():
146147
await process(d)
147148
```
148149

149-
## 1.2 Soft Interrupt Service Routines
150+
## 1.2 Soft Interrupt Service Routines
150151

151152
This also includes code scheduled by `micropython.schedule()` which is assumed
152153
to have been called from a hard ISR.
@@ -234,10 +235,20 @@ placeholder) before allowing other contexts to run.
234235

235236
If globals must be created or destroyed dynamically, a lock must be used.
236237

237-
## 1.6 Debugging
238+
## 1.6 Allocation
239+
240+
Memory allocation must be prevented from occurring while a garbage collection
241+
(GC) is in progress. Normally this is handled transparently by the GIL; where
242+
there is no GIL a lock is used. The one exception is the case of a hard ISR. It
243+
is invalid to have a hard ISR waiting on a lock. Consequently hard ISR's are
244+
disallowed from allocating and an exception is thrown if this is attempted.
245+
246+
Consequently code running in all other contexts is free to allocate.
247+
248+
## 1.7 Debugging
238249

239250
A key practical point is that coding errors in synchronising threads can be
240-
hard to locate: consequences can be extremely rare bugs or (in the case of
251+
hard to locate: consequences can be extremely rare bugs or (in the case of
241252
multi-core systems) crashes. It is vital to be careful in the way that
242253
communication between the contexts is achieved. This doc aims to provide some
243254
guidelines and code to assist in this task.
@@ -463,7 +474,7 @@ def core_2(getq, putq): # Run on core 2
463474
putq.put_sync(x, block=True) # Wait if queue fills.
464475
buf.clear()
465476
sleep_ms(30)
466-
477+
467478
async def sender(to_core2):
468479
x = 0
469480
while True:
@@ -648,8 +659,7 @@ again before it is accessed, the first data item will be lost.
648659
Blocking functions or methods have the potential of stalling the `uasyncio`
649660
scheduler. Short of rewriting them to work properly the only way to tame them
650661
is to run them in another thread. Any function to be run in this way must
651-
conform to the guiedelines above, notably with regard to allocation and side
652-
effects.
662+
conform to the guiedelines above, notably with regard to side effects.
653663

654664
## 4.1 Basic approach
655665

v3/primitives/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ def _handle_exception(loop, context):
4747
"ESwitch": "events",
4848
"EButton": "events",
4949
"RingbufQueue": "ringbuf_queue",
50+
"Keyboard": "events",
5051
}
5152

5253
# Copied from uasyncio.__init__.py

v3/primitives/events.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import uasyncio as asyncio
77
from . import Delay_ms
8+
from . import RingbufQueue
89

910
# An Event-like class that can wait on an iterable of Event-like instances.
1011
# .wait pauses until any passed event is set.
@@ -28,7 +29,7 @@ async def wt(self, event):
2829
await event.wait()
2930
self.evt.set()
3031
self.trig_event = event
31-
32+
3233
def event(self):
3334
return self.trig_event
3435

@@ -140,7 +141,7 @@ async def _ltf(self): # Long timeout
140141
await self._ltim.wait()
141142
self._ltim.clear() # Clear the event
142143
self.long.set() # User event
143-
144+
144145
# Runs if suppress set. Delay response to single press until sure it is a single short pulse.
145146
async def _dtf(self):
146147
while True:
@@ -164,3 +165,40 @@ def deinit(self):
164165
task.cancel()
165166
for evt in (self.press, self.double, self.long, self.release):
166167
evt.clear()
168+
169+
# A crosspoint array of pushbuttons
170+
# Tuples/lists of pins. Rows are OUT, cols are IN
171+
class Keyboard(RingbufQueue):
172+
def __init__(self, rowpins, colpins, *, buffer=bytearray(10), db_delay=50):
173+
super().__init__(buffer)
174+
self.rowpins = rowpins
175+
self.colpins = colpins
176+
self.db_delay = db_delay # Deounce delay in ms
177+
for opin in self.rowpins: # Initialise output pins
178+
opin(0)
179+
asyncio.create_task(self.scan(len(rowpins) * len(colpins)))
180+
181+
async def scan(self, nbuttons):
182+
prev = 0
183+
while True:
184+
await asyncio.sleep_ms(0)
185+
cur = 0
186+
for opin in self.rowpins:
187+
opin(1) # Assert output
188+
for ipin in self.colpins:
189+
cur <<= 1
190+
cur |= ipin()
191+
opin(0)
192+
if cur != prev: # State change
193+
pressed = cur & ~prev
194+
prev = cur
195+
if pressed: # Ignore button release
196+
for v in range(nbuttons): # Find button index
197+
if pressed & 1:
198+
break
199+
pressed >>= 1
200+
try:
201+
self.put_nowait(v)
202+
except IndexError: # q full. Overwrite oldest
203+
pass
204+
await asyncio.sleep_ms(self.db_delay) # Wait out bounce

0 commit comments

Comments
 (0)